Chapter 2: Lexical Scope

"You Don't Know JS: Scope & Closures"

Posted by Wolfdu on July 10, 2017

本文为You don’t know JavaScript学习笔记

词法作用域(Lexical Scope)

作用域主要有两种工作模型。第一种最为普遍,被大多数编程语言所采用的词法作用域(Lexical Scope) ,本章主要学习这种作用域。另外一种叫做动态作用域,在此不做讨论。

1. 词法阶段

先搞清楚如下概念:

词法化:

词法化是大部分标准语言编译器的第一个标准工作阶段,词法化过程会对源代码中的字符进行检查,如果是有 状态的解析过程,还会赋予语义。

词法作用域:

简单来说,就是词法阶段的作用域。

分析如下代码:

function foo(a){
   var b = a * 2;

   function bar(c){
     console.log( a, b, c );
   }

   bar( b * 3 );
}

foo(2);// 2, 4 ,12

以上代码中有3个逐级嵌套的作用域。

可视为如下气泡图:

java-javascript

  • 1.全局作用域,其中只有一个标识符: foo
  • 2.foo所创建的作用域,其中有3个标识符:a,b和bar。
  • 3.bar所创建的作用域,其中有一个标识符: c。

词法作用域,由其对应的代码写在那里决定,他们都是逐级包含的。

没有任何函数的作用域可以同时出现在两个外部作用域中。

Engine的查找:

在上面的代码中,执行console.log(...)声明时,并查找a,b,c三个变量的引用。他首先会从内 部作用域即bar(…)函数作用域下开始。在Engine无法找到a的时候他会前往上一层作用域 即foo(…)作用域中继续查找直到找到最外层作用域。

作用域查找会在找到第一个匹配的标识符时停止。

遮蔽效应:

在多层嵌套的作用域中出现多个定义同名的标识符,内部的标识符就“遮蔽”了外部作用域的标识符。

非全局变量如果被遮蔽了,无论如何都无法被访问到。

函数的词法作用域:

无论函数在哪被调用,也无论如何被调用,他的词法作用域都只由他被声明时所处的位置决定。

词法作用域查找只会查找一级标识符。

2. 欺骗词法(Cheating Lexical)

词法作用域是由代码编写期间函数所声明的位置来决定的,那么我们如何在运行时“修改”词法作用域呢?

JS中有两种机制来实现这个目的。但是社区普遍认为在代码中使用这两种机制并不是明智的,所以此次学习 暂时不会去进一步深入。

2.1 eval

JavaScript中eval(...)函数可以接受一个字符串作为参数,并将其中的内容当做书写时就存在于程序中 的位置的代码一样。

2.2 with

with可以将一个没有或者有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被 处理为定义在这个作用域中的词法标识符。

不推荐使用eval(...)with的原因是会被严格模式所影响。with被完全禁止,在保留核心功能 前提下,间接或非安全的使用eval(...)也是被禁止的。

2.3 性能(Performance)

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

但是引擎在代码中发现了eval(...)或者with,他只能简单的假设关于标识符的位置的判断都是无效的 ,因为无法在此法阶段明确的知道eval(...)会接受到什么代码,这些代码会如何对作用域进行修改, 也无法知道传递给with用来创建的新词法作用域的对象内容到底是什么。

因此引擎在遇到eval或者with时,最为简单的处理就是完全不做优化(因为优化可能会无意义)。

所以不要使用他们。

小结

词法作用域的定义。即在词法阶段的作用域。

了解欺骗词法,以及它对性能的影响。