一, 词法阶段
词法作用域, 就是定义在词法阶段的作用域, 也是你再写代码时将变量和块作用域写在哪里来决定的.
看下如下代码
- function foo(a){
- var b = a * 2
- function bar (c){
- console.log(a,b,c)
- }
- bar(b*3)
- }
- foo(2)
- // 2,4 ,12
可以将它们想象成几个逐级包含的气泡
image.PNG
1. 包含着整个作用域, 其中只有一个标识符: foo
2. 包含着 foo 做创建的作用域, 其中有三个标识符: a,bar,b
3. 包含着 bar 所创建的作用域, 其中只有一个标识符 c
在这个代码片段中, 引擎执行 console.log(), 并查找 a,b,c 三个变量的引用, 它首先从最内部的作用域 bar 中开始查找, 找到了 c, 没有其他的就去上一层 foo 中继续查找, 找了 a,b, 如果 a,c 都存在与 bar 和 foo 中, 那他会直接使用 bar 中的变量, 无需再去 foo 中查找
总之, 作用域查找始终从运行时所需的最内部作用域开始, 逐级向上进行, 直到遇见第一个匹配的标识符为止.
全局变量会自动成为全局对象 (在浏览器中是 Windows) 的属性, 因此可以不直接通过全局对象的词法名称, 而是简介的通过对全局属性的引用来对其进行访问 Windows.a
无论函数在哪里被调用, 也无论它如何被调用, 它的词法作用域都治由函数声明时所处的位置决定
词法作用域只会查找以及标识符, 比如 a, b, 如果代码中引用了 foo.bar.baz, 词法作用域查找只会视图查找 foo, 找到这个变量后, 对象属性访问规则会分别接管对 bar,baz 属性的访问.
二, 欺骗词法
当然, 凡事无绝对, 在运行时也可以'修改'作用域.
在 JavaScript 中有两种机制来实现这个目的, 这两种都会导致性能下降. 见过的次数不多, 做了解使用.
eval()
eval()函数接受一个字符串为参数, 并将其中的内容视为好像在书写时就写在这个位置一样.
在执行 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 的词法作用域进行了修改, 所以当 console.log 执行的时候, 会在 foo 内部同时找到 a,b, 因此会输出 1,3, 而不是成情况下输出的 1,2
在严格模式中, eval()在运行时有自己的词法作用域, 意味着其中的声明无法修改所在的作用域, 所以上段代码在严格作用域下回输出 1,2
new Function(..)函数的行为也很类似, 最后一个参数可以接受代码字符串, 并将其转化为动态生成的函数 (前面的参数是这个新生成的函数的形参). 这种构建函数的语法比 eval(..) 略微安全一些, 但也要尽量避免使用.
width
with 通常被当做重复引用同一个对象中的多个属性的快捷方式, 不需要重复引用对象本身.
比如:
- 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;
- }
下面来看一下他的副作用
- function foo(obj){
- width (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) // 2 a 被泄漏到全局作用域上了
这个例子中创建了 o1,o2 两个对象, 其中一个具有 a 属性, 另一个没有. foo 函数接受一个 obj 参数, 该参数是一个对象引用, 并对这个对象引用执行了 width(obj){}.
当我们将 o1 传递进去, a=2 赋值操作找到了 o1.a 并将 2 赋值给它, 而当 o2 传递进去, o2 并没有 a 属性, 因此不会创建这个属性, o2.a 保持 undefined, 但是 a=2 却执行了 LHS 引用(查看《你不知道的 JavaScript》- 作用域是什么(01)), 创建了一个全局变量 a, 并将 2 赋值给它(非严格模式).
可以这样理解, 当我们传递 o1 给 with 时, with 所声明的作用域是 o1, 而这个作用域中含有一个同 o1.a 属性相符的标识符. 但当我们将 o2 作为作用域时, o2 的作用域, foo 的作用域, 全局作用域都没有找到标识符 a, 因此进行了正常的 LHS 标识符查找, 自动创建了一个全局变量(非严格模式).
性能
eval(..)和 with 会在运行时修改或创建新的作用域, 以此来欺骗其他在书写时定义的词法作用域.
你可能会问, 那又怎样呢? 如果它们能实现更复杂的功能, 并且代码更具有扩展性, 难道不是非常好的功能吗? 答案是否定的.
JavaScript 引擎会在编译阶段进行数项的性能优化. 其中有些优化依赖于能够根据代码的词法进行静态分析, 并预先确定所有变量和函数的定义位置, 才能在执行过程中快速找到标识符.
但如果引擎在代码中发现了 eval(..)或 with, 它只能简单地假设关于标识符位置的判断都是无效的, 因为无法在词法分析阶段明确知道 eval(..)会接收到什么代码, 这些代码会如何对作用域进行修改, 也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么. 最悲观的情况是如果出现了 eval(..)或 with, 所有的优化可能都是无意义的, 因此最简单的做法就是完全不做任何优化.
如果代码中大量使用 eval(..)或 with, 那么运行起来一定会变得非常慢. 无论引擎多聪明, 试图将这些悲观情况的副作用限制在最小范围内, 也无法避免如果没有这些优化, 代码会运行得更慢这个事实.
来源: http://www.jianshu.com/p/67002362b7f8