在JavaScript中作用域链的机制会引发一些副作用:闭包只能够获取包含函数中任何变量的最后一个值。在使用闭包的时候,我们一定要注意变量值的问题,因为这是经常会出错的地方。
下面我们以一个非常极端的例子来说明这个问题,在实际开发中我们一般不会这样编写代码。这个例子的代码如下:
function fn1(){ var arr = new Array(); //变量i保存在fn1作用域中 for(var i = 0; i < 10;i++){ arr[i] = function(){ return i; } } return arr; } var values = fn1(); for(var i = 0; i < values.length;i++){ //此时通过闭包来调用所有的函数,当输出i的时候会到上一级的作用域中查找,此时i的值是10,所以输出的都是10 document.write(values[i]()+"<br>"); }
执行上面的代码,我们预期会在页面中打印出0-9,但是实际上会打印10个10。我们来分析一下这段代码:实现代码中创建了一个函数fn1,在函数中创建了一个数组对象,并通过一个for循环为数组赋值,循环了10次,每一次往数组中填充一个匿名函数返回值,最后返回数组对象。接着获取fn1函数的引用,然后通过循环在页面中输出数组中的值。
上面的程序的作用域链内存模型如下图所示:
从图中我们可以看到,每次在fn1函数中的循环都会产生一个匿名函数,它们有各自的作用域链,它们的作用域链的高位都指向全局作用域,中间位指向外层的fn1作用域,低位才是指向自己的作用域。
当函数fn1执行完毕之后,fn1作用域中的属性i
的值为10,此时GC开始回收fn1,但是它发现有匿名函数指向fn1的作用域,所以fn1的作用域不会被回收。
在匿名函数执行的时候,它在自己的空间中查找属性i
,但是没有找到,于是就到它上级的fn1作用域中去查找,此时,fn1作用域中的i值为10,所以所以匿名函数都会获得相同的i值:10。
解决这个问题的方法是在匿名函数中再返回一个匿名函数,并通过一个变量来保存当前的数值。代码如下:
function fn1(){ var arr = new Array(); for(var i = 0; i < 10;i++){ arr[i] = function(num){ return function(){ return num; } }(i); } return arr; } var values = fn1(); for(var i = 0; i < values.length;i++){ //每一个fs都是在不同的作用域链中,num也是保存在不同的作用域中,所以输出0-9 document.write(values[i]()+"<br>"); }
此时,num
的值保存在每一个匿名函数自己的作用域中,并且数值刚好等于每次循环的索引值。这样,在每次调用匿名函数的时候,它会在自己的空间中找到num
属性,并且这些num
的值都是不同的,同时也不会再到fn1函数作用域中去查找i
属性。
上面的代码会产生20个匿名函数的作用域,如果代码中不是简单的返回值,而是一些更复杂的操作,将会占用大量的内存空间。
闭包中的this对象
在闭包中使用this
对象也会出现一些意想不到的问题。this
对象是在运行时基于函数的执行环境绑定的:对于全局函数,this
对象就是window
,而当函数在作为某个对象的方法被调用时,this
就是那个对象。在匿名函数中,this
对象通常是指向window
的。我们来看下面的例子:
var name = "window"; var person = { name : "Leon", age:22, say:function(){ return function(){ return this.name; } } } console.info(person.say()()); //控制台输出:window
上面的代码中,我们调用person
对象的say()
方法时,打印出来的不是person
对象的名字,而是全局的名字“window”。当完成person.say()
之后,该函数调用完毕,在该函数调用结束之前,this
是在指向person
的,但是在调用匿名函数的时候,this
就指向window
了,所以得到的结果是“window”。
解决这个问题的方法是在say()
方法中将this
引用赋值给一个临时变量。代码如下:
var name = "window"; var person = { name : "Leon", age:22, say:function(){ var that = this; return function(){ return that.name; } } } console.info(person.say()()); //控制台输出:Leon