前言
上篇文章我们了解到“作用域”是一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。
作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,我们会对这种作用域进行深入讨论。另外一种叫作动态作用域,仍有一些编程语言在使用(比如 Bash 脚本、Perl 中的一些模式等)。这一章我们将就词法作用域做一个详细的讨论
正文
词法阶段
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变
举个例子来进一步说明方便大家理解:
在这个例子中有三个逐级嵌套的作用域。
- 第一个作用域包含着整个全局作用域,其中只有一个标识符:foo。
- 第二个作用域包含着 foo 所创建的作用域,其中有三个标识符:a、bar 和 b。
- 第三个包含着 bar 所创建的作用域,其中只有一个标识符:c。
词法作用域的查找
作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
欺骗词法
词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢?JavaScript 中有两种机制来实现这个目的。社区普遍认为在代码中使用这两种机制并不是什么好注意。但是关于它们的争论通常会忽略掉最重要的点:欺骗词法作用域会导致性能下降。
eval
JavaScript 中的 eval(…) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。
具体可以看下面这个例子:
这段代码的输出结果并不是1, 2, 在调用foo的过程中相当于在foo的作用域创建了b,并覆盖了外部的外部作用域的同名变量,起到一个”欺骗“的作用
with
with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。现在已经不被推荐使用,后面会说明原因,我们来看一下with的应用进一步了解一下它的用法
但是其实,with可能会产生一些意料之外的场景,例如下面这个例子:
我们来看一下这个例子,首先是o1处,因为有a属性,所以正常赋值,而o2处并没有a属性,所以仍然是undefined,但是全局的a是哪里来的呢,我们上一章提到作用域的两种查询LHS和RHS,这里明显属于LHS,当LHS查询在作用域中找不到a属性的时候,便自动在全局创建了一个新的a变量,这就是with的作用域“欺诈”
所以这就是eval和with在用法功能上被禁用的原因
eval和with在性能上的损耗
eval和with不仅仅在用法功能上可能出现意料之外的词法欺骗情况,在性能上也会有很大的损耗
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。但如果引擎在代码中发现了 eval(…) 或 with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道 eval(…) 会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。
如果代码中大量使用 eval(…) 或 with,那么运行起来一定会变得非常慢。无论引擎多聪明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代码会运行得更慢这个事实。
小结
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
JavaScript 中有两个机制可以“欺骗”词法作用域:eval(…) 和 with。请避免对他们的使用