2023 年学习日志

书名JavaScript 高级程序设计
作者[美] 马特·弗里斯比
状态阅读中
  • 💡 根据遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容

  • 读书笔记正是帮助你记录和回顾的工具,不必拘泥于形式,其核心是:记录、翻看、思考

08-24 原始值和包装类型

原始值和包装类型简介

为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型:Boolean、Number 和 String。

每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露出操作原始值的各种方法。
比方说看下面这个例子:

let s1 = "some text";
let s2 = s1.substring(2);

在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring()方法,并把结果保存在 s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。

具体来说,当第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:

  1. 创建一个 String 类型的实例;
  2. 调用实例上的特定方法;
  3. 销毁实例;

类似

let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;

引用类型与原始值包装类型的主要区别:对象的生命周期

  • 在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁
  • 自动创建的原始值包装对象则只存在于访问它的那行代码执行期间,这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:
let s1 = "some text";
s1.color = "red";
console.log(s1.color); // undefined

这里的第二行代码尝试给字符串 s1 添加了一个 color 属性。可是,第三行代码访问 color 属性时,它却不见了。原因就是第二行代码运行时会临时创建一个 String 对象而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的 String 对象,但这个对象没有 color 属性。(有种阅后即焚的感觉,看完一行代码就烧掉)


08-22 正则迷你书

  • 正则表达式是匹配模式,要么匹配字符,要么匹配位置

08-21 正则RegExp

RegExp

简介

image.png

  • 这个正则表达式的 pattern(模式)可以是任何简单或复杂的正则表达式
  • 每个正则表达式可以带零个或多个 flags(标记),用于控制正则表达式的行为。

image.png
image.png

实例属性

image.png

实例方法

  • exec():RegExp 实例的主要方法是 exec(),主要用于配合捕获组使用。这个方法只接收一个参数,即要应

用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回
null。返回的数组虽然是 Array 的实例,但包含两个额外的属性:index 和 input。index 是字符串
中匹配模式的起始位置,input 是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串,
其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。image.png

  • test():正则表达式的另一个方法是 test(),接收一个字符串参数。如果输入的文本与模式匹配,则参数

返回 true,否则返回 false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。
test()经常用在 if 语句中:image.png

RegExp 构造函数属性

RegExp 构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了RegExp 构造函数的属性。
image.png
通过这些属性可以提取出与 exec()和 test()执行的操作相关的信息。来看下面的例子:

let text = 'this has been a short summer';
let pattern = /(.)hort/g;
if (pattern.test(text)) {
  console.log(RegExp.input); // this has been a short summer
  console.log(RegExp.leftContext); // this has been a
  console.log(RegExp.rightContext); // summer
  console.log(RegExp.lastMatch); // short
  console.log(RegExp.lastParen); // s
}

image.png
RegExp 还有其他几个构造函数属性,可以存储最多 9 个捕获组的匹配项。这些属性通过 RegExp.$1~RegExp.$9 来访问,分别包含第 1~9 个捕获组的匹配项。在调用 exec()test()时,这些属性就会被填充,然后就可以像下面这样使用它们:
image.png


08-17 Date 对象实例

Date 基础

  • 在不给 Date 构造函数传参数的情况下,创建的对象将保存当前日期和时间。
  • 基于其他日期和时 间创建日期对象必须传入其毫秒表示(UNIX 纪元 1970 年 1 月 1 日午夜之后的毫秒数)。
  • ECMAScript 为此提供了两个辅助方法:Date.parse()和 Date.UTC()
    • Date.parse()
      • 方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。
      • 所有实现都必须支持下列日期格式 :
        • “月/日/年”,如"5/23/2019"
        • “月名 日, 年”,如"May 23, 2019";
        • “周几 月名 日 年 时:分:秒 时区”,如"Tue May 23 2019 00:00:00 GMT-0700"
        • ISO 8601 扩展格式“YYYY-MM-DDTHH:mm:ss.sssZ”,如 2019-05-23T00:00:00(只适用于 兼容 ES5 的实现)。
      • 如果传给 Date.parse()的字符串并不表示日期,则该方法会返回 NaN。 如果直接把表示日期的字 符串传给 Date 构造函数,那么 Date 会在后台调用 Date.parse()。换句话说,下面这行代码跟前面 那行代码是等价的:
let someDate = new Date(Date.parse("May 23, 2019")); 
let someDate = new Date("May 23, 2019"); 
  • Date.UTC()
    • 也返回日期的毫秒表示
    • 传给 Date.UTC()的参数是年、零起点月数(1 月是 0,2 月是 1,以此类推)、日(131)、时(023)、 分、秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为 1 日。其他 参数的默认值都是 0。 (注意是 24小时制)
// GMT 时间 2000 年 1 月 1 日零点
let y2k = new Date(Date.UTC(2000, 0)); 
// GMT 时间 2005 年 5 月 5 日下午 5 点 55 分 55 秒
let allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55)); 
  - [UTC 与 GMT 的区别](https://www.yuque.com/tully/efwkni/xqf8dmpqbqngmpgh)
  • 还可以用于方便地用在代码分析中
// 起始时间
let start = Date.now(); 
// 调用函数
doSomething(); 
// 结束时间
let stop = Date.now(), 
result = stop - start; 

Date 继承的方法

let y2k1 = new Date('2000-01-01T00:00:00').toLocaleString();
let y2k2 = new Date('2000-01-01T00:00:00').toString();
let y2k3 = new Date('2000-01-01T00:00:00').valueOf();
console.log(y2k1); // 1/1/2000, 12:00:00 AM
console.log(y2k2); // Sat Jan 01 2000 00:00:00 GMT+0800 (China Standard Time)
console.log(y2k3); // 946656000000

Date 日期格式化方法

Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串:

  • toDateString() 显示日期中的周几、月、日、年(格式特定于实现)
  • toTimeString()显示日期中的时、分、秒和时区(格式特定于实现)
  • toLocaleDateString()显示日期中的周几、月、日、年(格式特定于实现和地区)
  • toLocaleTimeString()显示日期中的时、分、秒(格式特定于实现和地区)
  • toUTCString()显示完整的 UTC 日期(格式特定于实现)
console.log(new Date().toDateString());
console.log(new Date().toTimeString());
console.log(new Date().toLocaleDateString());
console.log(new Date().toLocaleTimeString());
console.log(new Date().toUTCString());
console.log(new Date().toString());

image.png
这些方法的输出与 toLocaleString()和 toString()一样,会因浏览器而异。因此不能用于在 用户界面上一致地显示日期


08-16 引用类型的概念

基本引用类型

  • 构造函数:用来创建新对象的函数

08-14 小程序,以及动画

繁琐知识点

  • 小程序全局的 App.json
  • KeyFrame 通过 js 来控制

08-10 上下文、垃圾回收

执行上下文与作用域

  • 最外层是全局上下文,它是根据 ECMAScript 实现的宿主环境所控制,像浏览器中就是 Windows
  • 上下文在其所有代码都执行完毕后会被销毁
  • 每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的 (这个东西是不是跟 this 有关系?)
  • 内部上下文可以通过作用域链访问外部上下文中的一切,但外 部上下文无法访问内部上下文中的任何东西。上下文之间的连接是线性的、有序的 ,, 每个上下文都可以 到上一级上下文中去搜索变量和函数 , 但任何上下文都不能到下一级上下文中去搜索。 函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的 访问规则
  • 作用域链增强: 某些语句会导致在作用域链前端临时添加一个上下文
    • try/catch 语句的 catch
    • with

变量声明 var let count

  • var:** **
    • 在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函 数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了, 那么它就会自动被添加到全局上下文
    • 提升:var 声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。这个现象叫作“提升”
  • let:
    • 块级作用域: 块级作用域由最近的一对包含花括号{}界定。
    • 暂时性死区: 严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的 缘故,实际上不能在声明之前使用 let 变量。因此,从写 JavaScript 代码的角度说,let 的提升跟 var 是不一样的。
var color = "blue";
function getColor() {
  let color = "red";
  {
    let color = "green";
    return color;
  }
}
console.log(getColor());
  • const :
    • 使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。
    • const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值 为其他引用值,但对象的键则不受限制
    • Object.freeze(), 这样再给属性赋值时虽然不会报错, 但会静默失败
    • 优化: 由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例 都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化
    • 最佳实践: 开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用 const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现 重新赋值导致的 bug

垃圾回收

  • 概念: JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。
  • 思路:确定哪个变量不会再使用,然后释放它占用的内存。 这个过程是周期性的
  • 不完美: 垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。
  • 我们以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或 堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部 变量了,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时 候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用,以及哪个变量不会再使用,以便回收 内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的 标记策略:标记清理和引用计数。

标记清理

  • 思路: 当变量进入上下文,比如在函数 内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永 远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时, 也会被加上离开上下文的标记。
  • 方法多种: 给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下 文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现 并不重要,关键是策略
  • 过程: 垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它 会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记 的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内 存清理,销毁带标记的所有值并收回它们的内存

引用计数

  • 思路: 思路是对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
  • 严重的问题:循环引用
function problem() { 
 let objectA = new Object(); 
 let objectB = new Object(); 
 objectA.someOtherObject = objectB; 
 objectB.anotherObject = objectA; 
}
  • 在这个例子中,objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是 2。在 标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA 和 objectB 在函数结束后还会存在,因为它们的引用数永远不会变成 0。如果函数被多次调 用,则会导致大量内存永远不会被释放。为此,Netscape 在 4.0 版放弃了引用计数,转而采用标记清理。 事实上,引用计数策略的问题还不止于此 。

性能

现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异, 但基本上都是根据已分配对象的大小和数量来判断的。比如,根据 V8 团队 2016 年的一篇博文的说法: “在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃 圾回收。”

  • 历史: 由于调度垃圾回收程序方面的问题会导致性能下降,IE 曾饱受诟病。它的策略是根据分配数,比如 分配了 256 个变量、4096 个对象/数组字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中某个 条件,垃圾回收程序就会运行。这样实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周 期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。由于对性能的严重影响,IE7 最终更新了垃圾回收程序。
  • IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触 发垃圾回收的阈值。IE7 的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%, 这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置 为默认值。这么一个简单的修改,极大地提升了重度依赖 JavaScript 的网页在浏览器中的性能。

内存管理

  • 在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。
  • 不过,JavaScript 运行在一个内存 管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动 浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系 统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程 中执行的语句数量
  • 解除引用: 将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行 代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫 作解除引用。 ( 不过要注意,解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关 的值已经不在上下文里了,因此它在下次垃圾回收时会被回收 )

优化

  • 通过 const 和 let 声明提升性能, ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程
  • 隐藏类和删除操作
    • 运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类 的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。比如下面的代码:
function Article() { 
 this.title = 'Inauguration Ceremony Features Kazoo Band'; 
} 
let a1 = new Article(); 
let a2 = new Article();
  • V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原 型。假设之后又添加了下面这行代码
a2.author = 'Jake'; 
  • 此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有 可能对性能产生明显影响。 当然,解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在 构造函数中一次性声明所有属性
  • 内存泄漏
    • 写得不好的 JavaScript 可能出现难以察觉且有害的内存泄漏问题。在内存有限的设备上,或者在函 数会被调用很多次的情况下,内存泄漏可能是个大问题。JavaScript 中的内存泄漏大部分是由不合理的 引用导致的
    • 闭包,定时器
// 定时器也可能会悄悄地导致内存泄漏。
let name = 'Jake'; 
setInterval(() => { 
 console.log(name); 
}, 100); 

// 使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏。请看下面的例子:
let outer = function() { 
 let name = 'Jake'; 
 return function() { 
 return name; 
 }; 
}; 
// 调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回
// 的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符
// 串),那可能就是个大问题了。
  • 静态分配与对象池
    • 为了提升 JavaScript 性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如 何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发 垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因 释放内存而损失的性能
    • 浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然 后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影 响性能。(例子看书)

08-08 对象

对象

  • ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义 的。因此,开发者不能在 JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]

琐碎知识点

  • 这里要记住,在调用一个函数而没有明确设置 this 值的情况下(即没有作为对象的方法调用,或
    者没有使用 call()/apply()调用),this 始终指向 Global 对象

08-07 原始值与引用

原始值与引用值

传递参数 *

  • 如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样
  • 所有函数的参数都是按值传递的:函数外的值会被复制到函数内部的参数中

很多开发者错误地认为, 当在局部作用域中修改对象而变化反映到全局时,就意味着参数是按引用传递的。为证明对象是按值传 递的,我们再来看看下面这个修改后的例子

function setName(obj) { 
 obj.name = "Nicholas"; 
 obj = new Object(); 
 obj.name = "Greg"; 
} 
let person = new Object(); 
setName(person); 
console.log(person.name);

如果 person 是按引用传递的,那么 person 应该自动将 指针改为指向 name 为"Greg"的对象。可是,当我们再次访问 person.name 时,它的值是"Nicholas", 这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指 向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了

确定类型

  • typeof 对原始值的判断有用,但是对引用值的用处不大, 更确切的说,它是判断一 个变量是否为字符串、数值、布尔值或 undefined 的最好方式
  • 如何判断是什么类型的对象: instanceof 操作符

执行上下文与作用域

每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的


08-04 原型链、代理与反射

原型链

重温一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型。如果原型是另一个类型的实例呢?那就意味 着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想

比方说,Object => Array => [1, 2, 3]

代理与反射

提供了拦截并向基本操作嵌入额外行为的能力

代理是目标对象的抽象 ,从很多方面看,代理类似 C++指针,因为它可以 用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。 但直接操作会绕过代理施予的行为

  • 使用代理的主要目的是可以定义捕获器(trap)
  • 代理可以在这些操作传播到目标对 象之前先调用捕获器函数,从而拦截并修改相应的行为

如何撤销代理

Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。

const target = {
  foo: "bar",
};
const handler = {
  get() {
    return "intercepted";
  },
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError

状态标记

const o = {}; 
if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) { 
 console.log('success'); 
} else { 
 console.log('failure'); 
} 

用一等函数替代操作符

Reflect.get() // 可以替代对象属性访问操作符。
Reflect.set() // 可以替代=赋值操作符。
Reflect.has() // 可以替代 in 操作符或 with()。
Reflect.deleteProperty() // 可以替代 delete 操作符。
Reflect.construct() // 可以替代 new 操作符

07-26、27 异步

异步函数 async / await

  • 为了解决异步结构组织代码的问题
const fn = async () => {
  console.log(1);
  // return "is Promise?";
};
console.log(fn());
  • 默认返回undefined,如果函数被 async 声明的话,函数执行后返回的值会被 Promise 包装

JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行

执行时机

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。 毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别

要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行

async function foo() {
  console.log(await Promise.resolve("foo"));
}
async function bar() {
  console.log(await "bar");
}
async function baz() {
  console.log("baz");
}
foo();
bar();
baz();
// baz
// foo
// bar
async function foo() {
  console.log(await Promise.resolve("2"));
  console.log(3);
  setTimeout(console.log, 0, 6);
}
async function bar() {
  console.log(await "4");
  console.log(5);
  setTimeout(console.log, 0, 7);

}
async function baz() {
  console.log("1");
}
foo();
bar();
baz();
// baz
// foo
// bar

因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。下面的例子演 示了这一点:

 async function foo() {
  console.log(2);
  await console.log(2.5);
  setTimeout(console.log, 0, 6);
  console.log(4);
  await console.log(4.5);
  setTimeout(console.log, 0, 7);
  console.log(5);
}
console.log(1);
foo();
console.log(3);

异步函数策略

实现 Sleep

function sleep(delay = 1000) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}
const fn = async () => {
  const t0 = new Date();
  await sleep();
  console.log(new Date() - t0);
};
fn();

平行加速

如果使用 await 时不留心,则很可能错过平行加速的机会。来看下面的例子,其中顺序等待了 5 个随机的超时:

async function randomDelay(id) { 
 // 延迟 0~1000 毫秒
 const delay = Math.random() * 1000; 
 return new Promise((resolve) => setTimeout(() => { 
 console.log(`${id} finished`); 
 resolve(); 
 }, delay)); 
} 
async function foo() { 
 const t0 = Date.now(); 
 await randomDelay(0); 
 await randomDelay(1); 
 await randomDelay(2); 
 await randomDelay(3); 
 await randomDelay(4); 
 console.log(`${Date.now() - t0}ms elapsed`); 
} 
foo(); 
// 0 finished 
// 1 finished 
// 2 finished 
// 3 finished 
// 4 finished 
// 877ms elapsed

加速的程序

async function randomDelay(id) {
  // 延迟 0~1000 毫秒
  const delay = Math.random() * 1000;
  return new Promise((resolve) =>
    setTimeout(() => {
      setTimeout(console.log, 0, `${id} finished`);
      resolve();
    }, delay)
  );
}
async function foo() {
  const t0 = Date.now();
  const p0 = randomDelay(0);
  const p1 = randomDelay(1);
  const p2 = randomDelay(2);
  const p3 = randomDelay(3);
  const p4 = randomDelay(4);
  await p0;
  await p1;
  await p2;
  await p3;
  await p4;
  setTimeout(console.log, 0, `${Date.now() - t0}ms elapsed`);
}
foo();

用 for 改写

async function randomDelay(id) {
  // 延迟 0~1000 毫秒
  const delay = Math.random() * 1000;
  return new Promise((resolve) =>
    setTimeout(() => {
      console.log(`${id} finished`);
      resolve(id);
    }, delay)
  );
}
async function foo() {
  const t0 = Date.now();
  const promises = Array(5)
    .fill(null)
    .map((_, i) => randomDelay(i));
  for (const p of promises) {
    console.log(`awaited ${await p}`);
  }
  console.log(`${Date.now() - t0}ms elapsed`);
}
foo();

串行执行期约

如何串行执行期约并把值传给后续的期约。使用 async/await,期约连锁会变 得很简单

async function addTwo(x) {
  return x + 2;
}
async function addThree(x) {
  return x + 3;
}
async function addFive(x) {
  return x + 5;
}
async function addTen(x) {
  for (const fn of [addTwo, addThree, addFive]) {
    console.log(await fn(x));
    x = await fn(x);
    // console.log(x);
    // await fn(x) 是结果,不是 Promise
  }
  return x;
}
addTen(9).then(console.log); // 19

07-22 函数合成

函数合成

到目前为止,我们讨论期约连锁一直围绕期约的串行执行,忽略了期约的另一个主要特性:异步产生值并将其传给处理程序。基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这很像 函数合成,即将多个函数合成为一个函数,比如:

function addTwo(x) {return x + 2;} 
function addThree(x) {return x + 3;} 
function addFive(x) {return x + 5;} 
function addTen(x) { 
 return addFive(addTwo(addThree(x))); 
} 
console.log(addTen(7)); // 17 

在这个例子中,有 3 个函数基于一个值合成为一个函数。类似地,期约也可以像这样合成起来,渐 进地消费一个值,并返回一个结果

function addTwo(x) {
  return x + 2;
}
function addThree(x) {
  return x + 3;
}
function addFive(x) {
  return x + 5;
}
function addTen(x) {
  return Promise.resolve(x).then(addTwo).then(addThree).then(addFive);
}
addTen(8).then(console.log); // 18

使用 Array.prototype.reduce()可以写成更简洁的形式

function addTwo(x) {
  return x + 2;
}
function addThree(x) {
  return x + 3;
}
function addFive(x) {
  return x + 5;
}
function addTen(x) {
  return [addTwo, addThree, addFive].reduce(
    (promise, fn) => promise.then(fn),
    Promise.resolve(x)
  );
}
addTen(8).then(console.log); // 18



07-21 异步

传递解决值和拒绝理由

到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由(如果拒绝)给相关状态的处理 程序。拿到返回值后,就可以进一步对这个值进行操作。比如,第一次网络请求返回的 JSON 是发送第 二次请求必需的数据,那么第一次请求返回的值就应该传给 onResolved 处理程序继续处理。当然,失 败的网络请求也应该把 HTTP 状态码传给 onRejected 处理程序。

在执行函数中,解决的值拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传的。然后,这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数。下面的例子展示了上述传递过程

let p1 = new Promise((resolve, reject) => resolve('foo')); 
p1.then((value) => console.log(value)); // foo 
let p2 = new Promise((resolve, reject) => reject('bar')); 
p2.catch((reason) => console.log(reason)); // bar 

拒绝期约与拒绝错误处理

拒绝期约类似于 throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。

let p1 = new Promise((resolve, reject) => reject(Error('foo'))); 
let p2 = new Promise((resolve, reject) => { throw Error('foo'); }); 
let p3 = Promise.resolve().then(() => { throw Error('foo'); }); 
let p4 = Promise.reject(Error('foo')); 
setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo 
// 也会抛出 4 个未捕获错误

期约可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。这样做主要是因为创建 错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的。例如,前面例 子中抛出的 4 个错误的栈追踪信息如下:

Uncaught (in promise) Error: foo 
 at Promise (test.html:5) 
 at new Promise (<anonymous>) 
 at test.html:5 
Uncaught (in promise) Error: foo 
 at Promise (test.html:6) 
 at new Promise (<anonymous>) 
 at test.html:6 
Uncaught (in promise) Error: foo 
 at test.html:8 
Uncaught (in promise) Error: foo 
 at Promise.resolve.then (test.html:7)

这个例子同样揭示了异步错误有意思的副作用。正常情况下,在通过 throw()关键字抛出错误时, JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令:

throw Error('foo'); 
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo

但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时 继续执行同步指令

Promise.reject(Error('foo')); 
console.log('bar'); 
// bar 
// Uncaught (in promise) Error: foo

这不包括捕获执行函数中的错误,在解决或拒绝期约之前,仍然可以使用 try/catch 在执行函数 中捕获错误:

let p = new Promise((resolve, reject) => {
  try {
    console.log(1);
    throw Error("foo");
    console.log(2);
  } catch (e) {
    console.log('error:>>', e);
  }
  resolve("bar");
});
setTimeout(console.log, 0, p);
// ?

then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之 后将其隔离,同时不影响正常逻辑执行

为此,onRejected 处理程序的任务应该是在捕获异步错误之 后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:

// 同步
console.log('begin synchronous execution'); 
try { 
 throw Error('foo'); 
} catch(e) { 
 console.log('caught error', e); 
} 
console.log('continue synchronous execution'); 
// begin synchronous execution 
// caught error Error: foo 
// continue synchronous execution 

// 异步的两个例子
new Promise((resolve, reject) => {
  console.log("begin asynchronous execution");
  reject(Error("bar"));
})
  .catch((e) => {
    console.log("caught error", e);
  })
  .then(() => {
    console.log("continue asynchronous execution");
  });


new Promise((resolve, reject) => {
  console.log("begin asynchronous execution");
  reject(Error("bar"));
})
  .then(() => {
    console.log("continue asynchronous execution");
  })
  .catch((e) => {
    console.log("caught error", e);
  });

期约连锁与期约合成

期约连锁

把期约逐个地串联起来是一种非常有用的编程模式。之所以可以这样做,是因为每个期约实例的方 法(then()、catch()和 finally())都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。比如

let p = new Promise((resolve, reject) => { 
 console.log('first'); 
 resolve(); 
}); 
p.then(() => console.log('second')) 
 .then(() => console.log('third')) 
 .then(() => console.log('fourth')); 
// first 
// second 
// third 
// fourth

这个实现最终执行了一连串同步任务。正因为如此,这种方式执行的任务没有那么有用,毕竟分别 使用 4 个同步函数也可以做到:

(() => console.log('first'))(); 
(() => console.log('second'))(); 
(() => console.log('third'))(); 
(() => console.log('fourth'))(); 

要真正执行异步任务,可以改写前面的例子,让每个执行器都返回一个期约实例。这样就可以让每个后续期约都等待之前的期约,也就是串行化异步任务。

比如,可以像下面这样让每个期约在一定时间后解决:

let p1 = new Promise((resolve, reject) => {
      console.log("p1 打开网页");
      setTimeout(resolve, 1000);
    });
    p1.then(
      () =>
        new Promise((resolve, reject) => {
          console.log("p2 请求登录接口,登录中ing");
          setTimeout(resolve, 2000);
        })
    )
      .then(
        () =>
          new Promise((resolve, reject) => {
            console.log("p3 加载图片中ing");
            setTimeout(resolve, 3000);
          })
      )
      .then(
        () =>
          new Promise((resolve, reject) => {
            console.log("p4 加载文字");
            setTimeout(resolve, 1000);
          })
      );

把生成期约的代码提取到一个工厂函数中,就可以写成这样

function delayedResolve(str) {
  return new Promise((resolve, reject) => {
    console.log(str);
    setTimeout(resolve, 1000);
  });
}
delayedResolve("p1 executor")
  .then(() => delayedResolve("p2 executor"))
  .then(() => delayedResolve("p3 executor"))
  .then(() => delayedResolve("p4 executor"));

每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。这种结构可以简 洁地将异步任务串行化,解决之前依赖回调的难题。假如这种情况下不使用期约,那么前面的代码可能 就要这样写了:

function delayedExecute(str, callback = null) {
  setTimeout(() => {
    console.log(str);
    callback && callback();
  }, 1000);
}
delayedExecute("p1 callback", () => {
  delayedExecute("p2 callback", () => {
    delayedExecute("p3 callback", () => {
      delayedExecute("p4 callback");
    });
  });
});

期约图

因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。这样,每个 期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等 待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序

下面的例子展示了一种期约有向图,也就是二叉树:

//     A
//    / \
//   B   C
//  /\   /\
// D  E F  G
let A = new Promise((resolve, reject) => {
  console.log("A");
  resolve();
});
let B = A.then(() => console.log("B"));
let C = A.then(() => console.log("C"));
B.then(() => console.log("D"));
B.then(() => console.log("E"));
C.then(() => console.log("F"));
C.then(() => console.log("G"));
// A
// B
// C
// D
// E
// F
// G

注意,日志的输出语句是对二叉树的层序遍历。如前所述,期约的处理程序是按照它们添加的顺序 执行的。由于期约的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。
树只是期约图的一种形式。考虑到根节点不一定唯一,且多个期约也可以组合成一个期约(通过下 一节介绍的 Promise.all()和 Promise.race()),所以有向非循环图是体现期约连锁可能性的最准确表达

Promise.all()和 Promise.race()

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个 可迭代对象,返回一个新期约

let p = Promise.all([ 
 Promise.resolve(), 
 new Promise((resolve, reject) => setTimeout(resolve, 1000)) 
]); 
setTimeout(console.log, 0, p); // Promise <pending> 
p.then(() => setTimeout(console.log, 0, 'all() resolved!')); 
// all() resolved!(大约 1 秒后)

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的 期约也会拒绝:

// 永远待定
let p1 = Promise.all([new Promise(() => {})]); 
setTimeout(console.log, 0, p1); // Promise <pending> 

// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([ 
 Promise.resolve(), 
 Promise.reject(), 
 Promise.resolve() 
]); 
setTimeout(console.log, 0, p2); // Promise <rejected> 
// Uncaught (in promise) undefined 

// 如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序:
let p = Promise.all([ 
 Promise.resolve(3), 
 Promise.resolve(), 
 Promise.resolve(4) 
]); 
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4] 

Promise.race()不会对解决或拒绝的期约区别对待。无论是解决还是拒绝,只要是第一个落定的 期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约:

let p1 = Promise.race([ 
 Promise.resolve(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000)) 
]); 
setTimeout(console.log, 0, p1); // Promise <resolved>: 3 



07-20 期约

期约 Promise & 异步函数

ECMAScript 6 新增了正式的 Promise(期约)引用类型,支持优雅地定义和组织异步逻辑。接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制

同步操作与异步操作更是代码所要依赖的核心机制。异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的。

异步操作并不一定计算量大或要等很长时间。只要你不想为等待某个异步操作而阻塞线 程执行,那么任何时候都可以使用。

回调函数的概念

回调函数是一种特殊的函数,它作为参数传递给另一个函数,并在被调用函数执行完毕后被调用。回调函数通常用于事件处理、异步编程和处理各种操作系统和框架的API。

以往的异步编程模式

异步返回值

假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?

function double(value, callback) {
  setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`)); 
// ?

嵌套异步回调 | 回调地狱

function double(value, success, failure) {
  setTimeout(() => {
    try {
      if (typeof value !== "number") {
        throw "Must provide number as first argument";
      }
      success(2 * value);
    } catch (e) {
      failure(e);
    }
  }, 1000);
}
const successCallback = (x) => {
  double(x, (y) => console.log(`Success: ${y}`));
};
const failureCallback = (e) => console.log(`Failure: ${e}`);

double(3, successCallback, failureCallback);
// ?

期约

基础

创建新期约时需要传入 **执行器(executor)**函数作为参数

let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending>

期约状态机

  • 待定(pending)
  • 兑现(fulfilled,有时候也称为“解决”,resolved)、
  • 拒绝(rejected)

待定(pending)是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现 (fulfilled)状态,或者代表失败的拒绝(rejected)状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。
而且,也不能保证期约必然会脱离待定状态。

用途

  • 首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。“待定” 表示尚未开始或者正在执行中。“兑现”表示已经成功完成,而“拒绝”则表示没有成功完成。
  • 期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问 这个值。相应地,如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。比如,假设期 约向服务器发送一个 HTTP 请求并预定会返回一个 JSON。如果请求返回范围在 200~299 的状态码,则 足以让期约的状态变为兑现。此时期约内部就可以收到一个 JSON 字符串。类似地,如果请求返回的状 态码不在 200~299 这个范围内,那么就会把期约状态切换为拒绝。此时拒绝的理由可能是一个 Error 对象,包含着 HTTP 状态码及相关错误消息 。

通过执行函数控制期约状态

由于期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。执行 器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是 通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用 resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛 出错误(后面会讨论这个错误)

let p1 = new Promise((resolve, reject) => resolve()); 
setTimeout(console.log, 0, p1); // Promise <resolved> 
let p2 = new Promise((resolve, reject) => reject()); 
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise) 

执行器函数是同步执行的

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
setTimeout(console.log, 0, p); 

幂等函数

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true

同步/异步执行的二元性

try { 
 throw new Error('foo'); 
} catch(e) { 
 console.log(e); // Error: foo 
} 
try { 
 Promise.reject(new Error('bar')); 
} catch(e) { 
 console.log(e); 
} 
// Uncaught (in promise) Error: bar 

第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。乍一看这可能 有点违反直觉,因为代码中确实是同步创建了一个拒绝的期约实例,而这个实例也抛出了包含拒绝理由 的错误。这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这 里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。 ??

** 在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法 **

期约的实例方法

  1. Promise.prototype.then() Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多 两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话, 则会在期约分别进入“兑现”和“拒绝”状态时执行
function onResolved(id) {
  setTimeout(console.log, 0, id, "resolved");
}
function onRejected(id) {
  setTimeout(console.log, 0, id, "rejected");
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
p1.then(
  () => onResolved("p1"),
  () => onRejected("p1")
);
p2.then(
  () => onResolved("p2"),
  () => onRejected("p2")
);
//(3 秒后)
// p1 resolved
// p2 rejected

Promise.prototype.then()方法返回一个新的期约**实例 **

这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回 值 undefined。** **

let p1 = Promise.resolve("foo");

// 若调用 then()时不传处理程序,则原样向后传
let p2 = p1.then();
setTimeout(console.log, 0, p2); // Promise <resolved>: foo

  // 这些都一样
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined

// 如果有显式的返回值,则 Promise.resolve()会包装这个值
// 这些都一样
let p6 = p1.then(() => "bar");
let p7 = p1.then(() => Promise.resolve("bar"));
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar

  // Promise.resolve()保留返回的期约
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

// 抛出异常会返回拒绝的期约:
let p10 = p1.then(() => {
  throw "baz";
});
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected> baz

//QA:注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:  (抛出异常,和返回错误值是不一样的!!!)
let p11 = p1.then(() => Error("qux"));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)

// 下面的代码展示了这两种同样的情况:
let p = Promise.reject(); 
let onRejected = function(e) { 
 setTimeout(console.log, 0, 'rejected'); 
}; 
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected 
p.catch(onRejected); // rejected

Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期 约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出 现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用 于添加清理代码

let p1 = Promise.resolve(); 
let p2 = Promise.reject(); 
let onFinally = function() { 
 setTimeout(console.log, 0, 'Finally!') 
} 
p1.finally(onFinally); // Finally 
p2.finally(onFinally); // Finally 
// Promise.prototype.finally()方法返回一个新的期约实例:
let p1 = new Promise(() => {}); 
let p2 = p1.finally();
setTimeout(console.log, 0, p1); // Promise <pending> 
setTimeout(console.log, 0, p2); // Promise <pending> 
setTimeout(console.log, 0, p1 === p2); // false 

axios 官网上的例子

Minimal Example | Axios Docs

非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处 理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联 的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy) 特性。下面的例子演示了这个特性

// 创建解决的期约
let p = Promise.resolve(); 
// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler')); 
// 同步输出,证明 then()已经返回
console.log('then() returns'); 
// 实际的输出:
// then() returns 
// onResolved handler 

在这个例子中,在一个解决期约上调用 then()会把 onResolved 处理程序推进消息队列。但这个 处理程序在当前线程上的同步代码执行完成前不会执行。因此,跟在 then()后面的同步代码一定先于 处理程序执行

问题

  • 链式调用
  • 整体的事件循环,以及消息队列是如何进入队列,是否是同步执行队列中的任务?
  • async 与 await

07-19 JavaScript 基础

第 1 章 什么是 JavaScript

从简单的输入验证脚本到强大的编程语言,JavaScript 的崛起没有任何人预测到。它很简单,学会 用只要几分钟;它又很复杂,掌握它要很多年。要真正学好用好 JavaScript,理解其本质、历史及局限性是非常重要的

历史回顾

它的主要用途是代替 Perl 等服务器端语言处理输入验证

当时,大多数用户使用 28.8kbit/s 的 调制解调器上网

为什么叫 JavaScript

1995 年,网景公司一位名叫 Brendan Eich 的工程师,开始为即将发布的 Netscape Navigator 2 开发一 个叫 Mocha(后来改名为 LiveScript)的脚本语言。当时的计划是在客户端和服务器端都使用它,它在 服务器端叫 LiveWire。 为了赶上发布时间,网景与 Sun 公司结为开发联盟,共同完成 LiveScript 的开发。就在 Netscape Navigator 2 正式发布前,网景把 LiveScript 改名为 JavaScript,以便搭上媒体当时热烈炒作 Java 的顺风车。

ECMA & TC 39 的概念

由于 JavaScript 1.0 很成功,网景又在 Netscape Navigator 3 中发布了 1.1 版本。尚未成熟的 Web 的受欢迎程度达到了历史新高,而网景则稳居市场领导者的位置。这时候,微软决定向 IE 投入更多资源。 就在 Netscape Navigator 3 发布后不久,微软发布了 IE3,其中包含自己名为 JScript(叫这个名字是为了 避免与网景发生许可纠纷)的 JavaScript 实现。1996 年 8 月,微软重磅进入 Web 浏览器领域,这是网景永远的痛,但它代表 JavaScript 作为一门语言向前迈进了一大步

微软的 JavaScript 实现意味着出现了两个版本的 JavaScript:Netscape Navigator 中的 JavaScript,以 及 IE 中的 JScript。与 C 语言以及很多其他编程语言不同,JavaScript 还没有规范其语法或特性的标准, 两个版本并存让这个问题更加突出了。随着业界担忧日甚,JavaScript 终于踏上了标准化的征程。

1997 年,JavaScript 1.1 作为提案被提交给欧洲计算机制造商协会(Ecma)。第 39 技术委员会(TC39) 承担了“标准化一门通用、跨平台、厂商中立的脚本语言的语法和语义”的任务(参见 TC39-ECMAScript)。 TC39 委员会由来自网景、Sun、微软、Borland、Nombas 和其他对这门脚本语言有兴趣的公司的工程师 组成。他们花了数月时间打造出 ECMA-262,也就是 ECMAScript(发音为“ek-ma-script”)这个新的脚本语言标准。 1998 年,国际标准化组织(ISO)和国际电工委员会(IEC)也将 ECMAScript 采纳为标准(ISO/ IEC-16262)。自此以后,各家浏览器均以 ECMAScript 作为自己 JavaScript 实现的依据,虽然具体实现 各有不同。

ECMA-262

ECMA-262 到底定义了什么?在基本的层面,它描述这门语言的如下部分:

  • 语法
  • 类型
  • 语句
  • 关键字
  • 保留字
  • 操作符
  • 全局对象

ECMAScript 只是对实现这个规范描述的所有方面的一门语言的称呼。JavaScript 实现了 ECMAScript,而 Adobe ActionScript 同样也实现了 ECMAScript。

ECMA Script 版本

  • ECMAScript 不同的版本以“edition”表示(也就是描述特定实现的 ECMA-262 的版本)。
  • ECMA-262 最近的版本是第 14 版,发布于 2023 年 6 月。

ECMA-262 - Ecma International

  • ECMA-262 的第 1 版本质上跟网景的 JavaScript 1.1 相同, 只不过删除了所有浏览器特定的代码,外加少量细微的修改。ECMA-262 要求支持 Unicode 标准(以支 持多语言),而且对象要与平台无关(Netscape JavaScript 1.1 的对象不是这样,比如它的 Date 对象就依 赖平台)。这也是 JavaScript 1.1 和 JavaScript 1.2 不符合 ECMA-262 第 1 版要求的原因。
  • ECMA-262 第 2 版只是做了一些编校工作,主要是为了更新之后严格符合 ISO/IEC-16262 的要求, 并没有增减或改变任何特性。ECMAScript 实现通常不使用第 2 版来衡量符合性(conformance)。
  • ECMA-262 第 3 版第一次真正对这个标准进行更新,更新了字符串处理、错误定义和数值输出。此 外还增加了对正则表达式、新的控制语句、try/catch 异常处理的支持,以及为了更好地让标准国际化 所做的少量修改。对很多人来说,这标志着 ECMAScript 作为一门真正的编程语言的时代终于到来了。
  • ECMA-262 第 4 版是对这门语言的一次彻底修订。作为对 JavaScript 在 Web 上日益成功的回应,开 发者开始修订 ECMAScript 以满足全球 Web 开发日益增长的需求。为此,Ecma T39 再次被召集起来, 以决定这门语言的未来。结果,他们制定的规范几乎在第 3 版基础上完全定义了一门新语言。第 4 版包 括强类型变量、新语句和数据结构、真正的类和经典的继承,以及操作数据的新手段。 与此同时,TC39 委员会的一个子委员会也提出了另外一份提案,叫作“ECMAScript 3.1”,只对这 门语言进行了较少的改进。这个子委员会的人认为第 4 版对这门语言来说跳跃太大了。因此,他们提出 了一个改动较小的提案,只要在现有 JavaScript 引擎基础上做一些增改就可以实现。最终,ES3.1 子委员 会赢得了 TC39 委员会的支持,ECMA-262 第 4 版在正式发布之前被放弃。
  • ECMAScript 3.1 变成了 ECMA-262 的第 5 版,于 2009 年 12 月 3 日正式发布。第 5 版致力于厘清 第 3 版存在的歧义,也增加了新功能。新功能包括原生的解析和序列化 JSON 数据的 JSON 对象、方便 继承和高级属性定义的方法,以及新的增强 ECMAScript 引擎解释和执行代码能力的严格模式。第 5 版 在 2011 年 6 月发布了一个维护性修订版,这个修订版只更正了规范中的错误,并未增加任何新的语言 或库特性。
  • ECMA-262 第 6 版,俗称 ES6、ES2015 或 ES Harmony(和谐版),于 2015 年 6 月发布。这一版包 含了大概这个规范有史以来最重要的一批增强特性。ES6 正式支持了类、模块、迭代器、生成器、箭头 函数、期约、反射、代理和众多新的数据类型
  • ECMA-262 第 7 版,也称为 ES7 或 ES2016,于 2016 年 6 月发布。这次修订只包含少量语法层面的 增强,如 Array.prototype.includes 和指数操作符。
  • ECMA-262 第 8 版,也称为 ES8、ES2017,完成于 2017 年 6 月。这一版主要增加了异步函数(async/ await)SharedArrayBuffer Atomics API,以及 Object.values()/Object.entries()/Object. getOwnPropertyDescriptors()和字符串填充方法,另外明确支持对象字面量最后的逗号。
  • ECMA-262 第 9 版,也称为 ES9、ES2018,发布于 2018 年 6 月。这次修订包括异步迭代、剩余和 扩展属性、一组新的正则表达式特性、Promise finally(),以及模板字面量修订。
  • ECMA-262第 10版,也称为 ES10、ES2019,发布于 2019年 6月。这次修订增加了 Array.prototype. flat()/flatMap()String.prototype.trimStart()/trimEnd()Object.fromEntries()方 法,以及 Symbol.prototype.description 属性,明确定义了 Function.prototype.toString() 的返回值并固定了 Array.prototype.sort()的顺序。另外,这次修订解决了与 JSON 字符串兼容的 问题,并定义了 catch 子句的可选绑定。

DOM

应用编程接口

相关资料

可通过“⌘+K”插入引用链接,或使用“本地文件”引入源文件。

JavaScript高级程序设计(第4版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值