闭包

红宝书上对闭包的定义:有权访问另外一个函数作用域中变量的函数。

MDN对闭包的定义是:是能够访问自由变量的函数。

自由变量:是指在当前函数中可以使用的(但是既不是arguments也不是本函数定义的局部变量)。

两个点:

  1. 是个函数
  2. 能访问另一个函数作用域中的变量,即使外层函数的上下文已经被销毁

就是说我们常见的比如内部函数从外部函数返回这种状态,该内部函数就是闭包。可以看如下特性中的示例!

说明闭包的几个特性:

  • 可以访问当前函数以外的变量
  • function outer() {
                var date = '11月1日';
                function inner(str) {
                    console.log(str + date)
                }
                return inner('today is ')
            }
            outer()
    
            function outer() {
                var date = '11月1日';
                return function () {
                    console.log('today is ' + date)
                }()
            }
            outer()   // 上下两例均返回“today is 11月1日”
  • 即使外部函数已经返回,闭包仍然能够访问外部定义的变量
  • function outer() {
                var date = '11月1日';
                function inner() {
                    console.log('today is ' + date)
                }
                return inner
            }
    
            // 以下是拆成分步执行,实际等同于outer()();
    // 先执行outer()得到一个返回值inner,此时outer函数执行完毕,跳出outer这个外层函数
    // 然后执行inner(),但是此时依然可用outer定义的变量date var getDate = outer() getDate()
  • 闭包可以修改外部函数的变量的值
  • function outer() {
                var date = '11月1日';
                function inner(newDate) {
                    date = newDate   // 将传入的值替换掉外层的date
                    console.log('today is ' + date)
                }
                return inner
            }
    
            var getDate = outer()
            getDate('191101')   // “today is 191101”

闭包的作用域链:

以下例分析:

        var scope = 'global';
        function checkscope() {
            var scope = 'local';
            function fun() {
                return scope;
            }
            return fun;
        }

        var check = checkscope();
        console.log(check());   // 'local'

执行过程:

  1. 进入全局代码,创建全局执行上下文并压入执行上下文栈
  2. 全局上下文初始化
  3. 执行checkscope函数,创建checkscope执行上下文并将其压入执行栈
  4. checkscope函数上下文初始化,创建变量对象、作用域链、this
  5. checkscope函数执行,执行完后checkscope执行上下文从执行栈中弹出
  6. 由于checkscope函数返回了一个f函数,因此创建f()执行上下文,将fun()的执行上下文压入执行栈,然后执行fun(),同样的,创建变量对象、作用域链、this
  7. fun()执行完毕后,从执行栈中弹出。

这个流程可以看到,checkscope执行完毕后是带着返回值弹出了执行栈的,在fun执行的时候checkscope函数的上下文已经被销毁了,但是,函数fun执行上下文维护了一个作用域链,结构如下:

funContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

指向关系是:当前作用域 -> checkscope -> 全局,不论checkscope 是否被销毁,fun函数都可以通过fun的作用域链找到它,这是闭包实现的关键

有关全局环境下函数嵌套与非嵌套时作用域链的指向分析,参考:深入浅出图解作用域链和闭包

 

一些常见的闭包考题

题1:

        var data = [];

        for (var i = 0; i < 3; i++) {
            data[i] = function () {
                console.log(i);
            };
        }

        data[0]();  
        data[1]();
        data[2]();

答案很显然:3、3、3

上面这个题的分析参照我的另一篇博客分析:let和const,里面详细的分析了这个题的过程~

如何才能让这个题输出我们想要的0、1、2呢?

解法一:博客当中给出了使用let的写法,很简单只需要将for中的var替换成let即可

解法二:

还有别的方法吗?本片将采用闭包的方式解决这个问题~~~回想一下闭包的状态是什么?内层函数可以使用外层函数定义的变量呀!

所以第一步:我们在function中return一个新的函数,在内层函数中访问变量 i。

然后考虑我们如何才能把当前的i传到外层function中呢?立刻我们联想到利用参数!

第二步:外层函数自执行,将 i 作为参数传入外层的function中

因此得到如下优化后的代码:

        var data = [];

        for (var i = 0; i < 3; i++) {
            data[i] = (function (i) {
                return function () {
                    console.log(i);
                }
            })(i)
        }

        data[0]();
        data[1]();
        data[2]();

结果为:0、1、2

正是我们要的结果啦~

解法三:

这里还可以改成我们常见的定时器写法

        for (var i = 0; i < 3; i++) {
            (function (i) {
                setTimeout(function () {
                    console.log(i)
                }, 100 * i)
            })(i)
        }

解法三其实和解法二的本质相同,都是将变量 i 的值复制给外层function的参数 i ,在函数内部又创建一个用于访问 i 的匿名函数,这样每个函数都有一个 i 的副本,就不会相互影响!

题2:这两段代码在checkscope执行完后,f所引用的自由变量scope会被垃圾回收吗?why?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

checkscope()();  

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope(); 
foo();   

结论是:第一个代码段中的scope特定时间后会被回收,第二段代码的自由变量不会回收

分析:

现在主流浏览器的垃圾回收算法是:标记清除。当垃圾回收开始时,从root开始寻找这个对象的引用是否可达,也就是找是否存在相互引用,如果引用链断裂,那么这个对象就可以被回收!

对于第一段代码,checkscope()执行完毕后被弹出执行栈,并且也没有其他引用,Root开始查找时不可达,因此闭包引用的自由变量scope过段时间可以被回收

对于第二段代码,由于var foo = checkscope(),checkscope()执行完成后,将foo()执行上下文压入执行栈,foo()指向堆中的自由变量 f ,对于Root来说可达,因此不会被回收!!

如果想要scope一定可以被回收,只要加:foo = null;即可!