JavaScript 异步执行与任务机制、闭包、执行上下文与 try-catch-finally 机制解析

108 阅读6分钟

在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。

我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

宏观和微观任务

JavaScript 引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环。

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

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

Promise

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

闭包

环境部分

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

表达式部分: 函数体

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

执行上下文

相比普通函数,JavaScript 函数的主要复杂性来自于它携带的“环境部分”。JavaScript 中与闭包“环境部分”相对应的术语是“词法环境”,JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文”。

执行上下文在 ES3 中,包含三个部分。

  • scope:作用域,也常常被叫做作用域链。
  • variable object:变量对象,用于存储变量的对象。
  • this value:this 值。

在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。

  • lexical environment:词法环境,当获取变量时使用。
  • variable environment:变量环境,当声明变量时使用。
  • this value:this 值。

在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容。

  1. lexical environment:词法环境,当获取变量或者 this 值时使用。
  2. variable environment:变量环境,当声明变量时使用。
  3. code evaluation state:用于恢复代码执行位置。
  4. Function:执行的任务是函数时使用,表示正在被执行的函数。
  5. ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
  6. Realm:使用的基础库和内置对象实例。
  7. Generator:仅生成器上下文有这个属性,表示当前生成器。

try中放return,finally还会执行吗

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

  } finally {
    console.log("a")
  }
}

console.log(foo()); 
// a
// 0

finally 确实执行了,而且 return 语句也生效了,foo() 返回了结果 0。

在finally中在放入一个return 看看

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

  } finally {
    console.log("a")
    return 1;
  }
}

console.log(foo());
// a
// 1

可以看出来return被覆盖了。

来分析一下吧。

这一机制的基础正是 JavaScript 语句执行的完成状态,我们用一个标准类型来表示:Completion Record

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

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

JavaScript 正是依靠语句的 Completion Record 类型,方才可以在语句的复杂嵌套结构中,实现各种控制。主要分为四类:普通语句,语句块,控制性语句,带标签的语句。

普通语句

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

  • 声明类语句
    • var 声明
    • const 声明
    • let 声明
    • 函数声明
    • 类声明
  • 表达式语句
  • 空语句
    • debugger

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

普通语句执行后,会得到 [[type]] 为 normal 的 Completion Record,JavaScript 引擎遇到这样的 Completion Record,会继续执行下一条语句。这些语句中,只有表达式语句会产生 [[value]]。

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

语句块

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

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

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

控制型语句

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

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

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

image.png

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

带标签的语句

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

firstStatement: var i = 1;

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

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

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