JavaScript闭包学习笔记

时间:2022-12-14 22:48:12

JavaScript闭包学习笔记

闭包是指有权访问另一个函数作用域中局部变量的函数。

闭包的定义简单明了,我们来仔细分析一下这句话。JavaScript中由于只有在进入一个环境(全局执行环境和函数环境)时,才会创建变量对象(不了解变量对象是什么的可以去看这篇文章),而只有在创建变量对象时才会改变当前执行环境的作用域,而在这个函数作用域执行完之后,这个函数作用域就会被销毁。那么想要在函数外部访问到函数作用域内的变量,应该怎么做呢。

原理

这就引出了JavaScript中闭包产生的原因:JavaScript中的函数运行在它们被定义的作用域内,而不是它们被执行的作用域内。这样就可以通过在fn1函数内部定义fn2函数,fn2就可以访问到fn1内部的局部变量。那么又需要在fn1内定义fn2,有需要在fn1外部访问到fn2,我们一般把fn2作为fn1的返回值返回或者把fn2设为全局变量来实现。

简单的代码就是这样的:

function fn1() {
    var count = 0;
    function fn2() {
        return count++;
    }
    return fn2;
}
var fn = fn1();
console.log(fn());//0
console.log(fn());//1
console.log(fn());//2

来简单分析一下执行的过程。

首先执行fn1,并且把返回结果赋值给fn,fn就是fn2了。

重点来了, 执行fn,会返回count,然后解释器就会去顺着作用域链去查找count,由于fn2是在fn1内定义的,所以fn2的作用域链中有fn1的作用域,所以会访问到fn1中的count,把count返回,并且操作count加一。

如果把作用域链搞懂了,理解起闭包来,还是很简单的。

作用

说了这么多,这个东西有什么用呢?

闭包最大的特点就是可以访问到函数内部的变量,所以我们可以利用这个特性完成对于一些对象封装。实现只能通过指定方法访问“私有”变量的效果。

function Person() {
    var name = null;
    return {
        setName: function(nameSet) {
            name = nameSet;
        },
        getName: function() {
            return name;
        }
    }
}
var zachery = new Person();
zachery.setName("zachery");
console.log(zachery.name);//undefined
console.log(zachery.getName());//zachery

通过这样的方式,我们就可以限制只通过get和set方法对于私有变量进行获取和赋值操作。

副作用

由于闭包的存在,也引起了一些副作用,比如说:

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

这里代码的原意是从小到大每隔一秒输出 i, 期待的结果是0,1,2,3,4,5,6,7,8,9

然而输出的结果却是10,10,10,10,10,10,10,10,10,10

造成这个结果的原因有两方面:

  1. JavaScript中事件队列会在主线程空闲时执行
  2. JavaScript的函数作用域

由于JavaScript的事件队列在主线程空闲时才会执行,所以当settimeout中的function执行时,外部的循环已经执行完了。但是由于settimeout中的匿名函数可以访问外部的作用域,所以外部的作用域并没有被销毁掉。当需要输出i时,就会去顺着作用域链找i,而匿名函数内部是没有i的,所以最终会在外部的作用域中找到i,而外部由于循环已经执行结束,这时的i已经变成了10,所以最终的输出就是10个10了。

既然有问题,那么肯定是有解决办法的:

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

解决方法也是利用闭包。既然错误的原因是通过作用域链找到了最外部,那么就在中间再创建一个闭包,让查找作用域链时到这里就找到i。

从头到尾分析一下。首先是主线程的执行,执行到循环内部,遇到一个立即执行的匿名函数,并且需要传入一个i(在每个循环都传入了自己当时的i,这就是最终输出的i是不同的原因),那么在settimeout中的函数能够访问的外部函数作用域不再是全局的作用域了,而是循环中的匿名函数的作用域。所以在1秒后触发事件时,会在匿名函数中查找i,所以最终的输出就是我们的期望值。

小结

闭包的概念建立在作用域的基础之上,而闭包产生的原因就是JavaScript中的函数运行在它们被定义的作用域里而不是它们被执行的作用域里。这些还只是闭包的一些原理,应用什么的等以后有了更多的代码量再来总结。由于还处在JavaScript初级阶段,对于闭包的应用以及理解可能还不够透彻,如果文章有问题,欢迎指出。