1. 事件循环

一个 JavaScript 引擎会常驻于内存中,它等待着我们(宿主:浏览器、Node)把 JavaScript 代码或者函数传递给它执行。我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

既然 JavaScript 是单线程的,那么所有的任务就需要排队执行。

  • JavaScript 中的任务可以被划分为宏任务(Macrotask)或者微任务(Microtask)
  • 像鼠标事件,键盘事件, “setTimeout”等就属于宏任务,需要注意的是,主线程的整体代码(script 标签),也是一个宏任务
  • process.nextTick,Promise.then(), MutaionObserver 就属于微任务

简单概括一下事件循环,就是

1.执行宏任务队列中第一个任务,执行完后移除它

2.执行所有的微任务,执行完后移除它们

3.执行下一轮宏任务(重复步骤 2)

如此循环就形成了 event loop,其中,每轮执行一个宏任务和所有的微任务。

浏览器通常会尝试每秒渲染 60 次页面,以达到每秒 60 帧(60 fps)的速度。在页面渲染时,任何任务都无法再进行修改。

如果想要实现平滑流畅的应用,单个任务和该任务附属的所有微任务,都应在 16ms 内完成。

2. 微任务的执行

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

javascript执行adb JavaScript执行流程_前端

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

接下来,我们来详细介绍一下 Promise。

2.2 Promise

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

Promise 的基本用法示例如下:

function sleep(duration) {
  return new Promise(function(resolve, reject) {
    setTimeout(resolve, duration);
  });
}
sleep(1000).then(() => console.log('finished'));

web前端开发学习Q-q-u-n:784783012 ,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频)

为了理解微任务始终先于宏任务,我们设计一个实验:执行一个耗时 1 秒的 Promise。

setTimeout(() => console.log('d'), 0);
var r = new Promise(function(resolve, reject) {
  resolve();
});
r.then(() => {
  var begin = Date.now();
  while (Date.now() - begin < 1000);
  console.log('c1');
  new Promise(function(resolve, reject) {
    resolve();
  }).then(() => console.log('c2'));
});

这里我们强制了 1 秒的执行耗时,这样,我们可以确保任务 c2 是在 d 之后被添加到任务队列。

我们可以看到,即使耗时一秒的 c1 执行完毕,再 enque 的 c2,仍然先于 d 执行了,这很好地解释了微任务优先的原理。

通过一系列的实验,我们可以总结一下如何分析异步执行的顺序:

  • 首先我们分析有多少个宏任务;
  • 在每个宏任务中,分析有多少个微任务;
  • 根据调用次序,确定宏任务中的微任务执行次序;
  • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
  • 确定整个顺序。

接下来我们看道例子来介绍上面流程:

Promise.resolve().then(() => {
  console.log('Promise1');
  setTimeout(() => {
    console.log('setTimeout2');
  }, 0);
});
setTimeout(() => {
  console.log('setTimeout1');
  Promise.resolve().then(() => {
    console.log('Promise2');
  });
}, 0);
  • 一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出 Promise1,同时会生成一个宏任务 setTimeout2
  • 然后去查看宏任务队列,宏任务 setTimeout1 在 setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1
  • 在执行宏任务 setTimeout1 时会生成微任务 Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promise2
  • 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2

最后输出结果是 Promise1,setTimeout1,Promise2,setTimeout2

2.3 执行机制

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务

3. 函数的执行

理解函数执行过程的知识,先理清这些概念也非常重要。

  • 闭包;
  • 作用域链;
  • 执行上下文;
  • this 值。

实际上,尽管它们是表示不同的意思的术语,所指向的几乎是同一部分知识,那就是函数执行过程相关的知识。我们可以简单看一下图。

javascript执行adb JavaScript执行流程_前端_02

3.1 闭包

3.1.1 闭包的原理

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

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

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

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

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

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

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

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

(通俗解释:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。)

3.2 作用域链

3.2.1 作用域

JavaScript 中的作用域是指变量的可访问性,也就是说,程序的哪些部分可以访问变量。

ES3 中规定的执行上下文的一部分。

3.2.2 作用域种类
  • 全局作用域:不在任何函数或块(一对花括号)内的变量都在全局范围内。可以从程序中的任何位置访问全局范围中的变量。
  • 函数作用域:在函数内声明的变量在函数作用域内。它们只能在该函数内访问,这意味着无法从外部代码访问它们。
  • 块作用域:ES6 引入了 let 和 const 变量,与 var 变量不同,它们可以作用于最近的花括号对。这意味着,不能从那对花括号外面访问它们。
3.2.3 作用域链

当在 JavaScript 中使用变量时,JavaScript 引擎将尝试在当前作用域中查找变量的值。如果它找不到变量,它将查看外部作用域,并将继续这样做,直到找到变量或达到全局作用域。

如果它仍然无法找到变量,它将隐式声明全局作用域内的变量(如果不是在严格模式下)或返回错误。

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

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

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

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

因为这部分术语经历了比较多的版本和社区的演绎,所以定义比较混乱,这里我们先来理一下 JavaScript 中的概念。

3.3.1 执行上下文在 ES3 中
  • scope:作用域,也常常被叫做作用域链。
  • variable object:变量对象,用于存储变量的对象。
  • this value:this 值。
3.3.2 在 ES5 中
  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • this value:this 值。
3.3.3 在 ES2018 中
  • lexical environment:词法环境,当获取变量或者 this 值时使用。
  • variable environment:变量环境,当声明变量时使用
  • code evaluation state:用于恢复代码执行位置。
  • Function:执行的任务是函数时使用,表示正在被执行的函数。
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
  • Realm:使用的基础库和内置对象实例。
  • Generator:仅生成器上下文有这个属性,表示当前生成器。

尽管我们介绍了这些定义,但我并不打算按照 JavaScript 标准的思路,从实现的角度去介绍函数的执行过程,这是不容易被理解的。

我想试着从代码实例出发,跟你一起推导函数执行过程中需要哪些信息,它们又对应着执行上下文中的哪些部分。

比如,我们看以下的这段 JavaScript 代码:

var b = {};
let c = 1;
this.a = 2;

要想正确执行它,我们需要知道以下信息:

  • var 把 b 声明到哪里;
  • b 表示哪个变量;
  • b 的原型是哪个对象;
  • let 把 c 声明到哪里;
  • this 指向哪个对象。

这些信息就需要执行上下文来给出了,这段代码出现在不同的位置,甚至在每次执行中,会关联到不同的执行上下文,所以,同样的代码会产生不一样的行为。

var 声明与赋值

我们来分析一段代码:

var b = 1;

通常我们认为它声明了 b,并且为它赋值为 1,var 声明作用域函数执行的作用域。也就是说,var 会穿透 for 、if 等语句。

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

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

(function() {
  var a;
  //code
})();

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

(function() {
  var a;
  //code
})();

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

void (function() {
  var a;
  //code
})();

web前端开发学习Q-q-u-n:784783012 ,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频)

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

let

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

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

我简单统计了下,以下语句会产生 let 使用的作用域:

• for;
• if;
• switch;
• try/catch/finally
Relam

在最新的标准(9.0)中,JavaScript 引入了一个新概念 Realm,它的中文意思是“国度”“领域”“范围”。这个英文的用法就有点比喻的意思,几个翻译都不太适合 JavaScript 语境,所以这里就不翻译啦。

我们继续来看这段代码:

var b = {};

在 ES2016 之前的版本中,标准中甚少提及{}的原型问题。但在实际的前端开发中,通过 iframe 等方式创建多 window 环境并非罕见的操作,所以,这才促成了新概念 Realm 的引入。

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

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

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

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); //object object

console.log(b1 instanceof Object, b2 instanceof Object); //false true

可以看到,由于 b1、 b2 由同样的代码“ {} ”在不同的 Realm 中执行,所以表现出了不同的行为。

3.4 各种函数

在 ES2018 中,函数已经是一个很复杂的体系了,我在这里整理了一下。

  • 第一种,普通函数:用 function 关键字定义的函数。
  • 第二种,箭头函数:用 => 运算符定义的函数。
  • 第三种,方法:在 class 中定义的函数。
  • 第四种,生成器函数:用 function * 定义的函数。
  • 第五种,类:用 class 定义的类,实际上也是函数。
  • 第六 / 七 / 八种,异步函数:普通函数、箭头函数和生成器函数加上 async 关键字

对普通变量而言,这些函数并没有本质区别,都是遵循了“继承定义时环境”的规则,它们的一个行为差异在于 this 关键字。

3.5 this 值

3.5.1 this 关键字的取值

this 是 JavaScript 中的一个关键字,它的使用方法类似于一个变量。

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

function showThis() {
  console.log(this);
}

var o = {
  showThis: showThis
};

showThis(); // global
o.showThis(); // o

在这个例子中,我们定义了函数 showThis,我们把它赋值给一个对象 o 的属性,然后尝试分别使用两个引用来调用同一个函数,结果得到了不同的 this 值。

普通函数的 this 值由“调用它所使用的引用”决定,其中奥秘就在于:我们获取函数的表达式,它实际上返回的并非函数本身,而是一个 Reference 类型。

Reference 类型由两部分组成:一个对象和一个属性值。不难理解 o.showThis 产生的 Reference 类型,即由对象 o 和属性“showThis”构成。

当做一些算术运算(或者其他运算时),Reference 类型会被解引用,即获取真正的值(被引用的内容)来参与运算,而类似函数调用、delete 等操作,都需要用到 Reference 类型中的对象。

在这个例子中,Reference 类型中的对象被当作 this 值,传入了执行函数时的上下文当中。

至此,我们对 this 的解释已经非常清晰了:调用函数时使用的引用,决定了函数执行时刻的 this 值。

实际上从运行时的角度来看,this 跟面向对象毫无关联,它是与函数调用时使用的表达式相关。

这个设计来自 JavaScript 早年,通过这样的方式,巧妙地模仿了 Java 的语法,但是仍然保持了纯粹的“无类”运行时设施。

如果,我们把这个例子稍作修改,换成箭头函数,结果就不一样了:

const showThis = () => {
  console.log(this);
};

var o = {
  showThis: showThis
};

showThis(); // global
o.showThis(); // global

我们看到,改为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。

接下来我们看看“方法”,它的行为又不一样了:

class C {
  showThis() {
    console.log(this);
  }
}
var o = new C();
var showThis = o.showThis;

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

这里我们创建了一个类 C,并且实例化出对象 o,再把 o 的方法赋值给了变量 showThis。

这时候,我们使用 showThis 这个引用去调用方法时,得到了 undefined。

所以,在方法中,我们看到 this 的行为也不太一样,它得到了 undefined 的结果。

按照我们上面的方法,不难验证出:生成器函数、异步生成器函数和异步普通函数跟普通函数行为是一致的,异步箭头函数与箭头函数行为是一致的。

3.5.2 this 关键字的机制

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

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

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

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

var a = 1;
foo();

// 在别处定义了 foo:

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

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

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

javascript执行adb JavaScript执行流程_前端教程_03

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

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

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

  • lexical:表示从上下文中找 this,这对应了箭头函数。
  • global:表示当 this 为 undefined 时,取全局对象,对应了普通函数。
  • strict:当严格模式时使用,this 严格按照调用时传入的值,可能为 null 或者 undefined。

非常有意思的是,方法的行为跟普通函数有差异,恰恰是因为 class 设计成了默认按 strict 模式执行。

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

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

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

var o = {};
o.foo = function foo() {
  console.log(this);
  return () => {
    console.log(this);
    return () => console.log(this);
  };
};

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

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

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

3.5.3 操作 this 的内置函数

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

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 值固定了参数:

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

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

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

4. 语句级的执行

语句是任何编程语言的基础结构,与 JavaScript 对象一样,JavaScript 语句同样具有“看起来很像其它语言,但是其实一点都不一样”的特点。

我们比较常见的语句包括变量声明、表达式、条件、循环等,这些都是大家非常熟悉的东西,对于它们的行为,我在这里就不赘述了。

为了了解 JavaScript 语句有哪些特别之处,首先我们要看一个不太常见的例子,我会通过这个例子,来向你介绍 JavaScript 语句执行机制涉及的一种基础类型:Completion 类型。

4.1 Completion 类型

我们来看一个例子。在函数 foo 中,使用了一组 try 语句。我们可以先来做一个小实验,在 try 中有 return 语句,finally 中的内容还会执行吗?我们来看一段代码。

function foo() {
  try {
    return 0;
  } catch (err) {
  } finally {
    console.log('a');
  }
}

console.log(foo());

通过实际试验,我们可以看到,finally 确实执行了,而且 return 语句也生效了,foo() 返回了结果 0。虽然 return 执行了,但是函数并没有立即返回,又执行了 finally 里面的内容,这样的行为违背了很多人的直觉。

如果在这个例子中,我们在 finally 中加入 return 语句,会发生什么呢?

function foo() {
  try {
    return 0;
  } catch (err) {
  } finally {
    return 1;
  }
}

console.log(foo());

web前端开发学习Q-q-u-n:784783012 ,分享学习的方法和需要注意的小细节,不停更新最新的教程和学习方法(详细的前端项目实战教学视频)

通过实际执行,我们看到,finally 中的 return “覆盖”了 try 中的 return。在一个函数中执行了两次 return,这已经超出了很多人的常识,也是其它语言中不会出现的一种行为。

面对如此怪异的行为,我们当然可以把它作为一个孤立的知识去记忆,但是实际上,这背后有一套机制在运作。

这一机制的基础正是 JavaScript 语句执行的完成状态,我们用一个标准类型来表示:Completion Record(我在类型一节提到过,Completion Record 用于描述异常、跳出等语句执行过程)。

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

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

JavaScript 正是依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中,实现各种控制。接下来我们要来了解一下 JavaScript 使用 Completion Record 类型,控制语句执行的过程。

首先我们来看看语句有几种分类。

javascript执行adb JavaScript执行流程_web前端_04

4.2 普通的语句

在 JavaScript 中,我们把不带控制能力的语句称为普通语句。

这些语句在执行时,从前到后顺次执行(我们这里先忽略 var 和函数声明的预处理机制),没有任何分支或者重复执行逻辑。

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

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

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

javascript执行adb JavaScript执行流程_javascript执行adb_05

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

4.3 语句块

介绍完了普通语句,我们再来介绍一个比较特殊的语句:语句块。

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

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

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

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

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

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

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

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

{
  var i = 1; // normal, empty, empty
  return i; // return, 1, empty
  i++;
  console.log(i);
} // return, 1, empty

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

接下来我们就具体讲讲控制类语句。

4.4 控制形语句

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

控制类语句分成两部分,一类是对其内部造成影响,如 if、switch、while/for、try。

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

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

javascript执行adb JavaScript执行流程_javascript执行adb_06

通过这个表,我们不难发现知识的盲点,也就是我们最初的的 case 中的 try 和 return 的组合了。

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

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

4.5 带标签的语句

前文我重点讲了 type 在语句控制中的作用,接下来我们重点来讲一下最后一个字段:target,这涉及了 JavaScript 中的一个语法,带标签的语句。

实际上,任何 JavaScript 语句是可以加标签的,在语句前加冒号即可:

firstStatement: var i = 1;

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

outer: while (true) {
  inner: while (true) {
    break outer;
  }
}
console.log('finished');

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