# 什么是闭包

闭包就是在一个变量对象里持有另一个变量对象里内容的引用时,就会产生闭包。常见的表现形式就是,内部函数持有外部函数的变量,我们可以通过返回内部函数去让更外层的作用域能够访问到内部函数的父函数里的变量。

function foo(){
    var a = 10;
    function fn(){
        a++; //fn 持有外层作用域 foo 函数里的变量 a
        console.log(a);
    }
    return fn;
}
var f = foo();  // 最外层作用域获得 fn 的引用
f(); // 11
f(); // 12
f(); // 13
var f2 = foo();
f2(); // 11
f2(); // 12

调用fn后产生的闭包

原本执行完 foo (),Stack 里 foo 的执行上下文的变量对象就要销毁。但是由于全局作用域的变量 f 持有其内部函数 fn 的引用而 fn 又持有变量 a 的引用。所以 foo 的执行上下文的变量对象一直不能释放回收,当我们调用 f () 时依然可以访问到变量 a。

闭包内存示意图

本质上所谓的闭包是一个引用关系,该引用关系存在于内部函数中,内部引用的是外部函数 (外部作用域) 的变量的对象

# 为什么会形成闭包

闭包的形成需要三个条件:

  • 函数嵌套,本质上就是函数作用域的嵌套
  • 内部函数持有外部函数的局部变量
  • 外部函数被使用

函数嵌套我们很好理解,因为函数的作用域在函数声明的时候就已经确定,所以函数要嵌套声明。内部函数要持有外部函数变量是因为如果内部函数没有持有外部函数变量,则内部函数在形成变量对象的时候并不会把外部函数的变量对象加入作用域链中,会直接越过它。这一点我们在 js执行上下文机制 一文中已经详细说明。上面两条我们只是声明了函数并没有调用它,所以第三条外部函数被调用也就很好理解。

当出现了符合上述三个条件的情况时,就会产生闭包。但在实际运行环境中,内部函数也要调用或者引用才会产生 Closure。这是因为部分浏览器会对内部函数做优化,当内部函数不使用或者不引用时我们去调试它并不会产生一个 Closure 对象。因为我们不去使用内部函数就相当于在代码执行过程中没有具体地体现出这种引用关系,尽管我们在定义的时候形成了这种引用关系,所以为了节省内存开销浏览器就不会生成 Closure。

function foo(){
    var a = 10;
    function fn(){
        a++; //fn 持有外层作用域 foo 函数里的变量 a
        console.log(a);
    }
    fn();
}
foo(); // 11

上面代码中同样会产生闭包,和第一个例子的代码相比只是我们不能多次访问而已。每调用一次外部函数就会产生一个 Closure

调用foo依然产生闭包

以函数 fn 的视角来看,我们将函数 foo 里面的变量 a 称为自由变量,因为它并未在 fn 中定义但它却在 fn 中使用了。所谓自由变量即在当前作用域里未被定义但却使用了的变量,它在查找时会向上级作用域逐级查找直到找到为止,若最终在全局作用域中都未找到则报错 xxx is not defined

一般闭包的产生必定伴随着自由变量的产生,所以当我们确定闭包里变量的值时我们可以套用自由变量的查找规则:所有自由变量的值在查找时都是按照在定义阶段的上级作用域里查找,而不是执行阶段查找。

# 闭包的生命周期

产生: ** 在嵌套内部函数定义完时就产生了 (不是在调用),因为本质上闭包就是一种引用关系。** 当外部函数调用的时候,浏览器就会具体产生一个 Closure

死亡:在嵌套的内部函数成为垃圾对象时,也就是引用关系断裂的时候闭包就会死亡

# 闭包的作用

闭包可以延长外部函数变量对象的生命周期,而且它也让函数外部也可以间接操作到函数内部的变量。虽然闭包可以将外部函数的变量对象保留下来,但是浏览器为了性能后期将外部函数中不被内部函数使用的变量清除了。

function foo(){
    var a = 10;
    var b = 20;
    function fn(){
        console.log(a);  // 只使用变量 a
    }
    return fn;
}
var f = foo();
f(); // 10

不被内部函数引用的变量被清除

# 闭包的缺点

因为闭包的关系,外部函数的变量对象不会在执行完后就马上被销毁。这就导致了内存泄漏的问题,如果我们不去手动释放那么这个变量对象就会一直存在于内存中。当内存泄漏过多就会让页面越来越卡,最后导致内存溢出,程序崩溃。但是要解决这个问题的方式也很简单,那就是让持有引用的变量置为 null,把引用关系断了就可以让 GC 去正常回收内存。

# 应用闭包

js 的模块化就用到了闭包,使用它可以防止全局变量污染。多个模块引入的同时不会产生因为变量重名而覆盖的问题。

此外还有一种经典用法就是让闭包去保存索引号

<!-- 使让点击每个 li 都会输出其数组下标的位置 -->
<body>
    <ul>
        <li>输出0</li>
        <li>输出1</li>
        <li>输出2</li>
        <li>输出3</li>
    </ul>
<script>
    var btns = document.querySelectorAll('li');
    for(var index = 0;index<btns.length;index++){
        // 利用闭包保存外部传入的 index 值
        (function(i){
            btns[i].onclick = function(){
                console.log(i);
            }
        })(index);
    }
</script>

对于上面的例题还有另外一种解法,就是利用 let 关键字绑定块级作用域也可以达到闭包的效果

// 只需将 for 循环里的 var index 改成 let index
var btns = document.querySelectorAll('li');
for(let index = 0;index<btns.length;index++){
    btns[index].onclick = function(){ console.log(index); };
}

Block

Block 会保存下 index 的值,当点击事件触发时就会输出当时保存下 index 值

当外部函数调用多少次,内部就回产生多少个闭包。但当外部函数被连续调用,则它一直持有该变量

# 自测题

// 代码片段一
var name = "The Window";
var object = {
    name: "My Object",
    getNameFunc: function () {
        return function () {
            return this.name;
        };
    }
};
console.log(object.getNameFunc()());  
// 代码片段二
var name2 = "The Window";
var object2 = {
    name2: "My Object",
    getNameFunc: function () {
        var that = this;
        return function () {
            return that.name2;
        };
    }
};
console.log(object2.getNameFunc()());	
// 代码片段三
function fun(n, o) {
    console.log(o)
    return {
        fun: function (m) {
            return fun(m, n)
        }
    }
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
var b = fun(0).fun(1).fun(2).fun(3)