六、作用域,作用域链,预编译,闭包基础
使用AO,GO说明作用域和作用域链
AO与函数有关,函数能创造出独立的空间,但是这句话不太对,接下来就是解释:
对象
每个对象都有属性和方法:
var obj = {
name: 'white',
address: 'xxx',
teach: function(){}
}
而函数也是一个对象,因为它也有自己的属性,例如.name
能访问函数名称,.length
能获取形参长度。
函数也是一种对象类型,一种引用值类型,一种引用值
但是对象有一些属性我们无法访问的:JS引擎内部固有的隐式属性(私有属性)。
例如:[[scope]]
[[scope]] (作用域)
它是什么:
- 函数创建时,生成的一个JS内部的隐式属性[[scope]],名为作用域,其内部对应存储着scope chain(作用域链)。
- 它是存储作用域链的容器,作用域链中存着AO/GO:
- AO:函数执行期上下文
- GO:全局的执行期上下文
- 函数执行完成以后,AO是要被销毁的;每一次新执行函数前一刻(预编译),都会生成新的AO,即AO是一个即时容器。
- 作用域链让AO,GO按序形成一个链式关系,连接起来。
- 查找变量从作用域链顶端,依次往下找。
注:
- 函数是什么时候被定义(创建):预编译的过程中。
- 解释:全局函数在被全局预编译的时候定义,此时生成了[[scope]],scope chain中存储GO。
- 当全局函数被执行的前一刻(在自己函数预编译的过程中),scope chain 中存储了函数的AO,全局的GO。
- 同理,在全局函数预编译的过程中,内部的函数被定义,生成了自己的[[scope]],scope chain 中存储了外部函数的AO,全局的GO;当内部函数被执行的前一刻(预编译的过程中),在scope chain中存储了自己函数的AO,外部函数的AO,全局的GO。
- 每个函数在被定义时,它的作用域链上一定有GO!
- 预编译过程分为两种:
- 全局预编译过程:GO
- 函数预编译过程:AO
总结:只要函数被定义时就会生成自己的[[scope]],scope chain,作用域链上存在它所处环境的作用域;当函数被执行的前一刻(预编译过程),作用域链上就会存储自己的作用域AO + 之前它所处的作用域。
例如:
function a(){
function b(){
var b = 2;
}
var a = 1;
b();
}
var c = 3;
a();
在全局预编译过程中,a被定义,生成[[scope]]:
这里a函数被执行的前一刻 -> a函数预编译的过程:
外层函数执行时,内层函数被定义:a函数执行时(函数预编译),b函数被定义:
这里有点小问题:b函数在被定义时,就已经生成了[[scope]],但是b函数被定义时是在a函数的环境下,所以b函数这时的作用域链就是a函数被执行期的作用域链。
在b函数被执行前一刻(函数b预编译时),存储函数b的作用域链,顶端第0位存储b的AO,a函数的AO和全局的GO依次往下排列。
b函数被执行结束后,b函数AO被销毁,b函数回归被定义时的状态,作用域链中有a函数的AO,全局的GO。
a函数被执行结束后,a函数AO被销毁,b函数的[[scope]]被销毁,a函数回归被定义时的状态,作用域链中有全局的GO。
外部函数被执行前一刻(预编译),内部函数被定义,例如:
function a(){
function b(){
}
b();
}
a();
a定义:a.[[scope]] ->
0:GO
a执行:a.[[scope]] ->
0:a->AO
1:GO
b定义:b.[[scope]] ->
0:a->AO
1:GO
b执行:b.[[scope]] ->
0:b->AO
1:a->AO
2:GO
b结束:b.[[scope]] ->
0:a->AO
1:GO
a结束:a.[[scope]] -> 0:GO
b.[[scope]] X 函数结束;函数结束作用域链回到函数被定义的状态,内部函数作用域[[scope]]结束。
闭包基础
当内部函数被返回到外部并保存时,一定会产生闭包,闭包会使原来的作用域链不释放。
闭包过于深入会导致内存泄漏,或加载过慢。
写闭包,其实是写函数产生的一种现象:作用域链不释放。
产生闭包时:函数内部中初始化的同名变量不属于暗示全局变量,而是变量的重新赋值。
闭包简单解释:
function test1(){
function test2(){
/*内部函数被返回到函数外部并保存时,一定会产生闭包
* 闭包会使原来的作用域链不释放
* 函数内部中初始化的同名变量不属于暗示全局变量
*/
//a = 5;
var b = 2;
console.log(a);
}
var a = 1;
return test2;
}
var c = 3;
var test3 = test1();
test3();
GO中test3是由test1执行时返回的test2函数赋值。
test1被执行的前一刻(函数预编译)test2被声明,此时test2的[[scope]]生成,scope chain生成。此时test2还未被执行,scope chain内部存储着外层函数的执行上下文,即test1的AO,全局的GO。
做题:
function test(){
var n = 100;
function add(){
n++;
console.log(n);
}
function reduce(){
n--;
console.log(n);
}
// 把两个函数返回出函数外部
return [add, reduce]
}
var arr = test();
arr[0]();
arr[1]();
执行结果为:101,100
解释:
GO ={
arr: undefined-> Array[add,reduce]
test: fucntion()
}
test的AO= {
n:undefined ->100,
add: function,
reduce: function
}
test被执行时,test的scope chain 上有自己的AO,外部的GO。
test被执行时,两个内部函数add,reduce被定义了,未被执行,它们的scope chain上与test的相同,为test的AO,和GO。
test执行完毕,test与自己的AO连接消失,但test的AO被未被销毁,因为add和reduce的scope chain中依然连接着test的AO。
当arr[0]()被执行,即函数add被执行时,它的scope chain上从顶部向下依次为自己的AO,test的AO,和GO。
add在自己的AO中寻找变量n没找到,再去test的AO中寻找变量n找到了,拿来使用。
arr[0],arr[1]执行结束,他们的AO也被销毁,但函数test的AO并没有销毁,还被add和reduce的scope chain 连着。 -->这就导致内存泄漏了
再次使用arr[0]时,add的AO又会被再次生成。而test的AO一直未被销毁…
两种形式的闭包
再说一次闭包的定义:闭包是一种现象。
内部函数被返回到函数外部并保存,一定会产生闭包;而闭包会导致作用域链不被释放。
我自己理解:再加一句:内部的函数共享函数中的变量。
-
对象的形式写闭包:
function sunSched(){ var sunSched = ''; var operation = { setSched: function(thing){ sunSched = thing; }, showSched: function(){ console.log("MY schedule on sunday is " + sunSched); } } return operation; } var sunSched = sunSched(); sunSched.setSched('paly game'); //这里的 sunSched 相当于函数中的 operation sunSched.showSched(); //'MY schedule on sunday is paly game'
-
内部函数的形式写闭包
function sunSched(){ var sunSched = ''; function setSched(thing){ sunSched = thing; } function showSched(){ console.log("MY schedule on sunday is " + sunSched); } return [setSched, showSched] } var sunSched = sunSched(); sunSched[0]('listen to music'); //这里的 sunSched[0] 相当于内部函 setSched sunSched[1]()