JavaScript闭包和作用域
文章目录
前言
深入了解闭包和作用域链就需先了解函数预编译的过程
一、预编译
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.模块化开发,防止污染全局变量