1)、宏观和微观任务
- 宏观任务—宿主发起的任务;
- 微观任务—JavaScript 引擎发起的任务;
- 在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列;
- 有了宏观任务和微观任务机制,就可以实现 JavaScript 引擎级和宿主级的任务了,例如:Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。
2)、Promise
- Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。
function sleep(duration) { return new Promise(function (resolve) { console.log("start"); setTimeout(resolve, duration); }); } await sleep(1000).then(() => console.log("finished")); - Promise 里的代码比 setTimeout 先执行,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。
var r = new Promise(function (resolve, reject) { console.log("a"); resolve(); }); setTimeout(() => console.log("d"), 0); r.then(() => console.log("c")); console.log("b"); - 为了理解微任务始终先于宏任务,我们设计一个实验:执行一个耗时 1 秒的 Promise。我们可以看到,即使耗时一秒的 c1 执行完毕,再 enque 的 c2,仍然先于 d 执行了,这很好地解释了微任务优先的原理。
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")); }); - 通过一系列的实验,我们可以总结一下如何分析异步执行的顺序:
- 首先我们分析有多少个宏任务;
- 在每个宏任务中,分析有多少个微任务;
- 根据调用次序,确定宏任务中的微任务执行次序;
- 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
- 确定整个顺序。
3)、async/await
- async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数;
- async 函数是一种特殊语法,特征是在 function 关键字之前加上 async 关键字,这样,就定义了一个 async 函数,我们可以在其中使用 await 来等待一个 Promise。
4)、闭包
-
闭包其实只是一个绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境,就像人在外星中需要自带吸氧的装备一样,这个函数也带有在程序中生存的环境。这个古典的闭包定义中,闭包包含两个部分。
- 环境部分
- 环境:函数的词法环境(执行上下文的一部分)
- 标识符列表:函数中用到的未声明的变量
- 表达式部分:函数体
- 环境部分
5)、执行上下文
- 在 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:仅生成器上下文有这个属性,表示当前生成器。
6)、变量声明
```js
var b = {}
let c = 1
this.a = 2;
```
-
要想正确执行它,我们需要知道以下信息:
- var 把 b 声明到哪里;
- b 表示哪个变量;
- b 的原型是哪个对象;
- let 把 c 声明到哪里;
- this 指向哪个对象。
-
这些信息就需要执行上下文来给出了,这段代码出现在不同的位置,甚至在每次执行中,会关联到不同的执行上下文,所以,同样的代码会产生不一样的行为。
-
var 声明与赋值,只有 var,没有 let 的旧 JavaScript 时代,诞生了一个技巧,叫做:立即执行的函数表达式(IIFE),通过创建一个函数,并且立即执行,来构造一个新的域,从而控制 var 的范围。
-
let 是 ES6 开始引入的新的变量声明模式,比起 var 的诸多弊病,let 做了非常明确的梳理和规定。为了实现 let,JavaScript 在运行时引入了块级作用域。也就是说,在 let 出现之前,JavaScript 的 if for 等语句皆不产生作用域。我简单统计了下,以下语句会产生 let 使用的作用域:
- for;
- if;
- switch;
- try/catch/finally。
-
Realm: Realm 中包含一组完整的内置对象,而且是复制关系。对不同 Realm 中的对象操作,会有一些需要格外注意的问题,比如 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
7)、 函数
-
普通函数:用 function 关键字定义的函数。
function foo() { // code } -
箭头函数:用 => 运算符定义的函数。
const foo = () => { // code }; -
方法:在 class 中定义的函数。。
class C { foo() { //code } } -
生成器函数:用 function * 定义的函数。
function* foo() { // code } -
类:用 class 定义的类,实际上也是函数。
class Foo { constructor() { //code } } -
异步函数:普通函数、箭头函数和生成器函数加上 async 关键字。
async function foo(){ // code } const foo = async () => { // code } async function foo*(){ // code } -
this 关键字的行为
-
对普通变量而言,这些函数并没有本质区别,都是遵循了“继承定义时环境”的规则,它们的一个行为差异在于 this 关键字。
-
this 是执行上下文中很重要的一个组成部分。同一个函数调用方式不同,得到的 this 值也不同,我们看一个例子:
function showThis() { console.log(this); } var o = { showThis: showThis, }; showThis(); // global o.showThis(); // o -
调用函数时使用的引用,决定了函数执行时刻的 this 值,实际上从运行时的角度来看,this 跟面向对象毫无关联,它是与函数调用时使用的表达式相关。
-
为箭头函数后,不论用什么引用来调用它,都不影响它的 this 值。
-
嵌套的箭头函数中的代码都指向外层 this。
-
操作 this 的内置函数,Function.prototype.call 和 Function.prototype.apply 可以指定函数调用时传入的 this 值,Function.prototype.bind 它可以生成一个绑定过的函数,这个函数的 this 值固定了参数。
function foo(a, b, c) { console.log(this); console.log(a, b, c); } foo.call({}, 1, 2, 3); foo.apply({}, [1, 2, 3]);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,通过 new 调用函数,跟直接调用的 this 取值有明显区别,仅普通函数和类能够跟 new 搭配使用.
8)、try 里面放 return,finally 还会执行吗
- 我们来看一个例子。在函数 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());
通过实际执行,我们看到,finally 中的 return “覆盖”了 try 中的 return。在一个函数中执行了两次 return,这已经超出了很多人的常识,也是其它语言中不会出现的一种行为。
