ECMAScript 2023將新增的9個數組方法

語言: CN / TW / HK

大家好,我是 CUGGZ。

ECMAScript 規範每年都會更新一次,正式標準化 JavaScript 語言的 ECMAScript 的下一次年度更新將在 2023 年 6 月左右獲得批准,這將是 ECMAScript 的第 14 版。所有在 2023 年 3 月之前達到階段 4 的提案都將包含在 ECMAScript 2023 標準中。 對於一個提案,從提出到最後被納入 ECMAScript 標準,總共分為五步:

  • stage0(strawman):任何TC39的成員都可以提交。
  • stage1(proposal):進入此階段就意味著這一提案被認為是正式的了,需要對此提案的場景與API進行詳盡的描述。
  • stage2(draft):演進到這一階段的提案如果能最終進入到標準,那麼在之後的階段都不會有太大的變化,因為理論上只接受增量修改。
  • state3(candidate):這一階段的提案只有在遇到了重大問題才會修改,規範文件需要被全面的完成。
  • state4(finished):這一階段的提案將會被納入到ES每年釋出的規範之中。

根據 Erick Wendel(微軟 MVP、谷歌開發專家、@nodejs合作者)的預測,ECMAScript 2023 可能會新增以下陣列方法(:three:、:four:為所處提案階段):

  • :three: Array.prototype.toReversed()
  • :three: Array.prototype.toSorted()
  • :three: Array.prototype.toSpliced()
  • :three: Array.prototype.with()
  • :three: Array.prototype.group()
  • :three: Array.prototype.groupToMap()
  • :four: Array.prototype.findLast()
  • :four: Array.prototype.findLastIndex()
  • :three: Array.fromAsync()

下面就來看看這些方法是如何使用的吧!

1. 通過副本更改陣列

通過副本更改陣列的提案目前處於第 3 階段。該提案為陣列和型別化陣列提出了四種新的方法:

  • Array.prototype.toReversed()
  • Array.prototype.toSorted()
  • Array.prototype.toSpliced()
  • Array.prototype.with()

提案地址:https://github.com/tc39/proposal-change-array-by-copy

為什麼會有這個提案呢?我們知道,大多數的陣列方法都是非破壞性的,也就是說,在陣列執行該方法時,不會改變原陣列,比如 filter() 方法:

const arr = ['a', 'b', 'b', 'a'];
const result = arr.filter(x => x !== 'b');
console.log(result); // ['a', 'a']

當然,也有一些是破壞性的方法,它們在執行時會改變原陣列,比如 sort() 方法:

const arr = ['c', 'a', 'b'];
const result = arr.sort();
console.log(result); // ['a', 'b', 'c']

在陣列的方法中,下面的方法是具有破壞性的:

  • reverse()
  • sort()
  • splice()

如果我們想要這些陣列方法應用於陣列而不改變它,可以使用下面任意一種形式:

const sorted1 = arr.slice().sort();
const sorted2 = [...arr].sort();
const sorted3 = Array.from(arr).sort();

可以看到,我們首先需要建立陣列的副本,再對這個副本進行修改。

因此改提案就引入了這三個方法的非破壞性版本,因此不需要手動建立副本再進行操作:

  • reverse() 的非破壞性版本:toReversed()
  • sort() 非破壞性版本:toSorted(compareFn)
  • splice() 非破壞性版本:toSpliced(start, deleteCount, ...items)

該提案將這些函式屬性引入到 Array.prototype:

  • Array.prototype.toReversed() -> Array
  • Array.prototype.toSorted(compareFn) -> Array
  • Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
  • Array.prototype.with(index, value) -> Array

除此之外,該提案還還提出了一個新的非破壞性方法:with()。該方法會以非破壞性的方式替換給定 index 處的陣列元素,即 arr[index]=value 的非破壞性版本。

所有這些方法都將保持目標陣列不變,並返回它的副本並執行更改。這些方法適用於陣列,也適用於型別化陣列,即以下類的例項:

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BigUint64Array

TypedArray是一種通用的固定長度緩衝區型別,允許讀取緩衝區中的二進位制資料。其在WEBGL規範中被引入用於解決Javascript處理二進位制資料的問題。型別化陣列也是陣列,只不過其元素被設定為特定型別的值。

型別化陣列的核心就是一個名為 ArrayBuffer 的型別。每個ArrayBuffer物件表示的只是記憶體中指定的位元組數,但不會指定這些位元組用於儲存什麼型別的資料。通過ArrayBuffer能做的就是為了將來使用而分配一定數量的位元組。

這些提案也適用於元組,元組相當於不可變的陣列。它們擁有陣列的所有方法——除了破壞性的方法。因此,將後者的非破壞性版本新增到陣列對元組是有幫助的,這意味著我們可以使用相同的方法來非破壞性地更改陣列和元組。

(1)Array.prototype.toReversed()

toReversed() 是 reverse() 方法的非破壞性版本:

const arr = ['a', 'b', 'c'];
const result = arr.toReversed();
console.log(result); // ['c', 'b', 'a']
console.log(arr);    // ['a', 'b', 'c']

下面是 toReversed() 方法的一個簡單的 polyfill:

if (!Array.prototype.toReversed) {
  Array.prototype.toReversed = function () {
    return this.slice().reverse();
  };
}

(2)Array.prototype.toSorted()

toSorted() 是 sort() 方法的非破壞性版本:

const arr = ['c', 'a', 'b'];
const result = arr.toSorted();
console.log(result);  // ['a', 'b', 'c']
console.log(arr);     // ['c', 'a', 'b']

下面是 toSorted() 方法的一個簡單的 polyfill:

if (!Array.prototype.toSorted) {
  Array.prototype.toSorted = function (compareFn) {
    return this.slice().sort(compareFn);
  };
}

(3)Array.prototype.toSpliced()

splice() 方法比其他幾種方法都複雜,其使用形式:splice(start, deleteCount, ...items)。該方法會從從 start 索引處開始刪除 deleteCount個元素,然後在 start 索引處開始插入item 中的元素,最後返回已經刪除的元素。

toSpliced 是 splice() 方法的非破壞性版本,它會返回更新後的陣列,原陣列不會變化,並且我們無法再得到已經刪除的元素:

const arr = ['a', 'b', 'c', 'd'];
const result = arr.toSpliced(1, 2, 'X');
console.log(result); // ['a', 'X', 'd']
console.log(arr);    // ['a', 'b', 'c', 'd']

下面是 toSpliced() 方法的一個簡單的 polyfill:

if (!Array.prototype.toSpliced) {
  Array.prototype.toSpliced = function (start, deleteCount, ...items) {
    const copy = this.slice();
    copy.splice(start, deleteCount, ...items);
    return copy;
  };
}

(4)Array.prototype.with()

with()方法的使用形式:with(index, value),它是 arr[index] = value 的非破壞性版本。

const arr = ['a', 'b', 'c'];
const result = arr.with(1, 'X');
console.log(result);  // ['a', 'X', 'c']
console.log(arr);     // ['a', 'b', 'c']

下面是 with() 方法的一個簡單的 polyfill:

if (!Array.prototype.with) {
  Array.prototype.with = function (index, value) {
    const copy = this.slice();
    copy[index] = value;
    return copy;
  };
}

2. 陣列分組

(1)概述

在日常開發中,陣列分組是一種極其常見的操作。因此,proposal-array-grouping 提案就提出了兩個新的陣列方法:

  • array.group(callback, thisArg?)
  • array.groupToMap(callback, thisArg?)

提案地址:https://github.com/tc39/proposal-array-grouping

下面是這兩個方法的型別簽名:

Array<Elem>.prototype.group<GroupKey extends (string|symbol)>(
  callback: (value: Elem, index: number, array: Array<Elem>) => GroupKey,
  thisArg?: any
): {[k: GroupKey]: Array<Elem>}

Array<Elem>.prototype.groupToMap<GroupKey>(
  callback: (value: Elem, index: number, array: Array<Elem>) => GroupKey,
  thisArg?: any
): Map<GroupKey, Array<Elem>>

這兩個方法都用來對陣列進行分組:

  • 輸入:一個數組;
  • 輸出:組,每個組都有一個組key,以及一個包含組成員的陣列。

這兩個方法都會對陣列進行遍歷,它們會向其回撥請求組鍵並將元素新增到相應的組中。這兩個方法在表示組的方式上有所不同:

  • group():將組儲存在物件中:組鍵儲存為屬性鍵,組成員儲存為屬性值;
  • groupToMap():將組儲存在 Map 中:組鍵儲存為 Map 鍵,組成員儲存為 Map 值。

那這兩個方法該如何選擇呢?我們知道,JavaScript 中物件是支援解構的,如果想要使用解構來獲取陣列中的值,比如,對於上面物件,可以通過解構獲取三個不同組的值:

const {vegetables, fruit, meat} = result;

而 Map 的好處就是它的 key 不限於字串和symbol,更加自由。

(2)使用

下面來看幾個實用例子。假如執行 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} = settled.group(x => x.status);

// fulfilled 結果如下:
[
  { status: 'fulfilled', value: 'Jane' },
  { status: 'fulfilled', value: 'John' },
]

// rejected 結果如下:
[
  { status: 'rejected', reason: 'Jhon' },
  { status: 'rejected', reason: 'Jaen' },
  { status: 'rejected', reason: 'Jnoh' },
]

在這個例子中,使用 group() 的效果會更好,因為可以使用解構輕鬆獲取需要組的值。

假如想要對以下陣列中人根據國家進行分組:

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' },
];

const result = persons.groupToMap((person) => person.country);

// result 的執行結果和以下 Map 是等價的:
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' },
    ]
  ],
])

在這個例子中,groupToMap() 是更好的選擇,因為哦嗯嗯可以在Map 中使用任何型別的鍵,而在物件中,鍵值只能是字串或symbol。

(3)polyfill

下面來實現一下這兩個方法:

  • Array.prototype.group
Array.prototype.group = function (callback, thisArg) {
  const result = Object.create(null);
  for (const [index, elem] of this.entries()) {
    const groupKey = callback.call(thisArg, elem, index, this);
    if (! (groupKey in result)) {
      result[groupKey] = [];
    }
    result[groupKey].push(elem);
  }
  return result;
};
  • Array.prototype.groupToMap
Array.prototype.groupToMap = function (callback, thisArg) {
  const result = new Map();
  for (const [index, elem] of this.entries()) {
    const groupKey = callback.call(thisArg, elem, index, this);
    let group = result.get(groupKey);
    if (group === undefined) {
      group = [];
      result.set(groupKey, group);
    }
    group.push(elem);
  }
  return result;
};

3. 從尾到頭搜尋陣列

(1)概述

在 JavaScript 中,通過 find() 和 findIndex()  查詢陣列中的值是一種常見做法。不過,這些方法從陣列的開始進行遍歷:

const array = [{v: 1}, {v: 2}, {v: 3}, {v: 4}, {v: 5}];

array.find(elem => elem.v > 3); // {v: 4}
array.findIndex(elem => elem.v > 3); // 3

如果要從陣列的末尾開始遍歷,就必須反轉陣列並使用上述方法。這樣做就需要一個額外的陣列操作。幸運的是,Wenlu Wang 和 Daniel Rosenwasser 關於findLast() 和 findLastIndex() 的 ECMAScript 提案解決了這一問題。該提案的一個重要原因就是:語義。

提案地址:https://github.com/tc39/proposal-array-find-from-last

(2)使用

它們的用法和find()、findIndex()類似,唯一不同的是它們是 從後向前 遍歷陣列,這兩個方法適用於陣列和類陣列。

findLast() 會返回第一個查詢到的元素,如果沒有找到,就會返回undefined;

findLastIndex() 會返回第一個查詢到的元素的索引。如果沒有找到,就會返回 -1;

const array = [{v: 1}, {v: 2}, {v: 3}, {v: 4}, {v: 5}];

array.findLast(elem => elem.v > 3); // {v: 5}
array.findLastIndex(elem => elem.v > 3); // 4
array.findLastIndex(elem => elem.v > 5); // undefined

(3)polyfill

下面來實現一下這兩個方法:

  • Array.prototype.findLast
Array.prototype.findLast = function(arr, callback, thisArg) {
  for (let index = arr.length - 1; index >= 0; index--) {
    const value = arr[index];
    if (callback.call(thisArg, value, index, arr)) {
      return value;
    }
  }
  return undefined;
}
  • Array.prototype.findLastIndex
Array.prototype.findLastIndex = function(arr, callback, thisArg) {
  for (let index = arr.length - 1; index >= 0; index--) {
    const value = arr[index];
    if (callback.call(thisArg, value, index, arr)) {
      return index;
    }
  }
  return -1;
}

(4)參考原始碼

lodash 中也提供了類似方法,下面是相關原始碼:

  • findLast()
import findLastIndex from './findLastIndex.js'
import isArrayLike from './isArrayLike.js'

/**
 * This method is like `find` except that it iterates over elements of
 * `collection` from right to left.
 *
 * @since 2.0.0
 * @category Collection
 * @param {Array|Object} collection The collection to inspect.
 * @param {Function} predicate The function invoked per iteration.
 * @param {number} [fromIndex=collection.length-1] The index to search from.
 * @returns {*} Returns the matched element, else `undefined`.
 * @see find, findIndex, findKey, findLastIndex, findLastKey
 * @example
 *
 * findLast([1, 2, 3, 4], n => n % 2 == 1)
 * // => 3
 */
function findLast(collection, predicate, fromIndex) {
  let iteratee
  const iterable = Object(collection)
  if (!isArrayLike(collection)) {
    collection = Object.keys(collection)
    iteratee = predicate
    predicate = (key) => iteratee(iterable[key], key, iterable)
  }
  const index = findLastIndex(collection, predicate, fromIndex)
  return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined
}

export default findLast
  • findLastIndex()
import baseFindIndex from './.internal/baseFindIndex.js'
import toInteger from './toInteger.js'

/**
 * This method is like `findIndex` except that it iterates over elements
 * of `collection` from right to left.
 *
 * @since 2.0.0
 * @category Array
 * @param {Array} array The array to inspect.
 * @param {Function} predicate The function invoked per iteration.
 * @param {number} [fromIndex=array.length-1] The index to search from.
 * @returns {number} Returns the index of the found element, else `-1`.
 * @see find, findIndex, findKey, findLast, findLastKey
 * @example
 *
 * const users = [
 *   { 'user': 'barney',  'active': true },
 *   { 'user': 'fred',    'active': false },
 *   { 'user': 'pebbles', 'active': false }
 * ]
 *
 * findLastIndex(users, ({ user }) => user == 'pebbles')
 * // => 2
 */
function findLastIndex(array, predicate, fromIndex) {
  const length = array == null ? 0 : array.length
  if (!length) {
    return -1
  }
  let index = length - 1
  if (fromIndex !== undefined) {
    index = toInteger(fromIndex)
    index = fromIndex < 0
      ? Math.max(length + index, 0)
      : Math.min(index, length - 1)
  }
  return baseFindIndex(array, predicate, index, true)
}

export default findLastIndex

4. Array.fromAsync

在 JavaScript 中內建了 Array.from 方法,它用於將類陣列或者可迭代物件生成一個新的陣列例項。在ECMAScript 2018中引入了非同步可迭代物件。而JavaScript中一直缺少直接從非同步可迭代物件生成陣列的內建方法。

proposal-array-from-async 提案中提出來的 Array.fromAsync 方法就是為了解決這個問題而提出來的。

下面來看一個簡單的例子:

async function * asyncGen (n) {
  for (let i = 0; i < n; i++)
    yield i * 2;
}

// arr 將變為 [0, 2, 4, 6]`
const arr = [];
for await (const v of asyncGen(4)) {
  arr.push(v);
}

// 與上述方式是等價的
const arr = await Array.fromAsync(asyncGen(4));

Array.fromAsync 可以將非同步迭代轉換為 promise,並將解析為新陣列。在 promise 解析之前,它將從輸入值中建立一個非同步迭代器,進行惰性的迭代,並將每個產生的值新增到新陣列中。

與其他基於 Promise 的 API 一樣,Array.fromAsync 總是會立即返回一個 promise。當 Array.fromAsync 的輸入在建立其非同步或同步迭代器時引發錯誤時,則此 promise 狀態將被置為 rejected。

提案地址:https://github.com/tc39/proposal-array-from-async