ECMA 笔记之引擎、编译器与作用域

ECMA 笔记之引擎、编译器与作用域

JavaScript 引擎

浏览器中存在渲染引擎和 JavaScript 引擎,分别负责显示请求的内容和解析并执行 JavaScript 代码。

什么是 JavaScript 引擎

什么是 JavaScript 引擎呢?百度百科的解释是:

JavaScript 引擎是一个专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器之中。

什么是 JavaScript 虚拟机 / JavaScript VM ?

++虚拟机++”是指软件驱动的给定的计算机系统的模拟器。

有很多类型的虚拟机,它们根据自己在多大程度上精确地模拟或代替真实的物理机器来分类。例如:

  • “系统虚拟机”提供了一个可以运行操作系统的完整仿真平台。Mac 用户很熟悉的 Parallels 就是一个允许你在 Mac 上运行 Windows 系统虚拟机。

  • “进程虚拟机”不具备全部的功能,能运行一个程序或者进程。Wine 是一个允许你在 Linux 机器上运行 Windows 应用的进程虚拟机,但是并不在 Linux 中提供完整的 Windows 操作系统。

JavaScript 虚拟机是一种 进程虚拟机,专门设计来解释和执行的 JavaScript 代码。

那么,确切来讲,到底什么是 JavaScript 引擎,它做了什么

JavaScript 引擎的基本工作是把开发人员写的 JavaScript 代码转换成高效、优化的代码,这样就可以通过浏览器进行解释甚至嵌入到应用中。事实上,++JavaScriptCore 自称为“优化虚拟机”++

更准确地讲,每个 JavaScript 引擎都实现了一个版本的 ECMAScript,JavaScript 是它的一个分支。随着 ECMAScript 的不断发展,JavaScript 引擎也不断改进。之所以有这么多不同的引擎,是因为它们每个都被设计运行在不同的 web 浏览器、headless 浏览器、或者像 Node.js 那样的运行时环境中。

你也许熟悉 web 浏览器,那什么是 headless 浏览器呢?它是一个没有图形用户界面的 web 浏览器。它们在对 web 产品进行自动化测试时十分有用。一个很棒的例子就是 PhantomJS。那 Node.js 又和 JavaScript 引擎有什么关系?Node.js 是一个异步的、事件驱动的框架,让你在服务器端可以使用 JavaScript。既然他们是驱动 JavaScript 的工具,所以它们也是由 JavaScript 引擎驱动。

按照上述关于虚拟机的定义,把 JavaScript 引擎称作进程虚拟机就很好理解了,因为它的唯一的目的就是读取和编译 JavaScript 代码。这并不意味着它只是个简单的引擎。比如,++JavaScriptCore++ 就有六个“构建模块”可以分析、解释、优化、垃圾回收 JavaScript 代码。

JavaScript 引擎的作用

要解释这一概念,需要了解一些其它语言编译原理的基础概念和现代语言需要的一些新编译技术。

首先来看 C/C++ 语言。它们是比较悠久的语言了,实际上就是使用编译器直接将它们编译成本地代码,这一切都是由开发人员在代码编写完成之后实施。用户只是使用这些编译好的本地代码,这些本地代码被系统的加载器加载执行,这些本地代码(也就是机器指令)由操作系统调度 CPU 直接执行,无需其它额外的辅助虚拟机等。这一过程基本上是从源代码开始,然后抽象语法树,之后中间表示,最后到本地代码

其次,来看看 Python 等脚本语言。处理脚本语言通常的做法是:开发者将写好的代码直接交给用户,用户使用脚本的解释器将脚本文件加载然后解释执行(当然,现在 Python 也可以支持将脚本编译生成中间表示)。所以这表示,对于脚本语言,没有开发人员的编译过程,当然也不是绝对,这主要因为使用场景不一样,它的目的不是高性能。这一过程是源代码,到抽象语法树,再到解释器解释执行

然后来看看 Java 语言。其做法可以理解为比较明显的两个阶段,首先,是像 C++ 语言一样的编译器,但是,同 C++ 编译器生成的本地代码结果不同,经过编译器编译之后的是字节码,字节码是平台无关的。在运行字节码阶段,Java 的运行环境也就是 Java 虚拟机会加载字节码,使用解释执行这些字节码。如果仅是这样,那 Java 的性能就差 C++ 太多了。现代 Java 虚拟机一般都引入了 JIT(Just-In-Time) 技术,也就是前面说的将字节码转变成本地代码来提高执行效率。这主要两个阶段,第一阶段对时间要求不严格,第二阶段则对每个步骤所花费的时间非常敏感,时间越短越好。

最后,回到 JavaScript 语言上来。

前面已经说了它是一种解释性脚本语言。是的,它的确是,但是随着众多工程师不断投入资源来提高它的速度,这使得它能够使用了 Java 虚拟机和 C++ 编译器中众多的技术,它的工作方式也在演变。

早期也是一样由解释器来解释它们即可,就是将源代码转变成抽象语法树,然后在抽象语法树上解释执行。随着将 Java 虚拟机的 JIT(Just-In-Time) 技术引入,现在的做法是将抽象语法树转成中间表示(也就是字节码),然后通过 JIT 技术转成本地代码,这能够大大的提高了执行效率。当然也有些做法直接从抽象语法树生成本地代码的 JIT 技术,例如 V8。

这是因为 JavaScript 跟 Java 还是有以下一些区别的:

  • 其一当然是类型,JavaScript 是无类型的语言,这使得对于对象的表示和属性的访问上比 Java 存在比较大的性能损失。不过现在有一些新的技术,参考 C++ 或者 Java 的类型系统的优点,构建隐式的类型信息,这些后面逐一介绍。

  • 其二是 Java 语言通常是将源代码编译成字节码,这个同执行阶段是分开的,也就是从源代码到抽象语法树到字节码这段时间的长短是无所谓的(或者说不是特别重要),所以主要是尽可能的生成高效的字节码即可。而对于 JavaScript 而言,这些都是在网页和 JavaScript 文件下载后同执行阶段一起在网页的加载和渲染过程中来实施的,所以对它们的处理时间也有着很高的要求。

描述 JavaScript 代码执行的过程,这一过程中因为不同技术的引入,导致非常复杂,而且因为都是在代码运行过程中来处理这些步骤,所以每个阶段时间越少越好,而且每引入一个阶段都是额外的时间开销,可能最后的本地代码执行效率很高,但是之前步骤如果耗费太多时间的话,最后的执行结果可能并不会好。所以不同的 JavaScript 引擎选择了不同的路径,这里先不仔细介绍,后面再描述它们。

所以一个 JavaScript 引擎不外乎包括以下部分:

  • 编译器:主要工作是将源代码编译成抽象语法树,然后在某些引擎中还包含将抽象语法树转换成字节码。

  • 解释器:在某些引擎中,解释器主要是接受字节码,解释执行这个字节码,然后也依赖来及回收机制等。

  • JIT工具:一个能够能够JIT的工具,将字节码或者抽象语法树转换成本地代码,当然它也需要依赖牢记

  • 垃圾回收器和分析工具(profiler):它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效。

JavaScriptCore 引擎和 V8 引擎

在 WebKit 项目中,最初只有 JavaScriptCore 引擎。在 Blink 还未独立出来之前,为了支持不同的 JavaScript 引擎,WebKit 设计了一套接口可以切换使用不同的 JavaScript 引擎(事实上,这一接口会降低性能),所以,WebKit 当时可以支持两种类型的 JavaScript 引擎,那就是 JavaScriptCore 引擎和 V8 引擎。两者都是基于 WebKit 所提供的接口来同渲染引擎协同工作。

JavaScriptCore 引擎

JavaScriptCore 引擎是 WebKit 中默认的引擎,在早期阶段,它的性能不是特别突出。特别是,它只有解释器来解释执行 JavaScript 代码,效率十分的低效。

当然,从 2008 年开始,JavaScriptCore 引擎开始一个新的优化工作,就是重新实现了编译器和字节码解释器,这就是 SquirrelFish。该工作对于引擎的性能优化作了比较大的改进。

随后,苹果内部代号为”Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 项目的,它的性能还是非常出色的,鉴于其是内部项目,所以具体还有什么特别的处理就不得而知了。

在这之后,开发者们又将内嵌缓存、基于正则表达式的 JIT 和简单的 JIT 引入到 JavaScriptCore 中。然后,又陆续加入的字节码解释器。可以看出,JavaScriptCore 引擎也在不断的高速发展中。

源代码   -->   抽象语法树   -->   字节码

上图是 JavaScriptCore 最简单的处理部分,它主要是将源代码翻译成抽象语法树,之后是平台无关的字节码,在最初的版本中,字节码会被 JavaScriptCore 引擎解释执行。在后面的版本中,逐渐加入了 JIT 编译器,将热点函数生成本地代码,后面再详细介绍它们。

V8 引擎

V8 是一个开源项目,也是一个 JavaScript 引擎的实现。最开始是一帮语言方面的专家设计出来的,之后被 Google 收购,成为了 JavaScript 引擎和众多相关技术的引领者。

它的目的很简单,就是为了提高性能,除了性能还是性能。因为之前不管 JavaScriptCore 引擎还是其它 JavaScript 引擎,在当时的情况下,它们的性能都不能令人非常满意。为了达到高性能的 JavaScript 代码执行效率从而获得更好的网页浏览效果,它甚至采用直接将 JavaScript 编译成本地代码的方式。

V8 支持众多的操作系统,包括但是不限于 Windows、Linux、Android、Mac OS X 等。同时它也是能够支持众多的硬件架构,例如 IA32、X64、ARM、MIPS 等。这么看下来,它将主流软硬件平台一网打尽,由于它是一个开源项目,开发者可以自由的使用它的强大能力,一个例子就是目前炙手可热的 NodeJS 项目,它就是基于 V8 项目的。开源的好处就是大家可以很方便地学习、贡献和使用,下图是 V8 引擎最基础的代码执行过程。

源代码     -- 解析器 -->     抽象语法树     -- JIT 全代码生成器 -->     本地代码(根据数据分析器来进一步优化)

image

从图中可以看出,首先它也是将源代码转变成抽象语法树的,这一点同 JavaScriptCore 引擎一样,之后两个引擎开始分道扬镳。

不同于 JavaScriptCore 引擎,V8 引擎并不将抽象语法树转变成字节码或者其它中间表示,而是通过 JIT 编译器的全代码生成器(full code generator)从抽象语法树直接生成本地代码,所以没有像 Java 一样的虚拟机或者字节码解释器。

这样做的原因,主要是因为减少这抽象语法树到字节码的转换时间,这一切都在网页加载时候完成,虽然可以提高优化的可能,但是这些分析可能带来巨大的时间浪费。当然,缺点也很明显,至少包括两点:

  1. 第一是某些 JavaScript 使用场景其实使用解释器更为合适,因为没有必要生成本地代码;

  2. 第二是因为没有中间表示,会减少优化的机会因为缺少一个中间表示层。

在之后的版本中,V8 工程师们引入了 Crankshaft 编译器,它能够对热点的 JS 函数进行生成的分析和优化后再生成本地代码,原因在于不是所有的 JavaScript 代码都合适做如此深层次的优化,因为优化本身也需要花费一定的时间。

JavaScript 引擎与渲染引起

对于网页的工作来说,需要两个引擎:渲染引擎和 JavaScript 引擎。它们是两个独立的模块,负责不同的事情。渲染引擎负责网页的渲染;JavaScript 引擎负责 JavaScript 代码的解析和执行。

JavaScript 引擎提供调用接口被渲染引擎使用,渲染引擎使用 JavaScript 引擎来处理 JavaScript 代码并获取结果。

这当然不是全部,事情不是这么简单,JavaScript 引擎需要能够访问渲染引擎构建的 DOM 树,所以 JavaScript 引擎通常需要提供桥接的接口,而渲染引擎则根据桥接接口来提供让 JavaScript 访问 DOM 的能力。

在现在众多的 HTML5 能力中,很多都是通过 JavaScript 接口提供给开发者的,所以这部分同样需要根据桥接接口来实现具体类,以便让 JavaScript 引擎能够回调渲染引擎的具体实现。下图是一个简单的关于两者引擎的关系。

    渲染引擎    →     调用接口     →     JavaScript 引擎
                                                ⇣
       ⇡                                 
    基于桥接接口的实现(DOM、HTML5 功能)  ⇠   桥接接口

在 WebKit/Blink 项目中,这两种引擎通过桥接接口来访问 DOM 结构,这对性能来说是一个重大的损失,因为每次 JavaScript 代码访问 DOM 都需要通过复杂和低效的桥接接口来完成。

runtime

作为一门高级的脚本语言,JavaScript 的语言内建的功能就有不少功能一般是需要运行时库的支持的:

  • 动态内存管理:new 以及其它动态创建对象的语法(对象字面量、数组字面量、正则表达式字面量)、无限制的闭包,以及对应的自动回收无用内存的功能

  • 内建类型涉及的标准库函数实现:Object、Number、String、Boolean、Function、Date、RegExp 等内建类型,以及它们的相关函数等

  • 反射,typeofwith,类型间的转换等

  • eval (以及 Function 构造函数或其它能把字符串当作程序代码来执行的功能)

JavaScript 的编译

根据你与各种编程语言打交道的水平不同,这也许是不证自明的,或者这也许令人吃惊,尽管 JavaScript 一般被划分到“动态”或者“解释型”语言的范畴,但是其实它是一个++编译型语言++。它不像许多传统意义上的编译型语言那样预先被编译好,编译的结果也不能在各种不同的分布式系统间移植。

但是无论如何,JavaScript 引擎在实施许多与传统的语言编译器相同的步骤,虽然是以一种我们不易察觉的更精巧的方式。


在传统的编译型语言处理中,一块儿源代码,你的程序,在它被执行之前通常将会经历三个步骤,大致被称为“编译”:

(1) 分词/词法分析: 将一连串字符打断成(对于语言来说)有意义的片段,称为 token(记号)

举例来说,考虑这段程序:var a = 2;。这段程序很可能会被打断成如下 token:vara=2,和 ;。空格也许会被保留为一个 token,这要看它是否是有意义的。

注意: 分词和词法分析之间的区别是微妙和学术上的,其中心在于这些 token 是否以 无状态 或 有状态 的方式被识别。简而言之,如果分词器去调用有状态的解析规则来弄清 a 是否应当被考虑为一个不同的 token,还是只是其他 token 的一部分,那么这就是 词法分析。

(2) 解析/语法分析: 将一个 token 的流(数组)转换为一个嵌套元素的树,它综合地表示了程序的语法结构。这棵树称为“抽象语法树”(AST —— Abstract Syntax Tree)。

var a = 2; 的分析结果(++在线分析地址++):

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "a"
                    },
                    "init": {
                        "type": "Literal",
                        "value": 2,
                        "raw": "2"
                    }
                }
            ],
            "kind": "var"
        }
    ],
    "sourceType": "script"
}

var a = 2; 的树也许开始于称为 VariableDeclaration(变量声明)顶层节点,带有一个称为 Identifier(标识符)的子节点(它的值为 a),和另一个称为 AssignmentExpression(赋值表达式)的子节点,而这个子节点本身带有一个称为 NumericLiteral(数字字面量)的子节点(它的值为 2)。

(3) 代码生成: 这个处理将抽象语法树转换为可执行的代码。这一部分将根据语言,它的目标平台等因素有很大的不同。

所以,与其深陷细节,我们不如笼统地说,有一种方法将我们上面描述的 var a = 2; 的抽象语法树转换为机器指令,来实际上 创建 一个称为 a 的变量(包括分配内存等等),然后在 a 中存入一个值。

注意: 引擎如何管理系统资源的细节远比我们要挖掘的东西深刻,所以我们将理所当然地认为引擎有能力按其需要创建和存储变量。


和大多数其他语言的编译器一样,JavaScript 引擎要比这区区三步复杂太多了。例如,在解析和代码生成的处理中,一定会存在优化执行效率的步骤,包括压缩冗余元素,等等。

所以,我在此描绘的只是大框架。但是我想你很快就会明白为什么我们涵盖的这些细节是重要的,虽然是在很高的层次上。

其一,JavaScript 引擎没有(像其他语言的编译器那样)大把的时间去优化,因为 JavaScript 的编译和其他语言不同,不是提前发生在一个构建的步骤中。

对 JavaScript 来说,在许多情况下,编译发生在代码被执行前的仅仅几微秒之内(或更少!)。为了确保最快的性能,JS 引擎将使用所有的招数(比如 JIT,它可以懒编译甚至是热编译,等等),而这远超出了我们关于“作用域”的讨论。

为了简单起见,我们可以说,任何 JavaScript 代码段在它执行之前(通常是 刚好 在它执行之前!)都必须被编译。所以,JS 编译器将把程序 var a = 2; 拿过来,并首先编译它,然后准备运行它,通常是立即的。

JS 引擎在这一部分做了非常多的优化,一是针对语法分析和代码生成阶段进行优化(例如针对冗余元素进行优化等),目的是提高编译后的执行效率。二是针对编译过程进行优化(如 JIT,延迟编译甚至重编译),目的是缩短编译过程,保证性能最佳。

引擎、编译器与作用域

我们将采用的学习作用域的方法,并将程序处理过程想象为一场对话。但是, 在进行这场对话呢?

让我们见一见处理程序 var a = 2; 时进行互动的演员吧,这样我们就能理解稍后将要听到的它们的对话:

  • 引擎:负责从始至终的编译和执行我们的 JavaScript 程序。
  • 编译器引擎 的朋友之一;处理所有的解析和代码生成的重活儿(见前一节)。
  • 作用域引擎 的另一个朋友;收集并维护一张所有被声明的标识符(变量)的列表,并对当前执行中的代码如何访问这些变量强制实施一组严格的规则。

说白了,作用域是什么?是一个变量的“管家”,用一个事先定义好的规则(词法作用域),管理变量的查询与访问。

为了 全面理解 JavaScript 是如何工作的,你需要开始像 引擎(和它的朋友们)那样 思考,问它们问的问题,并像它们一样回答。

JavaScript 简单的工作过程

当你看到程序 var a = 2; 时,你很可能认为它是一个语句。但这不是 引擎 所看到的。事实上,引擎 看到两个不同的语句,一个是 编译器 将在编译期间处理的,一个是 引擎 将在执行期间处理的。

那么,让我们来分析 引擎 和它的朋友们将如何处理程序 var a = 2;

编译器 首先会对程序 var a = 2; 进行词法分析来将它分解为一系列 token,然后将这些 token 解析为一棵树。

但是当 编译器 到了代码生成阶段时,它会以一种与我们可能想象的不同的方式来对待这段程序。它将会这样处理:

  1. 遇到 var a,编译器 让 作用域 去查看对于这个特定的作用域集合,变量 a 是否已经存在了。如果存在,编译器 就忽略这个声明并继续前进。否则,编译器 就让 作用域 去为这个作用域集合声明一个称为 a 的新变量。

  2. 然后 编译器引擎 生成稍后要执行的代码,来处理赋值 a = 2引擎 运行的代码首先让 作用域 去查看在当前的作用域集合中是否有一个称为 a 的变量可以访问。如果有,引擎 就使用这个变量。如果没有,引擎 就查看 其他地方(作用域链)。

如果 引擎 最终找到一个变量,它就将值 2 赋予它。如果没有,引擎 将会举起它的手并喊出一个错误!

总结来说:对于一个变量赋值,发生了两个不同的动作:第一,编译器 声明一个变量(如果先前没有在当前作用域中声明过);第二,当执行时,引擎作用域 中查询这个变量并给它赋值(如果找到的话)。

编译器术语“LHS”与“RHS”

为了继续更深入地理解,我们需要一点儿更多的编译器术语。

引擎 执行 编译器 在第二步为它产生的代码时,它必须查询变量 a 来看它是否已经被声明过了,而且这个查询是咨询 作用域 的。但是 引擎 所实施的查询的类型会影响查询的结果

在例子 var a = 2; 中,引擎 将会对变量 a 实施一个“LHS”查询。另一种类型的查询称为“RHS”。

这两个术语表示赋值操作的“Left-hand Side(左手边)”和“Right-hand Side(右手边)”。

换言之,当一个变量出现在赋值操作的左手边时,会进行 LHS 查询,当一个变量出现在赋值操作的右手边时,会进行 RHS 查询。

实际上,我们可以表述得更准确一点儿。

对于我们的目的来说,一个 RHS 是难以察觉的,因为它简单地查询某个变量的值,而 LHS 查询是试着找到变量容器本身,以便它可以赋值。从这种意义上说,RHS 的含义实质上不是真正的“一个赋值的右手边”,更准确地说,它只是意味着“不是左手边”

你也可以认为“RHS”意味着“取得他/她的源(值)”,暗示着 RHS 的意思是“去取……的值”。

例如:

console.log(a);   

这个指向 a 的引用是一个 RHS 引用,因为这里没有东西被赋值给 a。而是我们在查询 a 并取得它的值,这样这个值可以被传递进 console.log(..)

作为对比:

a = 2;

这里指向 a 的引用是一个 LHS 引用,因为我们实际上不关心当前的值是什么,我们只是想找到这个变量,将它作为 = 2 赋值操作的目标。

注意: LHS 和 RHS 意味着“赋值的左/右手边”未必像字面上那样意味着“ = 赋值操作符的左/右边”。赋值有几种其他的发生形式,所以最好在概念上将它考虑为:“赋值的目标(LHS)”和“赋值的源(RHS)”。

考虑这段程序,它既有 LHS 引用又有 RHS 引用:

function foo(a) {
	console.log(a); // 2
}

foo(2);

调用 foo(..) 的最后一行作为一个函数调用要求一个指向 foo 的 RHS 引用,意味着,“去查询 foo 的值,并把它交给我”。另外,(..) 意味着 foo 的值应当被执行,所以它最好实际上是一个函数!

这里有一个微妙但重要的赋值,你可能错过了这个代码段隐含的 a = 2。它发生在当值 2 作为参数值传递给 foo(..) 函数时,值 2 被赋值 给了参数 a。为了(隐含地)给参数 a 赋值,进行了一个 LHS 查询。

函数体内部还有一个 a 的值的 RHS 引用,它的结果值被传入 console.log(..)console.log(..) 需要一个引用来执行。它为 console 对象进行一个 RHS 查询,然后发生一个属性解析来看它是否拥有一个称为 log 的方法。

为什么我们区别 LHS 和 RHS 那么重要?

因为在变量还没有被声明(在所有被查询的 作用域 中都没找到)的情况下,这两种类型的查询的行为不同。

考虑如下代码:

function foo(a) {
	console.log( a + b );
	b = a;
}

foo( 2 );

b 的 RHS 查询第一次发生时,它是找不到的。它被说成是一个“未声明”的变量,因为它在作用域中找不到。

如果 RHS 查询在嵌套的 作用域 的任何地方都找不到一个值,这会导致 引擎 抛出一个 ReferenceError。必须要注意的是这个错误的类型是 ReferenceError

相比之下,如果 引擎 在进行一个 LHS 查询,但到达了顶层(全局 作用域)都没有找到它,而且如果程序没有运行在“Strict模式”[^note-strictmode]下,那么这个全局 作用域 将会在 全局作用域中 创建一个同名的新变量,并把它交还给 引擎

“不,之前没有这样的东西,但是我可以帮忙给你创建一个。”

在 ES5 中被加入的“Strict模式”[^note-strictmode],有许多与一般/宽松/懒惰模式不同的行为。其中之一就是不允许自动/隐含的全局变量创建。在这种情况下,将不会有全局 作用域 的变量交回给 LHS 查询,并且类似于 RHS 的情况, 引擎 将抛出一个 ReferenceError

现在,如果一个 RHS 查询的变量被找到了,但是你试着去做一些这个值不可能做到的事,比如将一个非函数的值作为函数运行,或者引用 null 或者 undefined 值的属性,那么 引擎 就会抛出一个不同种类的错误,称为 TypeError

ReferenceError 是关于 作用域 解析失败的,而 TypeError 暗示着 作用域 解析成功了,但是试图对这个结果进行了一个非法/不可能的动作。

引擎/作用域对话

function foo(a) {
	console.log( a ); // 2
}

foo( 2 );

让我们将上面的(处理这个代码段的)交互想象为一场对话。这场对话将会有点儿像这样进行:

引擎:嘿 作用域,我有一个 foo 的 RHS 引用。听说过它吗?

作用域;啊,是的,听说过。编译器 刚在一秒钟之前声明了它。它是一个函数。给你。

引擎:太棒了,谢谢!好的,我要执行 foo 了。

引擎:嘿,作用域,我得到了一个 a 的 LHS 引用,听说过它吗?

作用域:啊,是的,听说过。编译器 刚才将它声明为 foo 的一个正式参数了。给你。

引擎:一如既往的给力,作用域。再次感谢你。现在,该把 2 赋值给 a 了。

引擎:嘿,作用域,很抱歉又一次打扰你。我需要 RHS 查询 console。听说过它吗?

作用域:没关系,引擎,这是我一天到晚的工作。是的,我得到 console 了。它是一个内建对象(宿主对象提供)。给你。

引擎:完美。查找 log(..)。好的,很好,它是一个函数。

引擎:嘿,作用域。你能帮我查一下 a 的 RHS 引用吗?我想我记得它,但只是想再次确认一下。

作用域:你是对的,引擎。同一个家伙,没变。给你。

引擎:酷。传递 a 的值,也就是 2,给 log(..)

模拟上述过程,检查下面例子中的 LHS 与 RHS:

function foo(a) {
	var b = a;
	return a + b;
}

var c = foo(2);

LHS 查询有 3 处:c = foo(2)、隐含的 a = 2、以及 b = a

RHS 查询有 4 处:foo(2)= aa ++ b

作用域是一组规则,它决定了一个变量(标识符)在哪里如何 被查找。这种查询也许是为了 向这个变量赋值,这时变量是一个 LHS(左手边)引用,或者是为 取得它的值,这时变量是一个 RHS(右手边)引用。

嵌套的作用域

我们说过 作用域 是通过标识符名称查询变量的一组规则。但是,通常会有多于一个的 作用域 需要考虑。

就像一个代码块儿或函数被嵌套在另一个代码块儿或函数中一样,作用域被嵌套在其他的作用域中。所以,如果在直接作用域中找不到一个变量的话,引擎 就会咨询下一个外层作用域,如此继续直到找到这个变量或者到达最外层作用域(也就是全局作用域)。

考虑这段代码:

function foo(a) {
	console.log( a + b );
}

var b = 2;

foo( 2 ); // 4

b 的 RHS 引用不能在函数 foo 的内部被解析,但是可以在它的外围 作用域(这个例子中是全局作用域)中解析。

所以,重返 引擎作用域 的对话,我们会听到:

引擎:“嘿,foo 的 作用域,听说过 b 吗?我得到一个它的 RHS 引用。”

作用域:“没有,从没听说过。问问别人吧。”

引擎:“嘿,foo 外面的 作用域,哦,你是全局 作用域,好吧,酷。听说过 b 吗?我得到一个它的 RHS 引用。”

作用域:“是的,当然有。给你。”

遍历嵌套 作用域 的简单规则:

引擎 从当前执行的 作用域 开始,在那里查找变量,如果没有找到,就向上走一级继续查找,如此类推。如果到了最外层的全局作用域,那么查找就会停止,无论它是否找到了变量。

学习资料

你不懂 JS:作用域与闭包

什么是 JavaScript 引擎

用最简单易懂的道理告诉你,为什么JavaScript在现代引擎(V8,JavaScriptCore)下,能表现出卓越性能!

理解 WebKit 和 Chromium: 前言

理解 WebKit 和 Chromium: JavaScript 引擎简介

JavaScript 运行原理解析

JavaScript 引擎

一篇给小白看的 JavaScript 引擎指南

浏览器的工作原理:现代Web浏览器的幕后故事

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值