用debug向你展示闭包

什么是闭包

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

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。

闭包内存示意图

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

为什么会形成闭包

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

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

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

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

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

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

调用foo依然产生闭包

闭包的生命周期

产生: 在嵌套内部函数定义完时就产生了(不是在调用),因为本质上闭包就是一种引用关系。当外部函数调用的时候,浏览器就会具体产生一个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) // undefined
a.fun(1)  // undefined
a.fun(2) // undefined
a.fun(3) // undefined
var b = fun(0).fun(1).fun(2).fun(3)  
复制代码

おすすめ

転載: juejin.im/post/7032564461240074276