2023 年 08 月学习日志

206 阅读52分钟
书名JavaScript 高级程序设计
作者[美] 马特·弗里斯比
状态阅读中

💡 根据遗忘曲线:如果没有记录和回顾,6 天后便会忘记 75%的内容,读书笔记正是帮助你记录和回顾的工具,不必拘泥于形式,其核心是:记录、翻看、思考

08-31 Set、weakSet、迭代与扩展操作、迭代器

set

image.png

weakSet

跟 weakMap 差不多

image.png

迭代与扩展操作

image.png

image.png

image.png

迭代器与生成器

迭代的英文“iteration”源自拉丁文 itero,意思是“重复”或“再来”。在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。ECMAScript 6 规范新增了两个高级特性:迭代器生成器。使用这两个特性,能够更清晰、高效、方便地实现迭代。

理解迭代

image.png

ES5 新增了 Array.prototype.forEach()方法,向通用迭代需求迈进了一步(但仍然不够理想):

let collection = ['foo', 'bar', 'baz'];
collection.forEach((item) => console.log(item));

这个方法解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。

在 ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。Python、Java、C++,还有其他很多语言都对这个模式提供了完备的支持。JavaScript 在 ECMAScript 6 以后也支持了迭代器模式。

迭代器无须了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值

可迭代协议

实现 Iterable 接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator 接口的对象的能力。在 ECMAScript 中,这意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。

很多内置对象都实现了 iterator 接口

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments
  • Nodelist 等 DOM 的集合类型

检查是否存在默认迭代器属性可以暴露这个工厂函数:

let num = 1;
let obj = {};
// 这两种类型没有实现迭代器工厂函数
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
let els = document.querySelectorAll('div');
// 这些类型都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); // f values() { [native code] }
console.log(arr[Symbol.iterator]); // f values() { [native code] }
console.log(map[Symbol.iterator]); // f values() { [native code] }
console.log(set[Symbol.iterator]); // f values() { [native code] }
console.log(els[Symbol.iterator]); // f values() { [native code] }
// 调用这个工厂函数会生成一个迭代器
console.log(str[Symbol.iterator]()); // StringIterator {}
console.log(arr[Symbol.iterator]()); // ArrayIterator {}
console.log(map[Symbol.iterator]()); // MapIterator {}
console.log(set[Symbol.iterator]()); // SetIterator {}
console.log(els[Symbol.iterator]()); // ArrayIterator {}

image.png

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值。若不调用 next(),则无法知道迭代器的当前位置

next()方法返回的迭代器对象 IteratorResult 包含两个属性:done 和 value。done 是一个布尔值,表示是否还可以再次调用 next()取得下一个值;value 包含可迭代对象的下一个值(done 为false),或者 undefined(done 为 true)。done: true 状态称为“耗尽”。

image.png

这里要分清楚几个概念:

  • 可迭代对象
  • 迭代器工厂函数:arr[Symbol.iterator]
  • 迭代器:iter = arr[Symbol.iterator]()
  • 迭代器结果对象:iter.next()

这里通过创建迭代器并调用 next()方法按顺序迭代了数组,直至不再产生新值。迭代器并不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大。只要迭代器到达 done: true 状态,后续调用 next()就一直返回同样的值了

let arr = ['foo'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
console.log(iter.next()); // { done: true, value: undefined }
console.log(iter.next()); // { done: true, value: undefined }
console.log(iter.next()); // { done: true, value: undefined }

image.png

image.png

提前终止迭代器

image.png


08-30 定性数组

定型数组

  • 其目标是开发一套 JavaScript API,从而充分利用 3D 图形 API 和 GPU 加速,以便在<canvas>元素上渲染复杂的图形

  • 在 WebGL 的早期版本中,因为 JavaScript 数组与原生数组之间不匹配,所以出现了性能问题。图形驱动程序 API 通常不需要以 JavaScript 默认双精度浮点格式传递给它们的数值,而这恰恰是 JavaScript 数组在内存中的格式。因此,每次 WebGL 与 JavaScript 运行时之间传递数组时,WebGL 绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值转型为新数组中的适当格式,而这些要花费很多时间。

  • 这当然是难以接受的,Mozilla 为解决这个问题而实现了 CanvasFloatArray。这是一个提供 JavaScript 接口的、C 语言风格的浮点值数组。JavaScript 运行时使用这个类型可以分配、读取和写入数组。这个数组可以直接传给底层图形驱动程序 API,也可以直接从底层获取到。最终,CanvasFloatArray 变成了 Float32Array,也就是今天定型数组中可用的第一个“类型”。

  • ArrayBuffer()是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间(书上是字节,但是看 MDN,以及书后面的内容,可以看到设置的是比特,看个人理解吧)

    image.png

    image.png

Map

基本操作

初始化

const m1 = new Map([
  ['key1', 'val1'],
  ['key2', 'val2'],
  ['key3', 'val3'],
]);
console.log(m1);
const m2 = new Map({
  [Symbol.iterator]: function* () {
    yield ['key1', 'val1'];
    yield ['key2', 'val2'];
    yield ['key3', 'val3'];
  },
});
console.log(m2);

初始化之后,可以使用 set() 方法再添加键/值对。另外,可以使用 get()has() 进行查询,可 以通过 size 属性获取映射中的键/值对的数量,还可以使用 delete()clear()删除值。

const m = new Map();
alert(m.has('firstName')); // false
alert(m.get('firstName')); // undefined
alert(m.size); // 0
m.set('firstName', 'Matt').set('lastName', 'Frisbie');
alert(m.has('firstName')); // true
alert(m.get('firstName')); // Matt
alert(m.size); // 2
m.delete('firstName'); // 只删除这一个键/值对
alert(m.has('firstName')); // false
alert(m.has('lastName')); // true
alert(m.size); // 1
m.clear(); // 清除这个映射实例中的所有键/值对
alert(m.has('firstName')); // false
alert(m.has('lastName')); // false
alert(m.size); // 0

与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的。

const m = new Map();
const objKey = {},
  objVal = {},
  arrKey = [],
  arrVal = [];
m.set(objKey, objVal);
m.set(arrKey, arrVal);
objKey.foo = 'foo';
objVal.bar = 'bar';
arrKey.push('foo');
arrVal.push('bar');
console.log(m.get(objKey)); // {bar: "bar"}
console.log(m.get(arrKey)); // ["bar"]

SameZeroValue 比较 也可能导致意想不到的冲突:

const m = new Map();
const a = 0/"", // NaN
b = 0/"", // NaN
pz = +0,
nz = -0;
alert(a === b); // false
alert(pz === nz); // true
m.set(a, "foo");
m.set(pz, "bar");
console.log(m)
alert(m.get(b)); // foo
alert(m.get(nz)); // bar

SameValueZero 是 ECMAScript 规范新增的相等性比较算法。关于 ECMAScript 的相等性比较,可以参考 MDN 文档中的文章 “Equality Comparisons and Sameness”

image.png

与 Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。

选择 Object 还是 map

image.png

WeakMap

基本概念

ECMAScript 6 新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/值对存储机制。WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式

弱键

WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。

const wm = new WeakMap();
wm.set({}, "val");

set() 方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标。

再看一个稍微不同的例子:

const wm = new WeakMap();
const container = {
  key: {},
};
wm.set(container.key, 'val');
function removeReference() {
  container.key = null;
}

这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉

不可迭代键

因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。当然,也用不着像 clear() 这样一次性销毁所有键/值的方法。WeakMap 确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问 WeakMap 实例,也没办法看到其中的内容。

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果 允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。

迭代一个 map 的例子

const m = new Map();
m.set('key1', 'val1');
m.set('key2', 'val2');
for (let [key, val] of m.entries()) {
  console.log(key, val);
}

尝试迭代 weakmap 的几个报错

image.png

image.png

题外话:写这个例子的时候又把 for...in 记混了

  • for in 不关心顺序,而且会遍历到继承的可枚举属性
  • 那么就要慎重使用 for in 了,或者最好不适用 for in,转而使用 for of 或者 forEach

参考:1. for...offor...in的区别

image.png

使用场景

image.png


08-29 Array

数组索引

  • 数组 length 属性的独特之处在于,它不是只读的。通过修改 length 属性,可以从数组末尾删除或添加元素。

各种方法

  • 检测数组: instanceof 足以

  • 迭代器: keys()values()entries()

  • 复制和填充方法: copyWithin(), fill()

  • 栈方法:pop(), push()

  • 队列方法:unshift(), shift()

  • 排序方法: reverse(), sort(), sort 默认是用 string 转型函数

    image.png

    以及比较函数的用法,核心:比较函数就是要返回小于 0、0 和大于 0 的数值,因此减法操作完全可以满足要求

    image.png

  • 操作方法:concat() slice() splice()

  • splice image.png

  • 搜索:indexOf()、lastIndexOf() includes() find()

  • 迭代方法

    image.png

  • 归并方法:ECMAScript 为数组提供了两个归并方法:reduce()reduceRight()。这两个方法都会迭代数 组的所有项,并在此基础上构建一个最终返回值。reduce() 方法从数组第一项开始遍历到最后一项。 而 reduceRight() 从最后一项开始遍历至第一项。

定型数组

提升向原生库传输数据的效率


08-28 集合引用类型

Object

  • Object 类型的属性会自动转换成字符串
  • 关于对象传递参数:最好的方式是对函数的必选参数使用命名参数,再通过一个对象字面量来封装多个可选参数
  • 存取属性的方法:
    • 点语法
    • 中括号:中括号的主要优势就是可以通过变量访问属性

Array

  • ECMAScript 数组也是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据
  • ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
  • 创建数组时可以给构造函数传一个值。这时候就有点问题了,因为如果这个值是数值,则会创建一个长度为指定数值的数组;而如果这个值是其他类型的,则会创建一个只包含该特定值的数组。

Array.from & Array.of

Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()和 of()。from()用于将类数组结构转换为数组实例,而 of()用于将一组参数转换为数组实例。

Array.from()的第一个参数是一个类数组对象,即任何可迭代的结构,或者有一个 length 属性 和可索引元素的结构

image.png

image.png

数组空位

  • ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined image.png
  • ES6 之前的方法不一定,比方说 join() 就会看成是空格,map() 方法中看成 undefined

image.png 由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,则可以显式地用 undefined 值代替。


08-26 单列内置对象 Global、Math、内存以及字节概念

  • ECMA-262 对内置对象的定义是:“任何由 ECMAScript 实现提供与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象”。
  • 这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括 Object、Array 和 String。

Global 方法

  • ECMA-262 规定 Global 对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。所以没有什么全局函数以及全局变量,在全局作用域中定义的变量以及函数都会变成 Global 定义的属性
  • 本书前面介绍的函数,包括 isNaN()isFinite()parseInt()parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法。

URL 编码

  • encodeURI()和 encodeURIComponent()方法用于编码统一资源标识符(URI),以便传给浏览器。有效的 URI 不能包含某些字符,比如空格。使用 URI 编码方法来编码 URI 可以让浏览器能够理解它们,同时又以特殊的 UTF-8 编码替换掉所有无效字符
  • 一般来说,使用 encodeURIComponent()应该比使用 encodeURI()的频率更高,这是因为编码查询字符串参数比编码基准 URI 的次数更多
let uri = 'http://www.wrox.com/illegal value.js#start';
// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURI(uri));
// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri));

与 encodeURI()和 encodeURIComponent()相对的是 decodeURI()和 decodeURIComponent()。

eval()方法

最后一个方法可能是整个 ECMAScript 语言中最强大的了,它就是 eval()。这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(JavaScript)字符串。

当解释器发现 eval()调用时,会将参数解释为实际的 ECMAScript 语句,然后将其插入到该位置。通过 eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意味着定义在包含上下文中的变量可以在 eval()调用内部被引用,比如下面这个例子:

let msg = 'hello world';
eval('console.log(msg)'); // "hello world"
  • 通过 eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在 一个字符串中的。它们只是在 eval()执行的时候才会被创建。
  • 在严格模式下,在 eval()内部创建的变量和函数无法被外部访问。
  • 解释代码字符串的能力是非常强大的,但也非常危险。在使用 eval()的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对 XSS 利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。

Global 属性

Global 对象有很多属性,其中一些前面已经提到过了。像 undefined、NaN 和 Infinity 等特殊 值都是 Global 对象的属性。此外,所有原生引用类型构造函数,比如 Object 和 Function,也都是 Global 对象的属性。下表列出了所有这些属性。

image.png

image.png


window 对象

虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global 对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性。

Math、window.crypto.getRandomValues() 以及字节

  • ECMAScript 提供了 Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助计算的属性和方法。
  • Math 对象上提供的计算要比直接在 JavaScript 实现的快得多, 因为 Math 对象上的计算使用了 JavaScript 引擎中更高效的实现和处理器指令。但使用 Math 计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。

image.png

  • min()和 max()方法
  • 接下来是用于把小数值舍入为整数的 4 个方法:Math.ceil()Math.floor()Math.round()Math.fround()
  • random()方法(Math.random()方法在这里出于演示目的是没有问题的。如果是为了加密而需要 生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用 window.crypto. getRandomValues()) js function selectFrom(lowerValue, upperValue) { let choices = upperValue - lowerValue + 1; return Math.floor(Math.random() * choices + lowerValue); } let num = selectFrom(2, 10); console.log(num); // 2~10 范围内的值,其中包含 2 和 10

用以下代码执行完

const arr = [
  new Int8Array(3),
  new Uint8Array(3),
  new Uint8ClampedArray(3),
  new Int16Array(3),
  new Uint16Array(3),
  new Int32Array(3),
  new Uint32Array(3),
  new BigInt64Array(3),
  new BigUint64Array(3),
];
for (let i = 0; i <= 8; i++) {
  self.crypto.getRandomValues(arr[i]);
}
console.log(arr);

之后的输出,看了一下 ArrayBuffer 的内存显示:

image.png

image.png

我的理解是: int8Array 中的 8 代表 比特(bit),8 个比特就是一个字节 1Bytenew BigInt64Array(3) 一个数字就要占 64 个 bit,对应 8 个字节,也就是 8Byte

image.png

image.png

为什么要用十六进制展示字节呢?

我的想法是:一个字节可以展示的二进制数字就是 0b00000000 ~ 0b11111111(0b代表二进制,0x代表十六进制), 对应的 16 进制就是 0x00~0xFF用两个字母就可以表达出内存了,所以 16 进制是给人来看的,2 进制是给计算机用于处理数据的。

用 winhex 打开一个文本可以查看它存储的数据

image.png

再打开一个简单的图片,总共有 1467 组 16 进制数字,所以大小就是 1467btye,即 1.467kb

image.png

存储单位的混淆普遍化

image.png

参考:1. 千字节 维基百科


08-25 String

常见的方法

  • trim()
  • padStart()/padEnd()
  • concat()
  • fromCharCode()
  • "A".charCodeAt()
  • slice()
  • substring()
  • match() 和 RegExp.exec() match()方法返回的数组与 RegExp 对象的 exec()方法返回的数组是一样的
  • localeCompare()

unicode

unicode 万国码:以书写系统为标准

三种概念:字符,字符集,字符编码

锟斤拷 �⊠ 是怎样炼成的——中文显示“⼊”门指南【柴知道】

关于字符编码的内容,看了一下翻译过来的内容,总结如下

  • 如果内存、文件或电子邮件中有一个字符串,您必须知道它的编码方式,否则您无法正确解释它或将其显示给用户。
  • 不存在纯文本这样的东西。There Ain’t No Such Thing As Plain Text.
  • unicode 类似把世界上所有的语言字符都做了映射处理

08-24 原始值和包装类型、Number

Number

let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

原始值和包装类型简介

为了方便操作原始值,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。如果请求返回范围在 200299 的状态码,则 足以让期约的状态变为兑现。此时期约内部就可以收到一个 JSON 字符串。类似地,如果请求返回的状 态码不在 200299 这个范围内,那么就会把期约状态切换为拒绝。此时拒绝的理由可能是一个 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 版)