本文已参与「新人创作礼」活动,一起开启掘金创作之路。
作用域想必大家都不陌生,那么作用域链是什么呢?作用域链,顾名思义,就是各个作用域之间形成的链状关系。接下来我们一起来深入理解一下。
首先,我们先来了解一下执行期上下文。
执行期上下文
在函数被执行的前一刻会进行预编译,此时函数会创建一个称为执行期上下文的内部对象(也就是AO对象)。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。
举个栗子:
function b(a) {
console.log(a)
}
b(1) //此时b会创建一个AO对象,但在函数执行完毕之后,该AO将被销毁
b(2) //一个函数可被调用多次当函数被重新调用时,又会重新创建一个新的AO对象,并在执行完毕之后被销毁
[[scope]]
[[scope]] 是函数的作用域,是不可访问的(指的是用户无法访问,仅供引擎访问),其中存储了运行期上下文的集合。
作用域链
接下来我们在作用域链的角度下来看一个函数
function a() {
function b() {
var b = 20
}
var a = 10
b()
console.log(a)
}
var glob = 100
a()
在v8引擎获取到这段代码之后,首先会对全局的代码进行预编译和执行,再是函数的调用,在函数调用的前一刻会进行函数的预编译,函数调用之后再是函数内部代码的执行。以上面代码为例:
- 函数 a 的定义会产生 a 的作用域(函数只要被创建出来,一定会有一个作用域属性),全局代码执行后会在a的作用域中创建出一个 GO 对象(全局作用域对象)
- 函数 a 的执行会在 a 的作用域中创建 a 的 AO ,此时 a 的 AO 排在 GO 的前面(后创建出来的会放在上面,和栈相似),此时在 a 的作用域里面既能访问 a 的 AO ,也能访问 GO。
- 函数 b 是在函数 a 中被定义的,可以理解为函数 b 出生在函数 a 中。函数 b 的执行会在 b 的作用域中创建 b 的 AO ,此时 b 的 AO 排在最前面,其次是 a 的 AO , GO排在最后面。同理,在 a 的作用域中可以访问 a 的 AO , b 的 AO ,以及 GO 。
或许有些同学对以上的描述已经看花了眼了,那么就让我们根据图片再来分析一下吧!
如图,当函数 a 被执行时,作用域 scope 中包含两个对象,分别是预编译创建的 Activation Object{}(AO对象)和Global Object{}(GO对象),并且在0位置放置的是函数 a 的 AO 对象,在位置1放置的是GO对象,AO对象位于GO对象之前。
当函数 b 被执行时,其作用域 scope 中包含三个对象,分别是预编译自身创建的 Activation Object{}(AO对象),函数 a 创建的 Activation Object{}(AO对象)以及Global Object{}(GO对象),在0位置放置的是函数 b 的 AO 对象,在位置1放置的是函数 a 的 AO 对象,在位置2放置的是GO对象,函数 b 的AO对象位于最前端。
查找变量
根据以上内容,我们可以知道查找变量的顺序是从作用域链的顶端依次往下查找。先从自身作用域开始查找,若不存在,则一层层往外继续查找,直到查找到该变量。
总结
作用域链指的是 [[scope]] 中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链。且作用域链只有一根,所有先后创建出来的东西都要存储在这根链上,后被创建的处于先被创建的之前。
看完这篇文章,或许你对以下问题已经有了答案——
*为什么外层函数不可以访问内层函数?*
因为函数的 AO 对象会在执行完毕之后销毁。内层的函数调用完毕之后,其AO对象被回收,在作用域链上面,内层函数的AO对象可能已经被销毁,所以但是外层函数未必执行完毕,如果此时外层函数想要访问内层函数,极大可能会产生bug,故认定外层函数无法访问内层函数。
以上是我对于作用域链的相关理解,希望看到这篇文章的你能够有所收获!