闭包与作用域,闭包的错误使用

时间:2021-01-18 22:44:46

初级菜鸟,若有错误,欢迎指出。

  • 闭包的简单例子
  • 作用域链
  • 闭包的错误使用

1、闭包的简单例子

先来看一个简单的例子:

function Fun(){
var num = 1;
return function(){
console.log(num);
};
}
Fun();
:
可以看到,上面这个函数中返回了一个匿名函数,且匿名函数中输出了外层函数的局部变量,这就形成了简单的闭包。那么,为什么说这就是闭包呢?

闭包的定义是:函数对象本身和这个函数关联的作用域链的结合。也许这很抽象无法理解,其实可以理解为“闭包 = Fun + Fun的作用域链”。我们知道,函数都有自己的作用域链,难道所有的函数都是闭包吗?在网上查阅相关资料时,看到这样一句话:javascript中所有的function都是一个闭包。如果闭包的定义确实如上面阐述的那样,我觉得这句话是对的。

2、作用域链
既然闭包和作用域息息相关,那就阐述一下我的理解。

function func(){
var mem = 1;

this.sayMem = function(){
console.log(++mem); //闭包
};
}
var ins = new func();
ins.sayMem();//2
ins.sayMem();//3
var ins2 = new func();
ins2.sayMem();//2
ins2.sayMem();//3
:
new关键字的步骤:①var obj={},②obj.__proto=func.prototype,③func.apply(obj),④return obj;

因此ins会拥有sayMem成员。但问题是new操作结束后理应释放func的局部变量mem,但sayMem函数不知什么时候会被调用,当sayMem函数被调用了,才会进入到sayMem函数的编译期环境,这才会发现形成了闭包,可是func函数的调用已经结束了!按理说mem变量应该都释放了。那么就产生了疑问,系统是怎么发现闭包的?是什么时候发现闭包的?造成闭包的变量是怎么保存的?

此时就是作用域需要解释的了。首先需要知道一个词语叫做“变量对象”,或者“活动对象”,每一个函数都会存在着一个变量对象,而函数的作用域链上可不止一个变量对象,而是往往有很多变量对象组成。变量对象就是一个函数的执行环境中的所有变量和函数,且变量对象的第一个成员就是函数的arguments成员,然后就是函数中定义的局部成员了。而作用域链的第一个变量对象就是执行流正执行的函数的变量对象,然后作用域链的第二个变量对象是包含第一个变量对象的函数的变量对象,以此类推,作用域链的最后一个变量对象一定是window下的变量对象(window的变量对象没有arguments成员)。

作用域链的创建时期:编译期先初步创建所有的函数(不包含函数内部定义的函数)的作用域链,当调用某个函数后,再生成作用域链中的第一个变量对象。也就是说,完整的函数作用域链是需要在编译期和执行期一起完成的,这样的好处是提高效率,没必要每一次调用函数都要完整的创建一次作用域链。
以上述例子为例:当执行var ins = new func();代码时:当执行流进入到func函数时,func函数的作用域链被确定:func的变量对象 ->window的变量对象。当执行ins.sayMem();代码时,执行流执行到sayMem函数时,sayMem函数的作用域链也确定下来了:sayMem的变量对象->func的变量对象->window的变量对象。但是随之也会产生一个问题,当调用完func函数的时候,sayMem函数的作用域链中已经保存func函数的变量对象了,那为何mem变量还不释放?那是因为sayMem函数的作用域链中保存的mem值其实是一个指针,指向了保存着mem值的空间,如果释放了,那sayMem函数的作用域链中保存的mem值不存在了。
那为什么ins中mem成员值改变后,ins2的mem成员不会受到影响。是因为new关键字的作用,ins与ins2在不同的空间。那么,造成闭包的变量是怎么保存的?网上的原话是:一个变量进入闭包后,会被复制到堆中,所有对该变量的使用都解析为引用这个堆中的变量。所以,使用这个变量的所有函数都会读取到最新的值,而不是你创建该函数时的值。
3、闭包的错误使用
闭包和计时器一起使用时,很容易发生问题,代码如下:

function addObj(thisObj, i){
thisObj['obj_'+i] = i;
}
function Fun(){
var thisObj = this;
for(var i = 5; i < 10; i++){
setInterval(function(){addObj(thisObj, i);}, 1000);
}
}

var ins = new Fun();
console.log(ins);
:
理想中输出结果应该是ins添加了5个成员,分别是ins.obj_0 = 0, ins.obj_1 = 1, ins.obj_2 = 2, ins.obj_3 = 3, ins.obj_4 = 4。然而结果却是只有一个成员,就是ins.obj_5 = 5。
造成这个错误的原因有两个:①对计时器错误的认识。②闭包的错误使用。
①在调用setInterval(fun, time)函数时,很多人会认为这表示的意思是在time时间过去后立马执行fun函数,然而setInterval函数同事件一样是异步的,必须先添加到队列中,等到同步代码执行完毕后才会依次执行队列中的函数,且队列中不仅仅只是setInterval函数在排队,因此time只是表示在time时间过去后把fun函数加入到队列准备执行。这个就不详细讲了,重点讲第二个。
② 闭包的错误使用在于每次给addObj函数传递参数i时,由于异步代码必须等待同步代码执行完后才会执行,所以for循环结束后才开始执行addObj函数,而此时i值已经变成5了,所以无论调用多少次addObj函数,都是对同一个成员obj_5的重复赋值而已。下面的代码是闭包的正确使用方法:

function addObj(thisObj, i){
thisObj['obj_'+i] = i;
}
function Fun(){
var thisObj = this;
for(var i = 5; i < 10; i++){
(function(num){
setInterval(function(){addObj(thisObj, num);}, 1000);
})(i);
}
}

var ins = new Fun();
console.log(ins);
:
这样就会输出我们预想的添加的五个成员了。这是因为此时的闭包不是对i的闭包了,而是对num的闭包,num是参数,也是匿名函数的局部变量,它指向了i所指向的空间。当i++时,i的空间没变,但是匿名函数的局部变量mem已经不是以前的mem了。因为匿名函数作用域的第一个变量对象在每一次调用都会重新申请空间,因此那5次对匿名函数的调用产生的5个mem变量只是同名不同空间,且各自保存了i的值。因此会得到预想中的输出结果。