探索-JavaScript-ES2025-版--八-

探索 JavaScript(ES2025 版)(八)

原文:exploringjs.com/js/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

VII 集合

原文:exploringjs.com/js/book/pt_collections.html

32 同步迭代 ES6

原文:exploringjs.com/js/book/ch_sync-iteration.html

  1. 32.1 同步迭代是什么?

  2. 32.2 核心迭代角色:可迭代对象和迭代器

  3. 32.3 遍历数据

    1. 32.3.1 手动遍历数据

    2. 32.3.2 使用 while 手动迭代

    3. 32.3.3 通过 Iterator.from() 获取迭代器 (ES2024)

    4. 32.3.4 通过基于迭代的语言结构进行迭代

    5. 32.3.5 将可迭代对象转换为数组:[...i]Array.from(i)

  4. 32.4 通过生成器处理可迭代对象

  5. 32.5 迭代 API 的继承(高级)

    1. 32.5.1 数组迭代器

    2. 32.5.2 生成器对象

  6. 32.6 可迭代迭代器

    1. 32.6.1 为什么内置迭代器是可迭代的?

    2. 32.6.2 当请求迭代器时,迭代器返回自身

    3. 32.6.3 迭代怪癖:两种可迭代对象

  7. 32.7 Iterator 类和迭代器辅助方法 (ES2025)

    1. 32.7.1 Iterator.prototype.* 方法

    2. 32.7.2 迭代器辅助方法的好处

    3. 32.7.3 Iterator.from():创建 API 迭代器

    4. 32.7.4 迭代器方法改变我们使用迭代的方式

    5. 32.7.5 将遗留的可迭代对象升级到 Iterator API

  8. 32.8 分组可迭代对象 (ES2024)

    1. 32.8.1 在 Map.groupBy()Object.groupBy() 之间选择

    2. 32.8.2 示例:处理情况

    3. 32.8.3 示例:按属性值分组

  9. 32.9 快速参考:同步迭代

    1. 32.9.1 同步迭代:数据生产者

    2. 32.9.2 同步迭代:数据消费者

  10. 32.10 快速参考:类 Iterator (ES2025)

    1. 32.10.1 创建迭代器

    2. 32.10.2 Iterator.*

    3. 32.10.3 Iterator.prototype.*:将索引传递给回调的方法

    4. 32.10.4 Iterator.prototype.*:返回迭代器的方法

    5. 32.10.5 Iterator.prototype.*: 返回布尔值的方法

    6. 32.10.6 Iterator.prototype.*: 返回其他类型值的方法

    7. 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:迭代有两个主要接口:IterableIterator。前者有一个返回后者的方法。

这些是迭代协议接口的类型定义(以 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
  • 值不是直接返回的,而是被包裹在具有两个属性的对象中:

    • .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-ofArray.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 迭代怪癖:两种类型的可迭代对象

可惜,可迭代迭代器意味着存在两种类型的可迭代对象:

  1. 一个可迭代迭代器是一次性可迭代的:当调用[Symbol.iterator]()时,它总是返回相同的迭代器(自身)(迭代继续)。

  2. 一个普通的可迭代对象(一个数组、一个集合等)是多次可迭代的:它总是返回一个新的迭代器(迭代重新开始)。

对于一次性可迭代,每次迭代都会移除更多元素,直到最终没有更多元素为止:

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 利益:支持迭代的数组的更多操作

使用迭代器辅助方法,任何支持迭代的数结构自动获得功能。

例如,集合不支持 filtermap 操作,但我们可以通过迭代器方法获得它们:

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的实例)。然而,并非所有库和用户代码中的可迭代对象都是这样。

术语:

  • 支持Iterator API 的实体: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-ofArray.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) ES2025

    Iterator<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>
    
    

    此方法返回一个迭代器,其值是 iteratorfilterFn 返回 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>
    
    

    此方法返回一个迭代器,其值是应用 mapFniterator 的值的结果的迭代器或可迭代对象。

    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>
    
    

    此方法返回一个迭代器,其值是应用 mapFniterator 的值的结果。

    assert.deepEqual(
      Iterator.from(['a', 'b', 'c', 'd']).map(x => x + x).toArray(),
      ['aa', 'bb', 'cc', 'dd']
    );
    
    
  • Iterator.prototype.take(limit) ES2025

    Iterator<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
    
    

    如果 fniterator 的每个值都返回 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
    
    

    如果 fniterator 的至少一个值返回 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
    
    

    此方法返回 iteratorfn 返回 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
    
    

    此方法使用函数 reduceriterator 的值合并成一个单一值。

    示例 - 连接迭代器的字符串:

    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() ES2025

    Iterator<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(高级)

原文:exploringjs.com/js/book/ch_sync-generators.html

  1. 33.1 什么是同步生成器?

    1. 33.1.1 生成器函数返回迭代器并通过 yield 填充它们

    2. 33.1.2 yield 暂停生成器函数

    3. 33.1.3 为什么 yield 会暂停执行?

    4. 33.1.4 示例:遍历可迭代对象

  2. 33.2 从生成器中调用生成器(高级)

    1. 33.2.1 通过 yield* 调用生成器

    2. 33.2.2 示例:遍历树

  3. 33.3 生成器的用例:重用遍历

    1. 33.3.1 背景:外部迭代与内部迭代

    2. 33.3.2 要重用的遍历

    3. 33.3.3 示例:内部迭代(推送)

    4. 33.3.4 外部迭代(拉取)

  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 关键字和一个星号的组合。

  • 方法:* 是一个修饰符(类似于 staticget)。

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(),我们首先必须创建迭代器/可迭代对象 itergenFunc2() 现在暂停在其主体“之前”。

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(),执行继续到我们之前暂停的地方。一旦我们遇到第二个 yieldgenFunc2() 暂停,.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() 的结果属性 .donetrue,这意味着迭代器已结束。

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

练习图标“exercise” 练习:遍历嵌套数组

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 数组(数组)

原文:exploringjs.com/js/book/ch_arrays.html

  1. 34.1 速查表:数组

    1. 34.1.1 使用数组

    2. 34.1.2 最常用的数组方法

  2. 34.2 使用数组的方式:固定布局与序列

  3. 34.3 基本数组操作

    1. 34.3.1 创建、读取、写入数组

    2. 34.3.2 数组的 .length 属性

    3. 34.3.3 通过负索引引用元素

    4. 34.3.4 清除数组

    5. 34.3.5 展开到数组字面量中

    6. 34.3.6 列出数组的索引和条目

    7. 34.3.7 检查值是否为数组:Array.isArray()

  4. 34.4 for-of 和数组

    1. 34.4.1 for-of: 遍历元素

    2. 34.4.2 for-of: 遍历索引

    3. 34.4.3 for-of: 遍历 [索引, 元素] 对

  5. 34.5 类似数组的对象

  6. 34.6 将可迭代对象、迭代器和类似数组值转换为数组

    1. 34.6.1 通过展开 (...) 将可迭代对象转换为数组

    2. 34.6.2 通过 .toArray() 将迭代器转换为数组(ES2025)

    3. 34.6.3 通过 Array.from() 将可迭代对象和类似数组对象转换为数组

  7. 34.7 复制数组

  8. 34.8 创建和填充任意长度的数组

    1. 34.8.1 创建数组并在之后添加元素

    2. 34.8.2 创建填充原始值的数组

    3. 34.8.3 创建填充对象的数组

    4. 34.8.4 使用整数范围创建数组

    5. 34.8.5 如果元素都是整数或浮点数,则类型化数组工作得很好

  9. 34.9 多维数组

  10. 34.10 数组实际上是字典(高级)

    1. 34.10.1 数组索引是(略微特殊的)属性键

    2. 34.10.2 数组可以有空洞

  11. 34.11 破坏性操作与非破坏性数组操作

    1. 34.11.1 如何使破坏性数组方法变为非破坏性

    2. 34.11.2 .reverse(), .sort(), .splice() 的非破坏性版本(ES2023)

  12. 34.12 在数组的两端添加和移除元素

    1. 34.12.1 在数组的两端破坏性地添加和移除元素

    2. 34.12.2 非破坏性地预加和尾加元素

  13. 34.13 接受元素回调的数组方法

  14. 34.14 使用元素回调进行转换:.map(), .filter(), .flatMap()

    1. 34.14.1 .map(): 每个输出元素都由其输入元素派生

    2. 34.14.2 .filter(): 只保留一些元素

    3. 34.14.3 .flatMap(): 每个输入元素产生零个或多个输出元素^(ES2019)

  15. 34.15 .reduce(): 为数组计算摘要

    1. 34.15.1 .reduce()工作概述

    2. 34.15.2 如果我们省略init会发生什么?

    3. 34.15.3 .reduceRight(): .reduce()的从尾到头的版本

  16. 34.16 .sort(): 排序数组

    1. 34.16.1 自定义排序顺序

    2. 34.16.2 排序数字

    3. 34.16.3 排序人类语言字符串

    4. 34.16.4 排序对象

  17. 34.17 分组数组元素

  18. 34.18 快速参考:Array

    1. 34.18.1 new Array()

    2. 34.18.2 Array.*

    3. 34.18.3 Array.prototype.*: 获取、设置和访问单个元素

    4. 34.18.4 Array.prototype.*: 键和值

    5. 34.18.5 Array.prototype.*: 在数组的两端破坏性地添加或移除元素

    6. 34.18.6 Array.prototype.*: 合并、提取和更改元素序列

    7. 34.18.7 Array.prototype.*: 搜索元素

    8. 34.18.8 Array.prototype.*: 过滤和映射

    9. 34.18.9 Array.prototype.*: 计算摘要

    10. 34.18.10 Array.prototype.*: 转换为字符串

    11. 34.18.11 Array.prototype.*: 排序和反转

    12. 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']
);

练习图标“exercise”练习:通过 .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 指定了 mapFuncthis

  • 它将 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']);

图标“提示”提示:记住 pushpopshiftunshift 的功能

我的建议是专注于记住以下两个方法:

  • .push() 是这四种方法中最常用的。一个常见的用例是组装输出数组:我们首先将索引 0 的元素推入;然后是索引 1 的元素;等等。

  • .shift() 可以用来消耗数组的元素:第一次 shift,我们得到索引 0 的元素;然后是索引 1 的元素;等等。

剩下的两个方法 popunshift 是这两个方法的逆操作。

图标“练习”练习:通过数组实现队列

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;
}

Icon “exercise”练习:通过 .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;
}

Icon “exercise”练习:通过 .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;
}

Icon “exercise”练习:.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.

  • initaccumulator 提供初始值。

  • 累加器包含减法操作的初步结果。当回调函数被调用时,它将累加器与当前数组元素结合并返回结果。该结果成为下一个累加器。

  • .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() 没有返回值,并抛出异常。

以下交互演示了带有和没有 initaccumulator 的初始值:

> ['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 ]

  • 这次,我们称参数为 az,因为这可以提供一个助记符:回调函数按升序排序,“从 az” (a - z)。

  • 这种技巧的缺点是,如果比较一个大的正数和一个大的负数,我们可能会得到算术溢出。

34.16.3 按人类语言字符串排序

当排序人类语言字符串时,我们需要意识到它们是根据它们的代码单元值(字符码)进行比较的:

> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
[ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]

所有无重音的大写字母都排在所有无重音的小写字母之前,而所有带重音的字母都排在无重音字母之前。如果我们想要为人类语言进行适当的排序,可以使用 IntlJavaScript 国际化 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?) ES6

    Array.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) ES6

    Array.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 1
    
    

    for-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' ]
    
    

    startend 可以是 负数。

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 快速参考的来源

posted @ 2025-12-12 18:01  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报