深入了解闭包和作用域链就需先了解函数预编译的过程
JavaScript:运行三部曲:
语法分析–预编译–解释执行
预编译:
发生在函数执行的前一刻。
函数声明整体提升,变量只声明提升。
1.函数预编译的过程:
1.创建AO对象Activation Object(执行期上下文,其作用就是我们理解的作用域,函数产生的执行空间库)
2.找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
3.将实参值与形参统一
4.找到函数声明,将函数名作为属性名,值为函数体。
例:
function test (a, b){ console.log(a); c = 0; var c; a = 3; b = 2; console.log(b); function b (){}; function d (){}; console.log(b); } test(1); /*答案:1,2,2 答题过程:找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined, AO{ a : 1, b : undefined, c : undefined } 函数声明 function b(){}和 function d(){},AO{ a : 1, b : function b(){}, c : undefined, d : function d(){} } 执行 console.log(a);答案是 1 执行 c = 0;变 AO{ a : 1, b : function b(){}, c : 0, d : function d(){} } var c 不用管,因为 c 已经在 AO 里面了 执行 a = 3;改 AO{ a : 3, b : function b(){}, c : 0, d : function d(){} } 执行 b = 2;改 AO{ a : 3, b : 2, c : 0, d : function d(){} } 执行 console.log(b);答案是 2 function b () {}和 function d(){}已经提过了,不用管 执行 console.log(b);答案是 2*/
2.全局预编译它和函数预编译步骤一样,但它创造的是GO(全局对象):
1.生成了一个 GO 的对象 Global Object(window 就是 GO)
2.找变量声明…
3.找函数声明…
任何全局变量都是 window 上的属性
变量没有声明就赋值了,归 window 所有,就是在 GO 里面预编译。
例 :
function test(){ var a = b =123; console.log(window.b); } test(); 答案 a 是 undefined,b 是 123 先生成 GO{ b : 123 } 再有 AO{ a : undefined }
想执行全局,先生成 GO,在执行 test 的前一刻生成 AO
函数里找变量,因为GO和AO有几层嵌套关系,近的优先,从近的到远的, AO里有就看 AO,AO 没有才看 GO。所以函数局部变量和全局变量同名,函数内只会用局部。
作用域定义:变量(变量作用于又称上下文)和函数生效(能被访问)的区域
全局、局部变量
作用域的访问顺序:函数外面不能用函数里面的。里面的可以访问外面的,外面的不能访问里面的,彼此独立的区间不能相互访问。
1.[[scope]]: 每个 javascript 函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 javascript 引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合。
2.执行期上下文: 当函数在执行的前一刻,会创建一个称为执行期上下文的内部对象(AO)。
一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
3.作用域链:[[scope]]中所存储的执行期上下文对象的集合(GO和AO),这个集合呈链式链接,我们把这种链式链接叫做作用域链。
4.查找变量: 在哪个函数里面查找变量,就从哪个函数作用域链的顶端依次向下查找(先查自己的AO,再查父级的AO,一直到最后的GO)。
函数类对象,我们能访问 test.name
test.[[scope]]隐式属性——作用域
作用域链图解:
function a (){ function b (){ var bb = 234; aa = 0; } var aa = 123; b(); console.log(aa) } var glob = 100; a();
0 是最顶端,1 是次顶端,查找顺序是从最顶端往下查
在全局预编译中函数a定义时,它的[[scope]]属性中有GO对象。
在函数a执行前先函数预编译,创建自己的AO对象,并存储在[[scope]]属性上,与之前存储的GO成链式。同时函数b被创建定义。
在b被创建时,它生成的[[scope]]属性直接存储了父级的[[scope]],它有了父级的AO和GO。
b函数执行前预编译,生成自己的AO,存储在[[scope]]属性中。
详解过程: 注意[[scope]]它是数组,存储的都是引用值。
b 中 a 的 AO 与 a 的 AO,就是同一个 AO,b 只是引用了 a 的 AO,GO 也都是同一个。
function b(){}执行完,干掉的是 b 自己的 AO(销毁执行期上下文)(去掉连接线),下次 function b 被执行时,产生的是新的 b 的 AO。b 执行完只会销毁自己的 AO,不会销毁 a 的 AO。会退回到b被定义时(仍有父级的AO和GO)。
function a(){}执行完,会把 a 自己的 AO 销毁【也会把 function b的[[scope]]也销毁】,只剩 GO(回归到 a 被定义的时候),等下次 function a再次被执行时,会产生一个全新的 AO,里面有一个新的 b 函数。。。。。。周而复始。
思考一个问题:如果 function a 不被执行,下面的 function b 和 function c 都是看不到的(也不会被执行,被折叠)。只有 function a 被执行,才能执行 function a 里面的内容a();不执行,根本看不到 function a (){}里面的内容,但我们想在a函数外面调用b函数怎么办呢,于是闭包出现了。
当内部函数被保存到外部时,将会生成闭包。但凡是内部的函数被保存到外部,一定生成闭包。
闭包的问题:闭包会导致原有作用域链不释放,作用域中的局部变量一直被使用着,导致该作用域释放不掉,造成内存泄露(就是占有过多内存,导致内存越来越少,就像泄露了一样)
例:
function a(){ function b(){ var b=456; console.log(a); console.log(b); } var a=123; return b; } var glob = a(); glob();
答案 123,456。
function a(){ }是在 return b 之后才执行完,才销毁。而return b 把 b(包括 a 的 AO)保存到外部了(放在全局)当 a 执行完砍掉自己的 AO 时(砍掉对AO存储地址的指针),因为b还保存着对a的AO的引用,所以内存清除机制不会清除掉a的AO, b 依然可以访问到 a 的 AO。
1.实现共有变量
function test(){ var num=100; function a(){ num++; } function b(){ num--; } return [a,b]; } var myArr=test(); myArr[0](); myArr[1]();
答案 101 和 100。
思考过程:说明两个用的是一个 AO。
myArr[0]是数组第一位的意思,即 a,myArr0;就是执行函数 a 的意思;
myArr[1]是数组第二位的意思,即 b,myArr1; 就是执行函数 b 的意思。
test doing test[[scope]] 0:testAO
1:GO
a defined a.[[scope]] 0 : testAO
1 : GO
b defined b.[[scope]] 0 : testAO
1 : GO
return[a, b]将 a 和 b 同时被定义的状态被保存出来了
当执行 myArr0;时
a doing a.[[scope]] 0 : aAO
1 : testAO
2 : GO
当执行 myArr1;时
b doing b.[[scope]] 0 : bAO
1 : a 运行后的 testAO
2 : GO
a 运行后的 testAO, 与 a doing 里面的 testAO 一模一样
a 和 b 连线的都是 test 环境,对应的一个闭包
2.可以做缓存(存储结构)
答案 i am eating banana,eat 和 push 操作的是同一个 food
在 function eater(){里面的 food}就相当于一个隐式存储的机构
obj 对象里面是可以有 function 方法的,也可以有属性,方法就是函数的表现形式
3.可以实现封装,属性私有化
只能调用函数方法,不能修改函数的属性。
4.模块化开发,防止污染全局变量