一直没有看到过介绍得很详细的,大多其他文章都没涉及到活动对象如何形成的,它代表着什么,作用域链又是如何形成的,其结构是什么样子的,
所以这里自己写了一个。尽可能详尽,清晰,好理解。希望能帮到你。
一、先搞清概念[看官莫急,看完就有详细解析] 虽然刚开始会很难理解,但看完定会让你豁然开朗,顿时整个灵魂都感觉到升华了一般。 毕竟很多工作了5、6年的前端开发工程师都不一定会有时间精力潜心研究透这个概念的深层次原理。
先要了解js里变量和函数声明时所在的作用域,这是基础内容,不再啰嗦,请查查其他文档,这里举个例子,你可以照着例子来理解(有时简单理解一个概念也是为了更好的去理解其他依赖于它的更高级的概念),
<script> var a=1; b=2; //a 和 b 的作用域贯穿整个script,其实也是因为挂在了window上,用控制台打印window.a 就知道了。 function func1(){ var a1 = 1; console.log(a); //因为a贯穿了整个,而func1也包含在整个里(window.func1也有值),所以这里也能访问到a function innerFunc1(){ console.log(a1) //同理 a1的作用域贯穿了 func1,所以在这里面依然能够访问到 } } </script>
总之你可以把作用域看成一个变量在一个函数或者全局里的生命周期 或 覆盖的、影响的范围。
注意了,下面开始介绍闭包原理了:
javascript函数作用域分两个阶段
一个叫创建时阶段
一个叫运行时阶段
定义1: 【看完例子以后不断对比着定义理解才能明白定义的意思】
所谓“创建时阶段”就是一个函数被以某种语法定义出来的时候,
当此函数被创建在一个运行时阶段的作用域中时,此函数才为闭包状态,才能有自己的“创建时阶段”的作用域。[此话意思是函数的创建时阶段是在其所在的运行时阶段的作用域中的,简单说就是在上一级函数运行时才有下一级函数的创建时阶段; 并且一旦处于创建时阶段,这函数就处于闭包状态了]
定义2:
所谓“运行时阶段”就是一个函数被()操作符标识为要运行的时候,
(提示所有被()操作符标识为运行的函数都要在一个“运行时阶段”的作用域中才能被运行,如果不在则无法被运行)
此时,函数本身会创建一个内部对象,叫“运行期上下文”对象,
“运行期上下文对象”有自己的[[scope]]属性,此属性将copy此函数“创建时阶段”的[[scope]]属性的值,也就是“创建时阶段的作用域链”做为“运行时阶段的作用域链”的最初形态,
有了这个最初形态以后,运行期上下文将组织聚集函数内部的所有标识符等等属性为一个对象,这个对象叫做"可变对象"(有的文章书籍里叫活动对象),然后将这个可变对象的引用压入自己作用域链(运行时作用域)的第一个位置。
[看到这里最好下载附件中的图解]
二、对定义的通俗阐述
<script type="text/javascript"> function a(){ var i=0 function b(){ console.log(i++) } return b; } var c=a(); c(); c(); </script>
function a(){} 这是a函数的创建时阶段。
a();调用中(即a函数内部)是a函数的运行时阶段。
若a()是在全局位置被调用的(本例就是),那称为: 全局对象(window)创建了a()函数的运行时阶段。
伪代码【js执行时可以看成这样】:
function a(){ /* 还没调用a()时,浏览器js引擎先扫描一遍文档,扫到function a()...就创建一个a函数的声明(或表达为声明一个函数a),a此时就处于创建时阶段。此时a被创建在全局作用域的运行时阶段【全局作用域在script一开始就处于运行阶段了】,因此a()函数处于闭包(此时是全局作用域中的闭包) ,此时a函数有自己的 “创建时阶段”的作用域(设为 var creatTimeScope=[[globalRef]](用这个表示此时是对全局对象(全局活动对象)的引用。用数组表示这是个类似堆栈,等下可以在堆栈中压东西到栈顶(相当于插入到数组头部)[被压入的引用, [globalRef]]。globalRef表示全局作用域的引用,也是个堆栈。 以下涉及到数组的伪代码都要看成堆栈 执行a()后,从此时开始,a函数就处于运行时阶段,运行时阶段对于a函数会做以下事情: 第1步:创建“运行期上下文”对象,暂时称之为 runTimeContextObj:相当于: var runTimeContextObj={[[scope]]:null, creatActiveObj:function(){ //注意,这是伪代码。这个结构后面会用来表达运行阶段所做的事情。 var activeObj = 组织聚集函数内部的所有标识符 return activeObj }} 第2步:“创建时阶段的作用域链”做为“运行时阶段的作用域链”的最初形态(理解为初始化): 就是说runTimeContextObj.[[scope]]=creatTimeScope (既也 =[[globalRef]],回忆上面的等式 creatTimeScope=[[globalRef]]). 第3步:创建活动对象: activeObj=runTimeContextObj.creatActiveObj();组织聚集函数内部的所有标识符的意思就是把var变量声明 或者 function函数声明收集起来挂在活动对象上作为其属性来用的,你可以认为 activeObj有了i属性既 activeObj.i 第4步:var activeObjRef=取activeObj地址 第5步:把活动对象压入运行时上下文对象的[[scope]]栈顶: runTimeContextObj.[[scope]].push(activeObjRef) 。压入的是个活动对象的引用,那结果将是: runTimeContextObj.[[scope]][activeObjRef,[globalRef]] 即a的运行时对象中拥有本运行时的活动对象的引用和全局活动对象的引用, 注意[activeObjRef,[globalRef]]要看成个堆栈 function b(){ //同a()被全局对象调用的原理,b在a()运行时阶段(a的运行时作用域)里被创建,于是 var creatTimeScope=[[a的runTimeContextObj的引用]] 【关键,类比a获取全局对象的引用,是同样原理的, 都是在上层函数运行时作用域里创建当前函数,使当前函数处于创建时阶段】 同a的原理,当b被执行时: b的上下文对象就成了runTimeContextObj.[[scope]][activeObjRef,[a的runTimeObj的引用]] 而a的runTimeContextObj的引用即a上面的: [activeObjRef,[globalRef]],就是说最终是 b的runTimeContextObj.[[scope]][activeObjRef,[a的activeObjRef,[globalRef]]],把从b当前的活动对象的引用到全局的活动对象的引用都串起来了, b中找变量的时候找不到就会沿着这个顺序(activeObjRef,a的activeObjRef,globalRef)往上找,比如找那个i,现在自己的活动对象中找activeObj.i,找不到(因为没有声明),然后就找a的activeObj.i,找到了。 } */ return b; } var funcb= a(); //全局对象处调用a() funcb();
一般情况下,函数中声明的变量在运行完(运行时结束之后,可以理解为调用结束之后)后,变量就会被垃圾回收而销毁,
如
function a(){ var i=2 } a(); console.log(i); //i在调用a()结束之后就销毁了
但是上面的例子中function b处于闭包时,i却没有被垃圾回收了呢?我们知道js中对象只要在内存中还有引用就不会被销毁,顺着这个思路,我们考虑上面伪代码:
var funcb= a() 已经把b的创建时作用域creatTimeScope赋值给了 全局变量funcb,此时creatTimeScope不会被垃圾回收; 等到b被运行时 funcb(),照上面的分析creatTimeScope给了其运行时上下文对象 (b的runTimeContextObj),也就是等价于 funcb指向了b的runTimeContextObj了,这个runTimeContextObj一直存在有funcb引用,当然也就不会销毁,里面的活动对象也就存在,所以顺着 runTimeContextObj.[[scope]][activeObjRef,[a的activeObjRef,[globalRef]]]能访问到a的activeObjRef中的i。
所以闭包的本质是上下文对象的引用没有被垃圾回收。
顺带说一句,上面说的顺着上下文对象访问活动对象的属性或方法的过程就是常说的作用域链。
三、常用场景
<ul id="ul1">
<li id="li1">
1
</li>
<li id="li2">
2
</li>
<li id="li3">
3
</li>
<li id="li4">
4
</li>
</ul>
<script>
var ul1=document.getElementById('ul1'), ul1Lis=ul1.getElementsByTagName('li')
// for(var i=0,len=ul1Lis.length;i<len;i++){
// ul1Lis[i].addEventListener('click', function(){
// alert(i) //都是4
// }, false);
// }
//改为闭包:
// for(var i=0,len=ul1Lis.length;i<len;i++){
// ul1Lis[i].addEventListener('click', (function(){
// var j=i //i是从这个外层函数执行时从for来的,且每次遍历都和下面的 return fun...共同放到一个闭包中,这个全局匿名函数的上一级是全局对象。用谷歌可以看作用域变量(Scope Variables)
// return function(){
// alert(j) //是每次循环变量中的i,这i已经被放在外层的那个匿名函数的闭包中了. 用两重闭包是因为addEventListener的第二个参数需要一个函数作为监听器
// }
// })(), false);
// }
function closureA(){
for(var i=0,len=ul1Lis.length;i<len;i++){
ul1Lis[i].addEventListener('click', (function(){
var j=i //对比上面那段,多了一级闭包的活动对象。
return function(){
alert(j)
}
})(), false);
}
}
closureA()
</script>
//特殊问题-------------------:
//函数内部没有var的语句成为了全局变量-------------------: function a(){ var1=1 function b(){ console.log(window.var1) //1 window.var1=2 //为了看下句引用的 var1 是从全局活动对象来的,还是从a的活动对象来的。 console.log(var1) //2 这样看来这是从全局活动对象来的 } return b; } a()() /** * a中的 var1没有var,这将会在a运行时的组织活动对象过程时不会把var1设置到a活动对象上(因为没有var,可见组织标识符的过程是看有没有var【function应该也类似】) * 当函数执行到此句时,js开始进入寻找变量的过程,沿着作用域链往上找,一直找到顶都没找到,那么就会在顶部创建一个这样名称的变量。相当于在开始时全局声明的var,只不过这时是声明和运行同时进行了。 * */ //with虽然插入了外部作用域的活动对象,但却不像函数那样把里面的var组织成活动对象-------------------: function a_with(){ var var2={ p1:1, p2:2 } function b(){ var var3=3 with(var2){ //当前函数的作用域链的前端又插入了一个对象:var2 var var4=4 //在b函数运行阶段,分析函数内的代码,组织变量时也会把这个放到b的活动对象中。而不是在with的作用域链中,难道with不创建活动对象,仅仅是把一个对象插入作用域链前面? console.log(var2.p1) console.log(var2.p2) console.log(var3) //访问自己函数内的变量也是跨了一个作用域哦。 console.log(var4) } console.log(var4) //4 } return b; } a_with()()