作者 | Elya
转载请注明出处:https://www.toutiao.com/i6794055913318646284/
接上文浏览器原理系列 - JS执行上下文详解(一):作用域,我们提到了作用域以及ES6是如何通过变量环境和词法环境来同时支持变量提升和块级作用域,本文我们聊聊作用域链。
二、作用域链
在开始之前我们来看下面这段代码:
function bar() {
console.log(myName);
}
function foo() {
var myName = "Tom";
bar();
}
var myName = "Linda";
foo();
这段代码的执行结果是打印 Linda 还是 Tom 呢?我相信等你看完本文会知道答案的。
2.1 作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer。
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。
比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。为了直观理解,你可以看下面这张图:

从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。
现在我们知道变量是通过作用域链来查找的了,不过还有一个疑问没有解开,foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
要回答这个问题,你还需要知道什么是词法作用域。这是因为在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。
2.2 词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
我们看看下面这张图:

图片来自网络
从图中可以看出,词法作用域就是根据代码的位置来决定的,其中 main 函数包含了 bar 函数,bar 函数中包含了 foo 函数,因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。
直白地说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
我们已经在全局作用域和函数级作用域分析了作用域链,那接下来我们再来看看块级作用域中变量是如何查找的?
2.3 块级作用域中的变量查找
来看下面这段代码:
function bar() {
var myName = "Firefox";
let test1 = 100;
if (1) {
let myName = "Chrome";
console.log(test);
}
}
function foo() {
var myName = "Safari";
let test = 2;
{
let test = 3;
bar();
}
}
var myName = "IE";
let test = 1;
foo();
要想得出其执行结果,那接下来我们就得站在作用域链和词法环境的角度来分析下其执行过程。下图是我画的执行到 console.log(test) 时调用栈的情况:

现在需要打印出来变量 test,那么就需要查找到 test 变量的值,其查找过程我已经在上图中使用序号 1、2、3、4、5 标记出来了。首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。
最后可以得出打印结果是1。