深入js——作用域链
作用域
作用域是可访问对象的集合,确定当前执行代码对变量的访问权限。
作用域可分为静态作用域和动态作用域,JavaScript采用静态作用域,也叫词法作用域。
静态作用域
函数的作用域在函数定义的时候就决定了,与函数如何被调用,在何处被调用无关。
var a = 1;
function foo() {
console.log(a); // 1
}
function bar () {
var a = 2;
foo()
}
bar()
以上代码,foo函数为打印出1。虽然foo是在bar函数中执行,但它的作用域是在它定义时就确定了。首先查找foo内部有没有a,没有就从外层找a,于是最终打印出了1。
[[scope]]
在将作用域链之前,先讲讲[[scope]]。用console.dir随便打印一个函数
function bar () {
var a = 1;
function foo () {
console.log(a)
}
console.dir(foo)
}
console.dir(bar)
bar()
可以看到有一个[[scope]]属性。从这个例子可以看出:
- [[scope]]是所有父变量对象的层级链
- [[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。
Q:有一点需要注意下,以上例子中如果foo函数中不使用bar中的变量,也就是如果不加
console.log(a)
,则foo的[[scope]]中不会有bar这一级。
A:这一点也很好理解,js在编译时就发现foo不会引用到bar中的变量,所以不将bar的作用域加入foo的父级层级中,因为没必要;但foo的可访问层级还是包括bar的,只要有使用到bar中的变量,bar就会加入foo的[[scope]]中。这一点从某种程度上说,也正好进一步说明了[[scope]]是在函数定义时(js编译时)就确定好了。
结合上一节所说的:js采用静态作用域,函数的作用域在定义时就确定了;[[scope]]也是在函数定义时就确定了,是函数的一个属性而不是上下文,与如何调用无关。
作用域链
定义
作用域链在进入上下文时被创建,定义为:当前上下文+[[scope]],即
Scope = AO|VO + [[Scope]]
也就是,将当前执行上下文的变量对象置于作用域数组前端;当查找变量时,首先查找当前AO|VO中是否存在,然后沿着函数定义时的层级([[scope]])查找,直到最后的Global。
实践
文章深入js——变量对象中提到了利用控制台的scope查看变量对象,这下我们就可以真正的好好看看scope了。
function bar () {
var a = 1;
function foo () {
debugger
var b = 2;
console.log(a)
}
foo()
}
bar()
可以看到,scope的最上层是Local,也就是当前执行上下文的变量对象,下面就是函数的[[scope]]属性里保存的父级层级链。点击Call Stack中的函数,还可以切换当前执行上下文,观察下面scope的变化。
整体流程
讲了变量对象和作用域,最后我们整体梳理下函数执行时发生了什么,整体流程是怎样的。
var a = 1;
function bar () {
var a = 2;
console.log(a)
}
bar()
- bar函数被创建,同时将父级作用域保存到其属性[[scope]]中
bar.[[scope]] = [
globalContext.VO
]
- 进入bar函数,创建bar执行上下文,bar执行上下文被push到执行上下文的栈顶
Stack = [
barContext,
globalContext
]
- 赋值bar函数的[[scope]]属性并创建作用域链
barContext = {
Scope: bar.[[scope]],
}
- 用 arguments 初始化活动对象,并加入形参、函数声明、变量声明
barContext = {
AO: {
arguments: {
length: 0
},
a: undefined
},
Scope: bar.[[scope]],
}
- 将活动对象AO压如作用域链顶端
barContext = {
AO: {
arguments: {
length: 0
},
a: undefined
},
Scope: bar.AO.concat(bar.[[scope]]),
}
- 执行bar函数