词法作用域
词法阶段
大部分标准化语言编辑器的第一个工作阶段叫做词法化,词法化会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义。
简单来说,词法作用域就是在词法阶段的作用域,
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c)
}
bar( b * 3)
}
foo(2)
作用域气泡由其对应的作用域代码写在哪里决定
查找
作用域气泡的结构和相互之间的位置关系给引擎提供了足够的位置信息。引擎用这些信息来查找标识符的位置。
作用域查找会在找到第一个匹配的标识符停止,在多层的嵌套作用域中可以定义同名的标识符,这叫遮蔽效应。
全局变量自动变成全局对象,因此可以不直接通过全局对象的词法名称,而是间接的通过全局对象属性的引用来对其访问。
通过这种技术可以访问那些被同名变量所遮蔽的全局变量,但是非全局的变量如果被遮蔽了,就无法被访问到。
无论函数在哪里被调用,也无论被如何调用,词法作用域都只有函数被声明时候所处的位置决定。
代码中查找foo.bar.baz,词法作用域查找只会试图查找foo标识符,找到这个变量后,对象属性访问规则会分别接管对bar和baz属性的访问。
欺骗词法
欺骗词法作用域会导致性能的下降
eval
JavaScript中的eval函数可以接受一个字符串作为参数,可以在写的代码中程序生成代码并运行,就好像是写在那个位置的一样。
function foo(str, a) {
eval(str) // 欺骗
console.log(a, b)
}
var b = 2;
foo("var b = 3", 1) // 1, 3
eval()调用var b = 3, 这段代码中会被当作本来就在那里一样处理,但是创建了新的变量b,会对原来的词法环境进行修改。相当于在foo内部创建了一个变量b,遮蔽了外部作用域的同名变量。当console.log(…) 被执行时,会在foo(…) 的内部同时找到a 和b,但是永远也无法找到外部的b。因此会输出“1, 3”而不是正常情况下会输出的“1, 2”。
在严格模式的程序下,eval在运行的时候由自己的词法作用域,意味着其中的声明无法修改所在的作用域
function foo(str) {
"use strict"
eval(str)
console.log(a) // RefferenceError: a is not defined
}
foo("var a = 2")
JavaScript中还有其他一些功能效果和eval(…)很相似。setTimeout(…) 和setInterval(…) 的第一个参数可以是字符串,字符串的内容可以被解释为一段动态生成的函数代码
with
with通常被当作重复引用一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a: 1,
b: 2,
c: 3
}
obj.a = 2
obj.b = 2
obj.c = 2
with(obj) {
a = 3;
b = 4;
c = 3;
}
with可以将一个没有或者有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被定义为在这个作用域的词法标识符。但是合格块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with所处的函数作用域中。
eval(…) 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。
性能
JavaScript引擎在编译阶段会进行数项的性能优化,其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置。如果在代码中使用的eval或者with,只能简单的假设关于标识符位置的并判断都是无效的,因为无法再词法分析阶段明确的知道eval会受到什么代码。这些代码会对作用域进行修改,无法知道传递给with用来创建新的词法作用域的对象的内容。
代码中大量使用eval或者with,运行起来会很缓慢