1. 前言
1.1 编译原理
在传统编译语言的流程中,程序中的一段代码在执行前会经历三个阶段,统称为编译。
- 分词/词法分析 这个过程会将字符串分解代码块(词法单元)
分词和词法分析的区别:判断a是一个独立的词法单元还是其他词法单元一部分,调用有状态的解析规则,那这个过程为词法分析
-
解析/语法分析 词法单元流转换成由元素逐级嵌套组成的语法程序结构的树(抽象语法树AST)
-
代码生成 将AST转换为可执行代码的过程。
但是js引擎要复杂的多,js引擎不会有大量的时间用来进行优化,js编译不是发生在构建之前。大部分情况下编译发生在执行代码的前几微秒。
- 引擎 从头到尾负责整个js程序的编译以及执行过程
- 编译器 负责语法分析以及代码生成
- 作用域 负责收集并维护由所有声明的标识符组成的一系列查询
1.2 编译器查找
1.LHS左侧
试图找到容器本身 从而对此赋值。 例如:a =2
2.RHS右侧
简单查找某个变量的值,取到源值。例如:console.log( a )
2. 词法作用域
2.1 词法阶段
作用域查找会在找到第一个匹配的标识符时停止。
作用域共有两种主要的工作模型。
- 动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。
- 词法作用域,是最为普遍的,被大多数编程语言所采用的词法作用域。词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
2.2 欺骗词法
JavaScript中存在两个机制可以“欺骗”词法作用域:eval(…)和with。
- eval(…)可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域。
- with本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域。
2.2.1 eval
JavaScript中的eval(..)函数可以接受一个字符串为参数,并将其中的内容当作好像在写时就存在于程序中这个位置的代码。
function f(a,b){
eval(a);//欺骗
console.log(b,c);
}
var c=2;
f("var c=3",1);//输出结果为1 3
eval(..)调用中的"var c=3" 这段代码会被当作本来就在那里一样来处理。 由于声明了一个新的变量c , 因此这段代码会在f(..)内部创建了一个变量c , 并遮蔽了外部作用域中的同名变量 。当console.log(..)被执行时 ,会在f(..)的内部同时找到 b 和 c ,但是永远也无法找到外部的 c 。因此会输出"1, 3"。
2.2.2.with
with通常被当作重复引用同一个对象中的多个属性的快捷方式,但其实还有其他用法。
function f(obj){
with(obj){
a=2;
}
}
var o1 ={ a:3 };
var o2 ={ b:3 };
f(o1);
console.log(o1.a);//输出结果为2
f(o2);
console.log(o2.a);//输出结果为undefined
console.log(a);//输出结果为2
我们将o1传递进去,a=2赋值操作找到了o1.a并将2赋值给它。而当o2传递进去,o2并没有a属性,因此不会创建这个属性o2.a保持undefined。
但是可以注意到一个奇怪的现象,为什么最后一行输出为2?实际上是因为a=2赋值操作创建了一个全局的变量。with可以将一个没有或者有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符,但是这个块内部正常的var声明并不会被限制在这个块的作用域中,而是添加到with所处的函数作用域中。
当我们传递o1给with时,with所声明的作用域是o1,而这个作用域中含有一个同o1.a属性相符的标识符。但当我们将o2作为作用域时,其中并没有a标识符,因此进行了正常的LHS标识符查找。o2的作用域,f的作用域和全局作用域中都没有找到标识符a,因此当a=2执行时,自动创建了一个全局变量(因为是非严格模式)。
2.2.3总结
eval(..)函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。
但这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。所以尽量不要使用它们。
3. 函数作用域和块作用域
3.1. 函数作用域
(function ...会被当做一个函数表达式而不是标准函数声明,如果function是声明中第一个词,会被当做函数声明。 2.1.1.匿名和具名 函数表达式可以匿名 setTimeot( function () {},1000); 2.1.2.立刻执行函数表达式(IIFE) ( function foo() { } )()由于函数被包含在括号内成为了一个表达式,在末尾加上()可立即执行。 ( function foo() { } )()改进形式( function foo() { } ()) 2.2.块作用域 2.2.1.Let 1.let将变量附加在一个已经存在的块作用域上的行为是隐式的, 2.let进行的声明不会在块作用域中进行提升,声明代码被运行前,并不存在。 2.2.2.Const 创建块作用域变量,但其值固定,修改会引起错误。