在计算机编程中,集合(Collection)是一些可变数量的数据项(可能为0)的分组,这些数据项对要解决的问题具有某些共同的重要性,并且需要以某种受控的方式一起操作。通常,数据项将具有相同的类型,或者在支持继承的语言中,数据项源自某些共同的祖先类型。集合是一个适用于抽象数据类型(abstract data type)的概念,并且不规定作为具体数据结构的具体实现,尽管通常有一个常规选择。
JavaScript 中,对象类型(Object type)的数据结构都是集合,包括按 index
值排序的索引集合和通过 key
值标记的键控集合。
索引集合
按索引值排序的数据集合,包括数组和类数组结构,如 Array
对象和 TypedArray
对象。
数组
const literalrArr = [1, 2, 3] // [1, 2, 3]
const ctorArr = Array(3) // [空属性 × 3]
// Array 构造函数可不使用 new 关键字
// 传入一个 >=0 的数值,则返回一个长度为该值的数组
// 不传参数,则返回一个空数组
数组是由名称和索引引用的值构成的有序列表。
- 数组是可调整大小的,并且可以包含不同的数据类型。
- 数组不是关联数组,因此,不能使用任意字符串作为索引访问数组元素,但必须使用非负整数(或对应的字符串形式)作为索引访问。
- 数组的索引从
0
开始。 - 数组复制操作创建浅拷贝,所有 JavaScript 对象的标准内置复制操作都会创建浅拷贝,而不是深拷贝。
JavaScript 中没有明确的数组数据类型,通过给数组元素赋值来填充数组,也可以使用预定义的 Array
对象及其方法来处理应用程序中的数组。
literalrArr[1] = {} // [1, {}, 3]
ctorArr.push(4, 5, 6) // [空属性 × 3, 4, 5, 6]
数组方法和空槽
稀疏数组中的空槽在数组方法之间的行为不一致。通常,旧方法会跳过空槽,而新方法将它们视为 undefined
。
在遍历多个元素的方法中,这些方法在访问索引之前执行 in
检查,并且不将空槽与 undefined
合并:concat()
、copyWithin()
、every()
、filter()
、flat()
、flatMap()
、forEach()
、indexOf()
、lastIndexOf()
、map()
、reduce()
、reduceRight()
、reverse()
、slice()
、some()
、sort()
、splice()
。
这些方法将空槽视为 undefined
:entries()
、fill()
、find()
、findIndex()
、findLast()
、findLastIndex()
、includes()
、join()
、keys()
、toLocaleString()
、values()
。
复制方法和修改方法
有些方法不会修改调用该方法的现有数组,而是返回一个新的数组。它们通过首先构造一个新数组,然后填充元素来实现。复制始终是浅层次的——该方法从不复制一开始创建的数组之外的任何内容。原始数组的元素将按以下方式复制到新数组中:
- 对象:对象引用被复制到新数组中。原数组和新数组都引用同一个对象。也就是说,如果一个被引用的对象被修改,新数组和原数组都可以看到更改。
- 基本类型,如字符串、数字和布尔值(不是
String
、Number
和Boolean
对象):它们的值被复制到新数组中。
其他方法会改变调用该方法的数组,在这种情况下,它们的返回值根据方法的不同而不同:有时是对相同数组的引用,有时是新数组的长度。通过访问 this.constructor[Symbol.species]
来创建新数组,以确定要使用的构造函数:concat()
、filter()
、flat()
、flatMap()
、map()
、slice()
、splice()
。总是使用基础构造函数创建新数组:toReversed()
、toSorted()
、toSpliced()
、with()
。
修改方法 | 相应的非修改方法 |
---|---|
copyWithin() |
没有相应的非修改方法 |
fill() |
没有相应的非修改方法 |
pop() |
slice(0, -1) |
push(v1, v2) |
concat([v1, v2]) |
reverse() |
toReversed() |
shift() |
slice(1) |
sort() |
toSorted() |
splice() |
toSpliced() |
unshift(v1, v2) |
toSpliced(0, 0, v1, v2) |
将改变原数组的方法转换为非修改方法的一种简单方式是使用展开语法 ...
或 slice()
先创建一个副本。
迭代方法
许多数组方法接受一个回调函数作为参数。回调函数按顺序为数组中的每个元素调用,且最多调用一次,并且回调函数的返回值用于确定方法的返回值。它们都具有相同的方法签名:
method(callbackFn, thisArg)
所有迭代方法都是复制方法和通用方法,尽管它们在处理空槽时的行为不同:every()
、filter()
、find()
、findIndex()
、findLast()
、findLastIndex()
、flatMap()
、forEach()
、map()
、some()
。
还有两个方法接受一个回调函数,并对数组中的每个元素最多运行一次,但它们的方法签名与典型的迭代方法略有不同:reduce()
、reduceRight()
。sort()
方法也接受一个回调函数,但它不是一个迭代方法。它会就地修改数组,不接受 thisArg
,并且可能在索引上多次调用回调函数。
类数组对象
类数组对象指的是在上面描述的 length
转换过程中不抛出的任何对象。在实践中,这样的对象应该实际具有 length
属性,并且索引元素的范围在 0
到 length - 1
之间。(如果它没有所有的索引,它将在功能上等同于稀疏数组。)许多 DOM 对象都是类数组对象——例如 NodeList
和 HTMLCollection
。arguments
对象也是类数组对象。
不能直接在类数组对象上调用数组方法,但可以通过 Function.prototype.call()
间接调用它们。数组原型方法也可以用于字符串,因为它们以类似于数组的方式提供对其中字符的顺序访问。
function printArguments() {
Array.prototype.forEach.call(arguments, (item) => {
console.log(item)
})
}
类数组转换为数组:
const arrayLike = {
0: 1,
length: 1,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
Array.prototype.slice.apply(arrayLike) // [1]
Array.from(arrayLike) // [1]
// 部署有迭代器的类数组
console.log([...arrayLike])
类型化数组
类型化数组(typed array)是一种类似数组的对象,并提供了一种用于在内存缓冲区中访问原始二进制数据的机制。
随着 Web 应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑、访问 WebSockets
的原始数据等,很明显有些时候如果使用 JavaScript 代码可以快速方便地通过类型化数组来操作原始的二进制数据将会非常有帮助。JavaScript 类型化数组中的每一个元素都是原始二进制值,而二进制值采用多种支持的格式之一(从 8 位整数到 6* 位浮点数)。
Array
存储的对象能动态增多和减少,并且可以存储任何 JavaScript 值。JavaScript 引擎会做一些内部优化,以便对数组的操作可以很快。但是,不要把类型化数组与普通数组混淆,因为在类型数组上调用 Array.isArray()
会返回 false
。此外,并不是所有可用于普通数组的方法都能被类型化数组所支持(如 push
和 pop
)。
为了达到最大的灵活性和效率,JavaScript 类型化数组将实现拆分为缓冲和视图两部分。缓冲描述的是一个数据分块。缓冲没有格式可言,并且不提供访问其内容的机制。为了访问在缓冲对象中包含的内存,你需要使用视图。视图提供了上下文——即数据类型、起始偏移量和元素数——将数据转换为实际有类型的数组。
类型化数组视图
类型化数组视图具有自描述性的名字和所有常用的数值类型:Int8Array
、Uint8Array
、Uint8ClampedArray
、Int16Array
、Uint16Array
、Int32Array
、Uint32Array
、Float32Array
、Float6*Array
、BigInt6*Array
、BigUint6*Array
。
ArrayBuffer
ArrayBuffer
是一种数据类型,用来表示一个通用的、固定长度的二进制数据缓冲区。不能直接操作 ArrayBuffer
中的内容;需要创建一个类型化数组的视图或一个描述缓冲数据格式的 DataView
,使用它们来读写缓冲区中的内容。
DataView
DataView
是一种底层接口,它提供有可以操作缓冲区中任意数据的访问器(getter
/setter
)API。
键控集合
由 key
值标记的数据容器;Map
和 Set
对象承载的数据元素可以按照插入时的顺序被迭代遍历。
映射(Map)
ECMAScript 2015 引入了一个新的数据结构来将一个值映射到另一个值。一个 Map
对象就是一个简单的键值对映射集合,可以按照数据插入时的顺序遍历所有的元素。Map
结构的实例有 size
属性和 set
、get
、has
、delete
和 clear
操作方法。可以使用for...of
循环来得到所有的 [key, value]
。
const m = new Map()
WeakMap
对象也是键值对的集合。它的键必须是对象类型,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 GC
回收掉。WeakMap
提供的接口与 Map
相同。
与 Map
对象不同的是,WeakMap
的键是不可枚举的。不提供列出其键的方法。列表是否存在取决于垃圾回收器的状态,是不可预知的。
Object和Map的比较
一般地,object
会被用于将字符串类型映射到数值。Object
允许设置键值对、根据键获取值、删除键、检测某个键是否存在。而 Map
具有更多的优势。
Object
的键均为String
类型,在Map
里键可以是任意类型。- 必须手动计算
Object
的尺寸,但是可以很容易地获取使用Map
的尺寸。 Map
的遍历遵循元素的插入顺序。Object
有原型,所以映射中有一些缺省的键。(可以用map = Object.create(null)
回避)。
这三条提示可以帮助决定用 Map
还是 Object
:
- 如果键在运行时才能知道,或者所有的键类型相同,所有的值类型相同,那就使用
Map
。 - 如果需要将原始值存储为键,则使用
Map
,因为Object
将每个键视为字符串,不管它是一个数字值、布尔值还是任何其他原始值。 - 如果需要对个别元素进行操作,使用
Object
。
与其他数据结构的互相转换
const m = new Map().set(true, 7).set({foo: 3}, ['abc'])
// Map 转为 数组
[...m]
// 数组 转为 Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
集合(Set)
ECMAScript 2015 引入了一个新的数据结构 Set
对象,一组值的集合,这些值是不重复的,可以按照添加顺序来遍历。Set
结构的实例有 size
属性和 add
、delete
、has
和 clear
操作方法。可以使用for...of
循环来得到所有的 [key, value]
。
const s = new Set()
WeakSet
对象是一组对象的集合。WeakSet
中的对象不重复且不可枚举。与Set对象的主要区别有:
WeakSet
中的值必须是对象类型,不可以是别的类型WeakSet
的“weak”指的是,对集合中的对象,如果不存在其他引用,那么该对象将可被垃圾回收。于是不存在一个当前可用对象组成的列表,所以WeakSet
不可枚举
Array和Set的对比
一般情况下,在 JavaScript 中使用数组来存储一组元素,而新的集合对象有这些优势:
- 数组中用于判断元素是否存在的
indexOf
函数效率低下。 Set
对象允许根据值删除元素,而数组中必须使用基于下标的splice
方法。- 数组的
indexOf
方法无法找到NaN
值。 Set
对象存储不重复的值,所以不需要手动处理包含重复值的情况。
与其他数据结构的互相转换
可以使用 Array.from
或展开操作符 ...
来完成集合到数组的转换。同样,Set
的构造器接受数组作为参数,可以完成从 Array
到 Set
的转换。需要重申的是,Set
对象中的值不重复,所以数组转换为集合时,所有重复值将会被删除。
// 数组 转为 集合
const s = new Set(...[1, 2, 3, 4])
// 集合 转为 数组
Array.from(s)
[...s]
映射和集合的比较
Map
的键和 Set
的值的等值判断都基于 same-value-zero algorithm
:
- 判断使用与
===
相似的规则; -0
和+0
相等;NaN
与自身相等(与===
有所不同)。
WeakSet
和 WeakMap
是基于弱引用的数据结构,ECMScript 2021 更进一步,提供了 WeakRef
对象,用于直接创建对象的弱引用。清理器注册表功能 FinalizationRegistry
,用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数。