JavaScript 代码的执行流程的第一步就是编译阶段:创建 执行上下文 和 可执行代码。
其中执行上下文中主要包括了
- 变量环境 (Viriable Environment):用于存放变量提升的内容。
- 词法环境 (Lexical Environment):作用域说白了就是变量与函数的可访问范围。
- this 的绑定
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
静态作用域与动态作用域
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
让我们认真看个例子就能明白之间的区别:
:::: tabs
::: tab 例子
js
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 结果是 1::: ::: tab 《JavaScript 权威指南》中的例子
js
var scope = 'global scope';
function checkscope() {
var scope = 'local scope';
function f() {
return scope;
}
return f();
}
checkscope(); // local scopejs
var scope = 'global scope';
function checkscope() {
var scope = 'local scope';
function f() {
return scope;
}
return f;
}
checkscope()();两段代码都会打印:local scope。
原因也很简单,因为 JavaScript 采用的是词法作用域,函数的作用域基于函数创建的位置。
:::
::::
变量提升所带来的问题
:::: tabs
::: tab 本应销毁的变量没有被销毁
js
function foo() {
for (var i = 0; i < 7; i++) {}
console.log(i); // 这里还可以访问到 i = 7
}
foo();在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。
:::
::: tab 变量容易在不被察觉的情况下被覆盖掉
js
var myname = 'alvin';
function showName() {
console.log(myname); // undefined
if (1) {
var myname = 'tony';
}
console.log(myname); // tony
}
showName();showName 函数的执行上下文创建,JavaScript 引擎便开始执行 showName 函数内部的代码了。首先:
js
console.log(myname);myname 在全局执行上下文和 showName 函数的执行上下文中都存在。在函数执行过程中,JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升,当前的执行上下文中就包含了变量 myname,而值是 undefined,所以获取到的 myname 的值就是 undefined。 :::
::::
上面我们介绍了变量提升而带来的一系列问题,为了解决这些问题,ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域。
变量在作用域中是如何查找的?
那么接下来,我们就要站在执行上下文的角度来揭开答案。
js
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
foo();当执行上面这段代码的时候,JavaScript 引擎会先对其进行编译并创建执行上下文,然后再按照顺序执行代码,关于如何创建执行上下文我们在前面的文章中已经分析过了,但是现在的情况有点不一样,我们引入了 let 关键字,let 关键字会创建块级作用域,那么 let 关键字是如何影响执行上下文的呢?
接下来我们就来一步步分析上面这段代码的执行流程。
编译阶段:编译并创建执行上下文
js
VariableEnvironment:
a -> undefined
c -> undefined
LexicalEnvironment:
b -> undefined
通过上图,我们可以得出以下结论:
- 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
- 通过 let 声明的变量,在编译阶段会被存放到词法环境(
Lexical Environment)中。 - 在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。
执行代码
接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就如下图所示:
js
VariableEnvironment:
a -> 1
c -> undefined
LexicalEnvironment:
b -> 2
从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。
其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过 let 或者 const 声明的变量。
再接下来,当执行到作用域块中的 console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
这样一个变量查找过程就完成了,你可以参考下图:

从上图你可以清晰地看出变量查找流程,不过要完整理解查找变量或者查找函数的流程,就涉及到作用域链了。
通过上面的分析,想必你已经理解了词法环境的结构和工作机制
- 块级作用域就是通过词法环境的栈结构来实现的,
- 变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。