词法作用域
作用域
有两种主要的,分别是(大多数编程语言所采用的)和(Bash脚本、Perl中的一些模式)。
词法阶段
词法作用域是定义词法阶段的作用域,即写代码时将变量和块作用域写在哪里来决定的。
如上图所示
- 包含整个全局作用域,其中只有一个标识符:foo。
- 包含着foo所创建的作用域,其中有三个标识符:a、bar和b。
- 包含着bar所创建的作用域,其中只有一个标识符:c。由此可以得出它们是。
查找
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。
还是如上一个代码图所示,引擎在执行 console.log()
声明,并查找a、b和c三个变量的引用。它首先从最内部的作用域,也就是 bar(..)
函数的作用域气泡开始查找。引擎无法在这里找到a,因此会从上一次所嵌套的 foo(..)
的作用域中继续查找。在这里找到了a,因此因此使用了这个引用。同理b和c。
。因此在多层的嵌套作用域中可以定义同名的标识符,这叫作“”(内部的标识符“遮蔽”了外部的标识符)。
,因此不可以直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。如 var a = 3;
可以通过 window.a
访问。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域只由函数被声明时所处的位置决定。
欺骗词法
可以修改写代码时函数所声明的位置,所谓欺骗词法,但欺骗词法作用域会导致性能下降。
eval
eval(..)
函数可以接受一个字符串为参数,并将其中的内容转为动态生成的代码。
function foo(str, a) {
eval(str); //欺骗!
console.log( a,b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1,3
eval(..)
其实在foo的作用域内创建了一个变量b,遮蔽了外部(全局)作用域中的同名变量。所以在查b时,在foo内部找到了第一个匹配的标识符的时候就停止了。eval(..)
可以间接调用使其运行在全局作用域中,对全局作用域进行修改。
但是在严格模式的程序中,eval(..)
在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域,如下案例所示:
function foo(str) {
"use strict";
eval( str );
console.log(a);
// ReferenceError: a is not defined
}
foo("var a = 2");
注意
和 eval(..)
类似的还有 setTimeout(..)
和 setInterval(..)
的第一个参数可以是字符串(内容会被解释诶一段动态生成的函数代码)。new Function(..)
的最后一个参数也可以接受代码字符串。。
with
with
通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a: 1,
b: 2,
c: 3
};
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with(obj) {
a = 3;
b = 4;
c = 5;
}
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); // 2 a被泄漏到全局作用域勒
案例说明
例子二可以看出 foo(..)
函数接受一个obj参数,该参数是对象的一个引用,并对这个对象执行了 with(obj){..}
。在 with
块内部其实执行了LHS操作。而执行 foo(o2)
的时候发现o2没有 a
这个属性,他不会创建这个属性,o2.a
保持 undefined
,但却在全局中创建一个变量a
(非严格模式)
这是因为 with
可以处理为一个,即对象的属性会被处理为定义在这个。
小结
JavaScript中有两个机制可以“欺骗”词法作用域:eval(..)
和with
。
TIP
eval(..)
函数如果接受了,就会,而 with
声明实际上是根据你传递给它的对象了一个的。切。