词法阶段
【要知道的】:
大部分标准语言编译器的第一个工作阶段就叫词法化(也称为单词化)
在这个过程中,编译器会对源码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义
词法作用域
词法作用域就是定义在词法阶段的作用域
换句话说,词法作用域就是你在写代码时,将变量和块作用域写在哪里的位置决定的
function foo(a){
var b = a * 2;
function bar(c){
console.log(a, b, c)
}
bar(b * 3)
}
foo(2); // 2, 4, 12
在上面的代码中有三个逐级嵌套的作用域
欺骗词法作用域
【要知道的】:
欺骗词法作用域会导致性能下降
eval
eval()
函数可以接受一个字符串为参数,并将其中的内容视为【好像在书写代码时】就存在这个位置一样。
也就是将eval函数里面的字符串参数,用程序把它生成代码并运行。就好像代码是写在那个位置一样
在执行eval()
之后的代码时,引擎并不知道也不在意前面的代码是以动态的形式插入进来,并对词法作用域的环境进行修改的。引擎只是会像往常一样进行词法作用域查找
function foo(str, a) {
eval(str); // 欺骗词法,相当于在这里有了 var b = 3
console.log(a, b);
}
var b = 2
foo("var b = 3;", 1) // 1, 3
上面的例子传递给eval()
的代码字符串时固定不变的,而实际上,我们可以非常容易的根据程序逻辑动态的将字符串拼接在一起,最后在传递给eval()
。如果eval()
中所执行的代码包含一个或多个声明(无论是变量还是函数)都会对eval()
所在的词法作用域进行修改。甚至我们还可以通过一些技巧间接的调用eval()
来对全局作用域进行修改
注意:
eval()
是在运行期来修改书法期的词法作用域。
注意:在严格模式下,eval()
在运行时有着自己的词法作用域,这也就意味着eval()
里面的声明无法修改所在的作用域了
function foo (str) {
"use strict"
eval(str) // var a = 2
console.log(a) // 报错:ReferenceError: a is not defined
}
foo("var a = 2")
因为eval在自己的作用域创建了a, 而console.log(a)
是在foo的作用进行RHS查找没有找到,然后又去全局作用域进行RHS查找,也没有找到,所以报错ReferenceError: a is not defined
with
通常情况下,当我们需要重复引用同一个对象中的多个属性时,我们一般会使用width()
比如:
var obj = {
a: 1,
b: 2,
c: 3
}
// 单调乏味的重复"obj"
obj.a = 2
obj.b = 3
obj.c = 4
// 简单的快捷方式
with(obj) {
a = 3,
b = 4,
c = 5
}
但实际上, with
不仅仅是为了方便访问对象的属性
function foo(obj) [
with(obj) {
a = 2
}
}
var o1 = {
a: 3
}
var o2 = {
b: 3
}
foo(o1)
console.log(o1.a) // 2
foo(o2)
console.log(o2.a) // undefined
console.log(a) // 糟糕,a被泄露到全局作用域上了
上面可以看到,我们声明了两个对象,分别有a属性和b属性,并分别将对象作为参数传递给foo
函数,并对这个参数引用执行了with(...)
,而在with()
内部只是简单的对a进行LHS引用并将2赋值给它。
当我们把o1
传递进去,with(o1)
会将o1
处理为一个完全隔离的词法作用域,而o1
的属性也会被处理为定义在这个作用域中的词法标识符。
可以这样理解:我们把
o1
传递给with
时,with
声明的作用域就是o1
,而执行a = 2
时,会对a
进行LHS查找,会找到o1
中的a
并赋值为2
同理当我们把o2
传递进去,with(o2)
会将o2
处理为一个完全隔离的词法作用域,但由于o2
中没有属性a
,因此不会创建这个属性,仍然保持o2.a
为undefined
,然后继续向上级作用域进行查找,直至找到全局作用域还是没有找到a
,所以当执行a = 2
时,就在全局作用域中创建了a
并赋值为2(前提是在非严格模式下,严格模式with被完全禁止)
【小结】:
with
可以将一个或多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象里面的属性也会被处理为这个作用域的词法标识符。
但是,尽管with
可以将一个对象处理为一个词法作用域,如果我们在这个块内部正常var
声明并不会限制在这个块作用域中,而是被添加到with
所处的函数作用域中
eval和with的区别
eval(...)
函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域
with(...)
函数则实际是根据你传递来的对象凭空创建了一个全新的词法作用域。
性能
由上可知,eval
和with
会在运行时修改或创建新的作用域,以此来欺骗在书写时定义的词法作用域。
js引擎会在编译阶段进行数项的性能优化。而其中有些优化是依赖于能够根据代码的词法进行静态分析,并预先确定所有的变量和函数的定义位置,这样在执行过程中才可以快速的查找标识符。
词法作用域意味着作用域是由书写代码是函数声明的位置来决定的。编译的词法分析阶段已经基本可以知道全部标识符在哪里以及是如何声明的,从而可以预测在执行的过程中如何对他们进行查找
但是如果引擎在代码中发现了eval
或with
,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确的指导eval(...)
会接收到什么代码,也不知道这些代码会对作用域进行怎样的修改。同样的也无法知道传递给with
用来创建新词法作用域的对象的内容到底是什么。
最不好的情况就是,如果出现了eval()
或with()
,所有的优化都可能是无意义的