NEWS

分享创造价值   合作实现共赢

JavaScript闭包和作用域分析

JavaScript高级程序设计中对闭包的定义:闭包是指有权访问另外一个函数作用域中变量的函数。在 JavaScript 中有两种作用域类型:局部作用域、全局作用域

JavaScript 拥有函数作用域:每个函数创建一个新的作用域。作用域决定了这些变量的可访问性(可见性)。函数内部定义的变量从函数外部是不可访问的(不可见的)

当你声明一个变量的时候,一般是这样的:

var a = 'a string';
var b = new String('a string');
复制代码

但这个时候你用typeof函数检测这两个变量的类型,就会发现以下结果:

console.log(typeof a);//string
console.log(typeof b);//object
复制代码

这是为什么呢?

这就要说到javaScript的变量存储,变量存储有两种方式:

其一:简单的值类型(undefined、number、string、boolean)存储在栈里。

其二:引用类型(函数、数组、对象、null)存储在堆里,栈里储存他们的内存地址(如下图)。

 

String,Number,Boolean等类都派生自Object对象,因此通过 new 关键字构造的她们都属于对象,而不是简单的值类型。

例子里的变量b,通过String构造函数声明,则b的__proto__指向String函数的prototype对象,因而b也继承有String函数的prototype的所有属性。

而变量a的声明方法是直接通过等号赋值,则变成了一个简单的值类型,存储在栈中。

 

作用域和​上下文环境

乍一听这个名词我们可能有点不太能理解,我们先这么浅薄地理解:

作用域是函数的一块“领地”,上下文环境保存作用域内的参数名和值,例如:

var a = 1,
    foo = function(b){
        console.log(a+b);
    };

foo(2);
复制代码

1、 因为我们的代码在全局环境内执行,在执行代码之前(即预编译),将先创建全局上下文环境,再把全局上下文环境压入 上下文栈 :

全局上下文环境
aundefinedfooundefinedthiswindow

 

2、然后我们执行代码(到调用foo函数之前),然后为变量赋值:

全局上下文环境a1foofunctionthiswindow

3、 然后我们调用foo函数,我们的上下文环境就要转到foo函数内部,并把foo函数执行上下文环境压入 上下文栈 ,:

foo函数执行上下文环境b2arguments[2]thiswindow

这时候 上下文栈 内有两个上下文环境:全局上下文 和 foo函数执行上下文。

 

但是在执行的时候,我们发现foo函数的作用域里没有变量a,我们要到哪里取呢?

答案是创建这个函数的作用域里取,foo函数是全局创建的,因此我们就回到全局作用域里找变量a。

然后看到全局上下文环境中,有变量a,它的值是1。

a + b == 1 + 2 == 3

因此,最后控制台输出:3

4、到此为止,foo函数执行完毕,foo函数执行上下文环境出栈销毁,最后留下全局上下文环境:

 

把整个过程连在一起,就是下面这张图:

 

作用域和上​下文的区别

作用域只是一个“地盘”,一个抽象的概念,其中没有变量。

要通过作用域对应的执行上下文环境来获取变量的值。

同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。

所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

闭​包

概念

了解到我们的先导知识后,我们最后来看闭包,闭包有两种情况:

  1. 函数作为返回值function fn(){ return function foo(){ console.log('闭包'); } } //调用fn立即执行foo 复制代码
  2. 函数作为参数被传递/* * @params n,m number型 * @return 返回两个数相加额结果 */ function add(n,m){ return n+m; } //闭包 (function fn(f){ var n = 1,m = 2; f(n,m); // 调用add函数 })(add); // add函数作为参数f传入 复制代码

闭包的重点其实就在于,函数执行完毕之后,上下文环境不会被销毁,例如:

浅谈JavaScript闭包和作用域问题

 

我们会发现,在给变量f赋值的时候函数fn()就执行完了,按理说,上下文环境应该销毁,我们应该访问不到a。

但其实fn的上下文环境并没有出栈,fn函数的上下文环境依旧可以访问到。

这也是为什么说闭包会导致内存泄漏和增加内存开销。

应用场​景

那在什么场景下可以用到闭包呢

1、模拟私有变量

var Counter = function() {
        var privateCounter = 0;
        function changeBy(val) {
            privateCounter += val;
        }
        return {
            increment: function() {
                changeBy(1);
            },
            decrement: function() {
                changeBy(-1);
            },
            value: function() {
                return privateCounter;
            }
        }   
    };
    
    var counter1 = Counter();// 上下文环境一
    var counter2 = Counter();// 上下文环境二,与环境一不共享变量

每个闭包有自己的上下文环境,上文的例子中,创建了两个私有成员,变量privateCounter和函数changeBy。

这两个变量都必须要用Counter.increment,Counter.decrement 和 Counter.value这三个方法中的一个调用,这就实现了私有成员。

2、函数防抖

/**
     * @function debounce 函数防抖
     * @param {Function} fn 需要防抖的函数
     * @param {Number} interval 间隔时间
     * @return {Function} 经过防抖处理的函数
     * */
    function debounce(fn, interval) {
        let timer = null; // 定时器
        return function() {
            // 清除上一次的定时器
            clearTimeout(timer);
            // 拿到当前的函数作用域
            let _this = this;
            // 拿到当前函数的参数数组
            let args = Array.prototype.slice.call(arguments, 0);
            // 开启倒计时定时器
            timer = setTimeout(function() {
                // 通过apply传递当前函数this,以及参数
                fn.apply(_this, args);
                // 默认300ms执行
            }, interval || 300)
        }
    }

3、函数节流

/**
 * @function throttle 函数节流
 * @param {Function} fn 需要节流的函数
 * @param {Number} interval 间隔时间
 * @return {Function} 经过节流处理的函数
 * */
function throttle(fn, interval) {
    let timer = null; // 定时器
    let firstTime = true; // 判断是否是第一次执行
    // 利用闭包
    return function() {
        // 拿到函数的参数数组
        let args = Array.prototype.slice.call(arguments, 0);
        // 拿到当前的函数作用域
        let _this = this;
        // 如果是第一次执行的话,需要立即执行该函数
        if(firstTime) {            
        // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 修改标识为null,释放内存
            firstTime = null;
        }
        // 如果当前有正在等待执行的函数则直接返回
        if(timer) return;
        // 开启一个倒计时定时器
        timer = setTimeout(function() {
            // 通过apply,绑定当前函数的作用域以及传递参数
            fn.apply(_this, args);
            // 清除之前的定时器
            timer = null;            // 默认300ms执行一次
        }, interval || 300)
    }
}

4、setTimeout场景

for(var i = 0; i < 5 ; i++ ){
    setTimeout(function(){console.log(i)},100);
}
复制代码

我们知道js是单线程的,setTimeout则是异步方法,因此每次遍历碰到setTimeout,就把里面的代码放到待执行栈里,等for循环遍历结束,再执行。

而因为i是用var定义的值类型,直接存储在栈内,每一次循环,i的值都被新值覆盖,因此最后一次循环结束,i=5。

然后才开始执行五次console.log(i);

即得到输出:5 5 5 5 5。

要解决这个问题,第一个方法是使用自执行函数提供闭包条件,再把i值保存到闭包中。

自执行函数会立即执行,因此setTimeout函数不会被压入待执行栈而立即执行。

for(var i = 0; i < 5 ; i++ ){
    (function(i){
        setTimeout(function(){console.log(i)},100);
    })(i)
}
// => 0 1 2 3 4

还有一种方法是把var改成let,此中原理也可参照扩展阅读中var和let的区别。

扩​展

var和let的区别

1、作用域var 的作用域在整个函数let 的作用域在{}内,例如:function fn(flag){ if(flag){ let i = 'success'; } console.log(i) } fn(true); //VM167:5 Uncaught ReferenceError: i is not defined 复制代码我们发现,i 的作用域在 if 语句里,一旦 if 语句执行结束,i 就被销毁,因此,当代码执行到console.log(i)的时候,自然找不到变量。把let给改成var,函数就能够正常运行。

浅谈JavaScript闭包和作用域问题

2、变量提升var 在函数声明的时候就创建了空间,并被赋值为undefined。(function fn(){ console.log(a); // =>undefined var a = 1; console.log(a); // =>1 })() 复制代码执行的顺序相当于:1、var a = undefined; 2、console.log(a); // =>undefined 3、a = 1; 4、console.log(a); // =>1 let 则只有到执行到声明语句的时候才创建空间。(function fn(){ console.log(a); let a = 1; console.log(a); })() //Uncaught ReferenceError: a is not defined 复制代码执行的顺序是:1、console.log(a)2、找不到a3、抛出异常Uncaught ReferenceError: a is not defined

相关文章