在之前的文章中我已经介绍了执行上下文的变量对象。在这一篇文章我要介绍执行上下文的作用域链了。
执行上下文.作用域链(scope chain)
作用域链与变量对象有着密不可分的关系,因为作用域链就是变量对象的数组!其中第一个是当前函数的活动对象,第二个是当前活动函数的父亲上下文的活动对象,第三个是当前活动函数的爷爷上下文的活动对象,……,最后一个是全局上下文的活动对象。当执行引擎在解析一个变量名的时候,会沿着当前执行函数的作用域链查找,如果在某个活动对象中找到了,则使用它。找不到则报错。
什么叫父亲上下文?这个函数在哪个上下文中被声明,那个上下文就是此函数的父亲上下文,这个函数的声明会被保存到父亲上下文的变量对象中。上层上下文(也就是所有前辈上下文)对于作用域链的创建至关重要,我们会在文章后面讲。
举个栗子:
var global_var = -1;
function outter() {
var outter_var = 111;
console.log(
"in outter, global_var = " + global_var +
", outter_var = " + outter_var
);
function inner() {
var inner_var = 333;
console.log(
"in inner, global_var = " + global_var +
", outter_var = " + outter_var +
", inner_var = " + inner_var
);
}
inner();
}
outter();
// 输出
// in outter, global_var = -1, outter_var = 111
// in inner, global_var = -1, outter_var = 111, inner_var = 333
开始执行outter()之前,创建的执行上下文.作用域链是:
[outter的变量对象, 全局的变量对象]
- 解析global_var的时候,在outter的变量对象中没有找到,因此在全局的变量对象中找,找到这个属性,所以可以打印出它的值-1。
- 解析outter_var的时候,在outter的变量对象中就找到了,因此不用往后找了,直接打印111。
inner的执行上下文.作用域链是:
[inner的变量对象,outter的变量对象, 全局的变量对象]
变量解析的过程同上。
inner的父亲上下文是outter,outter的父亲上下文是全局上下文。
为什么js有函数级作用域
在前一篇文章,我们解释过为什么js不是块级作用域,到这里我们又可以解释为什么js有函数级作用域了。如果你尝试在上面例子的outter()中打印inner_var,会报错。因为outter的作用域链是[outter的变量对象, 全局的变量对象],根本就没有inner的变量对象!自然也就找不到inner_var了。
作用域链是怎么创建的
虽然说作用域链是在函数执行之前,与执行上下文一起创建的,但是实际上其中很大一部分内容在定义的时候就已经确定了。
作用域链这个数组可以分成两部分:
- 第一个元素就是本函数的变量对象
- 后面的所有元素都是上层上下文的变量对象(按照由近到远的顺序排列)。在网上的一些资料中,所有上层上下文的链叫做[[Scope]]。
第一部分是在函数执行之前创建的。第二部分其实在函数声明的时候就已经确定了,[[Scope]]是函数的一个属性,从函数声明到函数销毁一直存在,且不会改变。我们在文章开头也说过,决定上层上下文的是函数在哪里声明,而不是函数在哪里调用。 因此,本函数的变量对象一旦创建好,执行引擎就可以合并[[Scope]],创建出一个作用域链了。
可以用这种方式轻松地找出[[Scope]]:[[Scope]]就是函数被声明时的上下文的作用域链。
举个例子:
function inner() {
// 创建作用域链:[inner的变量对象,全局上下文的变量对象],其中没有outter的变量对象!怪不得找到不到outter_var!
console.log("in inner, outter_var = "+outter_var);
}
// inner声明完毕,[[scope]]已经确定
// 因为此时的上下文是全局上下文,所以[[scope]]就是全局上下文的作用域链,也就是全局变量对象
function outter() {
var outter_var = 111;
inner();
}
outter(); // 报错:outter_var is not defined
如果将inner声明在outter里面,就不会报错啦:
function outter() {
var outter_var = 111;
function inner() {
// 创建作用域链:[inner的变量对象,outter的变量对象,全局上下文的变量对象]
console.log("in inner, outter_var = " + outter_var);
}
// 声明完毕,[[scope]]已经确定
// 因为此时的上下文是:outter上下文
// outter上下文的作用域链是:[outter上下文的变量对象,全局上下文的变量对象]
// 所以[[scope]]是:[outter上下文的变量对象,全局上下文的变量对象]
inner();
}
outter();
// 输出:in inner, outter_var = 111
解释闭包的原理
如果看过我之前的文章彻底理解js闭包现在应该已经能掌握闭包的使用方法了。是时候给闭包最后一击了!我们接下来要用作用域链来解释闭包的原理!
看看我在“彻底理解js闭包”中举的一个栗子:
function outter() {
var private= "I am private";
function show() {
console.log(private);
}
// [[scope]]已经确定:[outter上下文的变量对象,全局上下文变量对象]
return show;
}
var ref = outter();
// console.log(private); // 尝试直接访问private会报错:private is not defined
ref(); // 打印I am private
outter执行完以后,private
不会被销毁,并且只能被show方法所访问,而尝试直接访问它会出现报错。
- 首先我们解释为什么不能直接从外部访问它:
执行到报错的那一行的时候,当前上下文的作用域链是:[全局上下文变量对象],其中的变量对象中并没有private
(private在outter的上下文变量对象中)!这就是为什么我们不能直接访问private
。 - 然后我们解释为什么能够通过show来访问
private
: 调用ref(也就是show)的时候,因为show的[[scope]]是:
[outter变量对象,全局变量对象]
所以创建的作用域链是:
[show变量对象,outter变量对象,全局变量对象]
查找private的时候,在outter变量对象中找到了,所以可以访问到。
可见,闭包之所以能够“记忆”它被创建时候的环境,就是因为[[scope]]将函数声明时所在上下文的作用域链给存起来了!