你不知道的JS
(更新中)
作用域和闭包
编译原理
传统编译语言
分词/词法分析
将由字符组成的字符串分解成有意义的代码块(词法单元 token)
var a = 2; 会被拆为 var、a、=、2、;
解析/语法分析
将词法单元流(数组)转换成抽象语法树(Abstract Syntax Tree, AST)
var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下来是一个叫作 Identifier(它的值是a)的子节点,以及一个叫作AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫作NumericLiteral(它的值是2)的子节点。
代码生成
将 AST 转换为可执行代码的过程被称为代码生成。
简单来说就是有某种方法可以将var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作a 的变量(包括分配内存等),并将一个值储存在a 中。
总结

JS 的编译过程
复杂得多(时间短,需要优化性能)
对于JS来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如JIT,可以延迟编译甚至实施重编译)来保证性能最佳。
简单地说,任何JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript 编译器首先会对var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
处理工具
引擎
从头到尾负责整个JavaScript 程序的编译及执行过程。
编译器
负责语法分析及代码生成等
作用域
负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
var a = 2 后发生了什么
编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。然后开始生成代码。
在生成代码的过程中:变量的赋值操作会执行两个动作。
首先编译器会在当前作用域中声明一个变量(如果之前没有声明过,否则忽略声明继续编译)
然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值(找不到就抛出异常)。
引擎如何查找变量
LHS 查询
如果查找的目的是对变量进行赋值,那么就会使用LHS 查询
a = 2
RHS 查询
如果目的是获取变量的值,就会使用RHS 查询。
console.log(a)
作用域
作用域
作用域是根据名称查找变量的一套规则。
作用域链
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
词法作用域
大部分标准语言编译器的第一个工作阶段叫作词法化
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,是静态的。
欺骗词法
欺骗词法作用域会导致性能下降
eval
动态生成代码(即通过代码生成代码)
1 |
|
严格模式下,eval(..) 在运行时有其自己的词法作用域,无法欺骗
1 |
|
with
with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
严格模式禁止
1 |
|
存在全局变量泄露的问题
1 |
|
性能问题
性能很差,不要使用!
原因:JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。使用 eval 或 with 会使他们失效。