前言
学习闭包的时候,我的脑海中经常出现这样一个画面,作用域链、函数、闭包、变量之间组成了像漫威一样的多元宇宙,他们之间的关系千丝万缕,而闭包就是沟通宇宙间的黑洞。
宇宙
-
全局变量:全局变量globalData是最外层的作用域,它就像承载一切的最外层宇宙。全局变量永远存在,就像宇宙永恒一般。
-
作用域:作用域是每一个函数的执行环境,它就像一个个分支宇宙,衍生于最外层宇宙。
-
作用域链:作用域链的前端始终都是当前执行的代码所在环境的变量对象,这条链从自己的作用域开始不断溯源,一直到全局执行环境。作用域链,就像是通往每个宇宙空间通道。
-
活动对象:活动对象随着函数被创建而创建,随着函数被销毁而被销毁,包含arguments和value。活动对象就像是每个次级宇宙中的一切物质(属性和方法),
-
闭包:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。说白了,闭包就是能读取其他函数内部数据的函数。由于JavaScript中只有函数内部的子函数可以读取局部数据,所以js的闭包,其实就是定义在函数内部的函数。闭包本质上是连接函数内部和外部的桥梁。 闭包在这个多元宇宙中,是一个特殊的存在,他是沟通不同级宇宙间活动对象的虫洞。
-
因为闭包而产生的情况:
当二级宇宙被销毁时,二级宇宙销毁了它的作用域链(也就是说,随着二级宇宙被销毁,去往二级宇宙的路被销毁了,我们再也不能通过这条路去拿二级宇宙的东西了)。
但是由于三级宇宙仍然通过闭包虫洞在使用二级宇宙的活动对象,所以二级宇宙的活动对象并没有被销毁,还是占着内存空间。
我们还是可以通过闭包借道三级宇宙去访问二级宇宙的属性和方法。而只有当三级宇宙也被销毁,真正的二级宇宙的活动对象才会被销毁。
这似乎印证了一句格言:
一个人一生要经历两次死亡,一次是身体死亡(作用域链销毁),一次是世间最后一个记得你的人死去(所有拥有你的作用域的函数都被销毁)。
正文
作用域链的创建
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后初始化函数的活动对象。
作用域链中函数本身的活动对象始终处于第一位,外部函数的活动对象处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
function compare(value1, value2){
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
var result = compare(5, 10);
全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。
在创建函数时,会创建一个包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。
当调用函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象被创建并被推入执行环境作用域链的前端。作用域链本质上是一个指向变量对象的指针列表。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。
(控制台打印的window对象中的某个参数的作用域,全局作用域global在作用域链数组的最后一项)
存在闭包时作用域链的情况
因为在一个函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中。 所以,当外层函数在执行完毕后,如果内层函数的作用域链仍然在引用这个活动对象,那么外层函数的活动对象也不会被销毁。换句话说,当外层函数return后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到内层函数也被销毁后,外层的活动对象才会被销毁。
// 没用使用闭包的时候,变量a存在于内层函数的活动对象,即使先调用了b(),也无法在外层作用域找到a
function b(){
var a = 'aaa'
}
b();
console.log(a)// 'a is not defined'
// 使用闭包后,虽然a是局部变量,但是b()通过return一个子函数,并且在外部定义新函数进行接收,最终打印出了变量a
function b(){
var a = 'aaa'
return function(){
console.log(a)
}
}
b()()// 'aaa'
闭包的作用
- 可以读取函数内部的变量
- 可以让某些变量长期保存在内存中实现共享
闭包的缺点
- 闭包会携带包含它的函数的作用域,所以闭包比其他函数更占内存;
- 不合理地使用闭包会导致已经完成的函数的活动对象仍然没有被回收;
- 闭包允许了在父函数外部改变函数内部的值,这增加了父函数中值被错误改变的风险。
使用中出现的问题
作用域链的配置机制引出一个副作用,即闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
上面的代码是一个常见的错误,我们希望索引0时返回0,索引1时返回1。但是实际上全部结果都是返回10。原因就是二级活动对象里面并没有i这个变量,我们每次调用,都直接取了createFunctions活动对象中i变量的最后一次赋值。
解决上面代码出现的问题,我们通常会在每次for循环时都自执行一次function,这实际上就是将参数往下传递,在每个二级活动对象中都定义一个属于自己的变量i
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
}(i);
}
return result;
}
关于this
在全局函数中,this指向window对象,而当函数被作为某个对象的方法调用时,this指向当前对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。 (当然,在通过 call()或 apply()改变函数执行环境的情况下,this 就会指向其他对象)