你不知道的JS(二)-词法作用域

265 阅读3分钟

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的,编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而预测在执行过程中如何对它们进行查找。

function foo(a) {
	var b = a * 2;
    function bar(c) {
    	console.log(a, b, c);
    }
    bar( b * 3 );
}

作用域的查找会在找到第一个匹配的标识符时停止。引擎执行 console.log(...) 声明,并查找a, b, c 三个变量的引用,会先从 bar(...) 函数进行查找,引擎在这里无法找到变量 a ,就会去作用域 foo(...) 中查找,在这里找到 a .对 b 也一样。

欺骗词法

词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢?

欺骗词法作用域会导致性能下降。

  1. eval()
function foo(str, a) {
	eval(str); // 欺骗
    console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1); // 1,3

eval(...)会把字符串当作本来就在那里一样进行处理。由于这里新声明来一个变量 b,因此对 foo(...) 的词法作用域进行来修改。遮蔽来全局作用域的同名变量。因此会输出1, 3.而非正常情况下的 1, 2.

但是在严格模式下,eval() 会由自己的词法作用域,意味着其中的声明无法修改所在的作用域,输出结果为1, 2

与其产生类似效果的还有

setTimeout(), setInterval() // 第一个参数可以是字符串

new Function() // 最后一个参数可以接受代码字符串

这些构建的时候比 eval() 略微安全,但尽量避免使用。

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 ); // 2    ---- 	a 被泄漏到全局作用域上了

当我们将 o1 传进去,a = 2赋值操作找到了 o1.a 并且将 2 赋值给它,而当 o2 传递进去的时候,o2 并没有 a 这个属性,因此不会创建这个属性,o2.a 保持undefined。由于 o2,foo 和 全局作用域中都没有找到标识符 a ,当执行到 a = 2; 时,自动创建一个全局变量(非严格模式下)。在严格模式下 with 被完全禁止。

性能

JS引擎会在编译阶段进行数项的性能优化,其中有些优化依赖能够根据代码的词法进行静态分析,并预先确定所有变量和函数定义位置,才能在执行过程中快速找到标识符。

但如果引擎在代码中发现 eavl() 或 with ,它只能简单的假设关于标识符位置的判断时无效的,因为并不知道 这两个里面会接收或者传递什么,对词法作用域进行怎样的修改。因此如果代码中出现大量的 eval() 或 with ,那么运行起来一定会变得非常慢。