编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。
- 词法分析
将由字符组成的字符串分解成 (对编程语言来说) 有意义的代码块,这些代码块被称为词法单元(token)。 - 语法分析
这个过程是将词法单元流 (数组) 转换成一个代表了程序语法结构的树。这个树被称为“抽象语法树”(AST)。 - 代码生成
将 AST 转换为可执行代码的过程被称为代码生成。AST 转化为一组机器指令。
JavaScript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒 (甚至更短) 的时间内。
引擎、编译器、作用域
引擎:
从头到尾负责整个 JavaScript 程序的编译及执行过程。
编译器:
负责语法分析及代码生成等脏活累活。
作用域:
负责收集并维护由所有声明的标识符 (变量) 组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
编译器可以控制作用域、而引擎更多的是查询(有可能会抛出异常)。
引擎在查询作用域的时候,有两种查询方法:
LHS:当变量出现在赋值操作的左侧时,试图找到变量的位置。如果在顶层 (全局作用域) 中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量。
RHS:当变量出现在赋值操作的右侧时,简单地查询值。在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。
词法作用域
作用域有两种主要的工作模型:词法作用域(静态)、动态作用域;
词法作用域是在编译器词法分析的时候生成的。
eval 和 with 会动态改变词法作用域,这样会影响 js 引擎编译阶段的优化工作,会慢。所以严格模式禁用
声明提升
js 的代码在生成前,会先对代码进行编译,编译的一部分工作就是找到所有的声明,然后建立作用域将其关联起来,因此,在当前作用域内包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
定义声明是在编译阶段进行的,而赋值是在执行阶段进行的。
提升的优先级:
函数提升的优先级比变量高(函数声明会提升到变量声明之前,变量声明一定会被忽略)
对于同名的变量声明,Javascript 采用的是忽略原则,后声明的会被忽略
对于同名的函数声明,Javascript 采用的是覆盖原则,先声明的会被覆盖
示例 1:
console.log(c) // ReferenceError
b = function c() {};
等号右边的是不会变量提升的哟,可以见得编译过程中没有管等号右边。
示例 2:
b = function c() {};
console.log(c) // ReferenceError
执行过程中创建了 c 变量,但是只能在函数 c 中使用
示例 3:
b = function c() {c = 3;};
b();
console.log(c); // ReferenceError
等价于
b = function() {
let c = 3;
c = 4 // 没有赋值到 window 上哟
};
b();
console.log(c); // ReferenceError