原文鏈接:css-tricks.com
第9版ECMAScript標准於2018年6月發布,正式名稱為ECMAScript 2018(簡稱ES2018)。從ES2016開始,ECMAScript規范的新版本每年發布一次,而不是每隔幾年發布一次,相應的,每版增加的功能也更少一些。最新版本的標准通過添加4個新的RegExp特性、rest/spread屬性、異步迭代和Promise.prototype.finally來延續每年的發布周期。此外,ES2018取消了標記模板轉義序列的語法限制。
以下將逐一解釋這些變動。
Rest/Spread 特性
ES2015中添加的最有趣的特性之一是spread操作符。你可以用它替換cancat()和slice()方法,使數組的操作(復制、合並)更加簡單。
const arr1 = [10, 20, 30]; // make a copy of arr1 const copy = [...arr1]; console.log(copy); // → [10, 20, 30] const arr2 = [40, 50]; // merge arr2 with arr1 const merge = [...arr1, ...arr2]; console.log(merge); // → [10, 20, 30, 40, 50]
在數組必須以拆解的方式作為函數參數的情況下,spread操作符也很有用。例如:
const arr = [10, 20, 30] // equivalent to // console.log(Math.max(10, 20, 30)); console.log(Math.max(...arr)); // → 30
ES2018通過向對象文本添加擴展屬性進一步擴展了這種語法。他可以將一個對象的屬性拷貝到另一個對象上,參考以下情形:
const obj1 = { a: 10, b: 20 }; const obj2 = { ...obj1, c: 30 }; console.log(obj2); // → {a: 10, b: 20, c: 30}
在上述代碼中,spread操作符遍歷obj1屬性,並將其添加到obj2的屬性中;而在之前的版本中,如此處理會拋出一個異常。需要注意的是,如果存在相同的屬性名,只有最后一個會生效。
const obj1 = { a: 10, b: 20 }; const obj2 = { ...obj1, a: 30 }; console.log(obj2); // → {a: 30, b: 20}
同時,Spread操作符可以作為Object.assign() 的一個替代方案進行對象融合:
const obj1 = {a: 10}; const obj2 = {b: 20}; const obj3 = {c: 30}; // ES2018 console.log({...obj1, ...obj2, ...obj3}); // → {a: 10, b: 20, c: 30} // ES2015 console.log(Object.assign({}, obj1, obj2, obj3)); // → {a: 10, b: 20, c: 30}
然而,在進行對象融合時,Spread操作結果並不總是與Object.assign()一致,例如:
Object.defineProperty(Object.prototype, 'a', { set(value) { console.log('set called!'); } }); const obj = {a: 10}; console.log({...obj}); // → {a: 10} console.log(Object.assign({}, obj)); // → set called! // → {}
在上述代碼中,Object.assign()方法繼承了setter屬性;而spread操作忽略了setter。
划重點:spread只復制枚舉屬性。在下面的例子中,type屬性不會出現在復制對象中,因為它的枚舉屬性被設置為false:
const car = { color: 'blue' }; Object.defineProperty(car, 'type', { value: 'coupe', enumerable: false }); console.log({...car}); // → {color: "blue"}
繼承的屬性即使是可枚舉的也會被忽略:
const car = { color: 'blue' }; const car2 = Object.create(car, { type: { value: 'coupe', enumerable: true, } }); console.log(car2.color); // → blue console.log(car2.hasOwnProperty('color')); // → false console.log(car2.type); // → coupe console.log(car2.hasOwnProperty('type')); // → true console.log({...car2}); // → {type: "coupe"}
在上述代碼中,car2繼承了car中的color屬性。因為spread操作只會復制對象自身的屬性,color並沒有出現在新的對象中。
spread只會進行淺拷貝,如果屬性的值是一個對象的話,只有對象的引用會被拷貝:
const obj = {x: {y: 10}}; const copy1 = {...obj}; const copy2 = {...obj}; console.log(copy1.x === copy2.x); // → true
copy1.x 和 copy2.x 指向同一個對象的引用,所以他們嚴格相等。
ES2015增加的另一個有用特性是rest參數,它允許JS使用……將值表示為數組:
const arr = [10, 20, 30]; const [x, ...rest] = arr; console.log(x); // → 10 console.log(rest); // → [20, 30]
在上述代碼中,arr中的第一項分配給x,其余元素分配給rest變量。這種模式稱為數組析構,非常流行,Ecma技術委員會決定為對象提供類似的功能:
const obj = { a: 10, b: 20, c: 30 }; const {a, ...rest} = obj; console.log(a); // → 10 console.log(rest); // → {b: 20, c: 30}
這段代碼使用析構賦值中的rest屬性將剩余的可枚舉屬性復制到一個新對象中。注意,rest屬性必須始終出現在對象的末尾,否則將拋出錯誤:
const obj = { a: 10, b: 20, c: 30 }; const {...rest, a} = obj; // → SyntaxError: Rest element must be last element
此外,在對象中使用多個rest語法會拋異常,除非它們是嵌套的:
const obj = { a: 10, b: { x: 20, y: 30, z: 40 } }; const {b: {x, ...rest1}, ...rest2} = obj; // no error const {...rest, ...rest2} = obj; // → SyntaxError: Rest element must be last element
Rest/Spread 特性支持
Chrome | Firefox | Safari | Edge |
60 | 55 | 11.1 | No |
Chrome Android |
Firefox Android |
iOS Safari |
Edge Mobile |
Samsung Internet |
Android Webview |
60 | 55 | 11.3 | No | 8.2 | 60 |
Node.js:
-
8.0.0 (需要 --harmony 運行環境)
-
8.3.0 (完全支持)
異步迭代
遍歷是編程的一個重要部分。JS提供了for、for…in和while以及map()、filter()和forEach()等遍歷數據的方法。在ES2015則引入了迭代器接口。
包含Symbol.iterator屬性的對象是可迭代對象,如字符串和集合對象(如Set、Map和Array)。如下為迭代遍歷的示例:
const arr = [10, 20, 30]; const iterator = arr[Symbol.iterator](); console.log(iterator.next()); // → {value: 10, done: false} console.log(iterator.next()); // → {value: 20, done: false} console.log(iterator.next()); // → {value: 30, done: false} console.log(iterator.next()); // → {value: undefined, done: true}
Symbol.iterator是指定返回迭代器的函數. 迭代器包含next()方法,返回包含value和done屬性的對象。其中value為下一個元素,done為布爾值,表示遍歷是否結束。
普通對象進行迭代需要定義Symbol.iterator屬性。示例如下:
const collection = { a: 10, b: 20, c: 30, [Symbol.iterator]() { const values = Object.keys(this); let i = 0; return { next: () => { return { value: this[values[i++]], done: i > values.length } } }; } }; const iterator = collection[Symbol.iterator](); console.log(iterator.next()); // → {value: 10, done: false} console.log(iterator.next()); // → {value: 20, done: false} console.log(iterator.next()); // → {value: 30, done: false} console.log(iterator.next()); // → {value: undefined, done: true}
對象的迭代器通過Object.keys()方法獲取屬性名數組,將其賦值給values常量,同時定義一個默認值為0的計數器。當迭代器開始執行時,會返回一個包含next()方法的對象。該方法會返回包含value和done的對象,value為下一迭代值,done為布爾值,表示迭代器是否到達終點。
上述實現方式還是過於復雜,可以通過generator函數簡化:
const collection = { a: 10, b: 20, c: 30, [Symbol.iterator]: function * () { for (let key in this) { yield this[key]; } } }; const iterator = collection[Symbol.iterator](); console.log(iterator.next()); // → {value: 10, done: false} console.log(iterator.next()); // → {value: 20, done: false} console.log(iterator.next()); // → {value: 30, done: false} console.log(iterator.next()); // → {value: undefined, done: true}
在該generator函數中,利用for in循環枚舉生成屬性值。結果與前面的示例完全相同,但是要短得多。
迭代器的缺點是不適合表示異步數據源。ES2018的解決方案是異步迭代器和異步迭代。異步迭代器與傳統迭代器的不同之處在於,它沒有返回{value, done}形式的普通對象,而是返回一個Promise,其resolve返回{value, done}對象。一個可異步迭代對象中包含Symbol.asyncIterator屬性(而不是Symbol.iterator),其功能為返回一個異步迭代器。
如下示例應該會使這一點更清楚:
const collection = { a: 10, b: 20, c: 30, [Symbol.asyncIterator]() { const values = Object.keys(this); let i = 0; return { next: () => { return Promise.resolve({ value: this[values[i++]], done: i > values.length }); } }; } }; const iterator = collection[Symbol.asyncIterator](); console.log(iterator.next().then(result => { console.log(result); // → {value: 10, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 20, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 30, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: undefined, done: true} }));
注意,promise+迭代器並不能代替異步迭代器。雖然一個普通的同步迭代器可以異步地確定值,但是它仍然需要同步地確定“完成”的狀態。
當然,您同樣可以使用generator函數簡化該過程,如下所示:
const collection = { a: 10, b: 20, c: 30, [Symbol.asyncIterator]: async function * () { for (let key in this) { yield this[key]; } } }; const iterator = collection[Symbol.asyncIterator](); console.log(iterator.next().then(result => { console.log(result); // → {value: 10, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 20, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: 30, done: false} })); console.log(iterator.next().then(result => { console.log(result); // → {value: undefined, done: true} }));
同樣,異步迭代執行后會返回一個包含next()方法的對象。調用next()會返回一個包含{value, done}的對象,而value值則變為一個promise對象
在可迭代對象上迭代的一個簡單方法是使用for of,但由於異步迭代對象的value和done並不是同步指定的,因此for of並不適用。基於此,ES2018提供了for await of方法。讓我們來看一個例子:
const collection = { a: 10, b: 20, c: 30, [Symbol.asyncIterator]: async function * () { for (let key in this) { yield this[key]; } } }; (async function () { for await (const x of collection) { console.log(x); } })(); // logs: // → 10 // → 20 // → 30
在本代碼中,for await of語句隱式調用了Symbol.asyncIterator方法。在每次循環時,都會調用迭代器的next()方法,該方法返回一個promise。promise對象的value屬性將被讀入x變量。循環繼續,直到返回對象的done屬性的值為true。
注意:for await of語句僅在異步生成器和異步函數中有效。違反此規則會報SyntaxError錯誤。
next()方法可能返回一個包含rejects的promise。要優雅地處理,你可以把for await of用try catch包裹,如下所示:
const collection = { [Symbol.asyncIterator]() { return { next: () => { return Promise.reject(new Error('Something went wrong.')) } }; } }; (async function() { try { for await (const value of collection) {} } catch (error) { console.log('Caught: ' + error.message); } })(); // logs: // → Caught: Something went wrong.
異步迭代器支持
Chrome | Firefox | Safari | Edge |
63 | 57 | 12 | No |
Chrome Android |
Firefox Android |
iOS Safari |
Edge Mobile |
Samsung Internet |
Android Webview |
63 | 57 | 12 | No | 8.2 | 63 |
Node.js:
-
8.0.0 (需要 --harmony\ async\ iteration標志)
-
8.3.0 (完全支持)
Promise.prototype.finally
ES2018的另一個令人興奮的新特性是finally()方法。幾個JavaScript庫以前實現過類似的方法,這在許多情況下都很有用。這鼓勵Ecma技術委員會正式將finally()添加到規范中。無論promise的結果如何,finally()方法中的代碼都會執行。讓我們看一個簡單的例子:
fetch('https://www.google.com') .then((response) => { console.log(response.status); }) .catch((error) => { console.log(error); }) .finally(() => { document.querySelector('#spinner').style.display = 'none'; });
無論操作是否成功,當您需要在操作完成后進行一些清理時,finally()方法就派上用場了。在這段代碼中,finally()方法在請求數據之后隱藏loading,無論請求是否成功。
您可以使用promise來實現相同的結果,使用then(func, func)而不是promise.finally(func),但是你必須在fulfillment handler和rejection handler中重復相同的代碼,或者為它聲明一個變量:
fetch('https://www.google.com') .then((response) => { console.log(response.status); }) .catch((error) => { console.log(error); }) .then(final, final); function final() { document.querySelector('#spinner').style.display = 'none'; }
與then()和catch()一樣,finally()方法總是返回一個promise,因此可以鏈接更多的方法。通常,您希望使用finally()作為最后一個鏈,但是在某些情況下,例如在發出HTTP請求時,最好將另一個catch()鏈接起來,以處理finally()中可能出現的錯誤。
Promise.prototype.finall支持
Chrome | Firefox | Safari | Edge |
63 | 58 | 11.1 | 18 |
Chrome Android |
Firefox Android |
iOS Safari |
Edge Mobile |
Samsung Internet |
Android Webview |
63 | 58 | 11.1 | No | 8.2 | 63 |
Node.js:
-
10.0.0 (完全支持)
新的正則表達式特性
ES2018為正則表達式添加了四個新特性,進一步提高了JavaScript的字符串處理能力。這些特點如下:
-
s (dotAll) 標志
-
命名捕獲組
-
Lookbehind 后行斷言
-
Unicode屬性轉義
s (dotAll) 標志
點(.)是正則表達式模式中的一個特殊字符,它匹配除換行符(如換行符(\n)或回車符(\r)之外的任何字符。匹配所有字符(包括換行符)的一種方法是使用一個包含兩個短字符的字符類,比如[\d\D]。這個表達式查詢數字(\d)或非數字(\D)字符。因此,它匹配任何字符:
console.log(/one[\d\D]two/.test('one\ntwo')); // → true
ES2018引入了一種模式,在這種模式中,點(.)可以用來實現相同的結果。通過在原正則表達式基礎上添加s表示,可以激活該模式:
console.log(/one.two/.test('one\ntwo')); // → false console.log(/one.two/s.test('one\ntwo')); // → true
使用標志位來定義新行為的好處是向后兼容性。因此,使用點字符的現有正則表達式模式不受影響。
命名捕獲組
在一些正則表達式模式中,使用數字進行匹配可能會令人混淆。例如,使用正則表達式/(\d{4})-(\d{2})-(\d{2})/來匹配日期。因為美式英語中的日期表示法和英式英語中的日期表示法不同,所以很難區分哪一組表示日期,哪一組表示月份:
const re = /(\d{4})-(\d{2})-(\d{2})/; const match= re.exec('2019-01-10'); console.log(match[0]); // → 2019-01-10 console.log(match[1]); // → 2019 console.log(match[2]); // → 01 console.log(match[3]); // → 10
ES2018引入了使用(?…)語法的命名捕獲組。因此,匹配日期的模式可以用一種不那么模棱兩可的方式來寫:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const match = re.exec('2019-01-10'); console.log(match.groups); // → {year: "2019", month: "01", day: "10"} console.log(match.groups.year); // → 2019 console.log(match.groups.month); // → 01 console.log(match.groups.day); // → 10
你可以在一個正則表達式中使用\k語法重復調用名稱捕獲組。例如,要在一個句子中找到連續重復的單詞,可以使用/\b(?\w+)\s+\k\b/:
const re = /\b(?<dup>\w+)\s+\k<dup>\b/; const match = re.exec('Get that that cat off the table!'); console.log(match.index); // → 4 console.log(match[0]); // → that that
要將命名捕獲組插入replace()方法的替換字符串中,需要使用$構造。例如:
const str = 'red & blue'; console.log(str.replace(/(red) & (blue)/, '$2 & $1')); // → blue & red console.log(str.replace(/(?<red>red) & (?<blue>blue)/, '$<blue> & $<red>')); // → blue & red
Lookbehind后行斷言
ES2018將lookbehind后行斷言引入JavaScript,以前JavaScript只支持前行斷言。后行斷言由(?<=…)表示,代表字符串中的一個位置,緊接該位置之前的字符序列能夠匹配pattern。例如,如果您想匹配以美元、英鎊或歐元表示的產品的價格,而不需要捕獲貨幣符號,您可以使用/(?<=\$|£|€)\d+(.\d*)?/:
const re = /(?<=\$|£|€)\d+(\.\d*)?/; console.log(re.exec('199')); // → null console.log(re.exec('$199')); // → ["199", undefined, index: 1, input: "$199", groups: undefined] console.log(re.exec('€50')); // → ["50", undefined, index: 1, input: "€50", groups: undefined]
還有一種負向后行斷言,表示為(?<!…),代表字符串中的一個位置,緊接該位置之前的字符序列不能匹配pattern。例如,如果模式/(?<!un)available/沒有“un”前綴,那么它將匹配可用的單詞:
const re = /(?<!un)available/; console.log(re.exec('We regret this service is currently unavailable')); // → null console.log(re.exec('The service is available')); // → ["available", index: 15, input: "The service is available", groups: undefined]
Unicode 屬性轉義
ES2018提供了一種新的轉義序列類型,稱為Unicode屬性轉義,可以匹配所有的Unicode。你可以使用\p{Number}來匹配所有的Unicode數字,例如,假設你想匹配的Unicode字符㉛字符串:
const str = '㉛'; console.log(/\d/u.test(str)); // → false console.log(/\p{Number}/u.test(str)); // → true
同樣的,你可以使用\p{Alphabetic}來匹配所有的Unicode單詞字符:
const str = 'ض'; console.log(/\p{Alphabetic}/u.test(str)); // → true // the \w shorthand cannot match ض console.log(/\w/u.test(str)); // → false
同樣有一個負向的Unicode屬性轉義模板 \P{...}:
console.log(/\P{Number}/u.test('㉛')); // → false console.log(/\P{Number}/u.test('ض')); // → true console.log(/\P{Alphabetic}/u.test('㉛')); // → true console.log(/\P{Alphabetic}/u.test('ض')); // → false
除了字母和數字之外,Unicode屬性轉義中還可以使用其他一些屬性。您可以在現行規范中找到受支持的Unicode屬性列表。
新正則表達式支持
Chrome | Firefox | Safari | Edge | |
s (dotAll) Flag | 62 | No | 11.1 | No |
Named Capture Groups | 64 | No | 11.1 | No |
Lookbehind Assertions | 62 | No | No | No |
Unicode Property Escapes | 64 | No | 11.1 | No |
Chrome (Android) |
Firefox (Android) |
iOS Safari |
Edge Mobile |
Samsung Internet |
Android Webview |
|
s (dotAll) Flag | 62 | No | 11.3 | No | 8.2 | 62 |
Named Capture Groups |
64 | No | 11.3 | No | No | 64 |
Lookbehind Assertions |
62 | No | No | No | 8.2 | 62 |
Unicode Property Escapes |
64 | No | 11.3 | No | No | 64 |
Node.js:
-
8.3.0 (需要 --harmony 標志)
-
8.10.0 (支持 s (dotAll) 標志和后行斷言)
-
10.0.0 (全部支持)
模板文字修訂
當模板文字前緊跟着一個表達式時,它被稱為帶標記的模板文字。當您想用函數解析模板文字時,帶標記的模板就派上用場了。考慮下面的例子:
function fn(string, substitute) { if(substitute === 'ES6') { substitute = 'ES2015' } return substitute + string[1]; } const version = 'ES6'; const result = fn${version} was a major update; console.log(result); // → ES2015 was a major update
在這段代碼中,模板文字調用了一個標記表達式(函數):修改字符串中的變量部分。
在ES2018之前,標記模板文字具有與轉義序列相關的語法限制。后跟特定字符序列的反斜杠被視為特殊字符:十六進制轉義的\x、unicode轉義的\u和八進制轉義的\u。因此,像“C:\xxx\uuu”或“\ubuntu”這樣的字符串被解釋器認為是無效的轉義序列,並且會拋出一個SyntaxError。
ES2018從標記模板中移除這些限制,並不是拋出錯誤,而是將無效的轉義序列表示為undefined:
function fn(string, substitute) { console.log(substitute); // → escape sequences: console.log(string[1]); // → undefined } const str = 'escape sequences:'; const result = fn${str} \ubuntu C:\xxx\uuu;
注意,在常規模板文字中使用非法轉義序列仍然會導致錯誤:
const result = \ubuntu; // → SyntaxError: Invalid Unicode escape sequence
模板文字修訂支持
Chrome | Firefox | Safari | Edge |
63 | 58 | 11.1 | 18 |
Chrome Android |
Firefox Android |
iOS Safari |
Edge Mobile |
Samsung Internet |
Android Webview |
63 | 58 | 11.1 | No | 8.2 | 63 |
Node.js:
-
8.3.0 (需要 --harmony 標志)
-
8.10.0 (全部支持)
總結
我們已經很好地了解了ES2018中引入的幾個關鍵特性,包括異步迭代、rest/spread屬性、Promise.prototype.finally()以及正則表達式新特性的添加。盡管一些瀏覽器廠商還沒有完全實現其中的一些特性,但是仍然可以用諸如Babel之類轉義器進行使用。
ECMAScript正在快速發展,經常會有新特性被引入,有興趣可以查詢已完成提案列表,了解全部最新內容。有沒有什么新功能讓你特別興奮?在評論中分享吧!