本文会介绍 JavaScript 的大致执行过程:词法分析、语法分析、编译时和运行时。之后介绍了作用域的基础知识以及 JavaScript 的词法作用域。
1 Js 的执行过程:
JavaScript 引擎在运行这段脚本会经历 2 个阶段,词法/语法分析 和 执行阶段。在执行阶段又划分为 编译时 和 运行时。这几个阶段的间隔时间会非常的短,可以理解为当引擎刚刚完成了词法分析,便会立刻开始编译阶段和运行阶段。而不像传统的 C,Java 等语言,编译阶段和运行阶段可以分开执行。
- C:源代码
.c
----[编译为]---->.s
----[汇编为]---->.obj
---------->.exe
可执行文件。 - Java:源代码
.java
----[编译为]---->.class
JavaScript 的执行,一共有 2 个阶段:
1.1 词法/语法分析阶段
主要是为了分析代码的语法是否正确,如果不正确会抛出语法错误(syntaxError
)并停止执行代码。
- 词法分析(Tokenizing) 。将代码拆分成具有意义的最小代码块,也称之为词法单元(token) ,比如代码
var a = 2;
,会拆分成:var
,a
,=
,2
,;
。 - 语法分析(Parsing) 。将词法单元流,按照语法结构转为 抽象语法树(Abstract Syntax Tree, AST) 。转换后的抽象语法树,具有了逐级嵌套的树状结构。
1.2 执行阶段
词法/语法分析阶段后,
-
引擎根据抽象语法树分析代码的 词法作用域,进而形成 运行环境。一个运行环境,就是一个词法作用域。
引擎从 全局运行环境(全局词法作用域) 开始,进入每个运行环境(词法作用域)中。先编译,后运行该运行环境中的代码。运行完毕后,进入下一个运行环境。这样一直到所有运行环境执行完毕。
-
创建一个 调用栈 call stack。
当进入某一个运行环境中,会进入:
1 编译阶段
根据运行环境的词法作用域,创建 执行上下文(execution context) ,并放入 调用栈(call stack) 中。 栈底是 全局执行上下文(global execution context) ,栈顶则是当前的执行上下文。
创建当前执行上下文,其基本结构如下:
-
variable environment 变量环境
- outer 外部环境
-
lexical environment 词法环境
-
this value
2 运行阶段
根据创建好的 当前执行上下文,开始逐次运行 当前运行环境 中的全部代码。
- 当代码全部执行完毕后,弹出当前执行上下文。
如果在代码执行中,遇到一个函数调用,会暂停当前执行上下文的执行,创建这个函数调用的执行上下文,把这个执行上下文压入 调用栈 中。此时会进入这个调用函数的编译和运行阶段,直到该函数内的全部代码执行完毕,弹出该执行上下文。
注意:不同的运行环境执行都会进入到 编译和执行两个阶段。而词法/语法分析阶段不区分运行环境,对全部代码进行解析。
更多关于调用栈、执行上下文内容的知识,参见 “执行上下文” 章节。
2 作用域 scope
作用域是一个区域,规定了代码中变量和函数的可访问范围。所以,作用域决定了变量和函数的可见性和生命周期。
2.1 作用域的两种工作模型:
动态作用域
动态作用域中,对作用域的定义发生在 运行阶段。
动态作用域不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。作用域链是基于调用栈的,而不是根据代码结构的作用域嵌套。
词法作用域
词法作用域中,对作用域的定义发生在 词法分析阶段。 换句话说,每个变量都有一个对应它的词法作用域:无论函数在哪里被调用,无论如何调用,它的词法作用域都 只由函数被声明时所处的位置决定。
- 词法作用域只查找一级标识符,如果存在这样一个代码:
foo.bar.baz.name
。Js 引擎会通过词法作用域查找到foo
,剩余的查找工作会通过对象的属性访问去查找。如果想了解如何通过对象属性查找,参加 “对象” 章节。 - 根据代码的层层结构,形成了作用域链。
JavaScript 同绝大多数编程语言一样,使用静态的词法作用域。
3 词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
词法作用域的划分:
全局作用域
全局作用域中的对象在代码中的任何地方都能访问。随着代码的执行完毕,生命周期结束。全局作用域在一个完整的代码脚本中,有且只有一个。
函数作用域
一个函数内部,便是一个函数作用域。函数内部定义的变量会随着函数执行结束而销毁。
闭包
闭包是一个变量的集合,也是一个函数作用域。当一个函数执行完毕后,本应被销毁,但其内部定义的变量,依然被其他作用域引用(持有)。这时,这个函数就会形成一个闭包。为了节省开销,JS 引擎会销毁这个函数自身绝大多数的内容(这些内容只要不影响这些被引用变量的调用,都会被销毁)。而这些没有被销毁的、被引用的变量和支撑内容所组成的集合,就是闭包。
块级作用域
使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句、try
/ catch
/ finally
,单独的一个 {}
都被看作是一个块级作用域。
块级作用域仅限制 let
和 const
变量的可访问范围;不限制 var
变量和 function
函数的可访问范围。
分析打印结果:
function bar() {
console.log(myName)
}
function foo() {
var myName = "Ninjee"
bar()
}
var myName = "Moxy"
foo() // Moxy
原因:
根据作用域关系,bar
和 foo
两个函数作用域都在全局作用域中。作用域的判定是函数声明的位置。可以看到,这两个函数的声明没有嵌套的关系,所以两个函数的作用域也没有嵌套关系。
代码中,bar
在 foo
的内部调用,在函数调用上出现了嵌套关系,误导了作用域的判断。
需要记住:作用域链和执行上下文栈完全是两套逻辑;前者在词法分析阶段被确定,后者在编译时和运行时被确定。
更多关于作用域链、闭包的相关知识,参见 “作用域链和闭包” 。
引用
《你不知道的 JavaScript》
winter - 重学前端
李兵 - 浏览器工作原理与实战