ECMAScript 2023:為JavaScript帶來新的數(shù)組復制方法

大廠技術 高級前端 Node進階
點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
ECMAScript 2023 規(guī)范最近已經(jīng)定稿,其中提出的 Array 對象新方法將為 JavaScript 帶來更好的可預測性和可維護性。toSorted、toReversed、toSpliced 和 with 方法允許用戶在不更改數(shù)據(jù)的情況下對數(shù)據(jù)執(zhí)行操作,實質是先制造副本再更改該副本。
Array 對象總是有點自我分裂。sort、reverse 和 splice 等方法會就地更改數(shù)組,concat、map 和 filter 等其他方法則是先創(chuàng)建數(shù)組副本,再對副本執(zhí)行操作。當我們通過操作讓對象產(chǎn)生變異時,則會產(chǎn)生一種副作用,導致系統(tǒng)其他位置發(fā)生意外行為。
舉例來說,當 reverse 一個數(shù)組時會發(fā)生如下情況。
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const reversed = languages.reverse();console.log(reversed);// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]console.log(languages);// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]console.log(Object.is(languages, reversed));// => true
可以看到,原始數(shù)組已經(jīng)反轉,但即使我們將反轉數(shù)組的結果分配給一個新變量,兩個變量也仍指向同一數(shù)組。
數(shù)組變異方法中一個最著名的問題,就是在 React 組件中使用時的異常。我們無法變異數(shù)組,之后嘗試將其設置為新狀態(tài),因為數(shù)組本身是同一個對象且不會觸發(fā)新的渲染。相反,我們需要先復制該數(shù)組,然后改變副本再將其設置為新狀態(tài)。因此,React 文檔專門有一整頁解釋了如何更新狀態(tài)數(shù)組。
解決這個問題的方法,是先復制數(shù)組,之后再執(zhí)行變異。我們可以通過幾種不同方法來生成數(shù)組副本,包括:Array.from,展開運算符,或者調用不帶參數(shù)的 slice 函數(shù)。
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const reversed = Array.from(languages).reverse();// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]console.log(languages);// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]console.log(Object.is(languages, reversed));// => false
有辦法能解決當然很好,總之請千萬注意不同復制操作間是有區(qū)別的。
此次公布的新方法正是為此而生。toSorted、toReversed、toSpliced 和 with 都能復制原始數(shù)組、變更副本再返回結果。如此一來,每項操作都更易于編寫,開發(fā)者只需調用一個函數(shù)即可,代碼閱讀起來也更容易、不必預先考慮到底要用具體哪種數(shù)組復制方法。下面,我們來看這幾種新方法的區(qū)別。
其中 toSorted 函數(shù)會返回一個新的、經(jīng)過排序的數(shù)組。
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const sorted = languages.toSorted();console.log(sorted);// => [ 'CoffeeScript', 'JavaScript', 'TypeScript' ]console.log(languages);// => [ 'JavaScript', 'TypeScript', 'CoffeeScript' ]
除了復制之外,sort 函數(shù)還會引發(fā)一些意想不到的行為,toSorted 也繼承了這種特點。所以在對帶有重音字符的數(shù)字或字符串進行排序時,大家仍然要小心。比如準備一個 comparator 比較器函數(shù)(例如 String's localeCompare)來生成當前查找的結果。
const numbers = [5, 3, 10, 7, 1];const sorted = numbers.toSorted();console.log(sorted);// => [ 1, 10, 3, 5, 7 ]const sortedCorrectly = numbers.toSorted((a, b) => a - b);console.log(sortedCorrectly);// => [ 1, 3, 5, 7, 10 ]
const strings = ["abc", "?bc", "def"];const sorted = strings.toSorted();console.log(sorted);// => [ 'abc', 'def', '?bc' ]const sortedCorrectly = strings.toSorted((a, b) => a.localeCompare(b));console.log(sortedCorrectly);// => [ 'abc', '?bc', 'def' ]
使用 toReversed 函數(shù),會返回一個按相反順序排序的新數(shù)組。
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const reversed = languages.toReversed();console.log(reversed);// => [ 'CoffeeScript', 'TypeScript', 'JavaScript' ]
toSpliced 函數(shù)與原始版本的 splice 略有不同。splice 是在提供的索引處刪除和添加元素來更改現(xiàn)有數(shù)組,再返回一個包含數(shù)組中所刪除元素的數(shù)組。toSpliced 則直接返回一個新數(shù)組,其中不含被刪除的元素,且包含所添加的元素。其工作方式如下:
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const spliced = languages.toSpliced(2, 1, "Dart", "WebAssembly");console.log(spliced);// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]
如果我們使用 splice 作為返回值,那么 toSpliced 就不能直接作為替代使用。換言之,如果大家想在不改變原始數(shù)組的情況下知曉被刪除的元素是什么,就應使用 slice 復制方法。
更麻煩的是,splice 和 slice 使用的參數(shù)也有不同。splice 使用的是一個索引加該索引之后待刪除的元素數(shù)量;slice 則使用兩個索引,分別對應開始和結束。如果要使用 toSpliced 代替 splice,但又想獲取被刪除的元素,則可對原始數(shù)組應用 toSpliced 和 slice,如下所示:
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const startDeletingAt = 2;const deleteCount = 1;const spliced = languages.toSpliced(startDeletingAt, deleteCount, "Dart", "WebAssembly");const removed = languages.slice(startDeletingAt, startDeletingAt + deleteCount);console.log(spliced);// => [ 'JavaScript', 'TypeScript', 'Dart', 'WebAssembly' ]console.log(removed);// => [ 'CoffeeScript' ]
with 函數(shù)所代表的復制方法,等同于使用方括號表示方來更改數(shù)組內的一個元素。因此,與其通過以下方式直接更改數(shù)組:
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];languages[2] = "WebAssembly";console.log(languages);// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]
可以復制該數(shù)組再執(zhí)行更改:
const languages = ["JavaScript", "TypeScript", "CoffeeScript"];const updated = languages.with(2, "WebAssembly");console.log(updated);// => [ 'JavaScript', 'TypeScript', 'WebAssembly' ]console.log(languages);// => [ 'JavaScript', 'TypeScript', CoffeeScript' ]
此次發(fā)布的新方法不僅適用于常規(guī)的數(shù)組對象。您可以在任意 TypedArray 上使用 toSorted、toReversed 和 with 方法,包括 Int8Array 到 BigUint64Array 等各種類型。但因為 TypedArrays 沒有 splice 方法,因此無法使用 toSpliced 方法。
前文提到,map、filter 和 concat 等方法也都采取先復制再更改的思路,但這些方法與新的復制方法間仍有不同。如果對內置的 Array 對象進行擴展,并在實例上使用 map、flatMap、filter 或 concat,則會返回相同類型的新實例。但如果您擴展一個 Array 并使用 toSorted、toReversed、toSpliced 或者 with,則返回的仍是普通 Array。
class MyArray extends Array {}const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");const upcase = languages.map(language => language.toUpperCase());console.log(upcase instanceof MyArray);// => trueconst reversed = languages.toReversed();console.log(reversed instanceof MyArray);// => false
可以使用 MyArray.from 將其轉回您的自定義 Array:
class MyArray extends Array {}const languages = new MyArray("JavaScript", "TypeScript", "CoffeeScript");const reversed = MyArray.from(languages.toReversed());console.log(reversed instance of MyArray);// => true
雖然 ECMAScript 2023 的規(guī)范剛剛成形,但已經(jīng)為本文提到的新數(shù)組方法提供了良好支持。Chrome 110、Safari 16.3、Node.js 20 和 Deno1.31 都支持這四種新方法,尚不支持的平臺也有 polyfills 和 shims 作為過渡方案。
很高興看到 ECMAScript 標準新增了這么多有意義的內容,讓我們能輕松編寫出可預測性更好的代碼。其他一些提案也已被納入 ES2023,感興趣的朋友可以移步此處:https://github.com/tc39/proposals/blob/HEAD/finished-proposals.md
至于未來的規(guī)范發(fā)展方向,推薦大家參考整個 TC39 提案庫:https://github.com/tc39/proposals
Array.prototype.findLast 和 Array.prototype.findLastIndex
let nums = [5,4,3,2,1];let lastEven = nums.findLast((num) => num % 2 === 0); // 2let lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0); // 3
#! for JS
此腳本的第一行以 #!開頭,表示可在注釋中包含任意文本。
// in the Script Goal;console.log(1);
在弱集合和注冊表中使用符號
注意:注冊的符號不可作為 weakmap 鍵。
let sym = Symbol("foo");let obj = {name: "bar"};let wm = new WeakMap();wm.set(sym, obj);console.log(wm.get(sym)); // {name: "bar"}
sym = Symbol("foo");let ws = new WeakSet();ws.add(sym);console.log(ws.has(sym)); // true
sym = Symbol("foo");let wr = new WeakRef(sym);console.log(wr.deref()); // Symbol(foo)
sym = Symbol("foo");let cb = (value) => {console.log("Finalized:", value);};let fr = new FinalizationRegistry(cb);obj = {name: "bar"};fr.register(obj, "bar", sym);fr.unregister(sym);
返回更改后的 Array 和 TypeArray 副本。
注意:類型數(shù)組不可 tospliced。
const greek = ['gamma', 'aplha', 'beta']greek.toSorted(); // [ 'aplha', 'beta', 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]const nums = [0, -1, 3, 2, 4]nums.toSorted((n1, n2) => n1 - n2); // [-1,0,2,3,4]nums; // [0, -1, 3, 2, 4]
const greek = ['gamma', 'aplha', 'beta']greek.toReversed(); // [ 'beta', 'aplha', 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]
const greek = ['gamma', 'aplha', 'beta']greek..toSpliced(1,2); // [ 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]greek.toSpliced(1,2, ...['delta']); // [ 'gamma', 'delta' ]greek; // [ 'gamma', 'aplha', 'beta' ]
const greek = ['gamma', 'aplha', 'beta']greek..toSpliced(1,2); // [ 'gamma' ]greek; // [ 'gamma', 'aplha', 'beta' ]greek.toSpliced(1,2, ...['delta']); // [ 'gamma', 'delta' ]greek; // [ 'gamma', 'aplha', 'beta' ]
const greek = ['gamma', 'aplha', 'beta'];greek.with(2, 'bravo'); // [ 'gamma', 'aplha', 'bravo' ]greek; // ['gamma', 'aplha', 'beta'];
參考鏈接:https://h3manth.com/ES2023/
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一下
