Skip to content

Latest commit

 

History

History
executable file
·
847 lines (636 loc) · 50.3 KB

05、集合引用类型.md

File metadata and controls

executable file
·
847 lines (636 loc) · 50.3 KB

集合引用类型

本节主要包含:

  • 对象
  • 数组与定型数组
  • Map 、 WeakMap 、 Set 以及 WeakSet 类型

Object 概述

Object 是 ECMAScript 中最常用的类型之一。虽然 Object 的实例没有多少功能,但很适合存储和在应用程序间交换数据。

显式地创建 Object 的实例有两种方式:

  • 第一种是使用 new 操作符和 Object 构造函数。
  • 另一种方式是使用对象字面量(object literal)表示法。对象字面量是对象定义的简写形式,目的是为了简化包含大量属性的对象的创建。
    • 在使用对象字面量表示法定义对象时,并不会实际调用 Object 构造函数。
    • 数值属性会自动转换为字符串(最好显示用字符串,避免编辑器报错等)。
// new
let person1 = new Object();
person1.name = "Nicholas";
person1.age = 29;

// 字面量
let person2 = {
  name: "Nicholas",
  age: 29,
};

取值:对象的属性一般是通过点语法来存取的,但也可以使用中括号来存取属性。在使用中括号时,要在括号内使用属性名的字符串形式。

所有对象都有 toLocaleString() 、 toString() 和 valueOf() 方法。

后面章节再详述对象。

Array

  • ECMAScript 数组也是一组有序的数据,数组中每个槽位可以存储任意类型的数据。
  • ECMAScript 数组是动态大小的,会随着数据添加而自动增长。
    • 换句话就是取的索引超过数组的长度,会返回 undefined ,但不会报错。

创建数组

  • 使用 Array 构造函数
    let colors = new Array(20);
  • 使用数组字面量表示法
    • 使用数组字面量表示法创建数组不会调用 Array 构造函数。
    let colors = ["red", "blue", "green"];
  • Array 构造函数的静态方法:from() 和 of()
    • from() 用于将类数组结构(即任何可迭代的结构,或者有一个 length 属性和可索引元素的结构)转换为数组实例
    • of() 用于将一组参数转换为数组实例

数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。

  • ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6 规范重新定义了该如何处理这些空位。
  • ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined。
  • ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异:
// 對於数据空位的处理
const options = [1, , , , 5];

// map()会跳过空位置
console.log(options.map(() => 6)); // [6, undefined, undefined, undefined, 6]

// join()视空位置为空字符串
console.log(options.join("-")); // "1----5"

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

数组索引

  • 要取得或设置数组的值,需要使用中括号并提供相应值的数字索引。
    • 索引小于数组包含的元素数,则返回存储在相应位置的元素
    • 把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值加 1。
  • 数组中元素的数量保存在 length 属性中,这个属性始终返回 0 或大于 0 的值
    • 数组 length 属性不是只读的。通过修改 length 属性,可以从数组末尾删除或添加元素。

数组最多可以包含 4 294 967 295 个(即 2^32-1 个)元素,这对于大多数编程任务应该足够了。如果尝试添加更多项,则会导致抛出错误。以这个最大值作为初始值创建数组,可能导致脚本运行时间过长的错误。

检测数组

可以使用以下三种方法

  • arr instanceof Array
  • Array.isArray(arr)
  • Object.prototype.toString.call(arr)=="[object Array]"

Array 的静态方法

方法 描述
Array.from() 从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
Array.isArray() 确定传递的值是否是一个 Array。
Array.of() 创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

使用

  • Array.from(arrayLike[, mapFn[, thisArg]])
    • 参数
      • arrayLike 想要转换成数组的伪数组对象或可迭代对象。
      • mapFn 可选 如果指定了该参数,新数组中的每个元素会执行该回调函数。
      • thisArg 可选 可选参数,执行回调函数 mapFn 时 this 对象。
    • 返回值
      • 一个新的数组实例。

数组去重:

  • Array.from(new Set(arr))
  • [...new Set(arr)]

Array 的实例方法

分类 名称 是否改变原数组 描述
迭代器方法 arr.keys() 返回数组索引的迭代器(Array Iterator)
arr.values() 返回数组元素的迭代器
arr.entries() 返回数组索引/值对的迭代器
复制 arr.copyWithin(target[, start[, end]]) 浅复制数组的一部分到同一数组中的另一个位置,并返回新数组。(不会改变原数组的长度 length,但是会改变原数组的内容,且需要时会创建新的属性)
填充 arr.fill(value[, start[, end]]) 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。
转换方法 arr.toLocaleString() 返回一个字符串表示数组中的元素。
arr.toString() 返回一个字符串,表示指定的数组及其元素。
arr.valueOf() 返回的数组本身
arr.join([separator]) 将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。
如果数组只有一个项目,那么将返回该项目而不使用分隔符。
栈方法 arr.push(ele1,...,elen) push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。身
arr.pop() 从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
队列方法 arr.shift() 从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。度。
arr.unshift(ele1,...,elen) 将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。
排序方法 arr.reverse() 将数组中元素的位置颠倒,并返回该数组。
数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。
arr.sort([compareFunction]) 用原地算法对数组的元素进行排序,并返回数组。
默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的。
操作方法 arr1.concat(arr2,...,arrn]) 合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
arr.slice([begin[, end]]) 返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括 end)。原始数组不会被改变。
arr.splice() 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。(下详)
搜索和位置方法( 严格相等) arr.indexOf() 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
arr.lastIndexOf() 返回指定元素(也即有效的 JavaScript 值或变量)在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。
arr.includes() 判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
搜索和位置方法( 断言函数) arr.find() 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
找到匹配项后,不再继续搜索。
arr.findIndex() 返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
找到匹配项后,不再继续搜索。
迭代方法 arr.every(callback() 测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。
arr.filter() 创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
arr.forEach() 对数组的每个元素执行一次给定的函数。
arr.map() 创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
arr.some() 测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。
归并方法 arr.reduce() 对数组中的每个元素执行一个由您提供的 reducer 函数(从左到有,升序执行),将其结果汇总为单个返回值。
arr.reduceRight() 接受一个函数作为累加器(accumulator)和数组的每个值(从右到左)将其减少为单个值。
arr.flat() 按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
arr.flatMap() 首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
它与 map 连着深度值为 1 的 flat 几乎相同,但 flatMap 通常在合并成一种方法的效率稍微高一些。

重点说明

  1. 关于转换方法

转换方法中,如果数组中某一项是 null 或 undefined ,则在 join() 、 toLocaleString() 、 toString() 和 valueOf() 返回的结果中会以空字符串表示。

  1. splice()

有 3 种不同的方式使用 splice():

  • 删除。

    • 需要给 splice() 传 2 个参数:要删除的第一个元素的位置和要删除的元素数量。
    var myFish = ["angel", "clown", "drum", "mandarin", "sturgeon"];
    var removed = myFish.splice(3, 1);
    // 运算后的 myFish: ["angel", "clown", "drum", "sturgeon"]
    // 被删除的元素: ["mandarin"]
  • 插入。

    • 需要给 splice() 传 3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素。
    • 第三个参数之后还可以传第四个、第五个参数,乃至任意多个要插入的元素。
    var myFish = ["angel", "clown", "mandarin", "sturgeon"];
    var removed = myFish.splice(2, 0, "drum");
    // 运算后的 myFish: ["angel", "clown", "drum", "mandarin", "sturgeon"],新增 “drum”
    // 被删除的元素: [], 没有元素被删除
  • 替换。

    • splice() 在删除元素的同时可以在指定位置插入新元素,同样要传入 3 个参数:开始位置、要删除元素的数量和要插入的任意多个元素。
    • 要插入的元素数量不一定跟删除的元素数量一致。
    var myFish = ["angel", "clown", "drum", "sturgeon"];
    var removed = myFish.splice(2, 1, "blue", "trumpet");
    // 运算后的 myFish: ["angel", "clown", "blue", "trumpet", "sturgeon"]
    // 被删除的元素: ["drum"]
  1. 迭代方法
  • every() 从数组中搜索符合某个条件的元素。但传入的函数必须对每一项都返回 true ,它才会返回 true。
  • some() 也是从数组中搜索符合某个条件的元素。只要有一项让传入的函数返回 true ,它就会返回 true 。
  • filter() 过滤,返回指定数组中满足给定的函数的项的新数组。
  • map() 对原始数组中同样位置的元素运行传入函数,並返回各项结果。适合创建一个与原始数组元素一一对应的新数组
  • forEach() 只会对每一项运行传入的函数,没有返回值
// map会对数组空位进行处理
let arr = [1, 2, , , 5];
console.log(arr.map((e) => e * 2)); // [ 2, 4, <2 empty items>, 10 ]
// foreach会忽略
arr.forEach((e, i) => console.log(i, e));
// 0 1
// 1 2
// 4 5
  1. 归并方法
    1. reduce()

描述:

reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

reducer()其实是一个非常有用的方法,应当熟练。

语法:

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

参数:

  • callback

    • 执行数组中每个值 (如果没有提供 initialValue 则第一个值除外)的函数,包含四个参数:
      • accumulator
        • 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 initialValue(见于下方)。
      • currentValue
        • 数组中正在处理的元素。
      • index 可选
        • 数组中正在处理的当前元素的索引。 如果提供了 initialValue,则起始索引号为 0,否则从索引 1 起始。
      • array 可选
        • 调用 reduce()的数组
  • initialValue 可选

    • 作为第一次调用 callback 函数时的第一个参数的值。
    • 如果没有提供初始值,则将使用数组中的第一个元素。
    • 在没有初始值的空数组上调用 reduce 将报错。

返回值

  • 函数累计处理的结果

示例

// 累加0到3。(initialValue为0,如果不显示指明,则会是数组第一个)
let ret = [0, 1, 2, 3].reduce((acc, cur) => acc + cur);
/*
过程分析:
callback 被调用3次

callback	accumulator	currentValue	currentIndex	array	        return value
first call	0	        1	            1	            [0, 1, 2, 3]	1
second call	1	        2	            2	            [0, 1, 2, 3]	3
third call	3	        3	            3	            [0, 1, 2, 3]	6

由reduce返回的值将是最后一次回调返回值(6)
*/
console.log(ret); // 5

// 如果有指定,那就是从指定值+数组第一个元素开始
let ret2 = [0, 1, 2, 3].reduce((acc, cur) => acc + cur, 10);
/*
过程分析:
callback 被调用4次

callback	accumulator currentValue    currentIndex	array	        return value
first call	10	        0	            0	            [0, 1, 2, 3]	10
second call	10	        1	            1	            [0, 1, 2, 3]	11
third call	11	        2	            2	            [0, 1, 2, 3]	13
fourth call	13	        3	            3	            [0, 1, 2, 3]	16

最后reduce()返回的值16。
*/
console.log(ret2); // 16

更多使用示例,参看 MDN 的Array.prototype.reduce()

reduce()和 reduceRight()的区别就是数组的遍历一个从左到右,一个从右到左。

    1. flat()

flat() 个人理解来讲,主要就是按照深度遍历嵌套数据,並返回为一个新的拍平的数组。如果在指定深度时值还是数组,则不会拍平该值。

let arr1 = [1, 2, [3, 4]];
console.log(arr1.flat());
// [1, 2, 3, 4]

let arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat());
// [1, 2, 3, 4, [5, 6]]

let arr3 = [1, 2, [3, 4, [5, 6]]];
console.log(arr3.flat(2));
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
let arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
console.log(arr4.flat(Infinity));
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// flat() 方法会移除数组中的空项(未到指定深度的空值当然依旧保留)
let arr5 = [1, 2, , [4, [5, , 7]]];
console.log(arr5.flat());
// [ 1, 2, 4, [ 5, <1 empty item>, 7 ] ]

使用 reduce+concat 替代 单层 flat:

let arr = [1, 2, [3, 4, [5, 6]]];

// 展开一层数组
let a = arr.flat();
// 等效于
let b = arr.reduce((acc, val) => acc.concat(val), []);

// 等效于 使用扩展运算符 ...
const flattened = (arr) => [].concat(...arr);

console.log(a); //  [ 1, 2, 3, 4, [ 5, 6 ] ]
console.log(b); // [ 1, 2, 3, 4, [ 5, 6 ] ]
console.log(flattened(arr)); // [ 1, 2, 3, 4, [ 5, 6 ] ]

可使用递归展开多层嵌套数组,可参看 MDN 的例子

// 使用 reduce、concat 和递归展开无限多层嵌套的数组
let arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];

function flatDeep(arr, d = 1) {
  return d > 0
    ? arr.reduce(
        (acc, val) =>
          acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val),
        []
      )
    : arr.slice();
}

console.log(flatDeep(arr1, Infinity));
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

flatMap 方法与 map 方法和深度 depth 为 1 的 flat 几乎相同.但 flatMap 通常在合并成一种方法的效率稍微高一些。

  1. 会改变原数组的实例方法

一转二排一复制

  • arr.splice()
  • arr.reverse()
  • arr.sort()
  • arr.copyWithin()

二栈二队一填充

  • arr.push()
  • arr.pop()
  • arr.unshift()
  • arr.shift()
  • arr.fill()

定型数组(TypedArray)

定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。

实际上,JavaScript 并没有“TypedArray”类型,它所指的其实是一种特殊的包含数值类型的数组

在 JS 中,数字是以 64 位浮点格式存储的,并按需转换为 32 位整数,所以算术运算非常慢,无法满足 WebGL 的需求。

因此在 ES6 中引入定型数组来解决这个问题,并提供更高性能的算术运算。

所谓定型数组,就是将任何数字转换为一个包含数字比特的数组,随后就可以通过我们熟悉的 JS 数组方法来进一步处理。

ArrayBuffer

  • ArrayBuffer 是所有定型数组及视图引用的基本单位
    • ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
    • 它是一个字节数组,通常在其他语言中称为“byte array”。
    • 不能直接操作 ArrayBuffer 的内容,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
  • ArrayBuffer() 是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间。
  • ArrayBuffer 一经创建就不能再调整大小。
    • 不过,可以使用 slice() 复制其全部或部分到一个新实例中(MDN 中显示不推荐使用)。

DataView

第一种允许读写 ArrayBuffer 的视图是 DataView 。

一个可以从 二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。

个人对这一块不是特別上心,所以只是简单了解概念。更多可参看 MDNJavaScript 标准内置对象 DataView学习了解。

定型数组

定型数组是另一种形式的 ArrayBuffer 视图。

与 DataView 的区别:它特定于一种 ElementType 且遵循系统原生的字节序。相应地,定型数组提供了适用面更广的 API 和更高的性能。设计定型数组的目的就是提高与 WebGL 等原生库交换二进制数据的效率。

创建定型数组

  • 包括读取已有的缓冲、使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。
  • 通过 <ElementType>.from()<ElementType>.of() 也可以创建定型数组

参看JavaScript 标准内置对象之 TypedArray

类型 单个元素值的范围 大小(bytes) 描述 Web IDL 类型 C 语言中的等价类型
Int8Array -128 to 127 1 8 位二进制有符号整数 byte int8_t
Uint8Array 0 to 255 1 8 位无符号整数(超出范围后从另一边界循环) octet uint8_t
Uint8ClampedArray 0 to 255 1 8 位无符号整数(超出范围后为边界值) octet uint8_t
Int16Array -32768 to 32767 2 16 位二进制有符号整数 short int16_t
Uint16Array 0 to 65535 2 16 位无符号整数 unsigned short uint16_t
Int32Array -2147483648 to 2147483647 4 32 位二进制有符号整数 long int32_t
Uint32Array 0 to 4294967295 4 32 位无符号整数 unsigned long uint32_t
Float32Array 1.2×10^-38 to 3.4×10^38 4 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567) unrestricted float float
Float64Array 5.0×10^-324 to 1.8×10^308 8 64 位 IEEE 浮点数(16 有效数字,如 1.123…15) unrestricted double double
BigInt64Array -2^63 to 2^63-1 8 64 位二进制有符号整数 bigint int64_t (signed long long)
BigUint64Array 0 to 2^64-1 8 64 位无符号整数 bigint uint64_t (unsigned long long)

操作定型数组

  • 定型数组支持大部分普通数组的操作符、方法和属性。
  • 定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小。
    • 因此,下列方法不适用于定型数组
      • concat()
      • pop()
      • push()
      • shift()
      • splice()
      • unshift()
  • 对此,定型数组也提供了两个新方法,可以快速向外或向内复制数据: set() 和 subarray() 。
    • set() 从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置。
    • subarray() 执行与 set() 相反的操作,它会基于从原始定型数组中复制的值返回一个新定型数组。

下溢和上溢

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

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

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

// 下溢的位会被转换为其无符号的等价值
// 0xFF 是以二补数形式表示的-1(截取到 8 位),
// 但 255 是一个无符号整数
unsignedInts[1] = -1; // 0xFF (truncated to 8 bits)
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]

const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]);
console.log(clampedInts); // [0, 0, 255, 255]

Map

作为 ECMAScript 6 的新增特性, Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。

使用 new 关键字和 Map 构造函数可以创建一个空映射:const m = new Map();

Map 实例属性 size 是可访问属性,用于返回 一个 Map 对象的成员数量。

  • size 属性的值是一个整数,表示 Map 对象有多少个键值对。
  • size 是只读属性,不能改变它的值。
// 基础创建(Map的键可以是任何值,对象、函数、字符串甚至NaN)
let myMap = new Map();

let keyObj = {};
let keyFunc = function () {};
let keyString = "a string";

myMap.set(keyString, "和键'a string'关联的值");
myMap.set(keyObj, "和键keyObj关联的值");
myMap.set(keyFunc, "和键keyFunc关联的值");
myMap.set(NaN, "not a number");

console.log(myMap.size); // 4

// 使用嵌套数组初始化映射
const m1 = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"],
]);
console.log(m1.size); // 3

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

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

Map 实例的方法

方法 作用
map1.clear() 移除 Map 对象中的所有元素。
map1.delete(key) 移除 Map 对象中指定的元素。
map1.entries() 返回一个新的包含 [key, value] 对的 Iterator 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同。
map1.forEach() 按照插入顺序依次对 Map 中每个键/值对执行一次给定的函数。
map1.set(key, value) 为 Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对。
map1.get(key) 返回某个 Map 对象中的一个指定元素。
map1.has(key) 返回一个 bool 值,用来表明 map 中是否存在指定元素。
map1.keys() 返回一个引用的 Iterator 对象。它包含按照顺序插入 Map 对象中每个元素的 key 值。
map1.values() 返回一个新的 Iterator 对象。它包含按顺序插入 Map 对象中每个元素的 value 值。

复制或合并 Maps

// Map 能像数组一样被复制:

let original = new Map([[1, "one"]]);
let clone = new Map(original);

console.log(clone.get(1)); // one
console.log(original === clone); // false. 浅比较 不为同一个对象的引用

// 备注:请记住,数据本身未被克隆。

Map 对象间可以进行合并,但是会保持键的唯一性

合并两个 Map 对象时,如果有重复的键值,则后面的会覆盖前面的。

let first = new Map([
  [1, "one"],
  [2, "two"],
  [3, "three"],
]);

let second = new Map([
  [1, "uno"],
  [2, "dos"],
]);

// 展开运算符本质上是将 Map 对象转换成数组。
let merged = new Map([...first, ...second]);

console.log(merged.get(1)); // uno
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three

Map 对象也能与数组合并

let first = new Map([
  [1, "one"],
  [2, "two"],
  [3, "three"],
]);

let second = new Map([
  [1, "uno"],
  [2, "dos"],
  [5, "fss"],
]);

// Map对象同数组进行合并时,如果有重复的键值,则后面的会覆盖前面的。
let merged = new Map([...first, ...second, [1, "eins", 5, "nexus"]]);

console.log(merged); // Map { 1 => 'eins', 2 => 'dos', 3 => 'three', 5 => 'fss' }
console.log(merged.keys()); // [Map Iterator] { 1, 2, 3, 5 }
console.log(merged.values()); // [Map Iterator] { 'eins', 'dos', 'three', 'fss' }

// 注意查看:键 1 是有覆盖之前的,但数组中的 `5, "nexus"`,其实是没有合并覆盖的。

Objects 和 maps 的比较

Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。

  • Objects 和 Maps 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Maps 使用。
  • 不过 在下列情况里使用 Map 会是更好的选择

以下内容来自 MDNJavaScript 标准内置对象之 Map

Map Object
意外的键 Map 默认情况不包含任何键。只包含显式插入的键。 一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
备注:虽然 ES5 开始可以用 Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见。
键的类型 一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。 一个 Object 的键必须是一个 String 或是 Symbol。
键的顺序 Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 一个 Object 的键是无序的
备注:自 ECMAScript 2015 规范以来,对象确实保留了字符串和 Symbol 键的创建顺序; 因此,在只有字符串键的对象上进行迭代将按插入顺序产生键。
Size Map 的键值对个数可以轻易地通过 size 属性获取 Object 的键值对个数只能手动计算
迭代 Map 是 iterable 的,所以可以直接被迭代。 迭代一个 Object 需要以某种方式获取它的键然后才能迭代。
性能 在频繁增删键值对的场景下表现更好。 在频繁添加和删除键值对的场景下未作出优化。

选择 Object 还是 Map

一般开发者选择 Object 还是 Map 只是个人偏好问题,影响不大。

对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。

  1. 内存占用
  • Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。
  • 批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。
  • 不同浏览器的情况不同,但给定固定大小的内存, Map 大约可以比 Object 多存储 50%的键/值对
  1. 插入性能
  • 向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。
  • 对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。
  • 如果代码涉及大量插入操作,那么显然 Map 的性能更佳。
  1. 查找速度
  • 与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。
  • 在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。
  • 对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。
  • 如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。
  1. 删除性能
  • 使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。
    • 为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null 。
  • 而对大多数浏览器引擎来说, Map 的 delete() 操作都比插入和查找更快。
    • 如果代码涉及大量删除操作,那么毫无疑问应该选择 Map

WeakMap

  • WeakMap 对象也是键值对的集合。
    • 它的键必须是对象类型,值可以是任意类型
      • 也就是键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出 TypeError 。
  • 它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 GC 回收掉
  • 与 Map 对象不同的是,WeakMap 的键是不可枚举的。不提供列出其键的方法。
    • 列表是否存在取决于垃圾回收器的状态,是不可预知的。
    • 如果 key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而得到不确定的结果。

简单说来:键必须是对象类型,无法迭代。一旦键没有被引用,就会被回收 (不用手动去清除键)

WeakMap 实例的方法

方法 作用
wm.delete(key) 从一个 WeakMap 对象中删除指定的元素。
wm.get(key) 返回 WeakMap 指定的元素。
找不到返回 undefined
wm.has(key) 根据 WeakMap 对象的元素中是否存在 key 键返回一个 boolean 值。
wm.set(key, value) 根据指定的 key 和 value 在 WeakMap 对象中添加新/更新元素。

Set

Set 对象是一组值的集合,这些值是不重复的,可以按照添加顺序来遍历。

因为 Set 中的值总是唯一的,所以需要判断两个值是否相等。

  • 在 ECMAScript 2015 之后,都是使用===操作符的结果了
  • NaN 和 undefined 都可以被存储在 Set 中, NaN 之间被视为相同的值(NaN 被认为是相同的,尽管 NaN !== NaN)。

Set 实例的属性 Set.prototype.size

  • size 的值是一个整数,表示 Set 对象有多少条目。size 的集合访问函数是 undefined; 不能改变这个属性。Set.prototype.size

  • size 的值是一个整数,表示 Set 对象有多少条目。size 的集合访问函数是 undefined; 不能改变这个属性。

创建 set 实例:

// 基本使用
let mySet = new Set();

mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, "some text" ]
let o = { a: 1, b: 2 };
mySet.add(o);

mySet.add({ a: 1, b: 2 }); // o 指向的是不同的对象,所以没问题

console.log(mySet); // Set { 1, 5, 'some text', { a: 1, b: 2 }, { a: 1, b: 2 } }

o.a = 3;
console.log(mySet); // Set { 1, 5, 'some text', { a: 3, b: 2 }, { a: 1, b: 2 } }

// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
console.log(s1.size); // 3
console.log(s1); // Set { 'val1', 'val2', 'val3' }

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

Set 实例的方法

方法 作用
mySet.add(value) 向一个 Set 对象的末尾添加一个指定的值。
mySet.clear() 清空一个 Set 对象中的所有元素。
mySet.delete(value) 从一个 Set 对象中删除指定的元素。
mySet.has(value) 返回一个布尔值来指示对应的值 value 是否存在 Set 对象中。
mySet.values() 按照元素插入顺序返回一个具有 Set 对象每个元素值的全新 Iterator 对象。
mySet.keys() mySet.values()的別名,行为一致。
mySet.entries() 返回一个新的迭代器对象 ,这个对象的元素是类似 [value, value] 形式的数组。
value 是集合对象中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序。
Set 对象没有 key,仅是为了和 Map 的 API 返回接口一致才是[value,value]结构。
mySet.forEach(callback[, thisArg]) 根据集合中元素的插入顺序,依次执行提供的回调函数。

定义正式集合操作的注意点:

  • 某些 Set 操作是有关联性的,因此最好让实现的方法能支持处理任意多个集合实例。
  • Set 保留插入顺序,所有方法返回的集合必须保证顺序。
  • 尽可能高效地使用内存。扩展操作符的语法很简洁,但尽可能避免集合和数组间的相互转换能够节省对象初始化成本。
  • 不要修改已有的集合实例。

WeakSet

WeakSet 对象允许你将弱保持对象存储在一个集合中。

WeakSet 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。

WeakSet 与 Set 对象的区别有两点:

  • 与 Set 相比,WeakSet 只能是对象的集合,而不能是任何类型的任意值。
  • WeakSet 持弱引用:集合中对象的引用为弱引用。
    • 如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。
    • 这也意味着 WeakSet 中没有存储当前对象的列表。 正因为这样,WeakSet 是不可枚举的

WeakSet 实例的方法

方法 作用
ws.add(value) 在 WeakSet 对象的最后一个元素后添加新的对象。返回 WeakSet 对象。
ws.delete(value) 从 WeakSet 对象中移除指定的元素。
成功移除元素则返回 true;key 没有在 WeakSet 中找到或者 key 不是一个对象,则返回 false。
ws.has(value) 根据 WeakSet 是否存在相应对象返回布尔值。
存在指定的元素,返回 true;否则返回 false。

小结

  • JavaScript 中的对象是引用值,可以通过几种内置引用类型创建特定类型的对象。

    • 引用类型与传统面向对象编程语言中的类相似,但实现不同。
    • Object 类型是一个基础类型,所有引用类型都从它继承了基本的行为。
    • Array 类型表示一组有序的值,并提供了操作和转换值的能力。
    • 定型数组包含一套不同的引用类型,用于管理数值在内存中的类型。
    • Date 类型提供了关于日期和时间的信息,包括当前日期和时间以及计算。
    • RegExp 类型是 ECMAScript 支持的正则表达式的接口,提供了大多数基本正则表达式以及一些高级正则表达式的能力。
  • JavaScript 比较独特的一点是,函数其实是 Function 类型的实例,这意味着函数也是对象。

    • 由于函数是对象,因此也就具有能够增强自身行为的方法。
  • 因为原始值包装类型的存在,所以 JavaScript 中的原始值可以拥有类似对象的行为。有 3 种原始值 包装类型: Boolean 、 Number 和 String 。它们都具有如下特点:

    • 每种包装类型都映射到同名的原始类型。
    • 在以读模式访问原始值时,后台会实例化一个原始值包装对象,通过这个对象可以操作数据。
    • 涉及原始值的语句只要一执行完毕,包装对象就会立即销毁。
  • JavaScript 还有两个在一开始执行代码时就存在的内置对象: Global 和 Math 。

    • 其中, Global 对象在大多数 ECMAScript 实现中无法直接访问。
    • 不过浏览器将 Global 实现为 window 对象。
    • 所有全局变量和函数都是 Global 对象的属性。
    • Math 对象包含辅助完成复杂数学计算的属性和方法。
  • ECMAScript 6 新增了一批引用类型: Map 、 WeakMap 、 Set 和 WeakSet 。这些类型为组织应用程序数据和简化内存管理提供了新能力。