ES6 学习笔记(2)

紧接上篇,篇幅太长。包括二进制数组,SetMapIteratorfor...ofGenerator 函数。

二进制数组

二进制数组是 JavaScript 操作二进制数据的一个接口。二进制数组由3类对象组成:

  1. ArrayBuffer 对象:代表内存中的一段二进制数据,可以通过”视图”进行操作。
  2. TypedArray 视图:共包括9种类型的视图。
  3. DataView 视图:可以自定义复合格式的视图,也可以自定义字节序。

简言之,ArrayBuffer 对象代表原始的二进制数据,TypedArray 视图用于 读/写 简单类型的二进制数据,DataView 视图用于读/写复杂类型的二进制数据。

ArrayBuffer 对象

ArrayBuffer 对象代表存储二进制数据的一段内存,不能直接读/写,只能通过视图(TypedArray 视图和 DataView 视图)读/写。ArrayBuffer 是一个构造函数,可分配一段可以存放数据的连续内存区域。

1
2
// 下面生成了一段 32 字节的内存区域,每个字节的值默认都是0。
let buf = new ArrayBuffer(32);

  1. ArrayBuffer.prototype.byteLength 返回所分配的内存区域的字节长度。
  2. ArrayBuffer.prototype.slice(start [, end]) 将内存区域的一部分复制生成一个新的 ArrayBuffer 对象。
  3. ArrayBuffer.isView() 返回一个布尔值,判断参数是否为 ArrayBuffer 的视图实例。

TypedArray 视图

ArrayBuffer 对象作为内存区域可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫作”视图”。TypedArray 视图的数组成员都是同一个数据类型。目前 TypedArray 视图一共包括9种类型,每一种视图都是一种构造函数:

  1. Int8Array 8位有符号整数,长度为1个字节。
  2. Uint8Array 8为无符号整数,长度为1个字节。
  3. Uint8ClampedArray 8为无符号整数,长度为1个字节,溢出处理不同。
  4. Int16Array 16位有符号整数,长度为2个字节。
  5. Uint16Array 16位无符号整数,长度为2个字节。
  6. Int32Array 32位有符号整数,长度为4个字节。
  7. Uint32Array 32位无符号整数,长度为4个字节。
  8. Float32Array 32位浮点数,长度为4个字节。
  9. Float64Array 64位浮点数,长度为8个字节。

上面9个构造函数生成的数组,都有 length 属性,都能用方括号运算符获取单个元素,所有数组方法都能在其上使用。普通数组与 TypedArray 数组的差异主要在以下方面:

  1. TypedArray 数组的所有成员都是同一种类型。
  2. TypedArray 数组的成员是连续的,不会有空位。
  3. TypedArray 数组成员的默认值为0。new Array(10) 返回一个普通数组,里面没有任何成员,只有10个空位。
  4. TypedArray 数组只是一层视图,本身不存储数据,它的数据都存储在底层的 ArrayBuffer 对象中。

同一个 ArrayBuffer 对象上,可以根据不同的数据类型建立多个视图。

1
TypedArray(buffer, byteOffset = 0, length ?)
  1. buffer 参数(必须): 视图对应的底层 ArrayBuffer 对象。
  2. byteOffset 参数(可选): 视图开始的字节序号,默认从0开始。
  3. length 参数(可选): 视图包含的数据个数,默认直到本段内存区域结束。

视图还可以不通过 ArrayBuffer 对象,而是直接分配内存生成。

1
2
3
4
let f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

TypedArray 数组的构造函数可以接受另一个 TypedArray 实例作为参数。此时生成的新数组只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存存储数据,不会在原数组的内存之上建立视图。

1
let typedArray = new Int8Array(new Uint8Array(4));

构造函数的参数也可以是一个普通数组,然后直接生成 TypedArray 实例。这时 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图。

1
2
3
let typedArray = new Uint8Array([1, 2, 3, 4]);
// 转换回普通数组
let normalArray = Array.prototype.slice.call(typedArray);

普通数组的方法和属性对 TypedArray 数组完全适用。不过 TypedArray 数组没有 concat 方法。另外,TypedArray 数组与普通数组一样部署了 Iterator 接口,可以遍历

1
2
3
4
let ui8 = Uint8Array.of(0, 1, 2);
for(let byte of ui8) {
console.log(byte);
}
  1. 每一种视图的构造函数,都有一个 BYTES_PER_ELEMENT 属性,表示这种数据类型占据的字节数。

    1
    2
    3
    4
    5
    6
    7
    8
    Int8Array.BYTES_PER_ELEMENT // 1
    Uint8Array.BYTES_PER_ELEMENT // 1
    Int16Array.BYTES_PER_ELEMENT // 2
    Uint8Array.BYTES_PER_ELEMENT // 2
    Int32Array.BYTES_PER_ELEMENT // 4
    Uint32Array.BYTES_PER_ELEMENT // 4
    Float32Array.BYTES_PER_ELEMENT // 4
    Float64Array.BYTES_PER_ELEMENT // 8
  2. ArrayBuffer 与字符串的互相转换,有一个前提,即字符串的方法是确定的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
    function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint16Array(buf));
    }
    // 字符串转为 ArrayBuffer 对象,参数为字符串
    function str2ab(str) {
    // 每个字符占用2个字节
    const buf = new ArrayBuffer(str.length * 2);
    const bufView = new Uint16Array(buf);
    for(let i = 0, strLen = str.length; i < str.length; i++) {
    bufView[i] = str.charCodeAt(i);
    }
    return buf;
    }
  3. 溢出
    TypedArray 数组对于溢出采用的处理方法是求余值。正向溢出的含义是输入值大于当前数据类型的最大值,最后的到的值就等于当前数据类型的最小值加上余值,再减去1;负向溢出等于当前数据类型的最大值减去余值,再加上1。另外,Uint8ClampedArray 视图的溢出与上面的规则有所不同,负向溢出都等于0,正向溢出都等于255。

    1
    2
    3
    4
    // 范围是 [-128, 127]
    let int = new Int8Array(1);
    int8[0] = 128; // -129
    int8[0] = -129; // 127
  4. TypedArray.prototype.buffer 返回整段内存区域对应的 ArrayBuffer 对象,该属性为只读属性。

    1
    2
    3
    // 下面 a 视图对象和 b 视图对象对应同一个 ArrayBuffer 对象,即同一段内存
    const a = new Float32Array(64);
    const b = new Uint8Array(a.buffer);
  5. TypedArray.prototype.byteLength 返回 TypedArray数组占据的内存长度,单位为字节。只读属性。

  6. TypedArray.prototype.byteOffset 返回 TypedArray 数组从底层 ArrayBuffer 对象的哪个字节开始。只读属性。

  7. TypedArray.prototype.length TypedArray 数组含有多少个成员。

    1
    2
    3
    const a = new Int16Array(8);
    a.length; // 8
    a.byteLength; // 16
  8. TypedArray.prototype.set(buf[, start]) 用于复制数组(普通数组或 TypedArray 数组),即将一段内存完全复制到另一段内存。

    1
    2
    3
    4
    let a = new Uint16Array(8);
    let b = new Uint16Array(10);
    // 从 b[2] 开始复制 a 数组的内容
    b.set(a, 2);
  9. TypedArray.prototype.subarray(start[, end]) 对于 TypedArray 数组的一部分再建立一个新的视图。

    1
    2
    3
    4
    let a = new Uint16Array(8);
    let b = a.subarray(2, 3);
    a.byteLength; // 16
    b.byteLength; // 2
  10. TypedArray.prototype.slice(start[, end]) 返回一个指定位置的新的 TypedArray 实例。

  11. TypedArray.of() 将参数转为一个 TypedArray 实例。

    1
    Float32Array.of(0.151, -8, 3.7) // Float32Array [0.151, -8, 3.7]
  12. TypedArray.from(arr[, fn]) 接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于此结构的 TypedArray 实例。参数 fn 是一个函数,对每个元素进行遍历。

    1
    2
    3
    // 可以将一种 TypedArray 实例转为另一种
    let ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
    ui16 instanceof Uint16Array; // true

复合视图

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存中可以依次存放不同类型的数据,这叫做“复合视图”。

1
2
3
4
const buffer = new ArrayBuffer(24);
const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

上面代码将一个24字节的 ArrayBufer 对象分成了3个部分:

  • 字节0到字节3: 1个32位无符号整数。
  • 字节4到字节19: 16个8位整数。
  • 字节20到字节23:1个32位浮点数。

    DataView 视图

    如果一段数据包括多种类型,这时除了建立 ArrayBuffer 对象的复合视图外,还可以通过 DataView 视图进行操作。

    1
    new DataView(ArrayBuffer [, start, length])
  1. DataView.prototype.buffer 返回对应的 ArrayBuffer 对象。
  2. DataView.prototype.byteLength 返回占据的内存字节长度。
  3. DataView.prototype.byteOffset 返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始。

    DataView 实例提供8个方法读取内存,这一系列方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。第二个参数指定数据的存储方式,默认是大端字节序解读数据,如果需要使用小端字节序解读,该参数指定 true。

  4. getInt8(nun [, false]) 读取1个字节,返回一个8位整数。

  5. getUint8(nun [, false]) 读取1个字节,返回一个无符号的8位整数。
  6. getInt16(nun [, false]) 读取2个字节,返回一个16位整数。
  7. getUint16(nun [, false]) 读取2个字节,返回一个无符号的16位整数。
  8. getInt32(nun [, false]) 读取4个字节,返回一个32位整数。
  9. getUint32(nun [, false]) 读取4个字节,返回一个无符号的32位整数。
  10. getFloat32(nun [, false]) 读取4个字节,返回一个32位浮点数。
  11. getFloat64(nun [, false]) 读取8个字节,返回一个64位浮点数。

DataView 视图提供8个方法写入内存,第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据,第三个参数 true 表示使用小端字节序写入。

  1. setInt8(num, data[, false]) 写入1个字节的8位整数。
  2. setUint8(num, data[, false]) 写入1个字节的8位无符号整数。
  3. setInt16(num, data[, false]) 写入2个字节的16位整数。
  4. seUint16(num, data[, false]) 写入2个字节的16位无符号整数。
  5. setInt32(num, data[, false]) 写入4个字节的32位整数。
  6. setUint32(num, data[, false]) 写入4个字节的32位无符号整数。
  7. setFloat32(num, data[, false]) 写入4个字节的32位浮点数。
  8. setFloat64(num, data[, false]) 写入8个字节的64位浮点数。

Set 和 Map 数据结构

Set

ES6 提供了新的数据结构 Set,类似于数组,但是成员的值都是唯一的,没有重复的值。
Set 本身是一个构造函数。向 Set 加入值不会发生类型转换,内部判断两个值是否相同使用的算法类似于精确相等运算符。

  1. Set.prototype.constructor 构造函数,默认就是 Set 函数。
  2. Set.prototype.size 返回 Set 实例的成员总数。

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面介绍4个操作方法:

  1. add(value) 添加某个值,返回 Set 结构本身。
  2. delete(value) 删除某个值,返回一个布尔值,表示删除是否成功。
  3. has(value) 返回一个布尔值,表示参数是否为 Set 的成员。
  4. clear() 清楚所有成员,没有返回值。

Array.from 方法可以将 Set 结构转为数组。如下去除数组的重复元素的方法:

1
2
3
function dedupe(array) {
return Array.from(new Set(array));
}

Set 结构实例有4个遍历方法,可用于遍历成员。其中,keys、values 和 entries 方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值,所以 keys 和 values 方法的行为完全一致。

  1. keys() 返回一个键名的遍历器
  2. values() 返回一个键值的遍历器。
  3. entries() 返回一个键值对的遍历器。
  4. forEach() 使用回调函数遍历每个成员。

Set 结构的实例默认可遍历,其默认遍历器生成函数就是它的 values 方法。这意味着,可以省略 values 方法,直接用 for…of 循环遍历 Set。由于扩展运算符(…)内部使用 for…of 循环,所以也可以用于 Set 结构。

1
2
3
4
5
6
let set = new Set(['red', 'green', 'blue']);
for(let x of set) {
console.log(x);
}
// red, green, blue
let arr = [...set]; // ['red', 'green', 'blue']

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是与 Set 有两个区别:

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。即如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 中。这个特点意味着无法引用 WeakSet 的成员,因此 WeakSet 是不可遍历的。

WeakSet 结构有以下3个方法:

  1. WeakSet.prototype.add(value) 向 WeakSet 实例添加一个新成员。
  2. WeakSet.prototype.delete(value) 清楚 WeakSet 实例的指定成员。
  3. WeakSet.prototype.has(value) 返回一个布尔值,表示某个值是否在 WeakSet 实例中。

Map

JavaScript 的对象(Object)本质上是键值对的集合(hash 结构),但是只能用字符串作为键。而 Map 数据结构,类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。作为构造函数,Map 接受一个数组作为参数,该数组的成员是一个个表示键值对数组。

1
2
let map = new Map([['name', '张三'], ['title', 'author']]);
map.size // 2

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。如果 Map 的键是一个简单类型的值,则只要两个值严格相等,Map 就将其视为一个键,包括0和-0。另外,虽然 NaN 不严格等于自身,但 Map 将其视为同一个键。

1
2
3
let map = new Map();
map.set(['a'], 555);
map.get(['a']); // undefined
  1. Map.prototype.size 返回 Map 结构的成员总数。
  2. Map.prototype.set(key, value) 设置 key 所对应的键值,然后返回整个 Map 结构,如果 key 已经有值,则键值会被更新。
  3. Map.prototype.get(key) 读取 key 对应的键值,如果找不到 key,返回 undefined。
  4. Map.prototype.has(key) 返回一个布尔值,表示某个值是否在 Map 数据结构中。
  5. Map.prototype.delete(key) 删除某个键,返回 true。如果删除失败,则返回 fal
  6. Map.prototype.clear() 清楚所有成员,没有返回值。
  7. Map.prototype.keys() 返回键名的遍历器。
  8. Map.prototype.values() 返回键值的遍历器。
  9. Map.prototype.entries() 返回所有成员的遍历器。
  10. Map.prototype.forEach() 遍历 Map 的所有成员。

Map 结构的默认遍历器接口就是 entries 方法。Map 结构转为数组结构比较快速的方法是结合使用扩展运算符(…)。

1
2
3
4
let map = new Map([[1, 'one'], [2, 'two'], [3, 'three']]);
map[Symbol.iterator] === map.entries;
[...map.entries()] // [[1, 'one'], [2, 'two'], [3, 'three']]
[...map] // [[1, 'one'], [2, 'two'], [3, 'three']]

WeakMap

WeakMap 结构与 Map 结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象不计入垃圾回收机制。WeakMap 只有 4 个方法可用:get()、set()、has() 和 delete()。

Iterator 和 for…of 循环

Iterator 的概念

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据接口,只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口。ES6 中,有3类数据结构原生具备 Iterator 接口:数组、某些类似数组的对象以及 Set 和 Map 结构。调用这个接口,就会返回一个遍历器对象。Iterator 的作用有3个:

  1. 为各种数据结构提供一个统一的,简便的访问接口。
  2. 数据结构的成员能够按某种次序排列。
  3. 主要供 for…of 消费。

Iterator 的遍历过程是这样的:

  1. 创建一个指针对象,指向当前数据结构的起始位置。遍历器对象本质上就是一个指针对象。
  2. 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。

每一次调用 next 方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value 和 done 两个属性的对象。其中,value 属性是当前成员的值,done 属性是一个布尔值,表示遍历是否结束。

以下几种场合会默认调用 Iterator 接口:

  1. for…of 循环
  2. 解构赋值:对数组和 Set 结构进行解构赋值时
  3. 扩展运算符(…)
  4. yield* 后面跟的是一个可遍历的结构,会调用该结构的遍历器接口。

    1
    2
    3
    4
    5
    6
    let generator = function* () {
    yield 1;
    yield* [2, 3, 4];
    yield 5;
    }
    const iterator = generator();
  5. 其它场合:由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合其实都调用了遍历器接口。例如 Array.from()、Map()、Set()、WeakMap()、WeakSet()、Promise.all()、Promise.race()

  6. 字符串:字符串是一个类似数组的对象,也原生具有 Iterator 接口。

遍历器对象的 return 和 throw

遍历器对象的 next 方法是必须部署的,return 和 throw 方法是可选的。return 的使用场景:如果 fro…of 循环提前退出(通常是因为出错,或者有 break 或 continue 语句),就会调用 return 方法;如果一个对象在完成遍历前需要清理或释放资源,就可以部署 return 方法。return 方法必须返回一个对象。throw 方法主要是配合 Generator 函数使用。

Generator 函数

Generator 函数是一个普通函数,但有两个特征:一是 function 命令与函数名之间有一个星号;二是函数体内部使用 yield 语句定义不同的内部状态。调用 Generator 函数后,该函数并不执行,返回的是一个指向内部状态的指针对象,即遍历器对象。下一步,必须调用遍历器对象的 next 方法,使得指针移向下一个状态。Generator 函数可以不用 yield 语句,就变成了一个单纯的暂缓执行函数。

遍历器对象的 next 方法的运行逻辑如下:

  1. 遇到 yield 语句就暂定执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回的对象的 value 属性值。
  2. 下一次调用 next 方法时再继续往下执行,直到遇到下一条 yield 语句。
  3. 如果没有再遇到新的 yield 语句,就一直运行到函数结束,知道 return 语句为止,并将 return 语句后面的表达式的值作为返回的对象的 value 属性。
  4. 如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined。

与 Iterator 接口的关系

任意一个对象的 Symbol.iterator 方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象。遍历器对象本身也有 Symbol.iterator 方法,执行后返回自身。

1
2
3
function * gen() {}
let g = gen();
g[Symbol.iterator] === g; // true

next 方法的参数

yield 语句本身没有返回值,或者说总是返回 undefined,next 方法可以带一个参数,该参数会被当作上一条 yield 语句的返回值。所以第一次使用 next 方法时不能带有参数。

for…of 循环

for…of 循环可以自动遍历 Generator 函数,且此时不再需要调用 next 方法。一旦 next 方法的返回对象的 done 属性为 true,for…of循环就会终止,且不包含该返回对象,所以下面的 return 语句返回的3不包括在 for…of 循环中。

1
2
3
4
5
6
7
8
function *foo() {
yield 1;
yield 2;
return 3;
}
for (let v of foo()) {
console.log(v);
} // 1 2

Generator.prototype.throw()

Generator 函数返回的遍历器对象都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。下面的代码中,遍历器对象i连续抛出两个错误,第一个错误被 Generator 函数体内的 catch 语句捕获,然后 Generator 函数执行完成,于是第二个错误被函数体外的 catch 语句捕获。如果 Generator 函数内部部署了 try…catch 代码块,那么遍历器的 throw 方法抛出的错误不影响下一次遍历,否则遍历直接终止且 throw 方法抛出的错误将被外部 try…catch 代码块捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let g = function * () {
while(true) {
try {
yield;
} catch(e) {
console.log('内部捕获', e);
}
}
}
let i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

Generator 函数内抛出的错误也可以被函数体外的 catch 捕获。一旦 Generator 执行过程中抛出错误,就不会再执行下去了。如果此后还调用 next 方法,将返回一个 value 属性等于 undefined、done 属性等于 true 的对象,即 javascript 引擎认为这个 Generator 已经运行结束。

Generator.prototype.return()

Generator 函数返回的遍历器对象还有一个 return 方法,可以返回给定的值,并终结 Generator 函数的遍历。如果 return 方法调用时不提供参数,则返回值的 value 属性为 undefined。如果 Generator 函数内部有 try…finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

yield* 语句

用来在一个 Generator 函数里面执行另一个 Generator 函数。如果 yield 命令后面跟的是一个遍历器对象,那么需要在 yield 命令后面加上星号,表明返回的是一个遍历器对象。这被称为 yield 语句。任何数据结构只要有 Iterator 接口,就可以用 yield 遍历。如果被代理的 Generator 函数有 return 语句,那么可以向代理它的 Generator 函数返回数据。

作为对象属性的 Generator 函数

1
2
3
4
let obj = {
* myGenerator1() {},
myGenerator2: function* () {}
}

应用

  1. 异步操作的同步化表达
  2. 控制流管理
  3. 部署 Iterator 接口
  4. 作为数据结构:可以对任意表达式提供类似数组的接口