JavaScript 整理

  在 JavaScript 的模块中,首先我们可以把语言按照文法、语义和运行时来拆分,这符合编程语言的一般规律: 用一定的词法和语句,表达一定语句,从而操作运行时

语义

文法

词法

语法

运行时

按照程序的一般规律,把运行时分为数据结构和算法部分: 数据结构包含类型和实例(JavaScript 的类型系统就是它的 7 种基本类型和 7 种语言类型,实例就是它的内置对象部分)。所谓算法就是 JavaScript 的执行过程。

数据结构

类型

JavaScript 的 7 种语言类型,包括 6 种原始数据类型和 Object

  1. Undefined
    • 含义: 尚未赋值的变量的值undefined
    • 取值: undefined (也可以通过void 0得到)
  2. Null
    • 含义: 表示变量值为null
    • 取值: null
  3. Boolean
    • 含义: Boolean表示逻辑意义上的真或假
    • 取值: true, false
  4. String
    • 含义: String表示文本数据 (采用 UTF16 编码, 处理非 BMP 的字符符需要格外小心)
    • 取值: 最大长度为2^53-1
    • Note: 现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF 是 Unicode 的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。
  5. Number
    • 含义: Number表示数字 (Number 是双精度 64 位浮点格式(IEEE 754)中的数字数据类型)
    • 取值: 取值范围 [-(2^53 -1) , 2^53 -1]
  6. Symbol (new in ECMAScript 6)
    • 含义: Symbol是一个唯一的不可变的原始值
    • 取值: 使用全局的Symbol函数创建 (不可以使用 new 创建 Symbol 对象, Symbol 值不能进行运算)
  7. Object
    • 含义: Object可以看做属性的集合
    • Object 属性主要分成两类 (通常用于定义属性的代码会产生数据属性,其中的 writableenumerableconfigurable 都默认为 true

JavaScript 的 7 种规范类型

  1. List 和 Record: 用于描述函数传参过程。
  2. Set: 主要用于解释字符集等。
  3. Completion Record: 用于描述异常、跳出等语句执行过程。
  4. Reference: 用于描述对象属性访问、delete 等。
  5. Property Descriptor: 用于描述对象的属性。
  6. Lexical Environment 和 Environment Record: 用于描述变量和作用域。
  7. Data Block: 用于描述二进制数据。
Object 对象

JavaScript 对象是键和值之间的映射。

Properties 属性

使用对象文字语法,初始化一组有限的属性;然后可以添加和删除属性。
属性值可以是任何类型的值,包括其他对象,这使得能够构建复杂的数据结构。使用键值标识属性。键值是StringSymbol值。
Object 属性主要分成两类: 数据属性访问器属性

  • 数据属性: 大多数时候我们只关心数据属性的值
    • value: 就是属性的值
    • writable: 决定属性能否被赋值
    • enumerable: 决定 for in 能否枚举该属性
    • configurable: 决定该属性能否被删除或者改变特征值
  • 访问器属性: 每次访问属性,都会执行 getter 或者 setter 函数
    • getter: 函数或 undefined,在取属性值时被调用
    • setter: 函数或 undefined,在设置属性值时被调用
    • enumerable: 决定 for in 能否枚举该属性
    • configurable: 决定该属性能否被删除或者改变特征值

我们可以使用内置函数 Object.getOwnPropertyDescriptor(obj, prop) 来查看;也可以使用Object.defineProperty(obj, prop, descriptor)
来定义属性。

实例

JavaScript 的实例对象主要分成宿主对象和内置对象两个部分

  • 宿主对象(host Objects): 由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。(宿主对象也分为固有的和用户可创建的两种,比如 document.createElement 就可以创建一些 dom 对象。)

  • 内置对象(Built-in Objects): 由 JavaScript 语言提供的对象。

    • 固有对象(Intrinsic Objects): 由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
    • 原生对象(Native Objects): 可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
    基本类型 基础功能和数据结构 错误类型 二进制操作 带类型的数组
    Boolean Array Error ArrayBuffer Float32Array
    String Date EvalError SharedArrayBuffer Float64Array
    Number RegExp RangeError DataView Int8Array
    Symbol Promise ReferenceError Int16Array
    Object Proxy SyntaxError Int32Array
    Map TypeError UInt8Array
    WeakMap URIError UInt16Array
    Set UInt32Array
    WeakSet UInt8ClampendArray
    Function
    • 普通对象(Ordinary Objects): 由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。
应用和机制

执行过程(算法)

如果我们是浏览器或者 Node 的开发者,我们该如何使用 JavaScript 引擎。

当拿到一段 JavaScript 代码时,浏览器或者 Node 环境首先要做的就是;传递给 JavaScript 引擎,并且要求它去执行。

然而,执行 JavaScript 并非一锤子买卖,宿主环境当遇到一些事件时,会继续把一段代码传递给 JavaScript 引擎去执行,此外,我们可能还会提供 API 给 JavaScript 引擎,比如 setTimeout 这样的 API,它会允许 JavaScript 在特定的时机执行。

所以,我们首先应该形成一个感性的认知: 一个 JavaScript 引擎会常驻于内存中,它等待着我们(宿主)把 JavaScript 代码或者函数传递给它执行。

在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,这也就意味着,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。

但是,在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。

由于我们这里主要讲 JavaScript 语言,那么采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

事件循环

JavaScript 引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以在 Node 术语中,也会把这个部分称为事件循环。

不过,术语本身并非我们需要重点讨论的内容,我们在这里把重点放在事件循环的原理上。在底层的 C/C++ 代码中,这个事件循环是一个跑在独立线程中的循环,我们用伪代码来表示,大概是这样的:

1
2
3
4
while(TRUE) {
r = wait();
execute(r);
}

我们可以看到,整个循环做的事情基本上就是反复“等待 - 执行”。当然,实际的代码中并没有这么简单,还有要判断循环是否结束、宏观任务队列等逻辑,这里为了方便你理解,我就把这些都省略掉了。

这里每次的执行过程,其实都是一个宏观任务。我们可以大概理解: 宏观任务的队列就相当于事件循环。

在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列:

event-loop

有了宏观任务和微观任务机制,我们就可以实现 JS 引擎级和宿主级的任务了,例如: Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。

微任务的执行

Promise

Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。

Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。

async/await

async/await 是 ES2016 新加入的特性,它提供了用 for、if 等代码结构来编写异步的方式。它的运行时基础是 Promise,面对这种比较新的特性,我们先来看一下基本用法。

async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。

async 函数是一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样,就定义了一个 async 函数,我们可以在其中使用 await 来等待一个 Promise。

generator/iterator 也常常被跟异步一起来讲,我们必须说明 generator/iterator 并非异步代码,只是在缺少 async/await 的时候,一些框架(最著名的要数 co)使用这样的特性来模拟 async/await。

但是 generator 并非被设计成实现异步,所以有了 async/await 之后,generator/iterator 来模拟异步的方法应该被废弃。

函数的执行

  • 闭包: closure
    • 环境部分
      • 环境
        • 变量环境
        • 词法环境
      • 标识符列表
    • λ 表达式
  • JavaScript 函数
    • 环境部分
      • 词法作用域: 执行上下文的一部分
        • scope
        • this 值
      • 函数里面不带 var/let/const 的变量
    • 函数
闭包
  1. 编译原理中,它是处理语法产生式的一个步骤
  2. 计算几何中,它表示包裹平面点集的凸多边形(翻译作凸包)
  3. 而在编程语言领域,它表示一种函数

闭包这个概念第一次出现在 1964 年的《The Computer Journal》上,由 P. J. Landin 在《The mechanical evaluation of expressions》一文中提出了 applicative expression 和 closure 的概念。

the-mechanical-evaluation-of-expressions

在上世纪 60 年代,主流的编程语言是基于 lambda 演算的函数式编程语言,所以这个最初的闭包定义,使用了大量的函数式术语。一个不太精确的描述是“带有一系列信息的 λ 表达式”。对函数式语言而言,λ 表达式其实就是函数。

我们可以这样简单理解一下,闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境

这个古典的闭包定义中,闭包包含两个部分。

  • 环境部分
    • 环境
    • 标识符列表
  • 表达式部分

当我们把视角放在 JavaScript 的标准中,我们发现,标准中并没有出现过 closure 这个术语,但是,我们却不难根据古典定义,在 JavaScript 中找到对应的闭包组成部分。

  • 环境部分
    • 环境: 函数的词法环境(执行上下文的一部分)
    • 标识符列表: 函数中用到的未声明的变量
  • 表达式部分: 函数体

至此,我们可以认为,JavaScript 中的函数完全符合闭包的定义。它的环境部分是函数词法环境部分组成,它的标识符列表是函数中用到的未声明变量,它的表达式部分就是函数体。

这里我们容易产生一个常见的概念误区,有些人会把 JavaScript 执行上下文,或者作用域(Scope,ES3 中规定的执行上下文的一部分)这个概念当作闭包。

实际上 JavaScript 中跟闭包对应的概念就是“函数”,可能是这个概念太过于普通,跟闭包看起来又没什么联系,所以大家才不自觉地把这个概念对应到了看起来更特别的“作用域”吧。

执行上下文: 执行的基础设施

相比普通函数,JavaScript 函数的主要复杂性来自于它携带的“环境部分”。当然,发展到今天的 JavaScript,它所定义的环境部分,已经比当初经典的定义复杂了很多。

JavaScript 中与闭包“环境部分”相对应的术语是“词法环境”,但是 JavaScript 函数比 λ 函数要复杂得多,我们还要处理 this、变量声明、with 等等一系列的复杂语法,λ 函数中可没有这些东西,所以,在 JavaScript 的设计中,词法环境只是 JavaScript 执行上下文的一部分。

JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为: “执行上下文”。

  • 执行上下文在 ES3 中,包含三个部分。
    • scope: 作用域,也常常被叫做作用域链。
    • variable object: 变量对象,用于存储变量的对象。
    • this value: this 值。
  • 在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。
    • lexical environment: 词法环境,当获取变量时使用。
    • variable environment: 变量环境,当声明变量时使用。
    • this value: this 值。
  • 在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容。
    • lexical environment: 词法环境,当获取变量或者 this 值时使用。
    • variable environment: 变量环境,当声明变量时使用
    • code evaluation state: 用于恢复代码执行位置。
    • Function: 执行的任务是函数时使用,表示正在被执行的函数。
    • ScriptOrModule: 执行的任务是脚本或者模块时使用,表示正在被执行的代码。
    • Realm: 使用的基础库和内置对象实例。
    • Generator: 仅生成器上下文有这个属性,表示当前生成器。
Realm

在最新的标准(9.0)中,JavaScript 引入了一个新概念 Realm,它的中文意思是“国度”“领域”“范围”。在 ES2016 之前的版本中,标准中甚少提及{}的原型问题。但在实际的前端开发中,通过 iframe 等方式创建多 window 环境并非罕见的操作,所以,这才促成了新概念 Realm 的引入。

Realm 中包含一组完整的内置对象,而且是复制关系。

对不同 Realm 中的对象操作,会有一些需要格外注意的问题,比如 typeof、instanceOf 几乎是失效的。

以下代码展示了在浏览器环境中获取来自两个 Realm 的对象,它们跟本土的 Object 做 instanceOf 时会产生差异:

1
2
3
4
5
6
7
8
9
10
var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"

var b1 = iframe.contentWindow.b;
var b2 = {};

console.log(typeof b1, typeof b2); //undefined object

console.log(b1 instanceof Object, b2 instanceof Object); //false true
函数的种类
  • 普通函数: 用 function 关键字定义的函数

  • 箭头函数: 用 => 运算符定义的函数

  • 方法: 在 class 中定义的函数

  • 生成器函数: 用 function * 定义的函数

    1
    2
    3
    function* foo() {
    // code
    }
  • 类: 用 class 定义的类,实际上也是函数

  • 异步函数: 普通函数、箭头函数和生成器函数加上 async 关键字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function foo(){
    // code
    }
    const foo = async () => {
    // code
    }
    async function foo*(){
    // code
    }
this 关键字的行为

this 是 JavaScript 中的一个关键字,它的使用方法类似于一个变量(但是 this 跟变量的行为有很多不同,上一节课我们讲了一些普通变量的行为和机制,也就是 var 声明和赋值、let 的内容)。

this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的 this 值也不同。

  • 调用函数时使用的引用,决定了函数执行时刻的 this 值。
  • 箭头函数的调用方式,不会影响它的 this 值。

    生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的,异步箭头函数与箭头函数行为是一致的。

this 关键字的机制

函数能够引用定义时的变量,如上文分析,函数也能记住定义时的 this,因此,函数内部必定有一个机制来保存这些信息。

在 JavaScript 标准中,为函数规定了用来保存定义时上下文的私有属性 [[Environment]]。

当一个函数执行时,会创建一条新的执行环境记录,记录的外层词法环境(outer lexical environment)会被设置成函数的 [[Environment]]。

这个动作就是切换上下文了,我们假设有这样的代码:

1
2
3
4
5
6
7
8
9
10
var a = 1;
foo();

在别处定义了 foo:

var b = 2;
function foo(){
console.log(b); // 2
console.log(a); // error
}

这里的 foo 能够访问 b(定义时词法环境),却不能访问 a(执行时的词法环境),这就是执行上下文的切换机制了。

JavaScript 用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。如下图所示:

javascript-execution-stack

当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。

而 this 则是一个更为复杂的机制,JavaScript 标准定义了 [[thisMode]] 私有属性。

[[thisMode]] 私有属性有三个取值。

lexical: 表示从上下文中找 this,这对应了箭头函数。
global: 表示当 this 为 undefined 时,取全局对象,对应了普通函数。
strict: 当严格模式时使用,this 严格按照调用时传入的值,可能为 null 或者 undefined。
非常有意思的是,方法的行为跟普通函数有差异,恰恰是因为 class 设计成了默认按 strict 模式执行。

我们可以用 strict 达成与上一节中方法的例子一样的效果:

1
2
3
4
5
6
7
8
9
10
11
'use strict';
function showThis() {
console.log(this);
}

var o = {
showThis: showThis,
};

showThis(); // undefined
o.showThis(); // o

函数创建新的执行上下文中的词法环境记录时,会根据 [[thisMode]] 来标记新纪录的 [[ThisBindingStatus]] 私有属性。

代码执行遇到 this 时,会逐层检查当前词法环境记录中的 [[ThisBindingStatus]],当找到有 this 的环境记录时获取 this 的值。

这样的规则的实际效果是,嵌套的箭头函数中的代码都指向外层 this,例如:

1
2
3
4
5
6
7
8
9
10
var o = {};
o.foo = function foo() {
console.log(this);
return () => {
console.log(this);
return () => console.log(this);
};
};

o.foo()()(); // o, o, o

这个例子中,我们定义了三层嵌套的函数,最外层为普通函数,两层都是箭头函数。

这里调用三个函数,获得的 this 值是一致的,都是对象 o。

操作 this 的内置函数

Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值,示例如下:

1
2
3
4
5
6
function foo(a, b, c) {
console.log(this);
console.log(a, b, c);
}
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);

这里 call 和 apply 作用是一样的,只是传参方式有区别。

此外,还有 Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数:

1
2
3
4
5
function foo(a, b, c) {
console.log(this);
console.log(a, b, c);
}
foo.bind({}, 1, 2, 3)();

有趣的是,call、bind 和 apply 用于不接受 this 的函数类型如箭头、class 都不会报错。

这时候,它们无法实现改变 this 的能力,但是可以实现传参。

new 与 this
  1. new 的执行过程

    • 以构造器的 prototype 属性(注意与私有字段 [[prototype]] 的区分)为原型,创建新对象;
    • 将 this 和调用参数传给构造器,执行;
    • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
  2. new 和函数的对应关系

    仅普通函数和类能够跟 new 搭配使用

    函数类型 new
    普通函数 新对象
    箭头函数 报错
    方法 报错
    生成器 报错
    新对象
    异步普通函数 报错
    异步箭头函数 报错
    异步生成器函数 报错

语句级的执行

JavaScript 语句执行机制涉及的一种基础类型: Completion 类型

JavaScript 语句执行的完成状态,我们用一个标准类型来表示: Completion Record

Completion Record 表示一个语句执行完之后的结果,它有三个字段:

  • [[type]] 表示完成的类型,有 break continue return throw 和 normal 几种类型;
  • [[value]] 表示语句的返回值,如果语句没有,则是 empty;
  • [[target]] 表示语句的目标,通常是一个 JavaScript 标签(标签在后文会有介绍)。

JavaScript 正是依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中,实现各种控制。

普通语句
  • 声明类语句
    • var 声明

      在只有 var,没有 let 的旧 JavaScript 时代,诞生了一个技巧,叫做: 立即执行的函数表达式(IIFE),通过创建一个函数,并且立即执行,来构造一个新的域,从而控制 var 的范围。

      由于语法规定了 function 关键字开头是函数声明,所以要想让函数变成函数表达式,我们必须得加点东西,最常见的做法是加括号。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > (function(){
      > var a;
      > //code
      > }());
      >
      > (function(){
      > var a;
      > //code
      > })();
      >

      但是,括号有个缺点,那就是如果上一行代码不写分号,括号会被解释为上一行代码最末的函数调用,产生完全不符合预期,并且难以调试的行为,加号等运算符也有类似的问题。所以一些推荐不加分号的代码风格规范,会要求在括号前面加上分号。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      > ;(function(){
      > var a;
      > //code
      > }())
      >
      > ;(function(){
      > var a;
      > //code
      > })()
      >

      我比较推荐的写法是使用 void 关键字。也就是下面的这种形式。

      1
      2
      3
      4
      5
      > void function(){
      > var a;
      > //code
      > }();
      >

      这有效避免了语法问题,同时,语义上 void 运算表示忽略后面表达式的值,变成 undefined,我们确实不关心 IIFE 的返回值,所以语义也更为合理。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      > var b;
      > void function(){
      > var env = {b:1};
      > b = 2;
      > console.log("In function b:", b);
      > with(env) {
      > var b = 3;
      > console.log("In with b:", b);
      > }
      > }();
      > console.log("Global b:", b);
      >
    • const 声明

    • let 声明

      let 是 ES6 开始引入的新的变量声明模式,比起 var 的诸多弊病,let 做了非常明确的梳理和规定。

      为了实现 let,JavaScript 在运行时引入了块级作用域。也就是说,在 let 出现之前,JavaScript 的 if for 等语句皆不产生作用域。

    • 函数声明

    • 类声明

  • 表达式语句
  • 空语句
  • with 语句
  • debugger 语句

普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句。

这些语句中,只有表达式语句会产生 [[value]],当然,从引擎控制的角度,这个 value 并没有什么用处。

如果你经常使用 chrome 自带的调试工具,可以知道,输入一个表达式,在控制台可以得到结果,但是在前面加上 var,就变成了 undefined。

chrome-console-sentence-execution

Chrome 控制台显示的正是语句的 Completion Record 的 [[value]]。

语句块

大括号括起来的一组语句,它是一种语句的复合结构,可以嵌套。

语句块本身并不复杂,我们需要注意的是语句块内部的语句的 Completion Record 的 [[type]] 如果不为 normal,会打断语句块后续的语句执行。

比如我们考虑,一个 [[type]] 为 return 的语句,出现在一个语句块中的情况。

从语句的这个 type 中,我们大概可以猜到它由哪些特定语句产生,我们就来说说最开始的例子中的 return。

return 语句可能产生 return 或者 throw 类型的 Completion Record。我们来看一个例子。

先给出一个内部为普通语句的语句块:

1
2
3
4
5
{
var i = 1; // normal, empty, empty
i++; // normal, 1, empty
console.log(i); //normal, undefined, empty
} // normal, undefined, empty

在每一行的注释中,都给出了语句的 Completion Record 值。

我们看到,在一个 block 中,如果每一个语句都是 normal 类型,那么它会顺次执行。接下来我们加入 return 试试看。

1
2
3
4
5
6
{
var i = 1; // normal, empty, empty
return i; // return, 1, empty
i++;
console.log(i);
} // return, 1, empty

但是假如我们在 block 中插入了一条 return 语句,产生了一个非 normal 记录,那么整个 block 会成为非 normal。这个结构就保证了非 normal 的完成类型可以穿透复杂的语句嵌套结构,产生控制效果。

控制型语句
  • if
  • switch
  • for
    • for
    • for…of
    • for-await-of
    • for…in
  • while
    • while
    • do-while
  • continue
  • break
  • return
  • throw
  • try

控制型语句带有 if、switch 关键字,它们会对不同类型的 Completion Record 产生反应。

控制类语句分成两部分,一类是对其内部造成影响,如 if、switch、while/for、try。另一类是对外部造成影响如 break、continue、return、throw,这两类语句的配合,会产生控制代码执行顺序和执行逻辑的效果,这也是我们编程的主要工作。

一般来说, for/while - break/continue 和 try - throw 这样比较符合逻辑的组合,是大家比较熟悉的,但是,实际上,我们需要控制语句跟 break 、continue 、return 、throw 四种类型与控制语句两两组合产生的效果。

break continue return throw
if 穿透 穿透 穿透 穿透
switch 消费 穿透 穿透 穿透
for/while 消费 消费 穿透 穿透
function 报错 报错 消费 穿透
try 特殊处理 特殊处理 特殊处理 消费
catch 特殊处理 特殊处理 特殊处理 穿透
finally 特殊处理 特殊处理 特殊处理 穿透

因为 finally 中的内容必须保证执行,所以 try/catch 执行完毕,即使得到的结果是非 normal 型的完成记录,也必须要执行 finally。

而当 finally 执行也得到了非 normal 记录,则会使 finally 中的记录作为整个 try 结构的结果。

带标签的语句

任何 JavaScript 语句是可以加标签的,在语句前加冒号即可。

大部分时候,这个东西类似于注释,没有任何用处。唯一有作用的时候是: 与完成记录类型中的 target 相配合,用于跳出多层循环。

1
2
3
4
5
6
outer: while (true) {
inner: while (true) {
break outer;
}
}
console.log('finished');

break/continue 语句如果后跟了关键字,会产生带 target 的完成记录。一旦完成记录带了 target,那么只有拥有对应 label 的循环语句会消费它。