在 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,但是增加了不少内容。
- lexical environment:词法环境,当获取变量或者 this 值时使用。
- variable environment:变量环境,当声明变量时使用。
- code evaluation state:用于恢复代码执行位置。
- Function:执行的任务是函数时使用,表示正在被执行的函数。
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。
- Realm:使用的基础库和内置对象实例。
- 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,这两类语句的配合,会产生控制代码执行顺序和执行逻辑的效果。
因为 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 的循环语句会消费它。