JavaScript系列——作用域链
作用域与作用域链
首先,什么是作用域?先看看 MDN文档的定义:
The current context of execution. The context in which values and expressions are "visible," or can be referenced. If a variable or other expression is not "in the current scope," then it is unavailable for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.
当前执行的上下文,变量的值和表达式被看见或者被引用的上下文。当一个变量不在作用域内,则无法引用。但是子作用域可以继承父作用域,反之不行。
而从子作用域往上追溯“寻祖”的过程,继承父作用域,父作用域的父作用域,一层一层往上,直到全局作用域,这个继承的链条,就可以理解为作用域链。例如下面一个例子:
var globalVar = 'Global';
function parent() {
var par = 'par';
function child1() {
var childVar = 'childVar';
console.log(childVar);
console.log(globalVar);
child2();
}
function child2() {
console.log(par);
}
child1();
}
parent(); // 输出:"childVar" "Global" "par"
上面的例子里,child1 函数和 child2 函数分别通过作用域链访问了父作用域的父作用域(globalVar),和父作用域(par),从而得到了函数需要输出的值。
这样我们就对作用域链的概念有个直观的了解:它是在代码上下文中查找标识符的对象列表.这也是 ECMA的文档 中的定义:
A scope chain is a list of objects that are searched when evaluating an Identifier. When control enters an execution context, a scope chain is created and populated with an initial set of objects, depending on the type of code. During execution within an execution context, the scope chain of the execution context is affected only by with statements (section 12.10) and catch clauses
我们可以得出几个特性:
- 当控制进入一段执行上下文的时候,作用域链才创建,并且已当前作用域作为开始。
- 作用域链从当前域开始一直追溯到父作用域一直到全局作用域。
- 作用域链受到 with 语句 和 catch 的影响。
面试题
有了这个认识,我们看几道面试题:
var a = 1
function fn1(){
function fn2(){
console.log(a)
}
function fn3(){
var a = 4
fn2()
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出 a=2
上题的取值过程如下:
fn1 赋值给 fn ---> 调用 fn ---> 调用 fn1 ---> return fn3 (此时var a =2,a已赋值。) ---> fn3 调用 fn2 ---> fn2 console.log(a) ---> 在 fn2 作用域中查找 a ---> 没有定义 a ---> 在 fn1 中查找 a ---> a = 2 ---> console.log(2)
我们可以看到作用域链是这样的:
fn2 ---> fn1 ---> GLOBAL
fn3 ---> fn1 ---> GLOBAL
由于 fn2 作用域中没有 a ,但是在 fn2 的父作用域中,在 fn2 执行之前 a 已经被定义且赋值为 2 ,因此 fn2 在父作用域中查找到 a = 2,因此输出:2。
看另一道类似的题:
var a = 1
function fn1(){
function fn3(){
function fn2(){
console.log(a)
}
fn2()
var a = 4
}
var a = 2
return fn3
}
var fn = fn1()
fn() //输出 ?
作用域链如下:
fn2 ---> fn3 ---> fn1 ---> GLOBAL
唯一和上一题不同的是,在 fn3 中由于变量声明前置我们可以改写成如下函数:
function fn3() {
var a;
function fn2() {
console.log(a);
}
fn2();
a = 4;
}
由于执行 fn2 的时候,a 已经被定义(变量声明前置),因此 fn2 在父作用域中找到了 a ,但是此时 a 尚未被赋值,在 fn2 执行之后才赋值,因此,console.log(a) 输出的是:"undefined"。
待补充 with 语句与 catch