探索-JavaScript-ES2025-版--八-
探索 JavaScript(ES2025 版)(八)
原文:
exploringjs.com/js/book/index.html译者:飞龙
VII 集合
原文:exploringjs.com/js/book/pt_collections.html
32 同步迭代 ES6
-
32.1 同步迭代是什么?
-
32.2 核心迭代角色:可迭代对象和迭代器
-
32.3 遍历数据
-
32.3.1 手动遍历数据
-
32.3.2 使用
while手动迭代 -
32.3.3 通过
Iterator.from()获取迭代器 (ES2024) -
32.3.4 通过基于迭代的语言结构进行迭代
-
-
32.4 通过生成器处理可迭代对象
-
32.5 迭代 API 的继承(高级)
-
32.5.1 数组迭代器
-
32.5.2 生成器对象
-
-
32.6 可迭代迭代器
-
32.6.1 为什么内置迭代器是可迭代的?
-
32.6.2 当请求迭代器时,迭代器返回自身
-
32.6.3 迭代怪癖:两种可迭代对象
-
-
32.7
Iterator类和迭代器辅助方法 (ES2025)-
32.7.1
Iterator.prototype.*方法 -
32.7.2 迭代器辅助方法的好处
-
32.7.3
Iterator.from():创建 API 迭代器 -
32.7.4 迭代器方法改变我们使用迭代的方式
-
32.7.5 将遗留的可迭代对象升级到
IteratorAPI
-
-
32.8 分组可迭代对象 (ES2024)
-
32.8.1 在
Map.groupBy()和Object.groupBy()之间选择 -
32.8.2 示例:处理情况
-
32.8.3 示例:按属性值分组
-
-
32.9 快速参考:同步迭代
-
32.9.1 同步迭代:数据生产者
-
32.9.2 同步迭代:数据消费者
-
-
32.10 快速参考:类
Iterator(ES2025)-
32.10.1 创建迭代器
-
32.10.2
Iterator.* -
32.10.3
Iterator.prototype.*:将索引传递给回调的方法 -
32.10.4
Iterator.prototype.*:返回迭代器的方法 -
32.10.5
Iterator.prototype.*: 返回布尔值的方法 -
32.10.6
Iterator.prototype.*: 返回其他类型值的方法 -
32.10.7
Iterator.prototype.*: 其他方法
-
32.1 关于同步迭代的什么?
同步迭代是一种协议(接口及其使用规则),它将 JavaScript 中的两组实体连接起来:
-
数据源:一方面,数据以各种形状和大小存在。在 JavaScript 的标准库中,我们有线性数据结构数组(Array)、有序集合(Set,元素按添加时间排序)、有序字典(Map,条目按添加时间排序)等。在库中,我们可能还会找到树形数据结构等。
-
数据消费者:另一方面,我们有一类只需要按顺序访问其输入的构造和算法:例如
for-of循环和将值展开到数组字面量中(通过...)。
迭代协议通过接口Iterable连接这两组对象:数据源通过它顺序地“传递”其内容;数据消费者从它那里获取输入。

图 32.1:如for-of循环这样的数据消费者使用接口Iterable。如Arrays这样的数据源实现该接口。
图 32.1 说明了迭代是如何工作的:数据消费者使用接口Iterable;数据源实现它。
JavaScript 实现接口的方式
在 JavaScript 中,一个对象如果实现了它所描述的所有方法,则认为它实现了接口。本章中提到的接口仅存在于 ECMAScript 规范中。
数据的提供者和消费者都从这种安排中受益:
-
如果我们开发一个新的数据结构,我们只需要实现
Iterable,然后就可以立即将其应用于一系列工具。 -
如果我们编写使用迭代的代码,它将自动与许多数据源一起工作。
32.2 核心迭代角色:可迭代对象和迭代器
两个角色(由接口描述)构成了迭代的核心(图 32.2):
-
一个可迭代对象是一个其内容可以被顺序遍历的对象。
-
一个迭代器是用于遍历的指针。

图 32.2:迭代有两个主要接口:Iterable和Iterator。前者有一个返回后者的方法。
这些是迭代协议接口的类型定义(以 TypeScript 的表示法):
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
abstract class Iterator<T> {
abstract next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}
接口的使用方法如下:
-
我们通过
Symbol.iterator键的方法来请求一个Iterable。 -
一个迭代器扩展了抽象类
Iterator,并通过其方法.next()返回迭代值。- 注意:在 ECMAScript 2025 之前,
Iterator只是一个接口。不存在全局可访问的类Iterator。
- 注意:在 ECMAScript 2025 之前,
-
值不是直接返回的,而是被包裹在具有两个属性的对象中:
-
.value是迭代值。 -
.done表示迭代是否已到达末尾。在最后一个迭代值之后为true,在此之前为false。
-
32.3 遍历数据
32.3.1 手动遍历数据
这就是使用迭代协议的一个例子:
const iterable = ['a', 'b'];
// The iterable is a factory for iterators:
const iterator = iterable[Symbol.iterator]();
// Call .next() until .done is true:
assert.deepEqual(
iterator.next(), { value: 'a', done: false }
);
assert.deepEqual(
iterator.next(), { value: 'b', done: false }
);
assert.deepEqual(
iterator.next(), { value: undefined, done: true }
);
32.3.2 使用 while 手动迭代
以下代码演示了如何使用 while 循环遍历可迭代对象:
function logAll(iterable) {
const iterator = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iterator.next();
if (done) break;
console.log(value);
}
}
logAll(['a', 'b']);
输出:
a
b
练习:手动使用同步迭代
exercises/sync-iteration/sync_iteration_manually_exrc.mjs
32.3.3 通过 Iterator.from() 获取迭代器 (ES2024)
内置的静态方法 Iterator.from() 提供了一种更优雅的方式来检索迭代器:
> const iterable = ['a', 'b'];
> iterable[Symbol.iterator]() instanceof Iterator
true
> Iterator.from(iterable) instanceof Iterator
true
32.3.4 通过基于迭代的语言结构进行迭代
我们已经看到了如何手动使用迭代协议,这相对比较繁琐。但协议并不是直接使用的目的——它是通过在其之上构建的高级语言结构来使用的。我们会注意到,当我们这样做时,我们从未看到迭代器。它们仅用于内部。
32.3.4.1 遍历数组
基于迭代的最重要的语言结构是 for-of 循环:
const iterable = ['hello', 'beautiful', 'world'];
for (const x of iterable) {
console.log(x);
}
输出:
hello
beautiful
world
另一个基于迭代的结构是将值扩展到数组字面量中:
assert.deepEqual(
['BEFORE', ...iterable, 'AFTER'],
['BEFORE', 'hello', 'beautiful', 'world', 'AFTER']
);
通过数组模式解构也使用底层的迭代:
const [first, second] = iterable;
assert.equal(first, 'hello');
assert.equal(second, 'beautiful');
32.3.4.2 遍历集合
集合也是可迭代的。请注意,迭代代码是相同的:它既看不到数组,也看不到集合,只看到可迭代对象。
const iterable = ['hello', 'beautiful', 'world'];
for (const x of iterable) {
console.log(x);
}
assert.deepEqual(
['BEFORE', ...iterable, 'AFTER'],
['BEFORE', 'hello', 'beautiful', 'world', 'AFTER']
);
const [first, second] = iterable;
assert.equal(first, 'hello');
assert.equal(second, 'beautiful');
32.3.5 将可迭代对象转换为数组:[...i] 和 Array.from(i)
这些是将可迭代对象转换为数组的方法:
const iterable = new Set().add('a').add('b').add('c');
assert.deepEqual(
[...iterable],
['a', 'b', 'c']
);
assert.deepEqual(
Array.from(iterable),
['a', 'b', 'c']
);
我倾向于更喜欢 Array.from(),因为它更具自描述性。
更多信息:“将可迭代对象、迭代器和类似数组的值转换为数组” (§34.6)
我们还可以创建一个迭代器,并使用迭代器方法来创建一个数组。迭代器方法将在后面解释。
assert.deepEqual(
Iterator.from(iterable).toArray(),
['a', 'b', 'c']
);
32.4 通过生成器处理可迭代对象
同步生成器函数和方法通过它们返回的迭代器(也是可迭代的)来暴露它们的生成值:
/** Synchronous generator function */
function* createSyncIterable() {
yield 'a';
yield 'b';
yield 'c';
}
生成器产生可迭代对象,但它们也可以消费它们。这使得它们成为转换可迭代对象的通用工具:
function* map(iterable, callback) {
for (const x of iterable) {
yield callback(x);
}
}
assert.deepEqual(
Array.from(
map([1, 2, 3, 4], x => x ** 2)
),
[1, 4, 9, 16]
);
function* filter(iterable, callback) {
for (const x of iterable) {
if (callback(x)) {
yield x;
}
}
}
assert.deepEqual(
Array.from(
filter([1, 2, 3, 4], x => (x % 2) === 0
)),
[2, 4]
);
更多信息:“同步生成器(ES6)(高级)”(§33)
32.5 迭代 API 的继承(高级)
JavaScript 标准库创建的所有迭代器都有一个共同的原型,ECMAScript 规范将其称为[%IteratorPrototype%](https://tc39.es/ecma262/#sec-%iteratorprototype%-object)并在内部使用。我们可以通过Iterator.prototype从 JavaScript 中访问它。
32.5.1 数组迭代器
我们可以这样创建一个数组迭代器:
const arrayIterator = [][Symbol.iterator]();
这个对象有一个具有两个属性的原型。我们可以称它为ArrayIteratorPrototype:
const ArrayIteratorPrototype = Object.getPrototypeOf(arrayIterator);
assert.deepEqual(
Reflect.ownKeys(ArrayIteratorPrototype),
[ 'next', Symbol.toStringTag ]
);
assert.equal(
ArrayIteratorPrototype[Symbol.toStringTag],
'Array Iterator'
);
ArrayIteratorPrototype的原型是%IteratorPrototype%。这个对象有一个键名为Symbol.iterator的方法。因此,所有内置迭代器都是可迭代的。
const IteratorPrototype = Object.getPrototypeOf(ArrayIteratorPrototype);
assert.equal(
IteratorPrototype, Iterator.prototype
);
assert.equal(
Object.hasOwn(Iterator.prototype, Symbol.iterator),
true
);
assert.equal(
typeof Iterator.prototype[Symbol.iterator],
'function'
);
Iterator.prototype的原型是Object.prototype。
assert.equal(
Object.getPrototypeOf(Iterator.prototype) === Object.prototype,
true
);
图 32.3 包含了这个原型链的图示。

图 32.3:原型链(从下到上):
-
首先是
[][Symbol.iterator]()的结果(%ArrayIterator%的一个实例) -
然后
%ArrayIteratorPrototype% -
然后
%IteratorPrototype% -
最后
Object.prototype
32.5.2 生成器对象
大概来说,生成器对象是生成器函数genFunc()产生的值的迭代器。我们通过调用genFunc()来创建它:
function* genFunc() {
yield 'a';
yield 'b';
}
const genObj = genFunc();
生成器对象是一个迭代器:
assert.deepEqual(
genObj.next(),
{ value: 'a', done: false }
);
assert.equal(
genObj instanceof Iterator,
true
);
assert.equal(
Iterator.prototype.isPrototypeOf(genObj),
true
);
32.6 可迭代迭代器
32.6.1 为什么内置迭代器是可迭代的?
正如我们所见,所有内置迭代器都是可迭代的:
// Array iterator
const arrayIterator = ['a', 'b'].values();
assert.equal(
// arrayIterator is a built-in iterator
arrayIterator instanceof Iterator, true
);
assert.equal(
// arrayIterator is iterable
Symbol.iterator in arrayIterator, true
);
// Generator object
function* gen() { yield 'hello' }
const genObj = gen();
assert.equal(
genObj instanceof Iterator, true
);
assert.equal(
Symbol.iterator in genObj, true
);
// Iterator returned by `Iterator` method
const iter = Iterator.from([1, 2]).map(x => x * 2);
assert.equal(
iter instanceof Iterator, true
);
assert.equal(
Symbol.iterator in iter, true
);
它的好处是我们能够遍历迭代器的值——例如,通过for-of和Array.from()。
另一个好处是生成器变得更加灵活。一方面,我们可以使用它们来实现迭代器:
class MyIterable {
/** This method must return an iterator */
* [Symbol.iterator]() {
yield 'good';
yield 'morning';
}
}
assert.deepEqual(
Array.from(new MyIterable()),
['good', 'morning']
);
另一方面,我们可以使用它们来实现可迭代对象:
function* createIterable() {
yield 'a';
yield 'b';
}
assert.deepEqual(
Array.from(createIterable()),
['good', 'morning']
);
32.6.2 迭代器在请求迭代器时返回自身
如果一个迭代器是可迭代的:它产生的迭代器是什么?当请求迭代器时,它简单地返回自身:
const iterator = Iterator.from(['a', 'b'])
assert.equal(
iterator[Symbol.iterator](),
iterator
);
32.6.3 迭代怪癖:两种类型的可迭代对象
可惜,可迭代迭代器意味着存在两种类型的可迭代对象:
-
一个可迭代迭代器是一次性可迭代的:当调用
[Symbol.iterator]()时,它总是返回相同的迭代器(自身)(迭代继续)。 -
一个普通的可迭代对象(一个数组、一个集合等)是多次可迭代的:它总是返回一个新的迭代器(迭代重新开始)。
对于一次性可迭代,每次迭代都会移除更多元素,直到最终没有更多元素为止:
const oneTime = ['a', 'b', 'c'].values();
for (const x of oneTime) {
assert.equal(
x, 'a'
);
break;
}
assert.deepEqual(
Array.from(oneTime),
['b', 'c']
);
assert.deepEqual(
Array.from(oneTime),
[]
);
使用多次可迭代的,每次迭代都是从新开始:
const manyTimes = ['a', 'b', 'c'];
for (const x of manyTimes) {
assert.equal(
x, 'a'
);
break;
}
assert.deepEqual(
Array.from(manyTimes),
['a', 'b', 'c']
);
assert.deepEqual(
Array.from(manyTimes),
['a', 'b', 'c']
);
以下代码是差异的另一个示例:
const oneTime = ['a', 'b', 'c'].values();
assert.deepEqual(
[...oneTime, ...oneTime, ...oneTime],
['a', 'b', 'c']
);
const manyTimes = ['a', 'b', 'c'];
assert.deepEqual(
[...manyTimes, ...manyTimes, ...manyTimes],
['a','b','c', 'a','b','c', 'a','b','c']
);
32.7 类Iterator和迭代器辅助方法(ES2025)
我们已经看到 %IteratorPrototype% 是所有内置迭代器的原型。ECMAScript 2025 引入了一个名为 Iterator 的类:
-
Iterator.prototype指的是%IteratorPrototype%。 -
%IteratorPrototype%.constructor指的是Iterator。
该类提供了以下功能:
-
Iterator.from(iterable)返回iterable的迭代器。我们将在后面详细探讨它。稍后。 -
Iterator.prototype包含各种辅助方法,这些方法由迭代器继承。
32.7.1 Iterator.prototype.* 方法
以下迭代器辅助方法与具有相同名称的数组方法类似:
-
返回迭代器的函数:
-
iterator.filter(filterFn) -
iterator.map(mapFn) -
iterator.flatMap(mapFn)
-
-
返回布尔值的函数:
-
iterator.some(fn) -
iterator.every(fn)
-
-
返回其他值的函数:
-
iterator.find(fn) -
iterator.reduce(reducer, initialValue?)
-
-
不返回值的函数:
iterator.forEach(fn)
这些辅助方法仅适用于迭代器:
-
iterator.drop(limit)- 返回没有前
limit个元素的iterator的迭代器。
- 返回没有前
-
iterator.take(limit)- 返回具有
iterator的前limit个元素的迭代器。
- 返回具有
-
iterator.toArray()- 将
iterator的所有剩余元素收集到一个数组中并返回它。
- 将
每个方法的简要说明,请参阅“快速参考:类 Iterator (ES2025)”(§32.10)。以下是这些方法实际应用的示例:
assert.deepEqual(
['a', 'b', 'c'].values().map(x => `=${x}=`).toArray(),
['=a=', '=b=', '=c=']
);
assert.deepEqual(
['a', 'b', 'c'].values().drop(1).toArray(),
['b', 'c']
);
assert.deepEqual(
['a', 'b', 'c'].values().take(2).toArray(),
['a', 'b']
);
数组方法 arr.values() 返回 arr 元素的迭代器。
练习:使用迭代器
-
为迭代器实现
.at():exercises/sync-iteration/iterator-at_test.mjs -
为迭代器实现
.findIndex():exercises/sync-iteration/iterator-find-index_test.mjs -
为迭代器实现
.slice():exercises/sync-iteration/slice-iterator_test.mjs -
为迭代器元素添加索引:
exercises/sync-iteration/add-index-to-iterator_test.mjs -
使用
iterator.reduce()计算迭代器的长度:exercises/sync-iteration/iterator-length_test.mjs
32.7.2 迭代器辅助方法的益处
32.7.2.1 利益:支持迭代的数组的更多操作
使用迭代器辅助方法,任何支持迭代的数结构自动获得功能。
例如,集合不支持 filter 和 map 操作,但我们可以通过迭代器方法获得它们:
assert.deepEqual(
new Set( // (A)
new Set([-5, 2, 6, -3]).values().filter(x => x >= 0)
),
new Set([2, 6])
);
assert.deepEqual(
new Set( // (B)
new Set([-5, 2, 6, -3]).values().map(x => x / 2)
),
new Set([-2.5, 1, 3, -1.5])
);
注意,new Set() 接受可迭代对象,因此可迭代的迭代器(行 A 和行 B)。
DOM 集合也没有 .filter() 和 .map() 方法:
const domCollection = document.querySelectorAll('a');
// Alas, the collection doesn’t have a method .map()
assert.equal('map' in domCollection, false);
// Solution: use an iterator
assert.deepEqual(
domCollection.values().map(x => x.href).toArray(),
['https://2ality.com', 'https://exploringjs.com']
);
练习:通过迭代器方法实现 .filter() 和 .map()
exercises/sync-iteration/set-operations-via-iterators_test.mjs
32.7.2.2 优势:没有中间数组,增量处理
如果我们链式操作返回数组的操作(行 A、行 B、行 C),那么每个操作都会产生一个新的数组:
function quoteNonEmptyLinesArray(str) {
return str
.split(/(?<=\r?\n)/) // (A)
.filter(line => line.trim().length > 0) // (B)
.map(line => '> ' + line) // (C)
;
}
行 A 中的正则表达式包含一个后视断言,这确保了.split()返回的行包括行终止符。
相比之下,以下代码中的每个操作(行 A、行 B、行 C)都返回一个迭代器,并且不会创建任何中间数组:
function quoteNonEmptyLinesIter(str) {
return splitLinesIter(str) // (A)
.filter(line => line.trim().length > 0) // (B)
.map(line => '> ' + line) // (C)
;
}
function* splitLinesIter(str) {
let prevIndex = 0;
while (true) {
const eolIndex = str.indexOf('\n', prevIndex);
if (eolIndex < 0) break;
// Including EOL
const line = str.slice(prevIndex, eolIndex + 1);
yield line;
prevIndex = eolIndex + 1;
}
if (prevIndex < str.length) {
yield str.slice(prevIndex);
}
}
使用quoteNonEmptyLinesIter()的示例:
assert.deepEqual(
Array.from(quoteNonEmptyLinesIter('have\n\na nice\n\nday')),
[
'> have\n',
'> a nice\n',
'> day',
]
);
注意,三行文本之间的空行已被过滤掉。
除了不创建中间数组之外,迭代器还提供了增量处理:
> const iter = quoteNonEmptyLinesIter('have\n\na nice\n\nday');
> iter.next()
{ value: '> have\n', done: false }
> iter.next()
{ value: '> a nice\n', done: false }
> iter.next()
{ value: '> day', done: false }
> iter.next()
{ value: undefined, done: true }
相比之下,quoteNonEmptyLinesArray()首先拆分所有行,然后过滤所有行,最后映射所有行。在处理大量数据时,增量处理很重要。迭代器辅助方法作为增量处理工具补充了生成器。
32.7.3 Iterator.from(): 创建 API 迭代器
所有内置的可迭代对象都自动支持新的 API,因为它们的迭代器已经具有Iterator.prototype作为原型(因此是Iterator的实例)。然而,并非所有库和用户代码中的可迭代对象都是这样。
术语:
-
支持
IteratorAPI 的实体:API 迭代器和API 可迭代对象 -
不支持的实体:遗留迭代器和遗留可迭代对象
Iterator.from(obj)是如何工作的?
-
如果
obj是可迭代的,那么它通过调用obj[Symbol.iterator]()来创建一个迭代器。-
如果新的迭代器是一个
Iterator实例,它将按原样返回。 -
否则,它会包装起来,使其成为一个
Iterator实例。
-
-
如果
obj是一个迭代器,Iterator.from()确保它是一个Iterator实例,并返回它。
在以下示例中,我们使用Iterator.from()将遗留迭代器转换为 API 迭代器:
// Not an instance of `Iterator`
const legacyIterator = {
next() {
// Infinite iterator (never done)
return { done: false, value: '#' };
}
};
assert.equal(
Iterator.from(legacyIterator) instanceof Iterator,
true
);
assert.deepEqual(
Iterator.from(legacyIterator).take(3).toArray(),
['#', '#', '#']
);
32.7.4 迭代器方法改变了我们使用迭代的方式
迭代器方法改变了我们使用迭代的方式:
-
以前,我们从未见过迭代器,总是与可迭代对象一起工作,例如通过
for-of或Array.from()。 -
现在,使用迭代器也是有用的,我们必须注意如何创建它们。
有趣的是,我们的关注点如何随着Array.prototype.keys()等方法的改变而转移,这些方法返回可迭代迭代器:在迭代器方法之前,我们使用结果作为可迭代对象。使用迭代器方法时,我们也将它们用作迭代器:
> Array.from(['a', 'b'].keys()) // iterable
[ 0, 1 ]
> ['a', 'b'].keys().toArray() // iterator
[ 0, 1 ]
更多信息,请参阅“创建迭代器”(§32.10.1)。
32.7.5 将遗留可迭代对象升级到Iterator API
这是一个遗留可迭代对象的示例:
class ValueIterable {
#values;
#index = 0;
constructor(...values) {
this.#values = values;
}
[Symbol.iterator]() {
return {
// Arrow function so that we can use the outer `this`
next: () => {
if (this.#index >= this.#values.length) {
return {done: true};
}
const value = this.#values[this.#index];
this.#index++;
return {done: false, value};
},
};
}
}
// legacyIterable is an iterable
const legacyIterable = new ValueIterable('a', 'b', 'c');
assert.deepEqual(
Array.from(new ValueIterable('a', 'b', 'c')),
['a', 'b', 'c']
);
// But its iterators are not instances of Iterator
const legacyIterator = legacyIterable[Symbol.iterator]();
assert.equal(
legacyIterator instanceof Iterator, false
);
如果我们想让ValueIterable支持Iterator API,我们必须使其迭代器成为Iterator实例:
class ValueIterable {
// ···
[Symbol.iterator]() {
return {
__proto__: Iterator.prototype,
next: () => {
// ···
},
};
}
}
这又是一个选项(尽管效率较低):
class ValueIterable {
// ···
[Symbol.iterator]() {
return Iterator.from({
next: () => {
// ···
},
});
}
}
我们还可以为迭代器创建一个类:
class ValueIterable {
#values;
#index = 0;
constructor(...values) {
this.#values = values;
}
[Symbol.iterator]() {
const outerThis = this;
// Because ValueIterator is nested, it can access the private fields of
// ValueIterable
class ValueIterator extends Iterator {
next() {
if (outerThis.#index >= outerThis.#values.length) {
return {done: true};
}
const value = outerThis.#values[outerThis.#index];
outerThis.#index++;
return {done: false, value};
}
}
return new ValueIterator();
}
}
32.8 分组可迭代对象(ES2024)
Map.groupBy() 将可迭代对象的项分组到 Map 条目中,其键由回调函数提供:
assert.deepEqual(
Map.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)),
new Map().set(0, [0]).set(-1, [-5,-4]).set(1, [3,8,9])
);
要分组的项可以来自任何可迭代对象:
function* generateNumbers() {
yield 2;
yield -7;
yield 4;
}
assert.deepEqual(
Map.groupBy(generateNumbers(), x => Math.sign(x)),
new Map().set(1, [2,4]).set(-1, [-7])
);
此外还有 Object.groupBy(),它生成一个对象而不是 Map:
assert.deepEqual(
Object.groupBy([0, -5, 3, -4, 8, 9], x => Math.sign(x)),
{'0': [0], '-1': [-5,-4], '1': [3,8,9], __proto__: null}
);
32.8.1 在 Map.groupBy() 和 Object.groupBy() 之间选择
-
你想按字符串和符号以外的键进行分组吗?
- 然后你需要一个 Map。对象只能有字符串和符号作为键。
-
你想解构
.groupBy()的结果(请参阅本节后面的示例)吗?- 然后你需要一个对象。
-
否则,你可以自由选择你喜欢的。
32.8.2 示例:处理情况
Promise 组合器 Promise.allSettled()(Promise.allSettled())返回如下数组:
const settled = [
{ status: 'rejected', reason: 'Jhon' },
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
];
我们可以按如下方式对数组元素进行分组:
const {fulfilled, rejected} = Object.groupBy(settled, x => x.status); // (A)
// Handle fulfilled results
assert.deepEqual(
fulfilled,
[
{ status: 'fulfilled', value: 'Jane' },
{ status: 'fulfilled', value: 'John' },
]
);
// Handle rejected results
assert.deepEqual(
rejected,
[
{ status: 'rejected', reason: 'Jhon' },
{ status: 'rejected', reason: 'Jaen' },
{ status: 'rejected', reason: 'Jnoh' },
]
);
对于此用例,Object.groupBy() 工作得更好,因为我们可以在 Map 中使用任意键,而在对象中,键仅限于字符串和符号。
32.8.3 示例:按属性值分组
在下一个示例中,我们想要按国家分组人员:
const persons = [
{ name: 'Louise', country: 'France' },
{ name: 'Felix', country: 'Germany' },
{ name: 'Ava', country: 'USA' },
{ name: 'Léo', country: 'France' },
{ name: 'Oliver', country: 'USA' },
{ name: 'Leni', country: 'Germany' },
];
assert.deepEqual(
Map.groupBy(persons, (person) => person.country),
new Map([
[
'France',
[
{ name: 'Louise', country: 'France' },
{ name: 'Léo', country: 'France' },
]
],
[
'Germany',
[
{ name: 'Felix', country: 'Germany' },
{ name: 'Leni', country: 'Germany' },
]
],
[
'USA',
[
{ name: 'Ava', country: 'USA' },
{ name: 'Oliver', country: 'USA' },
]
],
])
);
对于此用例,Map.groupBy() 是更好的选择,因为我们可以在 Map 中使用任意键,而在对象中,键仅限于字符串和符号。
练习:使用 Map.groupBy() 对对象数组进行分组
exercises/sync-iteration/count-cities_test.mjs
32.9 快速参考:同步迭代
32.9.1 同步迭代:数据生产者
这些数据结构是可迭代的:
-
字符串
-
数组
-
集合
-
Map
-
(浏览器:DOM 数据结构)
以下数据结构具有 .keys()、.values() 和 .entries() 方法,这些方法返回的不是数组:
-
数组
-
集合
-
Map
顺便提一下 - 以下静态方法列出属性键、值和条目(它们不是普通方法,因为那些可能会意外覆盖)。它们返回数组。
-
Object.keys(obj) -
Object.values(obj) -
Object.entries(obj)
同步生成器函数和方法通过它们返回的可迭代对象暴露其产生的值:
/** Synchronous generator function */
function* createSyncIterable() {
yield 'a';
yield 'b';
yield 'c';
}
assert.deepEqual(
Array.from(createSyncIterable()),
['a', 'b', 'c']
);
32.9.2 同步迭代:数据消费者
本节列出了通过同步迭代消耗数据的结构。
32.9.2.1 语言迭代结构
-
for-of循环:for (const x of iterable) { /*···*/ } -
通过(
...)扩展到数组字面量和函数调用:const arr = ['a', ...iterable, 'z']; func('a', ...iterable, 'z'); -
通过数组模式进行解构:
const [x, y] = iterable; -
yield*:function* generatorFunction() { yield* iterable; }
32.9.2.2 将可迭代对象转换为数据结构
-
Object.fromEntries():
const obj = Object.fromEntries(iterableOverKeyValuePairs); -
Array.from():
const arr = Array.from(iterable);替代方案 - 扩展:
const arr = [...iterable]; -
new Map()和new WeakMap():const m = new Map(iterableOverKeyValuePairs); const wm = new WeakMap(iterableOverKeyValuePairs); -
new Set()和new WeakSet():const s = new Set(iterableOverElements); const ws = new WeakSet(iterableOverElements);
32.9.2.3 将 Promise 的可迭代对象转换为 Promises
-
Promise 组合函数:
Promise.all()等。const promise1 = Promise.all(iterableOverPromises); const promise2 = Promise.race(iterableOverPromises); const promise3 = Promise.any(iterableOverPromises); const promise4 = Promise.allSettled(iterableOverPromises);
32.9.2.4 将可迭代对象分组到 Map 或对象中
-
“
Map.groupBy(items, computeGroupKey)” -
“
Object.groupBy(items, computeGroupKey)”
32.10 快速参考:Iterator类 (ES2025)
32.10.1 创建迭代器
Iterator类的这些方法允许我们增量地处理数据。让我们看看我们可以在哪里使用它们。
32.10.1.1 从可迭代对象获取迭代器
-
Iterator.from(iterable)总是返回Iterator的实例(在需要时将非实例转换为实例)。 -
iterable[Symbol.iterator]()返回一个迭代器:-
所有内置的数据结构的结果都是一个
Iterator的实例。 -
使用其他较旧的可迭代对象时,结果可能不是
Iterator的实例。
-
32.10.1.2 内置方法返回迭代器
数组、类型化数组、集合和 Map 有额外的返回迭代器的方法:
-
数组(类似地:类型化数组):
-
Array.prototype.keys()返回一个数字的迭代器。 -
Array.prototype.values()返回一个迭代器。 -
Array.prototype.entries()返回一个键值对的迭代器。键是数字。
-
-
集合:
-
Set.prototype.values()返回一个迭代器。 -
Set.prototype.keys()返回一个迭代器。等同于.values()。 -
Set.prototype.entries()返回一个值值对的迭代器(即,对的两个组件都是相同的值)。
-
-
Map:
-
Map.prototype.keys()返回一个迭代器。 -
Map.prototype.values()返回一个迭代器。 -
Map.prototype.entries()返回一个键值对的迭代器。
-
以下方法返回迭代器:
String.prototype.matchAll()返回一个匹配对象的迭代器。
32.10.1.3 迭代器的其他来源
生成器也返回迭代器:
function* gen() {}
assert.equal(
gen() instanceof Iterator,
true
);
32.10.2 Iterator.*
Iterator.from(iterableOrIterator)返回一个保证是Iterator实例的迭代器。如果参数是旧的可迭代对象或旧迭代器,它将包装结果,使其成为Iterator的实例。
32.10.3 Iterator.prototype.*: 将索引传递给回调的方法
一些迭代器方法会为迭代的值保持一个计数器,并将其传递给它们的回调:
-
.every() -
.filter() -
.find() -
.flatMap() -
.forEach() -
.map() -
.reduce() -
.some()
32.10.4 Iterator.prototype.*: 返回迭代器的函数
-
Iterator.prototype.drop(limit)ES2025Iterator<T>.prototype.drop(limit: number): Iterator<T>此方法返回一个迭代器,包含
iterator的所有值,除了前limit个。也就是说,迭代从迭代计数器为limit时开始。assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']).drop(1).toArray(), ['b', 'c', 'd'] ); -
Iterator.prototype.filter(filterFn)ES2025 | 回调获取计数器
Iterator<T>.prototype.filter( filterFn: (value: T, counter: number) => boolean ): Iterator<T>此方法返回一个迭代器,其值是
iterator中filterFn返回true的值。assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']).filter(x => x <= 'b').toArray(), ['a', 'b'] ); -
Iterator.prototype.flatMap(mapFn)ES2025 | 回调获取计数器
Iterator<T>.prototype.flatMap<U>( mapFn: (value: T, counter: number) => Iterable<U> | Iterator<U> ): Iterator<U>此方法返回一个迭代器,其值是应用
mapFn到iterator的值的结果的迭代器或可迭代对象。assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']) .flatMap((value, counter) => new Array(counter).fill(value)) .toArray(), ['b', 'c', 'c', 'd', 'd', 'd'] );更多信息请参阅具有相同名称的数组方法部分:“
.flatMap(): 每个输入元素产生零个或多个输出元素 (ES2019)” (§34.14.3)[(ch_arrays.html#Array.prototype.flatMap)]。 -
Iterator.prototype.map(mapFn)ES2025 | 回调获取计数器
Iterator<T>.prototype.map<U>( mapFn: (value: T, counter: number) => U ): Iterator<U>此方法返回一个迭代器,其值是应用
mapFn到iterator的值的结果。assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']).map(x => x + x).toArray(), ['aa', 'bb', 'cc', 'dd'] ); -
Iterator.prototype.take(limit)ES2025Iterator<T>.prototype.take(limit: number): Iterator<T>此方法返回一个迭代器,包含
iterator的前limit个值。assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']).take(1).toArray(), ['a'] );
32.10.5 Iterator.prototype.*: 返回布尔值的函数
-
Iterator.prototype.every(fn)ES2025 | 回调获取计数器
Iterator<T>.prototype.every( fn: (value: T, counter: number) => boolean ): boolean如果
fn对iterator的每个值都返回true,则此方法返回true。否则,它返回false。assert.equal( Iterator.from(['a', 'b', 'c', 'd']).every(x => x === 'c'), false ); -
Iterator.prototype.some(fn)ES2025 | 回调获取计数器
Iterator<T>.prototype.some( fn: (value: T, counter: number) => boolean ): boolean如果
fn对iterator的至少一个值返回true,则此方法返回true。否则,它返回false。assert.equal( Iterator.from(['a', 'b', 'c', 'd']).some(x => x === 'c'), true );
32.10.6 Iterator.prototype.*: 返回其他类型值的函数
-
Iterator.prototype.find(fn)ES2025 | 回调获取计数器
Iterator<T>.prototype.find( fn: (value: T, counter: number) => boolean ): T此方法返回
iterator中fn返回true的第一个值。如果没有这样的值,则返回undefined。assert.equal( Iterator.from(['a', 'b', 'c', 'd']).find((_, counter) => counter === 1), 'b' ); -
Iterator.prototype.reduce(reducer, initialValue?)ES2025 | 回调获取计数器
Iterator<T>.prototype.reduce<U>( reducer: (accumulator: U, value: T, counter: number) => U, initialValue?: U ): U此方法使用函数
reducer将iterator的值合并成一个单一值。示例 - 连接迭代器的字符串:
assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']).reduce((acc, v) => acc + v), 'abcd' );示例 - 计算一组数字的最小值:
const set = new Set([3, -2, -5, 4]); assert.equal( set.values().reduce((min, cur) => cur < min ? cur : min, Infinity), -5 );更多信息请参阅具有相同名称的数组方法部分:“
.reduce(): 计算数组的摘要” (§34.15)[(ch_arrays.html#Array.prototype.reduce)]。 -
Iterator.prototype.toArray()ES2025Iterator<T>.prototype.toArray(): Array<T>此方法返回
iterator的值在数组中的值。assert.deepEqual( Iterator.from(['a', 'b', 'c', 'd']).toArray(), ['a', 'b', 'c', 'd'] );
32.10.7 Iterator.prototype.*: 其他方法
-
Iterator.prototype.forEach(fn)ES2025 | 回调获取计数器
Iterator<T>.prototype.forEach( fn: (value: T, counter: number) => void ): void此方法将
fn应用到iterator的每个值上。const result = []; Iterator.from(['a', 'b', 'c', 'd']).forEach(x => result.unshift(x)) assert.deepEqual( result, ['d', 'c', 'b', 'a'] );
33 同步生成器 ES6(高级)
-
33.1 什么是同步生成器?
-
33.1.1 生成器函数返回迭代器并通过
yield填充它们 -
33.1.2
yield暂停生成器函数 -
33.1.3 为什么
yield会暂停执行? -
33.1.4 示例:遍历可迭代对象
-
-
33.2 从生成器中调用生成器(高级)
-
33.2.1 通过
yield*调用生成器 -
33.2.2 示例:遍历树
-
-
33.3 生成器的用例:重用遍历
-
33.3.1 背景:外部迭代与内部迭代
-
33.3.2 要重用的遍历
-
33.3.3 示例:内部迭代(推送)
-
33.3.4 外部迭代(拉取)
-
-
33.4 生成器的高级功能
33.1 什么是同步生成器?
同步生成器是函数定义和方法定义的特殊版本,有助于处理同步可迭代对象和同步迭代器。它们总是返回同步迭代器(也是可迭代的):
// Generator function declaration
function* genFunc1() { /*···*/ }
// Generator function expression
const genFunc2 = function* () { /*···*/ };
// Generator method definition in an object literal
const obj = {
* generatorMethod() {
// ···
}
};
// Generator method definition in a class definition
// (class declaration or class expression)
class MyClass {
* generatorMethod() {
// ···
}
}
星号 (*) 将函数和方法标记为生成器:
-
函数:伪关键字
function*是function关键字和一个星号的组合。 -
方法:
*是一个修饰符(类似于static和get)。
33.1.1 生成器函数返回迭代器并通过 yield 填充它们
如果我们调用一个生成器函数,它将返回一个迭代器(该迭代器也是可迭代的——因为所有内置迭代器都是)。生成器通过 yield 操作符填充该迭代器:
function* createIterator() {
yield 'a';
yield 'b';
}
// Converting the result to an Array
assert.deepEqual(
// Using an Iterator method
createIterator().toArray(),
['a', 'b']
);
assert.deepEqual(
// The iterator is iterable, so Array.from() works
Array.from(createIterator()),
['a', 'b']
);
// We can use for-of because the iterator is iterable
for (const x of createIterator()) {
console.log(x);
}
输出:
a
b
练习:创建一个整数范围的迭代器
exercises/sync-generators/integer-range_test.mjs
33.1.2 yield 暂停生成器函数
使用生成器函数涉及以下步骤:
-
函数调用它返回一个迭代器
iter。 -
重复遍历
iter将多次调用iter.next()。每次,我们都会跳入生成器函数的体内,直到有一个yield返回一个值。
因此,yield 不仅向迭代器添加值,还暂停并退出生成器函数:
-
与
return类似,yield退出函数体并返回一个值(通过.next())。 -
与
return不同,如果我们重复调用(.next()),执行将直接在yield之后恢复。
让我们通过以下生成器函数来检查这意味着什么。
let location = 0;
function* genFunc2() {
location = 1;
yield 'a';
location = 2;
yield 'b';
location = 3;
}
为了使用 genFunc2(),我们首先必须创建迭代器/可迭代对象 iter。genFunc2() 现在暂停在其主体“之前”。
const iter = genFunc2();
// genFunc2() is now paused “before” its body:
assert.equal(location, 0);
iter 实现了迭代协议。因此,我们通过 iter.next() 控制执行 genFunc2()。调用该方法会恢复暂停的 genFunc2() 并执行它,直到出现 yield。然后执行暂停,.next() 返回 yield 的操作数:
assert.deepEqual(
iter.next(), {value: 'a', done: false});
// genFunc2() is now paused directly after the first `yield`:
assert.equal(location, 1);
注意,返回的值 'a' 被封装在一个对象中,这是迭代器始终传递其值的方式。
我们再次调用 iter.next(),执行继续到我们之前暂停的地方。一旦我们遇到第二个 yield,genFunc2() 暂停,.next() 返回返回的值 'b'。
assert.deepEqual(
iter.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the second `yield`:
assert.equal(location, 2);
我们再次调用 iter.next(),执行继续,直到它离开 genFunc2() 的主体:
assert.deepEqual(
iter.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);
这次,.next() 的结果属性 .done 为 true,这意味着迭代器已结束。
33.1.3 为什么 yield 会暂停执行?
yield 暂停执行的好处是什么?为什么它不像数组方法 .push() 那样简单地工作,在暂停之前填充迭代器中的值?
由于暂停,生成器提供了许多 协程 的功能(想想那些在合作中多任务处理的进程)。例如,当我们请求迭代器的下一个值时,该值是按需计算的(懒加载)。以下两个生成器函数演示了这意味着什么。
/**
* Returns an iterator over lines
*/
function* genLines() {
yield 'A line';
yield 'Another line';
yield 'Last line';
}
/**
* Input: iterable over lines
* Output: iterator over numbered lines
*/
function* numberLines(lineIterable) {
let lineNumber = 1;
for (const line of lineIterable) { // input
yield lineNumber + ': ' + line; // output
lineNumber++;
}
}
注意,numberLines() 中的 yield 出现在 for-of 循环内部。yield 可以在循环中使用,但不能在回调函数中使用(稍后会有更多介绍)。
让我们将两个生成器结合起来产生迭代器 numberedLines:
const numberedLines = numberLines(genLines());
assert.deepEqual(
numberedLines.next(),
{value: '1: A line', done: false}
);
assert.deepEqual(
numberedLines.next(),
{value: '2: Another line', done: false}
);
使用生成器的关键好处是,一切都可以增量工作:通过 numberedLines.next(),我们只请求 numberedLines() 一个编号的行。反过来,它只请求 genLines() 一个未编号的行。
如果例如 genLines() 从大文本文件中读取其行,这种增量工作方式将继续有效:如果我们请求 numberedLines() 一个编号的行,一旦 genLines() 从文本文件中读取其第一行,我们就会得到一个编号的行。
没有生成器,genLines() 会首先读取所有行并返回它们。然后 numberLines() 会编号所有行并返回它们。因此,我们必须等待更长的时间才能得到第一个编号的行。
练习:将普通函数转换为生成器
exercises/sync-generators/fib_seq_test.mjs
33.1.4 示例:映射到可迭代对象
以下函数 mapIter() 与数组方法 .map() 类似,但它返回一个迭代器,而不是数组,并且按需产生结果。
function* mapIter(iterable, func) {
let index = 0;
for (const x of iterable) {
yield func(x, index);
index++;
}
}
const iterator = mapIter(['a', 'b'], x => x + x);
assert.deepEqual(
Array.from(iterator), ['aa', 'bb']
);
练习:过滤可迭代对象
exercises/sync-generators/filter_iter_gen_test.mjs
33.2 从生成器中调用生成器(高级)
33.2.1 通过yield*调用生成器
yield仅在生成器内部直接使用——到目前为止,我们还没有看到将 yield 委托给另一个函数或方法的方法。
让我们先看看什么是不起作用的:在以下示例中,我们希望compute()调用helper(),以便后者为前者产生两个值。然而,一种天真方法失败了:
function* helper() {
yield 'a';
yield 'b';
}
function* compute() {
// Nothing happens if we call `helper()`:
helper();
}
assert.deepEqual(
Array.from(compute()), []
);
为什么这不起作用?函数调用helper()返回一个迭代器,但我们忽略了它。
我们希望compute()产生helper()产生的所有值。这正是yield*操作符的作用:
function* helper() {
yield 'a';
yield 'b';
}
function* compute() {
yield* helper();
}
assert.deepEqual(
Array.from(compute()), ['a', 'b']
);
换句话说,之前的compute()大致等同于:
function* compute() {
for (const x of helper()) {
yield x;
}
}
注意,yield*与任何可迭代对象一起工作:
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
Array.from(gen()), [1, 2]
);
33.2.2 示例:遍历树
yield*允许我们在生成器中进行递归调用,这在迭代递归数据结构(如树)时非常有用。以以下二叉树数据结构为例。
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
/** Prefix iteration: parent before children */
* [Symbol.iterator]() {
yield this.value;
if (this.left) {
// Same as yield* this.left[Symbol.iterator]()
yield* this.left;
}
if (this.right) {
yield* this.right;
}
}
}
方法[Symbol.iterator]()添加了对迭代协议的支持,这意味着我们可以使用for-of循环来迭代BinaryTree的实例:
const tree = new BinaryTree('a',
new BinaryTree('b',
new BinaryTree('c'),
new BinaryTree('d')),
new BinaryTree('e'));
for (const x of tree) {
console.log(x);
}
输出:
a
b
c
d
e
练习:遍历嵌套数组
exercises/sync-generators/iter_nested_arrays_test.mjs
33.3 生成器的用例:重用遍历
生成器的一个重要用例是提取和重用遍历。
33.3.1 背景信息:外部迭代与内部迭代
在准备下一小节之前,我们需要了解两种不同的迭代对象“内部”值的方式:
-
外部迭代(拉取):我们的代码通过迭代协议向对象请求值。例如,
for-of循环基于 JavaScript 的迭代协议:for (const x of ['a', 'b']) { console.log(x); }输出:
a b -
内部迭代(推送):我们将回调函数传递给对象的方法,该方法将值传递给回调。例如,数组有
.forEach()方法:['a', 'b'].forEach((x) => { console.log(x); });输出:
a b
下一小节将提供两种迭代风格的示例。
33.3.2 要重用的遍历
例如,考虑以下函数,它遍历文件树并记录它们的路径(它使用Node.js API来完成此操作):
function logPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.join(dir, fileName);
console.log(filePath);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
logPaths(filePath); // recursive call
}
}
}
考虑以下目录:
mydir/
a.txt
b.txt
subdir/
c.txt
让我们在mydir/内部记录路径:
logPaths('mydir');
输出:
mydir/a.txt
mydir/b.txt
mydir/subdir
mydir/subdir/c.txt
我们如何重用这个遍历并做些其他的事情,而不仅仅是记录路径?
33.3.3 示例:内部迭代(推送)
重用遍历代码的另一种方式是通过内部迭代:每个遍历的值都传递给一个回调(行 A)。
function visitPaths(dir, callback) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.join(dir, fileName);
callback(filePath); // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
visitPaths(filePath, callback);
}
}
}
const paths = [];
visitPaths('mydir', p => paths.push(p));
assert.deepEqual(
paths,
[
'mydir/a.txt',
'mydir/b.txt',
'mydir/subdir',
'mydir/subdir/c.txt',
]);
33.3.4 外部迭代(拉取)
重用遍历代码的另一种方式是通过外部迭代:我们可以编写一个生成器,它产生所有遍历的值(行 A)。
function* iterPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.join(dir, fileName);
yield filePath; // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* iterPaths(filePath);
}
}
}
const paths = Array.from(iterPaths('mydir'));
33.4 生成器的高级功能
《探索 ES6》中的生成器章节涵盖了本书范围之外的两个特性:
-
yield也可以通过.next()的参数来接收数据。 -
生成器也可以
返回值(而不仅仅是yield它们)。这些值不会成为迭代值,但可以通过yield*来检索。
34 数组(数组)
-
34.1 速查表:数组
-
34.1.1 使用数组
-
34.1.2 最常用的数组方法
-
-
34.2 使用数组的方式:固定布局与序列
-
34.3 基本数组操作
-
34.3.1 创建、读取、写入数组
-
34.3.2 数组的
.length属性 -
34.3.3 通过负索引引用元素
-
34.3.4 清除数组
-
34.3.5 展开到数组字面量中
-
34.3.6 列出数组的索引和条目
-
34.3.7 检查值是否为数组:
Array.isArray()
-
-
34.4
for-of和数组-
34.4.1
for-of: 遍历元素 -
34.4.2
for-of: 遍历索引
-
-
34.5 类似数组的对象
-
34.6 将可迭代对象、迭代器和类似数组值转换为数组
-
34.6.1 通过展开 (
...) 将可迭代对象转换为数组 -
34.6.2 通过
.toArray()将迭代器转换为数组(ES2025) -
34.6.3 通过
Array.from()将可迭代对象和类似数组对象转换为数组
-
-
34.7 复制数组
-
34.8 创建和填充任意长度的数组
-
34.8.1 创建数组并在之后添加元素
-
34.8.2 创建填充原始值的数组
-
34.8.3 创建填充对象的数组
-
34.8.4 使用整数范围创建数组
-
34.8.5 如果元素都是整数或浮点数,则类型化数组工作得很好
-
-
34.9 多维数组
-
34.10 数组实际上是字典(高级)
-
34.10.1 数组索引是(略微特殊的)属性键
-
34.10.2 数组可以有空洞
-
-
34.11 破坏性操作与非破坏性数组操作
-
34.11.1 如何使破坏性数组方法变为非破坏性
-
34.11.2
.reverse(),.sort(),.splice()的非破坏性版本(ES2023)
-
-
34.12 在数组的两端添加和移除元素
-
34.12.1 在数组的两端破坏性地添加和移除元素
-
34.12.2 非破坏性地预加和尾加元素
-
-
34.13 接受元素回调的数组方法
-
34.14 使用元素回调进行转换:
.map(),.filter(),.flatMap()-
34.14.1
.map(): 每个输出元素都由其输入元素派生 -
34.14.2
.filter(): 只保留一些元素 -
34.14.3
.flatMap(): 每个输入元素产生零个或多个输出元素^(ES2019)
-
-
34.15
.reduce(): 为数组计算摘要-
34.15.1
.reduce()工作概述 -
34.15.2 如果我们省略
init会发生什么? -
34.15.3
.reduceRight():.reduce()的从尾到头的版本
-
-
34.16
.sort(): 排序数组-
34.16.1 自定义排序顺序
-
34.16.2 排序数字
-
34.16.3 排序人类语言字符串
-
34.16.4 排序对象
-
-
34.17 分组数组元素
-
34.18 快速参考:
Array-
34.18.1
new Array() -
34.18.2
Array.* -
34.18.3
Array.prototype.*: 获取、设置和访问单个元素 -
34.18.4
Array.prototype.*: 键和值 -
34.18.5
Array.prototype.*: 在数组的两端破坏性地添加或移除元素 -
34.18.6
Array.prototype.*: 合并、提取和更改元素序列 -
34.18.7
Array.prototype.*: 搜索元素 -
34.18.8
Array.prototype.*: 过滤和映射 -
34.18.9
Array.prototype.*: 计算摘要 -
34.18.10
Array.prototype.*: 转换为字符串 -
34.18.11
Array.prototype.*: 排序和反转 -
34.18.12 快速参考的来源
-
34.1 速查表:数组
JavaScript 数组是一个非常灵活的数据结构,用作列表、栈、队列、元组(例如,对)等。
一些与数组相关的操作会破坏性地更改数组。其他非破坏性地产生新的数组,将更改应用于原始内容的副本。
34.1.1 使用数组
创建数组,读取和写入元素:
// Creating an Array
const arr = ['a', 'b', 'c']; // Array literal
assert.deepEqual(
arr,
[ // Array literal
'a',
'b',
'c', // trailing commas are ignored
]
);
// Reading elements
assert.equal(
arr[0], 'a' // negative indices don’t work
);
assert.equal(
arr.at(-1), 'c' // negative indices work
);
// Writing an element
arr[0] = 'x';
assert.deepEqual(
arr, ['x', 'b', 'c']
);
数组的长度:
const arr = ['a', 'b', 'c'];
assert.equal(
arr.length, 3 // number of elements
);
arr.length = 1; // removing elements
assert.deepEqual(
arr, ['a']
);
arr[arr.length] = 'b'; // adding an element
assert.deepEqual(
arr, ['a', 'b']
);
通过 .push() 非破坏性地添加元素:
const arr = ['a', 'b'];
arr.push('c'); // adding an element
assert.deepEqual(
arr, ['a', 'b', 'c']
);
// Pushing Arrays (used as arguments via spreading (...)):
arr.push(...['d', 'e']);
assert.deepEqual(
arr, ['a', 'b', 'c', 'd', 'e']
);
通过扩展(...)非破坏性地添加元素:
const arr1 = ['a', 'b'];
const arr2 = ['c'];
assert.deepEqual(
[...arr1, ...arr2, 'd', 'e'],
['a', 'b', 'c', 'd', 'e']
);
遍历元素:
const arr = ['a', 'b', 'c'];
for (const value of arr) {
console.log(value);
}
输出:
a
b
c
遍历索引-值对:
const arr = ['a', 'b', 'c'];
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
输出:
0 a
1 b
2 c
34.1.2 最常用的数组方法
本节演示了一些常见的数组方法。在本章末尾有一个更全面的快速参考 Array。
在开始或末尾破坏性地添加或删除数组元素:
// Adding and removing at the start
const arr1 = ['■', '●'];
arr1.unshift('▲');
assert.deepEqual(
arr1, ['▲', '■', '●']
);
arr1.shift();
assert.deepEqual(
arr1, ['■', '●']
);
// Adding and removing at the end
const arr2 = ['■', '●'];
arr2.push('▲');
assert.deepEqual(
arr2, ['■', '●', '▲']
);
arr2.pop();
assert.deepEqual(
arr2, ['■', '●']
);
查找数组元素:
> ['■', '●', '■'].includes('■')
true
> ['■', '●', '■'].indexOf('■')
0
> ['■', '●', '■'].lastIndexOf('■')
2
> ['●', '', '▲'].find(x => x.length > 0)
'●'
> ['●', '', '▲'].findLast(x => x.length > 0)
'▲'
> ['●', '', '▲'].findIndex(x => x.length > 0)
0
> ['●', '', '▲'].findLastIndex(x => x.length > 0)
2
转换数组(创建新的而不改变原始的):
> ['▲', '●'].map(x => x+x)
['▲▲', '●●']
> ['■', '●', '■'].filter(x => x === '■')
['■', '■']
> ['▲', '●'].flatMap(x => [x,x])
['▲', '▲', '●', '●']
复制数组的部分:
> ['■', '●', '▲'].slice(1, 3)
['●', '▲']
> ['■', '●', '▲'].slice() // complete copy
['■', '●', '▲']
连接数组中的字符串:
> ['■','●','▲'].join('-')
'■-●-▲'
> ['■','●','▲'].join('')
'■●▲'
.sort() 对其接收者进行排序并返回它(如果我们不想改变接收者,可以使用 .toSorted()):
// By default, string representations of the Array elements
// are sorted lexicographically:
const arr = [200, 3, 10];
arr.sort();
assert.deepEqual(
arr, [10, 200, 3]
);
// Sorting can be customized via a callback:
assert.deepEqual(
[200, 3, 10].sort((a, z) => a - z), // sort numerically
[3, 10, 200]
);
34.2 使用数组的方式:固定布局与序列
这些是两种在 JavaScript 中使用数组最常见的方式:
-
固定布局数组:以这种方式使用,数组具有固定数量的索引元素。这些元素中的每一个都可以有不同的类型。
-
序列数组:以这种方式使用,数组具有可变数量的索引元素。这些元素中的每一个都具有相同的类型。序列数组非常灵活;我们可以将它们用作(传统的)数组、栈和队列。我们将在后面看到。
作为两种方式之间差异的例子,考虑由 Object.entries() 返回的数组:
> Object.entries({ a: 1, b: 2, c: 3 })
[
[ 'a', 1 ],
[ 'b', 2 ],
[ 'c', 3 ],
]
它是一系列 对 – 长度为二的固定布局数组。
34.3 基本数组操作
34.3.1 创建、读取、写入数组
创建数组最佳的方式是通过 数组字面量:
const arr = ['a', 'b', 'c'];
数组字面量以方括号 [] 开始和结束。它创建了一个包含三个 元素 的数组:'a'、'b' 和 'c'。
数组字面量中最后一个元素之后允许并忽略逗号:
const arr = [
'a',
'b',
'c',
];
要读取数组元素,我们在方括号中放置一个索引(索引从零开始):
const arr = ['a', 'b', 'c'];
assert.equal(arr[0], 'a');
要更改数组元素,我们通过索引分配给数组:
const arr = ['a', 'b', 'c'];
arr[0] = 'x';
assert.deepEqual(arr, ['x', 'b', 'c']);
数组索引的范围是 32 位(不包括最大长度):0, 2³²−1)
[34.3.2 数组的 .length
每个数组都有一个属性 .length,可以用来读取和更改(!)数组中的元素数量。
数组的长度始终是最高索引加一:
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
如果我们在长度的索引处写入数组,我们将追加一个元素:
arr[arr.length] = 'c';
assert.deepEqual(
arr, ['a', 'b', 'c']
);
assert.equal(arr.length, 3)
如果我们设置 .length,我们将修剪数组并移除元素:
arr.length = 1;
assert.deepEqual(
arr, ['a']
);
要(破坏性地)追加一个元素,我们也可以使用数组方法 .push():
const arr = ['a', 'b'];
arr.push('c');
assert.deepEqual(
arr, ['a', 'b', 'c']
);
练习:通过 .push() 删除空行
exercises/arrays/remove_empty_lines_push_test.mjs
34.3.3 通过负索引引用元素
大多数数组方法支持负索引。如果索引是负数,则将其添加到数组的长度中,以产生一个可用的索引。因此,以下两个 .slice() 调用是等效的:它们都从 arr 的最后一个元素开始复制。
> const arr = ['a', 'b', 'c'];
> arr.slice(-1)
[ 'c' ]
> arr.slice(arr.length - 1)
[ 'c' ]
34.3.3.1 .at():读取单个元素(支持负索引)^(ES2022)
数组方法 .at() 返回给定索引处的元素。它支持正索引和负索引(-1 指的是最后一个元素,-2 指的是倒数第二个元素等):
> ['a', 'b', 'c'].at(0)
'a'
> ['a', 'b', 'c'].at(-1)
'c'
相比之下,方括号运算符 [] 不支持负索引(并且不能更改,因为这会破坏现有代码)。它将它们解释为非元素属性的键:
const arr = ['a', 'b', 'c'];
arr[-1] = 'non-element property';
// The Array elements didn’t change:
assert.deepEqual(
Array.from(arr), // copy just the Array elements
['a', 'b', 'c']
);
assert.equal(
arr[-1], 'non-element property'
);
34.3.4 清除数组
我们可以通过将 .length 设置为零来清除数组:
const arr = ['a', 'b', 'c'];
arr.length = 0;
assert.deepEqual(arr, []);
我们还可以将一个新的空数组赋值给存储数组的变量:
let arr = ['a', 'b', 'c'];
arr = [];
assert.deepEqual(arr, []);
后者方法的优势在于不会影响指向同一数组的其他位置。如果我们确实想为每个人重置共享的数组,则需要使用前者方法。
34.3.5 展开到数组字面量
在数组字面量内部,一个 展开元素 由三个点 (...) 后跟一个表达式组成。它会导致表达式被评估然后迭代。每个迭代的值都成为额外的数组元素 – 例如:
> const iterable = ['b', 'c'];
> ['a', ...iterable, 'd']
[ 'a', 'b', 'c', 'd' ]
这意味着我们可以将任何可迭代对象转换为数组:
const iterable = new Set(['x', 'y']);
assert.deepEqual(
[...iterable],
['x', 'y']
);
由于数组是可迭代的,我们可以使用展开来复制它们:
const arr = ['a', 'b', 'c'];
const copy = [...arr];
展开对于将数组(和其他可迭代对象)连接到数组中也非常方便:
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
const concatenated = [...arr1, ...arr2, 'e'];
assert.deepEqual(
concatenated,
['a', 'b', 'c', 'd', 'e']
);
由于迭代时的展开,它仅在值是可迭代的时才有效:
> [...'abc'] // strings are iterable
[ 'a', 'b', 'c' ]
> [...123]
TypeError: 123 is not iterable
> [...undefined]
TypeError: undefined is not iterable
34.3.6 数组:列出索引和条目
方法 .keys() 列出数组的索引:
const arr = ['a', 'b'];
arr.prop = true; // needed later
assert.deepEqual(
arr.keys().toArray(), // (A)
[0, 1]
);
.keys() 返回一个迭代器。在行 A 中,我们将该迭代器转换为数组。
列出数组索引与列出属性不同。前者产生数字;后者产生字符串化的数字(以及非索引属性键):
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']
);
注意到对象属性 arr.prop 出现在结果中。
方法 .entries() 列出数组的元素内容,以 [索引, 元素] 对的形式:
assert.deepEqual(
arr.entries().toArray(),
[
[0, 'a'],
[1, 'b'],
]
);
Object.entries() 列出 [属性键,属性值] 对:
assert.deepEqual(
Object.entries(arr),
[
['0', 'a'],
['1', 'b'],
['prop', true],
]
);
我们再次在结果中看到 arr.prop。
34.3.7 检查值是否为数组:Array.isArray()
Array.isArray() 检查一个值是否为数组:
> Array.isArray([])
true
我们还可以使用 instanceof:
> [] instanceof Array
true
然而,instanceof 有一个缺点:如果值来自另一个 领域,则它不起作用。大致来说,领域是 JavaScript 全局作用域的一个实例。有些领域是相互隔离的(例如,浏览器中的 Web Workers),但也有一些领域之间可以移动数据——例如,浏览器中的同源 iframe。x instanceof Array 检查 x 的原型链,因此如果 x 是来自另一个领域的数组,则返回 false。
typeof 将数组视为对象:
> typeof []
'object'
34.4 for-of 和数组
我们在本章前面已经遇到了 for-of 循环。本节简要回顾了如何使用它来遍历数组。
34.4.1 for-of:遍历元素
以下 for-of 循环遍历数组的元素:
for (const element of ['a', 'b']) {
console.log(element);
}
输出:
a
b
34.4.2 for-of:遍历索引
此 for-of 循环遍历数组的索引:
for (const element of ['a', 'b'].keys()) {
console.log(element);
}
输出:
0
1
34.4.3 for-of:遍历 [索引, 元素] 对
以下 for-of 循环遍历 [索引, 元素] 对。解构(将在 后面 描述),为我们提供了在 for-of 循环头部设置 索引 和 元素 的便捷语法。
for (const [index, element] of ['a', 'b'].entries()) {
console.log(index, element);
}
输出:
0 a
1 b
34.5 类似数组的对象
一些与数组一起工作的操作只需要最基本的内容:值必须是 类似数组的。类似数组的值是一个具有以下属性的对象:
-
.length:包含类似数组的对象的长度。如果此属性不存在,则使用值0。 -
[0]:包含索引 0 处的元素(等等)。注意,如果我们使用数字作为属性名,它们总是被强制转换为字符串。因此,[0]获取键为'0'的属性的值。
例如,Array.from() 接受类似数组的对象并将它们转换为数组:
// .length is implicitly 0 in this case
assert.deepEqual(
Array.from({}),
[]
);
assert.deepEqual(
Array.from({length: 2, 0: 'a', 1: 'b'}),
[ 'a', 'b' ]
);
TypeScript 中类似数组的对象的接口是:
interface ArrayLike<T> {
length: number;
[n: number]: T;
}
类似数组的对象在现代 JavaScript 中相对较少
在 ES6 之前,类似数组的对象更为常见;现在我们很少看到它们。
34.6 将可迭代对象、迭代器和类似数组的值转换为数组
在本节中,我们将探讨如何将各种值转换为数组:
-
通过展开(
...)将可迭代对象转换为数组 -
通过
.toArray()将迭代器转换为数组 -
通过
Array.from()将可迭代对象和类似数组的对象转换为数组
34.6.1 通过展开(...)将可迭代对象转换为数组
在数组字面量内部,通过 ... 展开,可以将任何可迭代对象转换为一系列数组元素。例如:
const iterable = new Set(['a', 'b']);
assert.deepEqual(
[...iterable],
['a', 'b']
);
34.6.2 通过 .toArray() 将迭代器转换为数组(ES2025)
如果我们有一个迭代器,我们可以使用方法 .toArray() 将其值存储在数组中:
const iterable = new Set(['a', 'b']);
assert.deepEqual(
Iterator.from(iterable).toArray(),
['a', 'b']
);
除了 Iterator.from(),我们还可以使用返回迭代器的方法(例如 .keys()、.values() 和 .entries()):
assert.deepEqual(
iterable.values().toArray(),
['a', 'b']
);
34.6.3 通过 Array.from() 将可迭代和类似数组的对象转换为数组
Array.from() 可以在两种模式下使用。
34.6.3.1 Array.from() 的第一种模式:转换
第一种模式具有以下类型签名:
.from<T>(iterable: Iterable<T> | ArrayLike<T>): Array<T>
接口 Iterable 在同步迭代章节中展示。接口 ArrayLike 在本章较早的部分出现。
使用单个参数,Array.from() 将任何可迭代或类似数组的对象转换为数组:
> Array.from(new Set(['a', 'b'])) // iterable
[ 'a', 'b' ]
> Array.from({length: 2, 0:'a', 1:'b'}) // Array-like
[ 'a', 'b' ]
34.6.3.2 Array.from() 的第二种模式:转换和映射
Array.from() 的第二种模式涉及两个参数:
.from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapFunc: (v: T, i: number) => U,
thisArg?: any
): Array<U>
在这种模式下,Array.from() 做了几件事情:
-
它遍历
iterable。 -
它使用每个迭代的值调用
mapFunc。可选参数thisArg指定了mapFunc的this。 -
它将
mapFunc应用到每个迭代的值上。 -
它将结果收集到一个新数组中并返回它。
换句话说:我们从一个元素类型为 T 的可迭代对象转换到一个元素类型为 U 的数组。
这是一个示例:
> Array.from(new Set(['a', 'b']), x => x + x)
[ 'aa', 'bb' ]
34.7 复制数组
以下代码展示了复制数组 arr 的五种方式:
const arr = ['a', 'b'];
const shallowCopy1 = arr.slice();
const shallowCopy2 = Array.from(arr);
const shallowCopy3 = [...arr];
const shallowCopy4 = arr.values().toArray();
const deepCopy = structuredClone(arr);
const copies = [
shallowCopy1, shallowCopy2, shallowCopy3, shallowCopy4, deepCopy
];
for (const copy of copies) {
assert.deepEqual(copy, arr);
}
只有 structuredClone() 产生一个 深拷贝。在其他情况下,拷贝是 浅拷贝:它包含 [索引,元素] 条目的副本,但元素本身与原始数组共享。有关更多信息,包括 structuredClone() 的限制,请参阅“复制对象:展开与 Object.assign() 与 structuredClone()”(§30.5)。
34.8 创建和填充任意长度的数组
创建数组的最佳方式是通过数组字面量。然而,我们并不总是可以使用它:数组可能太大,我们在开发过程中可能不知道它的长度,或者我们可能希望保持其长度灵活。那么,我推荐以下技术来创建,并可能填充,数组。
34.8.1 创建数组并在之后添加元素
创建数组并在之后添加元素的最常见技术是先从一个空数组开始,然后将值推入其中:
const arr = [];
for (let i=0; i<3; i++) {
arr.push('*'.repeat(i));
}
assert.deepEqual(
arr, ['', '*', '**']
);
34.8.2 创建填充了原始值的数组
以下代码创建了一个填充了原始值的数组:
> new Array(3).fill(0)
[ 0, 0, 0 ]
.fill() 将每个数组元素或空位替换为给定的值。我们用它来填充一个有 3 个空位的数组:
> new Array(3)
[ , , ,]
注意,结果有三个 空位(空槽)- 数组字面量中的最后一个逗号总是被忽略。
34.8.3 创建填充了对象的数组
如果我们使用 .fill() 与一个对象,那么每个数组元素都将引用这个相同的单个对象:
const arr = new Array(3).fill({});
arr[0].prop = true;
assert.deepEqual(
arr, [
{prop: true},
{prop: true},
{prop: true},
]);
我们该如何解决这个问题?我们可以使用 Array.from():
> Array.from(new Array(3), () => ({}))
[{}, {}, {}]
使用两个参数调用 Array.from():
-
提取第一个参数(必须是可迭代的或类似数组的)的元素,
-
通过第二个参数中的回调函数映射它们,
-
将结果返回为数组。
与 .fill() 不同,它多次重用同一个对象,之前的代码为每个元素创建了一个新对象。
在这种情况下,我们能否使用 .map()?不幸的是,不能,因为 .map() 忽略但保留空位(而 Array.from() 将它们视为 undefined 元素):
> new Array(3).map(() => ({}))
[ , , ,]
对于较大的尺寸,第一个参数中的临时数组可能会消耗相当多的内存。以下方法没有这个缺点,但描述性较差:
> Array.from({length: 3}, () => ({}))
[{}, {}, {}]
我们不是使用临时数组,而是使用一个临时的 类似数组的对象。
34.8.4 使用一系列整数创建数组
要创建一个包含一系列整数的数组,我们使用 Array.from() 的方式与之前小节中的用法类似:
function createRange(start, end) {
return Array.from({length: end-start}, (_, i) => i+start);
}
assert.deepEqual(
createRange(2, 5),
[2, 3, 4]);
这里有一个创建从零开始的整数范围的替代方法,稍微有点技巧性:
/** Returns an iterable */
function createRange(end) {
return new Array(end).keys();
}
assert.deepEqual(
Array.from(createRange(4)),
[0, 1, 2, 3]
);
这之所以有效,是因为 .keys() 将 空位 当作 undefined 元素处理,并列出它们的索引。
34.8.5 如果元素都是整数或浮点数,则类型化数组工作得很好
当处理整数或浮点数数组时,我们应该考虑 类型化数组,它们是为了这个目的而创建的。
34.9 多维数组
JavaScript 没有真正的多维数组;我们需要求助于元素也是数组的数组:
function initMultiArray(...dimensions) {
function initMultiArrayRec(dimIndex) {
if (dimIndex >= dimensions.length) {
return 0;
} else {
const dim = dimensions[dimIndex];
const arr = [];
for (let i=0; i<dim; i++) {
arr.push(initMultiArrayRec(dimIndex+1));
}
return arr;
}
}
return initMultiArrayRec(0);
}
const arr = initMultiArray(4, 3, 2);
arr[3][2][1] = 'X'; // last in each dimension
assert.deepEqual(arr, [
[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
[ [ 0, 0 ], [ 0, 0 ], [ 0, 'X' ] ],
]);
34.10 数组实际上是字典(高级)
在本节中,我们考察数组如何确切地存储它们的元素:在属性中。我们通常不需要知道这一点,但它有助于理解一些较少见的数组现象。
34.10.1 数组索引是(稍微特殊一点的)属性键
你可能会认为数组元素是特殊的,因为我们通过数字访问它们。但用于这样做的方括号运算符 [] 与用于访问属性的运算符相同。它将任何非符号值强制转换为字符串。因此,数组元素(几乎)是正常属性(行 A),我们使用数字或字符串作为索引(行 B 和行 C)并没有关系:
const arr = ['a', 'b'];
arr.prop = 123;
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop'] // (A)
);
assert.equal(arr[0], 'a'); // (B)
assert.equal(arr['0'], 'a'); // (C)
用于数组元素的属性键(字符串!)被称为 索引。一个字符串 str 是索引,如果将其转换为 32 位无符号整数再转换回来,结果仍然是原始值。用公式表示:
ToString(ToUint32(str)) === str
34.10.1.1 内部,JavaScript 引擎优化了数组的管理方式
在使用 JavaScript 和 ECMAScript 规范时,数组元素是属性,数组索引是字符串值属性键。
然而,在内部,几乎所有的 JavaScript 引擎都优化了数组的管理方式:它们按顺序存储元素,并使用索引作为数字偏移量。如果数组的元素不是连续的,引擎可能会切换到更类似字典的表示形式 – 如果它们之间有空位。关于数组中空位的更多内容将在后面介绍。
34.10.1.2 列出索引
当列出属性键时,索引被特别处理 – 它们总是排在最前面,并且像数字一样排序('2' 在 '10' 之前):
const arr = [];
arr.prop = true;
arr[1] = 'b';
arr[0] = 'a';
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']
);
assert.deepEqual(
Object.entries(arr),
[['0', 'a'], ['1', 'b'], ['prop', true]]
);
注意,.keys() 和 .entries() 将数组索引视为数字,并忽略非索引属性:
assert.deepEqual(
Array.from(arr.keys()),
[0, 1]
);
assert.deepEqual(
Array.from(arr.entries()),
[[0, 'a'], [1, 'b']]
);
我们使用了 Array.from() 来将 .keys() 和 .entries() 返回的可迭代对象转换为数组。
34.10.2 数组可以有空位
在 JavaScript 中,我们区分两种数组:
-
如果所有索引
i(其中 0 ≤i<arr.length)都存在,数组arr就是 密集的。也就是说,索引形成一个连续的范围。 -
如果索引范围中有空位,数组就是 稀疏的。也就是说,某些索引缺失。
由于数组实际上是索引到值的字典,JavaScript 中的数组可以是稀疏的。
建议:避免空位
到目前为止,我们只看到了密集数组,并且确实建议避免空位:它们使我们的代码更复杂,并且数组方法处理它们的方式并不一致。此外,JavaScript 引擎优化密集数组,使它们更快。
34.10.2.1 创建数组空位
我们可以通过在分配元素时跳过索引来创建空位:
const arr = [];
arr[0] = 'a';
arr[2] = 'c';
assert.deepEqual(Object.keys(arr), ['0', '2']); // (A)
assert.equal(0 in arr, true); // element
assert.equal(1 in arr, false); // hole
在行 A 中,我们使用 Object.keys(),因为 arr.keys() 将空位视为 undefined 元素,并且不会揭示它们。
创建空位的另一种方法是省略数组字面量中的元素:
const arr = ['a', , 'c'];
assert.deepEqual(Object.keys(arr), ['0', '2']);
我们也可以通过增加 .length 来在末尾添加空位:
const arr = ['a'];
assert.deepEqual(Object.keys(arr), ['0']);
assert.equal(arr.length, 1);
arr.length = 3;
arr.push('x');
assert.deepEqual(Object.keys(arr), ['0', '3']);
assert.deepEqual(arr, ['a', /*hole*/, /*hole*/, 'x']);
删除数组元素也会创建空位:
const arr = ['a', 'b', 'c'];
assert.deepEqual(Object.keys(arr), ['0', '1', '2']);
delete arr[1];
assert.deepEqual(Object.keys(arr), ['0', '2']);
assert.deepEqual(arr, ['a', , 'c']);
34.10.2.2 数组操作如何处理空位?
然而,数组操作处理空位的方式有很多种。
一些数组操作会移除空位:
> ['a',,'b'].filter(x => true)
[ 'a', 'b' ]
一些数组操作会忽略空位:
> ['a', ,'a'].every(x => x === 'a')
true
一些数组操作忽略但保留空位:
> ['a',,'b'].map(x => 'c')
[ 'c', , 'c' ]
一些数组操作将空位视为 undefined 元素:
> Array.from(['a',,'b'])
[ 'a', undefined, 'b' ]
> Array.from(['a',,'b'], x => x + '!') // mapping
[ 'a!', 'undefined!', 'b!' ]
> ['a',,'b'].entries().toArray()
[[0, 'a'], [1, undefined], [2, 'b']]
Object.keys() 的工作方式与 .keys() 不同(字符串 vs. 数字,空位没有键):
> ['a',,'b'].keys().toArray()
[ 0, 1, 2 ]
> Object.keys(['a',,'b'])
[ '0', '2' ]
这里没有共同的规则可以记住。如果某个数组操作如何处理空位很重要,最佳方法是快速在控制台中测试。
34.11 数组操作:破坏性 vs. 非破坏性
一些数组操作是 破坏性的:它们会改变它们操作的数组 – 例如,设置一个元素:
const arr = ['a', 'b', 'c'];
arr[1] = 'x';
assert.deepEqual(
// The original was modified
arr, ['a', 'x', 'c']
);
其他数组操作是非破坏性的:它们产生包含所需更改的新数组,而不触及原始数组 – 例如,方法 .with() 是设置元素的不可破坏版本:
const arr = ['a', 'b', 'c'];
assert.deepEqual(
// Produces a copy with changes
arr.with(1, 'x'),
['a', 'x', 'c']
);
assert.deepEqual(
// The original is unchanged
arr, [ 'a', 'b', 'c' ]
);
34.11.1 如何使破坏性数组方法变为非破坏性
这些是三个常见的破坏性数组方法:
-
.reverse() -
.sort() -
.splice()
我们将在本章后面讨论 .sort() 和 .splice()。.reverse() 重新排列数组,使其元素的顺序颠倒:之前最后的元素现在排在第一位;倒数第二个元素排在第二位;等等:
const original = ['a', 'b', 'c'];
const reversed = original.reverse();
assert.deepEqual(reversed, ['c', 'b', 'a']);
assert.ok(reversed === original); // .reverse() returned `this`
assert.deepEqual(original, ['c', 'b', 'a']);
为了防止破坏性方法改变数组,我们可以在使用之前创建一个副本 - 例如:
const original = ['a', 'b', 'c'];
const reversed1 = original.slice().reverse();
const reversed2 = Array.from(original).reverse();
const reversed3 = [...original].reverse();
const reversed4 = original.values().toArray().reverse();
assert.deepEqual(original, ['a', 'b', 'c']);
另一个选项是使用破坏性方法的非破坏性版本。这正是我们将要探讨的。
34.11.2 .reverse(), .sort(), .splice() 的非破坏性版本 (ES2023)
这些是非破坏性版本的破坏性数组方法 .reverse(), .sort() 和 .splice():
-
.toReversed(): Array -
.toSorted(compareFn): Array -
.toSpliced(start, deleteCount, ...items): Array
我们在前面的小节中使用了 .reverse()。它的非破坏性版本的使用方式如下:
const original = ['a', 'b', 'c'];
const reversed = original.toReversed();
assert.deepEqual(reversed, ['c', 'b', 'a']);
// The original is unchanged
assert.deepEqual(original, ['a', 'b', 'c']);
34.12 在数组两端添加和删除元素
34.12.1 破坏性地在数组两端添加和删除元素
JavaScript 的 Array 非常灵活,更像是数组、栈和队列的组合。让我们探讨破坏性地添加和删除数组元素的方法。
.push() 在数组的末尾添加元素:
const arr1 = ['a', 'b'];
arr1.push('x', 'y'); // append single elements
assert.deepEqual(arr1, ['a', 'b', 'x', 'y']);
const arr2 = ['a', 'b'];
arr2.push(...['x', 'y']); // (A) append Array
assert.deepEqual(arr2, ['a', 'b', 'x', 'y']);
展开参数 (...) 是函数调用中的一个特性。在行 A 中,我们使用它来推入一个数组。
.pop() 是 .push() 的逆操作,并从数组的末尾删除元素:
const arr2 = ['a', 'b', 'c'];
assert.equal(arr2.pop(), 'c');
assert.deepEqual(arr2, ['a', 'b']);
.shift() 从数组的开头删除元素:
const arr1 = ['a', 'b', 'c'];
assert.equal(arr1.shift(), 'a');
assert.deepEqual(arr1, ['b', 'c']);
.unshift() 是 .shift() 的逆操作,并在数组的开头添加元素:
const arr1 = ['a', 'b'];
arr1.unshift('x', 'y'); // prepend single elements
assert.deepEqual(arr1, ['x', 'y', 'a', 'b']);
const arr2 = ['a', 'b'];
arr2.unshift(...['x', 'y']); // prepend Array
assert.deepEqual(arr2, ['x', 'y', 'a', 'b']);
提示:记住 push、pop、shift 和 unshift 的功能
我的建议是专注于记住以下两个方法:
-
.push()是这四种方法中最常用的。一个常见的用例是组装输出数组:我们首先将索引 0 的元素推入;然后是索引 1 的元素;等等。 -
.shift()可以用来消耗数组的元素:第一次 shift,我们得到索引 0 的元素;然后是索引 1 的元素;等等。
剩下的两个方法 pop 和 unshift 是这两个方法的逆操作。
练习:通过数组实现队列
exercises/arrays/queue_via_array_test.mjs
34.12.2 非破坏性地在数组两端添加和删除元素
展开元素 (...) 是数组字面量的一项特性。在本节中,我们将使用它来非破坏性地向数组前缀和后缀添加元素。
非破坏性前缀:
const arr1 = ['a', 'b'];
assert.deepEqual(
['x', 'y', ...arr1], // prepend single elements
['x', 'y', 'a', 'b']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!
const arr2 = ['a', 'b'];
assert.deepEqual(
[...['x', 'y'], ...arr2], // prepend Array
['x', 'y', 'a', 'b']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!
非破坏性追加:
const arr1 = ['a', 'b'];
assert.deepEqual(
[...arr1, 'x', 'y'], // append single elements
['a', 'b', 'x', 'y']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!
const arr2 = ['a', 'b'];
assert.deepEqual(
[...arr2, ...['x', 'y']], // append Array
['a', 'b', 'x', 'y']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!
34.13 接受元素回调的数组方法
以下数组方法接受回调函数,并将数组元素传递给它们:
-
查找:
-
.find -
.findLast -
.findIndex -
.findLastIndex
-
-
转换:
-
.map -
.flatMap -
.filter
-
-
计算数组的摘要:
-
.every -
.some -
.reduce -
.reduceRight
-
-
遍历数组:
.forEach
元素回调的类型签名如下所示:
callback: (value: T, index: number, array: Array<T>) => boolean
即,回调函数得到三个参数(它可以自由忽略任何一个):
-
value是最重要的一个。此参数持有当前正在处理的数组元素。 -
index可以额外告诉回调函数元素的索引。 -
array指向当前数组(方法调用的接收者)。某些算法需要引用整个数组 - 例如,为了搜索其他数据。此参数使我们能够为这样的算法编写可重用的回调函数。
回调函数预期返回的内容取决于传递给它的方法。可能包括:
-
.map()使用其回调函数返回的值填充其结果:> ['a', 'b', 'c'].map(x => x + x) [ 'aa', 'bb', 'cc' ] -
.find()返回第一个回调返回true的数组元素:> ['a', 'bb', 'ccc'].find(str => str.length >= 2) 'bb'
34.14 使用元素回调进行转换:.map(), .filter(), .flatMap()
在本节中,我们探讨接受 元素回调 的方法,这些回调告诉它们如何将输入数组转换为输出数组。
34.14.1 .map(): 每个输出元素是从其输入元素派生出来的
输出数组的每个元素都是将回调应用于相应输入元素的结果:
> [1, 2, 3].map(x => x * 3)
[ 3, 6, 9 ]
> ['how', 'are', 'you'].map(str => str.toUpperCase())
[ 'HOW', 'ARE', 'YOU' ]
> [true, true, true].map((_x, index) => index)
[ 0, 1, 2 ]
.map() 可以实现如下:
function map(arr, mapFunc) {
const result = [];
for (const [i, x] of arr.entries()) {
result.push(mapFunc(x, i, arr));
}
return result;
}
练习:通过 .map() 编号行
exercises/arrays/number_lines_test.mjs
34.14.2 .filter(): 只保留一些元素
数组方法 .filter() 返回一个数组,收集所有回调返回真值(truthy value)的元素。
例如:
> [-1, 2, 5, -7, 6].filter(x => x >= 0)
[ 2, 5, 6 ]
> ['a', 'b', 'c', 'd'].filter((_x,i) => (i%2)===0)
[ 'a', 'c' ]
.filter() 可以实现如下:
function filter(arr, filterFunc) {
const result = [];
for (const [i, x] of arr.entries()) {
if (filterFunc(x, i, arr)) {
result.push(x);
}
}
return result;
}
练习:通过 .filter() 删除空行
exercises/arrays/remove_empty_lines_filter_test.mjs
34.14.3 .flatMap(): 每个输入元素产生零个或多个输出元素 (ES2019)
Array<T>.prototype.flatMap() 的类型签名是:
.flatMap<U>(
callback: (value: T, index: number, array: Array<T>) => U|Array<U>,
thisValue?: any
): Array<U>
.map() 和 .flatMap() 都接受一个 callback 函数作为参数,该参数控制如何将输入数组转换为输出数组:
-
使用
.map(),每个输入数组元素被转换为恰好一个输出元素。也就是说,callback返回一个值。 -
使用
.flatMap(),每个输入数组元素被转换为零个或多个输出元素。也就是说,callback返回一个值数组(它也可以返回非数组值,但这很少见)。
下面是 .flatMap() 的实际应用:
> ['a', 'b', 'c'].flatMap(x => [x,x])
[ 'a', 'a', 'b', 'b', 'c', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [x])
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [])
[]
在探讨这个方法如何实现之前,我们将考虑一些用例。
34.14.3.1 用例:同时进行过滤和映射
数组方法 .map() 的结果总是与它调用的数组具有相同的长度。也就是说,它的回调不能跳过它不感兴趣的数组元素。.flatMap() 能够这样做在下一个示例中很有用。
假设,我们从 Promise.allSettled()(ch_promises.html#Promise.allSettled)得到了以下结果:
const result = [
{ status: 'fulfilled', value: 'sunshine.jpg' },
{ status: 'rejected', reason: 'FILE NOT FOUND' },
{ status: 'fulfilled', value: 'dog.jpg' },
{ status: 'rejected', reason: 'NOT AUTHORIZED' },
];
我们可以使用 .flatMap() 从 results 中提取仅值或仅错误:
const values = result.flatMap(
r => r.status === 'fulfilled' ? [r.value] : []
);
assert.deepEqual(
values, ['sunshine.jpg', 'dog.jpg']
);
const reasons = result.flatMap(
r => r.status === 'rejected' ? [r.reason] : []
);
assert.deepEqual(
reasons, ['FILE NOT FOUND', 'NOT AUTHORIZED']
);
34.14.3.2 用例:将单个输入值映射到多个输出值
数组方法 .map() 将每个输入数组元素映射到一个输出元素。但如果我们想将其映射到多个输出元素呢?
在以下示例中,这变得必要:
> stringsToCodePoints(['many', 'a', 'moon'])
['m', 'a', 'n', 'y', 'a', 'm', 'o', 'o', 'n']
我们希望将字符串数组转换为 Unicode 字符数组(代码点)。以下函数通过 .flatMap() 实现了这一点:
function stringsToCodePoints(strs) {
return strs.flatMap(str => Array.from(str));
}
34.14.3.3 简单实现
我们可以按以下方式实现 .flatMap()。注意:此实现比内置版本简单,例如,它执行了更多的检查。
function flatMap(arr, mapFunc) {
const result = [];
for (const [index, elem] of arr.entries()) {
const x = mapFunc(elem, index, arr);
// We allow mapFunc() to return non-Arrays
if (Array.isArray(x)) {
result.push(...x);
} else {
result.push(x);
}
}
return result;
}
练习:.flatMap()
-
exercises/arrays/convert_to_numbers_test.mjs -
exercises/arrays/replace_objects_test.mjs
34.15 .reduce(): 计算数组的总结
方法 .reduce() 是计算数组 arr 的“总结”的强大工具。总结可以是任何类型的值:
-
一个数字。例如,
arr中所有元素的总和。 -
一个数组。例如,
arr的一个副本,其中每个元素是原始元素的两倍。 -
等等。
在函数式编程中,reduce 也被称为 foldl(“左折叠”),在那里很受欢迎。一个需要注意的问题是,它可能会使代码难以理解。
34.15.1 .reduce() 的工作原理概述
我们将从 .reduce() 的工作原理概述开始。如果你觉得概述难以理解,你可以跳到示例,稍后再回来。
.reduce() 具有以下类型签名(在 Array<T> 内部):
.reduce<U>(
callback: (accumulator: U, elem: T, idx: number, arr: Array<T>) => U,
init?: U
): U
T 是数组元素的类型,U 是总结的类型。这两个可能相同,也可能不同。accumulator 只是“总结”的另一个名称。
要计算数组 arr 的总结,.reduce() 将所有数组元素逐个传递给其回调:
const accumulator_0 = callback(init, arr[0]);
const accumulator_1 = callback(accumulator_0, arr[1]);
const accumulator_2 = callback(accumulator_1, arr[2]);
// Etc.
-
init为accumulator提供初始值。 -
累加器包含减法操作的初步结果。当回调函数被调用时,它将累加器与当前数组元素结合并返回结果。该结果成为下一个累加器。
-
.reduce()的结果是最终的累加器——在访问所有元素后callback的最后一个结果。
换句话说:callback 执行了大部分工作;.reduce() 只是按有用方式调用它。
我们可以说回调将数组元素折叠到累加器中。这就是为什么在函数式编程中这个操作被称为“折叠”。
示例:对整个数组应用二元运算符
让我们看看 .reduce() 的一个实际例子:函数 addAll() 计算数组 arr 中所有数字的总和。
function addAll(arr) {
return arr.reduce(
(sum, element) => sum + element,
0 // `init` value
);
}
assert.equal(addAll([1, 2, 3]), 6); // (A)
assert.equal(addAll([7, -4, 2]), 5);
在这种情况下,累加器包含 callback 已经访问过的所有数组元素的总和。
如何从 A 行的数组中推导出结果 6?通过以下 callback 的调用:
callback(0, 1) --> 1
callback(1, 2) --> 3
callback(3, 3) --> 6
注意:
-
首先的参数是当前累加器(从
.reduce()的参数init开始)。 -
第二个参数是当前数组元素。
-
结果是下一个累加器。
-
callback的最后一个结果也是.reduce()的结果。
或者,我们也可以通过 for-of 循环来实现 addAll():
function addAll(arr) {
let sum = 0;
for (const element of arr) {
sum = sum + element;
}
return sum;
}
很难说哪一种实现方式是“更好”的:基于 .reduce() 的实现方式稍微简洁一些,而基于 for-of 的实现可能更容易理解——特别是如果某人不太熟悉函数式编程的话。
34.15.2 如果省略 init 会发生什么?
如果我们将参数 init 传递给 .reduce(),它就是累加器的初始值:
> [1, 2].reduce((acc, elem) => acc + elem, 100)
103
> [1].reduce((acc, elem) => acc + elem, 100)
101
> [].reduce((acc, elem) => acc + elem, 100)
100
如果数组为空,.reduce() 的结果就是 init。
如果我们省略 init,那么 .reduce() 将使用第一个数组元素初始化累加器:
> [1, 2].reduce((acc, elem) => acc + elem)
3
> [1].reduce((acc, elem) => acc + elem)
1
> [].reduce((acc, elem) => acc + elem)
TypeError: Reduce of empty array with no initial value
如果数组只有一个元素,.reduce() 的结果就是该元素。如果数组为空,.reduce() 没有返回值,并抛出异常。
以下交互演示了带有和没有 init 的 accumulator 的初始值:
> ['x', 'a', 'b'].reduce((acc, elem) => acc)
'x'
> ['a', 'b'].reduce((acc, elem) => acc, 'x')
'x'
示例:通过 .reduce() 查找索引
以下函数是数组方法 .indexOf() 的一个实现。它返回给定 searchValue 在数组 arr 中首次出现的位置索引:
const NOT_FOUND = -1;
function indexOf(arr, searchValue) {
return arr.reduce(
(result, elem, index) => {
if (result !== NOT_FOUND) {
// We have already found something: don’t change anything
return result;
} else if (elem === searchValue) {
return index;
} else {
return NOT_FOUND;
}
},
NOT_FOUND);
}
assert.equal(indexOf(['a', 'b', 'c'], 'b'), 1);
assert.equal(indexOf(['a', 'b', 'c'], 'x'), -1);
.reduce() 的一个限制是我们不能提前结束。在 for-of 循环中,一旦找到结果,我们就可以立即返回。
示例:加倍数组元素
函数 double(arr) 返回 inArr 的一个副本,其元素都乘以 2:
function double(inArr) {
return inArr.reduce(
(outArr, element) => {
outArr.push(element * 2);
return outArr;
},
[]);
}
assert.deepEqual(
double([1, 2, 3]),
[2, 4, 6]);
我们通过向其中推送来修改初始值 []。double() 的一个非破坏性、更函数式的版本如下:
function double(inArr) {
return inArr.reduce(
// Don’t change `outArr`, return a fresh Array
(outArr, element) => [...outArr, element * 2],
[]);
}
assert.deepEqual(
double([1, 2, 3]),
[2, 4, 6]);
这个版本更优雅,但速度更慢,并且使用更多的内存。
练习:.reduce()
-
map()通过.reduce():exercises/arrays/map_via_reduce_test.mjs -
filter()通过.reduce():exercises/arrays/filter_via_reduce_test.mjs -
countMatches()通过.reduce():exercises/arrays/count_matches_via_reduce_test.mjs
34.15.3 .reduceRight():.reduce() 的末尾到开头版本
.reduce() 从开始到结束访问元素:
> ['a', 'b', 'c'].reduce((acc, x) => acc + x)
'abc'
.reduceRight() 具有相同的功能,但按从末尾到开头的顺序访问元素:
> ['a', 'b', 'c'].reduceRight((acc, x) => acc + x)
'cba'
34.16 .sort():排序数组
.sort() 有以下类型定义:
sort(compareFunc?: (a: T, b: T) => number): this
默认情况下,.sort() 对元素的字符串表示进行排序。这些表示通过 < 进行比较。此运算符按字典顺序比较代码单元值(字符码):
.sort() 在原地排序;它改变并返回其接收者:
> const arr = ['a', 'c', 'b'];
> arr.sort() === arr
true
> arr
[ 'a', 'b', 'c' ]
.sort() 是稳定的
自从 ECMAScript 2019 以来,排序保证是稳定的:如果元素在排序中被认为是相等的,那么排序不会改变这些元素的顺序(相对于彼此)。
34.16.1 自定义排序顺序
我们可以通过参数 compareFunc 自定义排序顺序,该参数必须返回一个数字,该数字是:
-
如果
a小于b,则返回负数 -
如果
a等于b,则返回零 -
如果
a大于b,则返回正数
我们将在下一小节中看到一个比较函数的例子。
记住这些规则的提示
负数小于零(等等)。
34.16.2 按数字排序数组
字典序排序对数字来说效果不佳:
> [200, 3, 10].sort()
[ 10, 200, 3 ]
我们可以通过编写一个比较函数来解决这个问题:
function compareNumbers(a, b) {
if (a < b) {
return -1; // any negative number will do
} else if (a === b) {
return 0;
} else {
return 1; // any positive number will do
}
}
assert.deepEqual(
[200, 3, 10].sort(compareNumbers),
[3, 10, 200]
);
为什么 .sort() 不会自动选择正确的排序方法来处理数字?
它必须检查所有数组元素并确保它们是数字,然后才能从字典序排序切换到数值排序。
34.16.2.1 排序数字的一个技巧
以下技巧利用了(例如)“小于”的结果可以是任何负数的事实:
> [200, 3, 10].sort((a, z) => a - z)
[ 3, 10, 200 ]
-
这次,我们称参数为
a和z,因为这可以提供一个助记符:回调函数按升序排序,“从a到z” (a - z)。 -
这种技巧的缺点是,如果比较一个大的正数和一个大的负数,我们可能会得到算术溢出。
34.16.3 按人类语言字符串排序
当排序人类语言字符串时,我们需要意识到它们是根据它们的代码单元值(字符码)进行比较的:
> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
[ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]
所有无重音的大写字母都排在所有无重音的小写字母之前,而所有带重音的字母都排在无重音字母之前。如果我们想要为人类语言进行适当的排序,可以使用 Intl,JavaScript 国际化 API:
const arr = ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'];
assert.deepEqual(
arr.sort(new Intl.Collator('en').compare),
['cookie', 'Cookie', 'éclair', 'Éclair', 'pie', 'Pie']
);
34.16.4 排序对象
如果我们想要对对象进行排序,我们还需要使用比较函数。以下代码示例展示了如何按年龄对对象进行排序。
const arr = [ {age: 200}, {age: 3}, {age: 10} ];
assert.deepEqual(
arr.sort((obj1, obj2) => obj1.age - obj2.age),
[{ age: 3 }, { age: 10 }, { age: 200 }]
);
练习:按名称排序对象
exercises/arrays/sort_objects_test.mjs
34.17 分组数组元素
通过 Object.groupBy() 和 Map.groupBy() 进行分组适用于任何可迭代对象,因此也适用于数组:
assert.deepEqual(
Object.groupBy(
[0, -5, 3, -4, 8, 9],
x => Math.sign(x)
),
{
'0': [0],
'-1': [-5,-4],
'1': [3,8,9],
__proto__: null,
}
);
更多信息:“分组可迭代对象 (ES2024)” (§32.8)
34.18 快速参考:Array
图例:
-
R:方法不改变数组(非破坏性)。 -
W:方法改变数组(破坏性)。
负索引:如果一个方法支持负索引,这意味着在它们被使用之前,这些索引会被添加到 .length 上:-1 变为 this.length-1 等。换句话说:-1 指的是最后一个元素,-2 指的是倒数第二个元素,等等。.at() 是支持负索引的一个方法:
const arr = ['a', 'b', 'c'];
assert.equal(
arr.at(-1), 'c'
);
34.18.1 new Array()
-
new Array(len = 0)ES1创建一个长度为
len的数组,其中只包含空位:// Trailing commas are always ignored. // Therefore: number of commas = number of holes assert.deepEqual(new Array(3), [,,,]);
34.18.2 Array.*
-
Array.from(iterableOrArrayLike, mapFunc?)ES6Array.from<T>( iterableOrArrayLike: Iterable<T> | ArrayLike<T> ): Array<T> Array.from<T, U>( iterableOrArrayLike: Iterable<T> | ArrayLike<T>, mapFunc: (v: T, k: number) => U, thisArg?: any ): Array<U>-
将可迭代对象或 类似数组的对象 转换为数组。
-
输入值可以通过
mapFunc在它们被添加到输出数组之前进行翻译。
示例:
> Array.from(new Set(['a', 'b'])) // iterable [ 'a', 'b' ] > Array.from({length: 2, 0:'a', 1:'b'}) // Array-like object [ 'a', 'b' ] -
-
Array.of(...items)ES6Array.of<T>( ...items: Array<T> ): Array<T>这个静态方法主要用于
Array的子类,其中它充当自定义数组字面量:class MyArray extends Array {} assert.equal( MyArray.of('a', 'b') instanceof MyArray, true );
34.18.3 Array.prototype.*:获取、设置和访问单个元素
-
Array.prototype.at(index)ES2022 | 非破坏性
- 返回
index处的数组元素。如果没有这样的元素,则返回undefined。
这种方法基本上等同于通过方括号获取元素:
arr[index] === arr.at(index)使用
.at()的一个原因是因为它支持 负索引:> ['a', 'b', 'c'].at(0) 'a' > ['a', 'b', 'c'].at(-1) 'c' - 返回
-
Array.prototype.with(index, value)ES2023 | 非破坏性
- 返回方法调用接收者的一个不同元素:在
index处现在有value。
这种方法是通过方括号设置元素的不可破坏版本。它支持负索引:
> ['a', 'b', 'c'].with(2, 'x') [ 'a', 'b', 'x' ] > ['a', 'b', 'c'].with(-1, 'x') [ 'a', 'b', 'x' ] - 返回方法调用接收者的一个不同元素:在
-
Array.prototype.forEach(callback)ES5 | 非破坏性
Array<T>.prototype.forEach( callback: (value: T, index: number, array: Array<T>) => void, thisArg?: any ): void为每个元素调用
callback。['a', 'b'].forEach( (elem) => console.log(elem) ); ['a', 'b'].forEach( (elem, index) => console.log(elem, index) );输出:
a b a 0 b 1for-of循环通常是一个更好的选择:它更快,支持break,并且可以遍历任意可迭代对象。
34.18.4 Array.prototype.*:键和值
-
Array.prototype.keys()ES6 | 非破坏性
返回接收者键的可迭代对象。
> Array.from(['a', 'b'].keys()) [ 0, 1 ] -
Array.prototype.values()ES6 | 非修改
返回接收者值的可迭代对象。
> Array.from(['a', 'b'].values()) [ 'a', 'b' ] -
Array.prototype.entries()ES6 | 非修改
返回一个包含接收者元素的索引对的可迭代对象。
> Array.from(['a', 'b'].entries()) [ [ 0, 'a' ], [ 1, 'b' ] ]
34.18.5 Array.prototype.*: 在数组的两端破坏性地添加或删除元素
-
Array.prototype.pop()ES3 | 修改
移除并返回接收者的最后一个元素。也就是说,它将接收者的末尾视为栈。是
.push()的相反操作。> const arr = ['a', 'b', 'c']; > arr.pop() 'c' > arr [ 'a', 'b' ] -
Array.prototype.push(...items)ES3 | 修改
向接收者的末尾添加零个或多个
items。也就是说,它将接收者的末尾视为栈。这是.pop()的相反操作。> const arr = ['a', 'b']; > arr.push('c', 'd') 4 > arr [ 'a', 'b', 'c', 'd' ]我们可以通过将数组展开(
...)到参数中来推送一个数组:> const arr = ['x']; > arr.push(...['y', 'z']) 3 > arr [ 'x', 'y', 'z' ] -
Array.prototype.shift()ES3 | 修改
移除并返回接收者的第一个元素。是
.unshift()的逆操作。> const arr = ['a', 'b', 'c']; > arr.shift() 'a' > arr [ 'b', 'c' ] -
Array.prototype.unshift(...items)ES3 | 修改
在
start索引处插入items并返回修改后的长度。> const arr = ['c', 'd']; > arr.unshift('e', 'f') 4 > arr [ 'e', 'f', 'c', 'd' ]我们可以通过将数组展开(
...)到参数中来推送一个数组:> const arr = ['c']; > arr.unshift(...['a', 'b']) 3 > arr [ 'a', 'b', 'c' ]
34.18.6 Array.prototype.*: 合并、提取和更改元素序列
提示:区分 .slice() 和 .splice()
-
.slice()更常用。动词“slice”也比动词“splice”更常见。 -
使用
.splice()很少见:元素通常通过.filter()(非破坏性)移除。“Splice” 比 “slice” 多一个字母,该方法也做了更多。 -
Array.prototype.concat(...items)ES3 | 非修改
返回一个新的数组,它是接收者和所有
items的连接。非数组参数(如以下示例中的'b')被视为具有单个元素的数组。> ['a'].concat('b', ['c', 'd']) [ 'a', 'b', 'c', 'd' ] -
Array.prototype.slice(start?, end?)ES3 | 非修改
返回一个新的数组,包含接收者的元素,其索引在(包括)
start和(不包括)end之间。> ['a', 'b', 'c', 'd'].slice(1, 3) [ 'b', 'c' ] > ['a', 'b'].slice() // shallow copy [ 'a', 'b' ].slice()支持 负索引:> ['a', 'b', 'c'].slice(-2) [ 'b', 'c' ]它可以用作(浅度)复制数组:
const copy = original.slice(); -
Array.prototype.splice(start?, deleteCount?, ...items)ES3 | 修改
-
在索引
start, -
移除
deleteCount个元素(默认:所有剩余元素), -
将它们替换为
items。 -
它返回被删除的元素。
-
此方法的非破坏性版本是
.toSpliced()。
> const arr = ['a', 'b', 'c', 'd']; > arr.splice(1, 2, 'x', 'y') [ 'b', 'c' ] > arr [ 'a', 'x', 'y', 'd' ]如果缺少
deleteCount,.splice()会删除直到数组末尾:> const arr = ['a', 'b', 'c', 'd']; > arr.splice(2) [ 'c', 'd' ] > arr [ 'a', 'b' ]start可以是 负数:> ['a', 'b', 'c'].splice(-2) [ 'b', 'c' ] -
-
Array.prototype.toSpliced(start?, deleteCount?, ...items)ES2023 | 非修改
-
创建一个新的数组,从索引
start开始,deleteCount个元素被items替换。 -
如果
deleteCount缺失,则删除从start到末尾的所有元素。 -
此方法的破坏性版本是
.splice()。
> const arr = ['a', 'b', 'c', 'd']; > arr.toSpliced(1, 2, 'x', 'y') [ 'a', 'x', 'y', 'd' ]start可以是 负数:> ['a', 'b', 'c'].toSpliced(-2) [ 'a' ] -
-
Array.prototype.fill(start=0, end=this.length)ES6 | 修改
-
返回
this。 -
将
value赋值给从(包括)start到(不包括)end之间的每个索引。
> [0, 1, 2].fill('a') [ 'a', 'a', 'a' ]注意:不要使用此方法用对象
obj填充数组;然后每个元素都将引用相同的值(共享它)。在这种情况下,最好 使用Array.from()。 -
-
Array.prototype.copyWithin(target, start, end=this.length)ES6 | 修改
- 返回
this。
将索引范围从(包括)
start到(不包括)end的元素复制到以target开始的索引。正确处理重叠。> ['a', 'b', 'c', 'd'].copyWithin(0, 2, 4) [ 'c', 'd', 'c', 'd' ]start或end可以是 负数。 - 返回
34.18.7 Array.prototype.*: 搜索元素
-
Array.prototype.includes(searchElement, fromIndex)ES2016 | 非修改
如果接收器有一个元素的值是
searchElement,则返回true,否则返回false。搜索从索引fromIndex开始。> [0, 1, 2].includes(1) true > [0, 1, 2].includes(5) false -
Array.prototype.indexOf(searchElement, fromIndex)ES5 | 非修改
返回第一个严格等于
searchElement的元素的索引。如果没有这样的元素,则返回-1。从索引fromIndex开始搜索,然后访问更高的索引。> ['a', 'b', 'a'].indexOf('a') 0 > ['a', 'b', 'a'].indexOf('a', 1) 2 > ['a', 'b', 'a'].indexOf('c') -1 -
Array.prototype.lastIndexOf(searchElement, fromIndex)ES5 | 非修改
返回最后一个严格等于
searchElement的元素的索引。如果没有这样的元素,则返回-1。从索引fromIndex开始搜索,然后访问较低的索引。> ['a', 'b', 'a'].lastIndexOf('a') 2 > ['a', 'b', 'a'].lastIndexOf('a', 1) 0 > ['a', 'b', 'a'].lastIndexOf('c') -1 -
Array.prototype.find(predicate, thisArg?)ES6 | 非修改
Array<T>.prototype.find( predicate: (value: T, index: number, obj: Array<T>) => boolean, thisArg?: any ): T | undefined-
从数组的开始到末尾遍历。
-
返回第一个使
predicate返回真值的元素的值。 -
如果没有这样的元素,则返回
undefined。
> [-1, 2, -3].find(x => x < 0) -1 > [1, 2, 3].find(x => x < 0) undefined -
-
Array.prototype.findLast(predicate, thisArg?)ES2023 | 非修改
Array<T>.prototype.findLast( predicate: (value: T, index: number, obj: Array<T>) => boolean, thisArg?: any ): T | undefined-
从数组的末尾开始遍历数组。
-
返回第一个使
predicate返回真值的元素的值。 -
如果没有这样的元素,则返回
undefined。
> [-1, 2, -3].findLast(x => x < 0) -3 > [1, 2, 3].findLast(x => x < 0) undefined -
-
Array.prototype.findIndex(predicate, thisArg?)ES6 | 非修改
Array<T>.prototype.findIndex( predicate: (value: T, index: number, obj: Array<T>) => boolean, thisArg?: any ): number-
从数组的开始到末尾遍历。
-
返回第一个使
predicate返回真值的元素的索引。 -
如果没有这样的元素,则返回
-1。
> [-1, 2, -3].findIndex(x => x < 0) 0 > [1, 2, 3].findIndex(x => x < 0) -1 -
-
Array.prototype.findLastIndex(predicate, thisArg?)ES2023 | 非修改
Array<T>.prototype.findLastIndex( predicate: (value: T, index: number, obj: Array<T>) => boolean, thisArg?: any ): number-
从数组的末尾开始遍历。
-
返回第一个使
predicate返回真值的元素的索引。 -
如果没有这样的元素,则返回
-1。
> [-1, 2, -3].findLastIndex(x => x < 0) 2 > [1, 2, 3].findLastIndex(x => x < 0) -1 -
34.18.8 Array.prototype.*: 过滤和映射
-
Array.prototype.filter(predicate, thisArg?)ES5 | 非修改
Array<T>.prototype.filter( predicate: (value: T, index: number, array: Array<T>) => boolean, thisArg?: any ): Array<T>返回一个只包含那些使
predicate返回真值的元素的数组。> [1, -2, 3].filter(x => x > 0) [ 1, 3 ] -
Array.prototype.map(callback, thisArg?)ES5 | 非修改
Array<T>.prototype.map<U>( mapFunc: (value: T, index: number, array: Array<T>) => U, thisArg?: any ): Array<U>返回一个新的数组,其中每个元素都是将
mapFunc应用到接收器相应元素的结果。> [1, 2, 3].map(x => x * 2) [ 2, 4, 6 ] > ['a', 'b', 'c'].map((x, i) => i) [ 0, 1, 2 ] -
Array.prototype.flatMap(callback, thisArg?)ES2019 | 非修改
Array<T>.prototype.flatMap<U>( callback: (value: T, index: number, array: Array<T>) => U|Array<U>, thisValue?: any ): Array<U>结果是通过为原始数组的每个元素调用
callback()并连接它返回的数组来生成的。> ['a', 'b', 'c'].flatMap(x => [x,x]) [ 'a', 'a', 'b', 'b', 'c', 'c' ] > ['a', 'b', 'c'].flatMap(x => [x]) [ 'a', 'b', 'c' ] > ['a', 'b', 'c'].flatMap(x => []) [] -
Array.prototype.flat(depth = 1)ES2019 | 非修改
“扁平化”一个数组:它进入输入数组内部嵌套的数组,并创建一个副本,其中所有在
depth级别或以下找到的值都移动到顶层。> [ 1,2, [3,4], [[5,6]] ].flat(0) // no change [ 1, 2, [3,4], [[5,6]] ] > [ 1,2, [3,4], [[5,6]] ].flat(1) [1, 2, 3, 4, [5,6]] > [ 1,2, [3,4], [[5,6]] ].flat(2) [1, 2, 3, 4, 5, 6]
34.18.9 Array.prototype.*: 计算摘要
-
Array.prototype.every(predicate, thisArg?)ES5 | 非修改
Array<T>.prototype.every( predicate: (value: T, index: number, array: Array<T>) => boolean, thisArg?: any ): boolean如果
predicate对每个元素都返回一个真值,则返回true。否则,返回false:> [1, 2, 3].every(x => x > 0) true > [1, -2, 3].every(x => x > 0) false-
如果
predicate返回一个假值,则停止遍历数组(因为结果保证为false)。 -
对应于数学中的全称量化(“所有”,∀)。
-
相关方法:
.some()(“存在”)。
-
-
Array.prototype.some(predicate, thisArg?)ES5 | 非修改
Array<T>.prototype.some( predicate: (value: T, index: number, array: Array<T>) => boolean, thisArg?: any ): boolean如果
predicate对至少一个元素返回一个真值,则返回true。否则,返回false。> [1, 2, 3].some(x => x < 0) false > [1, -2, 3].some(x => x < 0) true-
如果
predicate返回一个真值,则停止遍历数组(因为结果保证为true)。 -
对应于存在量化(“存在”,∃)在数学中。
-
相关方法:
.every()(“所有”)。
-
-
Array.prototype.reduce(callback, initialValue?)ES5 | 非修改
Array<T>.prototype.reduce<U>( callback: (accumulator: U, elem: T, idx: number, arr: Array<T>) => U, initialValue?: U ): U此方法生成接收器的摘要:它将所有数组元素传递给
callback,该callback将当前摘要(在参数accumulator中)与当前数组元素结合,并返回下一个accumulator:const accumulator_0 = callback(initialValue, arr[0]); const accumulator_1 = callback(accumulator_0, arr[1]); const accumulator_2 = callback(accumulator_1, arr[2]); // Etc..reduce()的结果是callback访问所有数组元素后的最后一个结果。> [1, 2, 3].reduce((accu, x) => accu + x, 0) 6 > [1, 2, 3].reduce((accu, x) => accu + String(x), '') '123'如果没有提供
initialValue,则使用索引 0 的数组元素,并首先访问索引 1 的元素。因此,数组必须至少有长度 1。 -
Array.prototype.reduceRight(callback, initialValue?)ES5 | 非修改
Array<T>.prototype.reduceRight<U>( callback: (accumulator: U, elem: T, idx: number, arr: Array<T>) => U, initialValue?: U ): U与
.reduce()类似,但按反向顺序遍历数组元素,从最后一个元素开始。> [1, 2, 3].reduceRight((accu, x) => accu + String(x), '') '321'
34.18.10 Array.prototype.*: 转换为字符串
-
Array.prototype.join(separator = ',')ES1 | 非修改
通过将所有元素的字符串表示连接起来,并用
separator分隔它们来创建一个字符串。> ['a', 'b', 'c'].join('##') 'a##b##c' > ['a', 'b', 'c'].join() 'a,b,c' -
Array.prototype.toString()ES1 | 非修改
通过
String()将所有元素转换为字符串,在用逗号分隔的同时连接它们,并返回结果。> [1, 2, 3].toString() '1,2,3' > ['1', '2', '3'].toString() '1,2,3' > [].toString() '' -
Array.prototype.toLocaleString()ES3 | 非修改
与
.toString()类似,但在将元素通过逗号分隔并连接成单个字符串之前,通过.toLocaleString()将其元素转换为字符串(而不是通过.toString())。
34.18.11 Array.prototype.*: 排序和反转
-
Array.prototype.sort(compareFunc?)ES1 | 破坏性
Array<T>.prototype.sort( compareFunc?: (a: T, b: T) => number ): this-
对接收者进行排序并返回它。
-
此方法的非破坏性版本是
.toSorted()。 -
按字典顺序对元素的字符串表示进行排序。
排序数字:
// Default: lexicographical sorting assert.deepEqual( [200, 3, 10].sort(), [10, 200, 3] ); // Ascending numerical sorting (“from a to z”) assert.deepEqual( [200, 3, 10].sort((a, z) => a - z), [3, 10, 200] );排序字符串:默认情况下,字符串按代码单元值(字符码)排序,例如,所有无重音的大写字母都排在所有无重音的小写字母之前:
> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort() [ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]对于人类语言,我们可以使用
Intl.Collator:const arr = ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair']; assert.deepEqual( arr.sort(new Intl.Collator('en').compare), ['cookie', 'Cookie', 'éclair', 'Éclair', 'pie', 'Pie'] ); -
-
Array.prototype.toSorted(compareFunc?)ES2023 | 非破坏性
Array<T>.prototype.toSorted.toSorted( compareFunc?: (a: T, b: T) => number ): Array<T>-
返回当前数组的排序副本。
-
此方法的破坏性版本是
.sort()。
const original = ['y', 'z', 'x']; const sorted = original.toSorted(); assert.deepEqual( // The original is unchanged original, ['y', 'z', 'x'] ); assert.deepEqual( // The copy is sorted sorted, ['x', 'y', 'z'] );有关如何使用此方法的更多信息,请参阅
.sort()的描述。 -
-
Array.prototype.reverse()ES1 | 破坏性
将接收者的元素重新排列,使它们按逆序排列,然后返回接收者。
> const arr = ['a', 'b', 'c']; > arr.reverse() [ 'c', 'b', 'a' ] > arr [ 'c', 'b', 'a' ]此方法的非破坏性版本是
.toReversed()。 -
Array.prototype.toReversed()ES2023 | 非破坏性
-
返回当前数组的反转副本。
-
此方法的破坏性版本是
.reverse()。
const original = ['x', 'y', 'z']; const reversed = original.toReversed(); assert.deepEqual( // The original is unchanged original, ['x', 'y', 'z'] ); assert.deepEqual( // The copy is reversed reversed, ['z', 'y', 'x'] ); -
34.18.12 快速参考的来源


浙公网安备 33010602011771号