注意,该篇文章需要了解预编译,关于预编译,可以看:JS学习之预编译
一些概念
在讲之前,我们先来了解一下一些必须的概念:
[[scope]]:每个JS函数都有一个[[scope]]属性,可以供JS引擎存取,但是我们无法访问,该属性其实就是我们说的作用域(链),里面存储了执行期上下文的集合,注意是集合
。
执行期上下文、当函数执行时(执行前一刻,就是预编译时),会产生一个叫做执行期上下文的内部对象。该对象就是JS预编译时产生的AO(Actived Object)对象,每一个AO都是一个执行期上下文,函数运行一次会产生一个独立的执行期上下文,所以运行多次就会有多个执行期上下文,函数执行完毕后会销毁执行期上下文。
作用域链:[[scope]]属性中存储的执行期上下文的集合,是链式结构的,所以叫做作用域链。
函数作用域确定规则:函数定义时,该函数的[[scope]]属性,就存当前环境的作用域,函数执行时,会预编译,生成一个AO对象,放在定义时该函数生成的[[scope]]属性的顶端,这就是该函数运行时的作用域。由于每次执行都会生成一个新的AO对象,运行完就直接销毁,所以,每次执行相同函数作用域都是独一无二的,但是,他们定义时保存的作用域是一样的,因为定义时保存的是定义函数那时候所处的作用域。
查找变量规则:从作用域链的顶端一直往下查找。
例子解析
接下来,我们通过例子和图来解析一下函数定义、运行时的作用域。
首先,先上例子代码:
var global = 1;
function a(){
var ab = 'qwe'
var a = 123
function b(){
var a = 111
console.log('b运行时打印a: ' + a)
console.log(ab)
}
console.log('a函数运行时打印a:' + a)
console.log('a函数运行时打印global:' + global)
b()
}
console.log('全局中打印a: ' + a)
console.log('全局中打印global: ' + global)
a()
解析:
首先,当JS开始运行时,会预编译产生一个全局的GO对象,这也叫一个执行期上下文对象,存着我们全局的作用域,当前例子的全局作用域如下图:
接着,我们需要知道,a函数定义时是处于全局中的,所以它的[[scope]]属性在定义时存的是它处于的环境的作用域,它处于全局中,所以它定义时存的就是上面的全局的作用域,a没运行的情况下先不管里面的东西。
a定义时的[[scope]]属性:
接下来,运行到全局中的打印a,global,因为是全局的,所以从全局作用域(即一开始全局预编译的GO对象)中取值,结果如下:
接下来,运行a函数,运行之前需要预编译,会产生一个AO对象,这也叫一个执行期上下文对象,然后把这个产生的AO对象放在a函数的scope属性的顶端,这个AO跟之前的GO呈链式链接,如下图:
注意,关于GO,AO对象的赋值,并不是一开始就是图中的值,这是执行了赋值语句的值,一开始的取值变量为undefined,函数则为函数体,如果不了解的可以去看一下JS预编译
上面就是a函数执行时的作用域,由于是链式结构的,也叫作用域链,这是存储在a函数的[[scope]]属性中的,其中的每一个对象都叫执行期上下文对象,由于有多个执行期上下文对象,所以也叫执行期上下文的集合。
接下来,我们在a函数中打印了a,global变量,由于查找变量是从作用域链顶端的执行期上下文对象开始查找,没有的话就查找下一个执行期上下文对象,所以从a函数执行时产生的作用域链我们可以得出,a为a函数产生得到AO对象中的a,为123,global为全局的global,为1.
在a函数的函数体中,我们定义了b函数,根据函数定义时的作用域为当前的作用域,我们可以知道,b函数被定义时的[[scope]]属性存的如下图:
b函数被定义时的[[scope]] (a函数此次运行时的作用域)
其实,我们这时可以得出,由于是a函数的运行导致了b函数的定义,那么当a函数执行完之后,a函数会释放执行时产生的那个AO对象,回到A函数被定义时的状态,那么b函数是在a函数执行时产生的AO对象中的,那么,a函数执行完了,释放了AO,那么,b函数也就跟着被释放了,再调用是没用的,当然,也可以利用return 语句将b函数给抛出到外部,这就是闭包,这时由于外部有变量接收了b函数,就会保存着这一次a函数运行(b函被数定义)时的执行期上下文,注意,是这一次。
接下来,我们在a函数的运行中又运行了b函数,b函数运行时,会产生一个函数执行时的AO对象,又是一个执行期上下文对象,放到作用域链的顶端,就是b函数被定义时的[[scope]]属性的顶端,那么,如下图
b函数执行时的[[scope]]属性
上面就是b函数执行时[[scope]]属性存的作用域链,也可以叫作用域,这就是一个执行期上下文的集合。
接下来,b函数中打印了a和abc,我们可以从b的作用域链的顶端开始往下查找,a在当前AO对象中有值,取该值,abc在当前AO对象中没有,往上找,可以在a执行的AO对象中找到,为qwe,所以打印结果如下图
整个运行结果:
当b函数运行完时,会释放b函数运行时产生的AO对象,所以就回到了b被定义时的状态,例子中,b运行完了之后,a也运行完了,那么此时a也释放a运行时产生的AO,回到a函数被定义时的状态,这时,b函数也会随着a函数释放AO而被释放。
当然,如果此时再运行一次a函数,它会照着上面的流程再来一次,但是,在第二次运行中所产生a的AO对象,b的AO对象,虽然跟第一次运行时产生的AO对象一样,但他们本质上是不同的对象,只是里面的属性、值一毛一样而已,所以说每次执行函数,它的作用域都不同,这是因为执行完之后就释放最顶上的AO,再执行再生成,他们就是两个对象,肯定不同,就是长的一样而已,但是,虽然最顶上的AO不同,但是被定义时的[[scope]]属性存的是相同作用域,所以在函数执行时改变上一级AO,对下一次执行也会有影响,因为他们只是最顶级的AO不同,其他都是一样的
闭包
接下来,我们来谈谈闭包,那什么是闭包呢?
闭包: 当函数内部的函数被保存到了外部,就会生成闭包,也就是说,闭包可以在函数外部访问函数内部的作用域的变量。
接下来,我们修改一下上面的例子,如下:
var global = 1;
function a(){
var abc = 'qwe'
var a = 123
function b(){
var a = 111
console.log('测试闭包打印a: ' + a)
console.log('测试闭包打印abc: ' + abc)
abc = '测试闭包'
a = '测试闭包'
}
console.log('a函数运行时打印a:' + a)
console.log('a函数运行时打印global:' + global)
return b
}
console.log('全局中打印a: ' + a)
console.log('全局中打印global: ' + global)
var test = a()
test()
test()
分析:
首先,生成全局作用域GO,然后,a被定义时,将全局作用域放到[[scope]]属性中存起来,然后,a函数运行时生成一个a函数此次运行的独一无二的执行期上下文对象(AO对象)放在[[scope]]属性的顶端,接着,在a函数此次运行中又定义了b函数,那么b函数被定义时就保存着a函数此次执行的作用域,也就是存在a函数的[[scope]]属性里的作用域链,但是,注意,这里没有执行b函数,而是直接return了一个b函数,在全局中用test变量接收,那么,此时,b函数就被保存到了外部,返回之后a函数也执行完了,释放a函数执行时生成的AO对象,a函数就回归到被定义时的状态,但是,b函数此时被保存到了全局,b函数的[[scope]]属性则保存着刚才a函数运行时产生的作用域链,这时,我在全局调用test函数,也就是刚才保存出来的b函数,就可以访问到刚才的a函数执行所创建的那一个a函数的AO。
test变量接收b函数后的[[scope]]]属性:
当test函数运行时,生成一个新的AO,放在[[scope]]属性顶端,则此时的[[scope]]属性:
所以,我们第一次在b函数中打印abc,a的值为qwe, 111,
接下来,我们第二次执行test函数,这里要注意一个问题,在test函数中(被保存出来的b函数),我们访问了a,abc变量,第一个a变量,在test函数中就有声明,所以在其运行的AO对象能找到,运行完就会销毁(所以上一次运行中改变了它,但是第二次又新建了一个AO),但是,abc变量则是将b函数保存出来那一次的a函数运行所产生的AO对象中的,所以,第二次运行test函数时,除了顶端的AO对象,下面的作用域链都是之前运行a函数所产生的,那么,它不会被销毁,而我们在上一次运行中改变了它,此时就是
因此,我们此次打印出来的变量就会是
a:111(这一次新生成的AO对象中的),abc:‘测试闭包’(在将b函数保存出来时的那一次a函数执行中生成的a函数的那一次的AO对象)
闭包,其实就是将函数的作用域通过再返回一个函数,给外部调用,因为这个给保存到外部的函数被定义时会保存着上一个函数运行时的作用域,这样,我们就可以在函数外部调用函数内部的变量了。
闭包作用及危害
闭包可以使我们在函数外部访问函数内部的变量,可以帮我们实现私有变量,也可以实现一个缓存,等等等等,许多高级应用都用到闭包,但是,注意,闭包也会导致我们原有的作用域链不释放,(例子中,a函数执行完后该释放的作用域链被b函数带到函数外部了,没释放),所以如果滥用的话,很容易造成内存泄漏。