JavaScript 高级程序设计(第 4 版)- 集合引用类型

2023-05-17 15:05:50 浏览数 (2)

# Object

  • 显示创建方法
    • 使用 new 操作符和 Object 构造函数
    • 使用对象字面量,对象定义的简写形式,目的是为了简化包含大量属性的对象的创建
  • 可以通过点语法或中括号来存取属性

# Array

  • 创建方式
    • Array构造函数
    • 数组字面量
    • 静态方法,from() 和 of()。from()用于将类数组结构转换为数组实例,而of()用于将一组参数转换为数组实例
  • 数组空位
    • 使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)
    • ES6新增的方法将这些空位当成存在的元素,只不过值为undefined
    • ES6之前的方法则会忽略这个空位,但具体行为因方法而异
  • 数组索引
    • 要取得或设置数组的值,需要使用中括号并提供相应值的数字索引
    • 如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展
    • 通过修改数组length属性,可以从数组末尾删除或添加元素
  • 检测数组
    • 在只有一个网页的情况下用instanceof
    • 如果涉及多个全局上下文,可以使用Array.isArray()
  • 迭代器方法
    • keys()返回数组索引的迭代器
    • values()返回数组元素的迭代器
    • entries()返回索引/值的迭代器
  • 复制和填充方法
    • 批量复制方法copyWithin(),按指定范围浅复制数组中的部分内容,然后将他们插入到指定索引开始的位置
    • 填充数组方法fill(),向一个已有的数组中插入全部或部分相同的值
  • 转换方法
    • valueOf()返回数组本身
    • toString()返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串
    • 如果使用自定义分隔符,可以使用join()方法
  • 栈方法
    • push()接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
    • pop()用于删除数组的最后一项,同时减少数组的length值,返回被删除的项
  • 队列方法
    • shift()删除数组的第一项并返回它,然后数组长度减一,使用shift()和push(),可以把数组当成队列来使用
    • unshift()在数组开头添加任意多个值,然后返回新数组长度,使用unshift()和pop(),可以在相反方向上模拟队列
  • 排序方法
    • reverse()将数组元素反向排列
    • sort()默认会按照升序重新排列数组元素,会在每一项上调用String()转型函数,然后比较字符串
    • sort()也可以接受一个比较函数,比较函数接受两个参数,第一个参数应该排在第二个参数前面,就返回负值,相反负值,相等返回0
  • 操作方法
    • concat()可以在现有数组全部元素基础上创建一个新数组,先创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回新构建的数组
    • slice()用于创建一个包含原有数组中一个或多个元素的新数组,接收两个参数:返回元素的开始索引和结束索引。该操作不影响原始数组
    • splice()在数组中间插入元素
      • 删除,两个参数:要删除的第一个元素的位置和要删除的元素数量
      • 插入,三个参数:开始位置,0(要删除的元素数量)和要插入的元素(第三个参数后还可以传第四个及更多要插入的元素)
      • 替换:插入元素数量和删除元素数量一致即为替换
  • 搜索和位置方法
    • 严格相等搜索
      • indexOf() 和 lastIndex() 返回要查找的元素在数组中的位置,没有返回-1
      • includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项
      • 使用全等(===)比较
    • 断言函数搜索
      • 每个索引调用这个函数,断言函数的返回值决定了相应索引的元素是否被认为匹配
      • find()和findIndex()使用了断言函数,从数组最小索引开始,前者返回第一个匹配的元素,后者匹配第一个匹配元素索引,可接受第二个参数,用于指定断言函数内部this的值
  • 迭代方法
    • 迭代方法接收两个参数:以每一项为参数运行的函数,可选的作为函数运行上下文的作用域对象
    • 传入每个方法的函数接受三个参数:数组元素,元素索引和数组本身
    • every()对数组每一项运行传入的函数,如果每个函数都返回true,则这个方法返回true
    • filter()对数组每一项运行传入的函数,函数返回true的项会组成数组之后返回
    • forEach()对数组每一项都运行传入的函数,没有返回值
    • map()对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
    • some()对数组每一项都运行传入的函数,如果有一项函数返回true,则这个方法返回true
  • 归并方法
    • 两个归并方法:reduce()和reduceRight(),迭代数组的所有项,并在此基础上构建一个最终返回值
    • reduce()方法从数组第一项开始遍历到最后一项,reduceRight()从最后一项开始
    • 两个参数:对每一项都会运行的归并函数,可选的作为归并起点的初始值
    • 归并函数接收4个参数:上一个归并值,当前值,当前项的索引和数组本身
    • 如果没有归并起点传入,则第一次迭代将从数组的第二项开始,此时传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项

# 定型数组

定型数组是新增结构,目的是提升向原生库传输数据的效率。实际上,是一种特殊的包含数值类型的数组

# 历史

  • 在WebGL早期版本中,JS数组与原生数组之间不匹配出现了性能问题。
  • Mozilla为了解决该问题实现了CanvasFloatArray。一个提供JS接口的、C语言风格的浮点值数组
    • JS运行时使用这个类型可以分配、读取和写入数组
    • 该数组可以直接传递给底层图形驱动程序API,也可以直接从底层获取到
    • CanvasFloatArray最后变成了Float32Array,是定型数组第一个“类型”

# ArrayBuffer

  • Float32Array实际上是一种视图,可允许JS运行时访问一块名为ArrayBuffer的预分配内存
  • ArrayBuffer是所有定型数组及视图引用的基本单位
  • ArrayBuffer()是一个普通的JS构造函数,可用于在内存中分配特定数量的字节空间
  • ArrayBuffer一经创建就不能再调整大小,不过可以使用slice()复制其全部或部分到一个新实例中
  • ArrayBuffer某种程度上类似于C 的malloc()
  • 不能仅通过对ArrayBuffer的引用就读取或写入其内容。要读取或写入ArrayBuffer,就必须通过视图
  • 视图有不同的类型,但引用的都是ArrayBuffer中存储的二进制数据

# DataView

DataView是第一种允许读写ArrayBuffer的视图,专为文件I/O和网络I/O设计,其API支持对缓冲数据的高度控制,性能较较差。对缓冲内容没有预设,也不能迭代。必须在对已有的ArrayBuffer读取或写入时才能创建DataView实例,该实例可以使用全部或部分ArrayBuffer,且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置。

代码语言:javascript复制
const buf = new ArrayBuffer(16);

// DataView默认使用整个ArrayBuffer
const fullDataView = new DataView(buf);
console.log(fullDataView.byteOffset); // 0
console.log(fullDataView.byteLength); // 16
console.log(fullDataView.buffer === buf); // true

// 构造函数接收一个可选的字节偏移量和字节长度
// byteOffset=0 表示视图从缓冲起点开始
// byteLength=8 限制视图为前8个字节
const firstHalfDataView = new DataView(buf, 0, 8);
console.log(firstHalfDataView.byteOffset); // 0
console.log(firstHalfDataView.byteLength); // 8
console.log(firstHalfDataView.buffer === buf); // true

// 如果不指定,则DataView会使用剩余的缓冲
// byteOffset=8 表示视图从缓冲起点开始
// byteLength 未指定,则默认为剩余缓冲
const secondHalfDataView = new DataView(buf, 8);
console.log(secondHalfDataView.byteOffset); // 8
console.log(secondHalfDataView.byteLength); // 8
console.log(secondHalfDataView.buffer === buf); // true

要通过DataView读取缓冲,还需要几个组件

  • 首先要读或写字节偏移量,可看成DataView中的某种“地址”
  • DataView应该使用ElementType来实现JS的Number类型到缓冲内二进制格式的转换
  • 内存中值的字节序,默认是大端字节序
  1. ElementType
  • DataView对存储在缓冲内的数据类型没有预设,其暴露的API强制开发者在读、写时指定一个ElementType,然后DataView就会为读、写完成相应转换
  • ES6支持8种不同的ElementType

ElementType

字节

说明

等价的C类型

值的范围

Int8

1

8位有符号整数

signed char

-128~127

UInt8

1

8位无符号整数

unsigned char

0~255

Int16

2

16位有符号整数

short

-32768~32767

Uint16

2

16位无符号整数

unsigned short

0~65535

Int32

4

32位有符号整数

int

-2147483648~2147483647

Uint32

4

32位无符号整数

unsigned int

0~4294967295

Float32

4

32位IEEE754浮点数

float

-3.4e 38~ 3.4e 38

Float64

8

64位IEEE754浮点数

double

-1.7e 308~ 1.7e 308

  • DataView为上表中的每种类型都暴露了get和set方法,这些方法使用byteOffset定位要读取或写入值的位置。类型是可以互换使用的
代码语言:javascript复制
// 在内存中分配两个字节并声明一个DataView
const buf = new ArrayBuffer(2);
const view = new DataView(buf);

console.log(view.getInt8(0)); // 0
console.log(view.getInt8(0)); // 0
// 检查整个缓冲
console.log(view.getInt16(0)); // 0

view.setUint8(0, 255); // 将整个缓冲都设置为1 255=>11111111(2^8-1)

view.setUint8(1, 0xFF); // DataView会自动将数据转换为特定的ElementType

// 现在缓冲里都是1了
console.log(view.getInt16(0)); // -1 如果当成二补数的有符号整数 应该是-1

  1. 字节序
  • 字节序指计算系统维护的一种字节顺序的约定
  • DataView只支持两种约定:大端字节序和小端字节序
    • 大端字节序:网络字节序,最高位有效位保存在第一个字节,最低有效位保存在最后一个字节
    • 小端字节序:与大端字节序相反
  • JS运行时所在系统的原生字节序决定了如何读取或写入字节,但DataView并不遵守这个约定
  • 对于一段内存而言,DataView是一个中立接口,会遵守指定的字节序
  • DataView的所有API方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为true即可启用小端字节序
代码语言:javascript复制
const buf = new ArrayBuffer(2);
const view = new DataView(buf);

view.setUint8(0, 0x80); // 设置最左边为1
view.setUint8(1, 0x01); // 设置最右边为1

// 缓冲内容
// 0x8  0x0  0x0  0x1
// 1000 0000 0000 0001

// 按大端字节序读取Uint16
// 0x8001 = 2^15   2^0 = 32768   1 = 32769
console.log(view.getUint16(0));

// 按小端字节序读取Uint16
// 0x0180 = 2^8   2^7 = 256   128 = 384
console.log(view.getUint16(0, true));

// 按大端字节序写入Uint16
view.setUint16(0, 0x0004);
// 按小端字节序写入Uint16
view.setUint16(0, 0x0002, true);

  1. 边界情形
  • DataView完成读、写操作的前提是必须有充足的缓冲区,否则会抛出RangeError
  • DataView在写入缓冲里会尽最大努力把一个值转换为适当的类型,后背为0。如果无法转换,则抛出错误

# 定型数组

定型数组是另一种形式的ArrayBuffer视图。概念上与DataView接近,但定型数组特定于一种ElementType且遵循系统原生的字节序,提供了适用面更广的API和更高的性能。设计定型数组的目的就是提高与WebGL等原生库交换二进制数据的效率。

  • 创建定型数组的方式包括读取已有的缓冲、使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。
代码语言:javascript复制
const buf = new ArrayBuffer(12); // 创建一个12字节的缓冲
const ints = new Int32Array(buf); // 创建一个引用该缓冲的Int32Array

// 这个定型数组知道自己的每个元素需要4字节, 因此长度为3
console.log(ints.length); // 3

// 创建一个长度为6的Int32Array
const ints2 = new Int32Array(6);
// 每个数值使用4字节,因此ArrayBuffer是24字节
console.log(ints2.length); // 6
console.log(ints2.buffer.byteLength); // 24

// 创建一个包含[2,4,6,8]的Int32Array
const ints3 = new Int32Array([2, 4, 6, 8]);
console.log(ints3.length); // 4
console.log(ints3.buffer.byteLength); // 16
console.log(ints3[2]); // 6

// 通过复制ints3的值创建一个Int16Array
const ints4 = new Int16Array(ints3);
// 这个新类型数组会分配自己的缓冲
// 对应索引的每个值会相应地转换为新格式
console.log(ints4.length); // 4
console.log(ints4.buffer.byteLength); // 8
console.log(ints4[2]); // 6

// 基于普通数组来创建一个Int16Array
const ints5 = new Int16Array.from([3,5,7,9]);
console.log(ints5.length); // 4
console.log(ints5.buffer.byteLength); // 8
console.log(ints5[2]); // 7

// 基于传入的参数创建一个Float32Array
const floats = Float32Array.of(3.14, 2.718, 1.618);
console.log(floats.length); // 3
console.log(floats.buffer.byteLength); // 12
console.log(floats[2]); // 1.6180000305175781

  • 定型数组的构造函数和实例都有一个BYTES_PRE_ELEMENT属性,返回该类型数组中每个元素所占用的字节数
  • 如果定型数组没有用任何值初始化,则其关联的缓冲会以0填充
# 定型数组行为
  • 定型数组与普通数组都很相似,定型数组支持如下操作符、方法和属性
    • []
    • copyWithin()
    • entries()
    • every()
    • fill()
    • filter()
    • find()
    • findIndex()
    • forEach()
    • indexOf()
    • join()
    • keys()
    • lastIndexOf()
    • length
    • map()
    • reduce()
    • reduceRight()
    • reverse()
    • slice()
    • some()
    • sort()
    • toLocaleString()
    • toString()
    • values()
  • 返回新数组的方法也会返回包含同样元素类型的新定型数组
  • 定型数组有一个Symbol.iterator符号属性,因此可以通过for..of循环和扩展操作符来操作
# 合并、复制和修改定型数组

定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小,故以下方法不适用于定型数组

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

定型数组提供set()和subarray()快速向外或向内复制数据

  • set()从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置
  • subarray()基于从原始定型数组中复制的值返回一个新定型数组,复制值时的开始索引和结束索引是可选的
代码语言:javascript复制
// 创建长度为8的int16数组
const container = new Int16Array(8);
// 把定型数组复制为前4个值,偏移量默认为索引0
container.set(Int8Array.of(1,2,3,4));
console.log(container); // [1,2,3,4,0,0,0,0]
// 把普通数组复制为后4个值,偏移量4表示从索引4开始插入
container.set([5,6,7,8], 4);
console.log(container); // [1,2,3,4,5,6,7,8]
// 溢出时会抛出错误
container.set([5,6,7,8], 7); // RangeError

const source = Int16Array.of(2, 4, 6, 8);
// 把整个数组复制为一个同类型的新数组
const fullCopy = source.subarray();
console.log(fullCopy); // [2, 4, 6, 8]
// 从索引2开始复制数组
const halfCopy = source.subarray(2);
console.log(halfCopy); // [6, 8]
// 从索引1开始复制到索引3
const partialCopy = source.subarray(1, 3);
console.log(partialCopy); // [4, 6]

定型数组没有原生的拼接能力,可用相关API手动构建

代码语言:javascript复制
// 第一个参数是应该返回的数组类型
// 其余参数是应该拼接在一起的定型数组
function typedArrayConcat(typedArrayConstructor, ...typedArrays) {
  // 计算所有数组中包含的元素总数
  const numElements = typedArrays.reduce((x, y) => (x.length || x)   y.length);
  // 按照提供的类型创建一个数组,为所有元素留出空间
  const resultArray = new typedArrayConstructor(numElements);

  // 依次转移数组
  let currentOffset = 0;
  typedArrays.map(x => {
    resultArray.set(x, currentOffset);
    currentOffset  = x.length;
  });
  return resultArray;
}
const concatArray = typedArrayConcat(Int32Array,
  Int8Array.of(1,2,3),
  Int16Array.of(4,5,6),
  Float32Array.of(7.8.9));
console.log(concatArray); // [1,2,3,4,5,6,7,8,9]

# 下溢和上溢
  • 定型数组中值的下溢和上溢不会影响到其他索引,但仍需要考虑数组的元素应该是什么类型
  • 定型数组对于可以存储的每个索引只接受一个相关位,而不考虑他们对实际数值的影响
  • 除了8种元素类型,还有一种夹板数组类型:Uint8ClampedArray,不允许任何方向溢出,超出最大值255的值会被向下舍入为255,小于最小值0会被向上舍入为0(HTML5canvas元素的历史留存)
代码语言:javascript复制
// 长度为2的有符号整数数组
// 每个索引保存一个二补数形式的有符号整数,范围是-128(-1*2^7)~127(2^7-1)
const ints = new Int8Array(2);

// 长度为2的无符号整数数组
// 每个索引保存一个无符号整数,范围是0~255(2^8-1)
const unsignedInts = new Uint8Array(2);

// 上溢的位不会影响相邻索引,索引只取最低有效位上的8位
unsignedInts[1] = 256; // 0x100 => 0001 0000 0000
console.log(unsignedInts); // [0, 0]
unsignedInts[1] = 511; // 0x1FF => 0001 1111 1111
console.log(unsignedInts); // [0, 255]

// 下溢的位会被转换为其无符号的等价值
// 0xFF是以二补数形式表示的-1(截取到8位),但255是一个无符号整数
unsignedInts[1] = -1; // 0xFF
console.log(unsignedInts); // [0, 255]

// 上溢自动变成二补数形式
// 0x80是无符号整数的128,是二补数形式的-128
ints[1] = 128; // 0x80;
console.log(ints); // [0, -128]

// 下溢自动变成二补数形式
// 0xFF是无符号整数的255 ,是二补数形式的-1
ints[1] = 255; // 0xFF
console.log(ints); // [0, -1]

# Map

Map是一种新的集合类型,带来真正的键/值存储机制,Map的大多数特性都可以通过Object类型实现,但存在细微差异

# 基本API

  • 初始化
代码语言:javascript复制
const m = new Map(); // 创建空映射

// 创建的同时初始化
const m1 = new Map([
  ["key1", "value1"],
  ["key2", "value2"],
  ["key3", "value3"]
]); // 使用嵌套数组初始化

const m2 = new Map({
  [Symbol.iterator]: function*() {
    yield ["key1", "value1"];
    yield ["key2", "value2"];
    yield ["key3", "value3"];
  }
}); // 使用自定义迭代器初始化映射

const m3 = new Map([[]]); // 映射期待的键值对,无论是否提供
console.log(m3.has(undefined)); // true
console.log(m3.get(undefined)); // undefined

  • 添加、查询和删除
代码语言:javascript复制
const m = new Map();
console.log(m.has("firstName")); // false
console.log(m.get("firstName")); // undefined
console.log(m.size); // 0

m.set("firstName", "John")
  .set("lastName", "Frisbie");
console.log(m.has("firstName")); // true
console.log(m.get("firstName")); // John
console.log(m.size); // 2

m.delete("firstName");
console.log(m.has("firstName")); // false
console.log(m.has("lastName")); // true
console.log(m.size); // 1

m.clear();
console.log(m.has("firstName")); // false
console.log(m.has("lastName")); // false
console.log(m.size); // 0

  • Object只能使用数值、字符串或符号作为键,Map可以使用任何JS数据类型作为键。内部使用SameValueZero比较操作,相当于使用严格对象相等的标准来检查匹配性

# 顺序与迭代

  • 与Object类型的一个主要差异就是,Map实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作
代码语言:javascript复制
const m = new Map([
  ['key1', 'value1'],
  ['key2', 'value2'],
  ['key3', 'value3']
]);
console.log(m.entries === m[Symbol.iterator]); // true

for (let pair of m.entries()) {
  console.log(pair);
}
// [key1, value1]
// [key2, value2]
// [key3, value3]

for (let pair of m[Symbol.iterator]()) {
  console.log(pair);
}
// [key1, value1]
// [key2, value2]
// [key3, value3]

// entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组
console.log([...m]); // [[key1, value1], [key2, value2], [key3, value3]]

// forEach
m.forEach((val, key) => {
  console.log(`${key}->${val}`);
});
// key1 -> value1
// key2 -> value2
// key3 -> value3

for (let key of m.keys()) {
  console.log(key);
}
// key1
// key2
// key3

for (let val of m.values()) {
  console.log(val);
}
// value1
// value2
// value3

# 选择Object还是Map

  • 内存占用
    • 指定大小内存,Map可以比Object多存储50%键值对
  • 插入性能
    • 插入Map稍快
    • 插入速度不会随着键值对数量线性增加
  • 查找速度
    • 只包含少量键值对时,Object查找速度更快
  • 删除性能
    • Map的delete()操作比插入和查找更快
    • 涉及大量删除时选Map

# WeakMap

WeakMap是Map的兄弟类型,其API也是Map的子集,weak描述的是JS垃圾回收程序对待“弱映射”中键的方式

# 基本API

  • 弱映射中的键只能是Object或者继承自Object的类型,尝试使用费对象设置会抛出TypeError。值的类型没有限制
  • 初始化之后可以使用 set()再添加键/值对,可以使用 get()和 has()查询, 还可以使用 delete()删除
代码语言:javascript复制
const wm = new WeakMap();

const key1 = { id: 1 },
      key2 = { id: 2 },
      key3 = { id: 3 };
const wm1 = new WeakMap([
  [key1, "value1"],
  [key2, "value2"],
  [key3, "value3"]
]);
console.log(wm1.get(key1)); // value1
console.log(wm1.get(key2)); // value2
console.log(wm1.get(key3)); // value3

# 弱键

  • 弱映射的键是“弱弱地拿着”的,即这些键不属于正式的引用,不会阻止垃圾回收
  • 弱映射中值的引用不是“弱弱地拿着”,只要键存在,键值对就会存在于映射中,并被当做对值的引用,因此就不会被当做垃圾回收
代码语言:javascript复制
const wm = new WeakMap();
wm.set({}, "val");
// 因为没有指向这个对象的其他引用,当这行代码执行完后,对象键就会被当做垃圾回收
// 该键值对就从弱映射中消失,使其成为一个空映射
// 因为值也没有被引用,所以键值对被破坏后,值本身也会成为垃圾回收的目标

const wm = new WeakMap();
const container = {
  key: {}
};
wm.set(container.key, "val");
function removeReference() {
  container.key = null;
}
// container对象维护一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标
// 如果调用了removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以吧这个键值对清理掉

# 不可迭代键

因为WeakMap中的键值对任何时候可能被销毁,所以没必要提供迭代其键值对的能力。也用不着像clear()这样一次性毁掉所有键值的方法。所以不可能在不知道对象引用的情况下从弱映射中取得值。之所以限制只能用对象作为键,就是为了保证只有通过键对象的引用才能取得值。

# 使用弱映射

私有变量

  • 弱映射造就了在JS中实现真正私有变量的一种新方式
  • 私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值
代码语言:javascript复制
const wm = new WeakMap();
class User {
  constructor(id) {
    this.idProperty = Symbol("id");
    this.setId(id);
  }
  setPrivate(property, value) {
    const privateMembers = wm.get(this) || {};
    privateMembers[property] = value;
    wm.set(this, privateMembers);
  }
  getPrivate(property) {
    return wm.get(this)[property];
  }
  setId(id) {
    this.setPrivate(this.idProperty, id);
  }
  getId() {
    return this.getPrivate(this.idProperty);
  }
}
const user = new User(123);
console.log(user.getId()); // 123
user.setId(456);
console.log(user.getId()); // 456

  • 上述方案中,外部代码只需要拿到对象实例的引用和弱映射就可以取得私有变量了,为了解决这种问题,可以用一个闭包把WeakMap包装起来
代码语言:javascript复制
const User = (() => {
  const wm = new WeakMap();
  class User {
    constructor(id) {
      this.idProperty = Symbol("id");
      this.setId(id);
    }
    setPrivate(property, value) {
      const privateMembers = wm.get(this) || {};
      privateMembers[property] = value;
      wm.set(this, privateMembers);
    }
    getPrivate(property) {
      return wm.get(this)[property];
    }
    setId(id) {
      this.setPrivate(this.idProperty, id);
    }
    getId() {
      return this.getPrivate(this.idProperty);
    }
  }
  return User;
})();
const user = new User(123);
console.log(user.getId()); // 123
user.setId(456);
console.log(user.getId()); // 456

DOM节点元数据

  • 因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据
代码语言:javascript复制
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(loginButton, { disabled: true });
// 当节点从DOM树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用该对象)

# Set

Set很多方面像加强的Map,二者大多数API和行为都是共有的。

# 基本API

代码语言:javascript复制
const s = new Set();

// 使用数组初始化集合
const s1 = new Set(["value1", "value2", "value3"]);
console.log(s1.size); // 3

// 使用自定义迭代器初始化集合
const s2 = new Set({
  [Symbol.iterator]: function*() {
    yield "value1";
    yield "value2";
    yield "value3";
  }
});
console.log(s2.size); // 3

const s3 = new Set();

console.log(s3.has("Matt")); // false
console.log(s3.size); // 0

s3.add("Matt")
  .add("Frisbie");

console.log(s3.has("Matt")); // true
console.log(s3.size); // 2

s3.delete("Matt");

console.log(s3.has("Matt")); // false
console.log(s3.has("Frisbie")); // true
console.log(s3.size); // 1

s3.clear();

console.log(s3.has("Matt")); // false
console.log(s3.has("Frisbie")); // false
console.log(s3.size); // 0

  • 与Map类似,Set可以包含任何JS数据类型作为值,集合也使用SameValueZero操作,相当于使用严格对象相等的标准来检查匹配性

# 顺序与迭代

  • Set会维护值插入时的顺序,因此支持按顺序迭代
  • 集合实例提供一个迭代器,能以插入顺序生成集合内容,可以通过values()及其别名方法keys()(或者Symbol.iterator属性)取得这个迭代器
  • values()是默认迭代器,可以直接对集合实例使用扩展操作,把集合转换为数组
  • 集合的entries()方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合中每个值的重复

# 定义正式集合操作

  • 某些Set操作是有关联性的,因此最好让实现的方法能支持处理任意多个集合实例
  • Set保留插入顺序,所有方法返回的集合必须保证顺序
  • 尽可能高效使用内存。扩展操作符的语法很简洁,但尽可能避免集合和数组间的相互转换能节省对象初始化成本
  • 不要修改已有的集合实例。union(a,b)或a.union(b)应该返回包含结果的新集合实例
代码语言:javascript复制
class XSet extends Set {
  union(...sets) {
    return XSet.union(this, ...sets);
  }
  intersection(...sets) {
    return XSet.intersection(this, ...sets);
  }
  difference(set) {
    return XSet.difference(this, set);
  }
  symmetricDifference(set) {
    return XSet.symmetricDifference(this, set);
  }
  cartesinaProduct(set) {
    return XSet.cartesinaProduct(this, set);
  }
  powerSet() {
    return XSet.powerSet(this);
  }
  // 返回两个或多个集合并集
  static union(a, ...bSets) {
    const unionSet = new XSet(a);
    for (const b of bSets) {
      for (const bValue of b) {
        unionSet.add(bValue);
      }
    }
    return unionSet;
  }
  // 返回两个或更多集合的交集
  static intersection(a, ...bSets) {
    const intersectionSet = new Set(a);
    for (const aValue of intersectionSet) {
      for (const b of bSets) {
        if (!b.has(aValue)) {
          intersectionSet.delete(aValue);
        }
      }
    }
    return intersectionSet;
  }
  // 返回两个集合的差集
  static difference(a, b) {
    const differenceSet = new XSet(a);
    for (const bValue of b) {
      if (a.has(bValue)) {
        differenceSet..delete(bValue);
      }
    }
    return differenceSet;
  }
  // 返回两个集合的对称差集
  static symmetricDifference(a, b) {
    // 集合A与集合B中所有不属于A∩B的元素的集合
    return a.union(b).difference(a.intersection(b));
  }
  // 返回两个集合(数组对形式)的笛卡尔积
  // 必须返回数组集合,因为笛卡尔积可能包含相同值的对
  static cartesinaProduct(a, b) {
    const cartesinaProductSet = new XSet();
    for (const aValue of a) {
      for (const bValue of b) {
        cartesinaProductSet.add([aValue, bValue]);
      }
    }
    return cartesinaProductSet;
  }
  // 返回一个集合的幂集
  static powerSet(a) {
    const powerSet = new XSet().add(new XSet());
    for (const aValue of a) {
      for (const set of new XSet(powerSet)) {
        powerSet.add(new XSet(set).add(aValue));
      }
    }
    return powerSet;
  }
}

# WeakSet

WeakSet是Set的兄弟类型,其API是Set的子集。weak描述的是JS垃圾回收程序对待弱集合中值的方式

# 基本API

  • 弱集合中的值只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置值会抛出 TypeError。
  • 如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可迭代对象中的每个值都会按照迭代顺序插入到新实例中
  • 初始化之后可以使用 add()再添加新值,可以使用 has()查询,还可以使用 delete()删除
  • add()方法返回弱集合实例,因此可以把多个操作连缀起来,包括初始化声明
代码语言:javascript复制
const ws = new WeakSet();

const val1 = { id: 1 },
      val2 = { id: 2 },
      val3 = { id: 3 };
// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3]);

console.log(ws1.has(val1)); // true
console.log(ws1.has(val2)); // true
console.log(ws1.has(val3)); // true

// 原始值可以先包装成对象在用作值
const stringVal = new String("val1");
const ws3 = new WeakSet([stringVal]);
console.log(ws3.has(stringVal)); // true

const ws4 = new WeakSet();

const val4 = { id: 1 },
      val5 = { id: 2 };
console.log(ws4.has(val4)); // false
ws4.add(val4)
   .add(val5);
console.log(ws4.has(val4)); // true
console.log(ws4.has(val5)); // true
ws4.delete(val4);
console.log(ws4.has(val4)); // false

# 弱值

  • WeakSet中weak表示弱集合的值是弱弱地拿着,即这些值不属于正式的引用,不会阻止垃圾回收
代码语言:javascript复制
const ws = new WeakSet();

ws.add({}); 
// 当该行代码执行完后,该对象值就会被当做垃圾回收,这个值就从弱集合中消失,成为空集合

const ws2 = new WeakSet();
const container = {
  val: {}
};
ws.add(container.val);
function removeReference() {
  container.val = null;
}
// 执行removeReference()后会摧毁值对象的最后一个引用,垃圾回收程序就可以把这个值清理掉

# 不可迭代值

  • WeakSet 中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力
  • 也用不着像 clear()这样一次性销毁所有值的方法
  • WeakSet 之所以限制只能用对象作为值,是为了保证只有通过值对象的引用才能取得值

# 使用弱集合

  • 可以用于给对象打标签
代码语言:javascript复制
const disabledElements = new WeakSet();
const loginButton = document.querySelector('#login');

// 通过加入对应集合,给这个节点打上禁用标签
// 通过查询元素在不在disabledElements中,就可以知道它是不是被禁用了
disabledElements.add(loginButton); 
// 只要WeakSet中任何元素从DOM中移除,垃圾回收程序就可以忽略其存在,释放内存

# 迭代与扩展操作

  • ECMAScript 6 新增的迭代器和扩展操作符对集合引用类型让集合类型之间相互操作、复制和修改变得异常方便
  • Array、所有定型数组、Map和Set这4种原生集合类型定义了默认迭代器
    • 都支持顺序迭代,可以传入for-of循环
    • 都兼容扩展操作符
    • 都支持多种构建方法,如Array.of()和Array.from()静态方法

0 人点赞