ECMAScript 2016, 2017, 2018 新特性之必讀篇
JavaScript (ECMAScript). 版本的頻繁更新,讓我們很難去跟進又新增了哪些新特性。就不要說,找一些實用的程式碼示例。但在這篇文章裡,我將介紹18個有關 ECMAScript 2016, 2017, 和 2018 提議目前被通過的新特性。
這是一篇很長的文章,但其實很容易理解。讀到結尾,你將會收穫大量有關 ES 的新知識。文中部分圖片需要翻牆看。
1. Array.prototype.includes
includes 是數組裡很簡單的一個例項方法,幫助我們找到數組裡是否存在某值。(indexOf 不支援 NaN , 但 includes 支援)
Tips: 最開始,JavaScript 想命名為 contains , 但由於被 Mootools 佔用了,所以使用了 includes。
2. Exponentiation infix operator
類似於像 +, - 這種操作符,新增了一個指數操作符。 在 ECMAScript 2016 中, 使用 **
代替了 Math.pow
。
1. Object.values()
Object.values() 這個函式和 Object.keys() 功能上是類似的,但是返回的屬性值是物件自身的,不包括原型鏈上的任何值。
ECMAScript 2017 (ES8)— Object.values()
2. Object.entries()
Object.keys 只是返回數值, 但 Oject.entries() 以陣列的形式,返回了key 和 value。使得將物件變為 Map
更容易。
示例1:
ECMAScript 2017 (ES8) — Using Object.entries() in loops
示例2:
ECMAScript 2017 (ES8) — Using Object.entries() to convert Object to Map
3. String padding
String 新增了兩個字串方法, String.prototype.padStart
和 String.prototype.padEnd
。 這兩個方法可以讓我們在原始字串前或者後新增空白。
'someString'.padStart(numberOfCharcters [,stringForPadding]);
'5'.padStart(10) // ' 5'
'5'.padStart(10, '=*') //'=*=*=*=*=5'
'5'.padEnd(10) // '5 '
'5'.padEnd(10, '=*') //'5=*=*=*=*='
Tips: 這個方法在我們想讓console.log 或者終端輸出整齊的時候,特別有用。
3.1 padStart example:
在下面這個例子裡,我們有多個長度不一的數字, 當我們想讓他們都展示10位數的時候,我們可以使用 padStart(10, '0')
ECMAScript 2017 — padStart example
3.2 padEnd example:
當我們輸出多個 item ,希望屬性值右對齊的時候,可以使用 padEnd
。
下面這個例子裡用到了 padStart, padEnd, Object.entries
ECMAScript 2017 — padEnd, padStart and Object.Entries example
const cars = {
'?BMW': '10',
'?Tesla': '5',
'?Lamborghini': '0'
}
Object.entries(cars).map(([name, count]) => {
//padEnd appends ' -' until the name becomes 20 characters
//padStart prepends '0' until the count becomes 3 characters.
console.log(`${name.padEnd(20, ' -')} Count: ${count.padStart(3, '0')}`)
});
//Prints..
// ?BMW - - - - - - - Count: 010
// ?Tesla - - - - - - Count: 005
// ?Lamborghini - - - Count: 000
3.3 ⚠️ padStart and padEnd on Emojis and other double-byte chars
表情符號和其他多位元組的字元編碼,是使用多位元組的 unicode 編碼方式的,所以 padStart 和 padEnd 可能不適用。
比如我們想要寫十個?的表情符號。結果如下:
//Notice that instead of 5 hearts, there are only 2 hearts and 1 heart that looks odd!
'heart'.padStart(10, "❤️"); // prints.. '❤️❤️❤heart'
因為 ? 這個字元,2個字的位元組數是 '\u2764\uFE0F'
, 它本身就是10個位元組長度。最後一個位元組使用了 ❤️ 的第一個位元組 \u2764
產生的。
所以結果是 ❤️❤️❤heart
PS: 你可以使用 這個連結
地址unicode 位元組數轉換。
4. Object.getOwnPropertyDescriptors
這個方法返回一個物件的所有屬性,包括 getter 裡的 get 和 setter 裡的 set 。之所以新增這個屬性是為了讓物件可以允許複製和克隆一個物件到另一個物件,包括 getter
和 setter
函式。和 Object.assign
相對。
Object.assign
是淺拷貝所有屬性和值,不包括 getter
和 setter
函式。
下面這個例子就是兩者的一個區別。將原物件 Car
拷貝到 ElectricCar
,你就會發現 Object.getOwnPropertyDescriptors
拷貝了 getter
和 setter
, 而
Object.assign
沒有。
before
Before — Using Object.assign
after
ECMAScript 2017 (ES8) — Object.getOwnPropertyDescriptors
var Car = {
name: 'BMW',
price: 1000000,
set discount(x) {
this.d = x;
},
get discount() {
return this.d;
},
};
//Print details of Car object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(Car, 'discount'));
//prints..
// {
// get: [Function: get],
// set: [Function: set],
// enumerable: true,
// configurable: true
// }
//Copy Car's properties to ElectricCar using Object.assign
const ElectricCar = Object.assign({}, Car);
//Print details of ElectricCar object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar, 'discount'));
//prints..
// {
// value: undefined,
// writable: true,
// enumerable: true,
// configurable: true
// }
//⚠️Notice that getters and setters are missing in ElectricCar object for 'discount' property !??
//Copy Car's properties to ElectricCar2 using Object.defineProperties
//and extract Car's properties using Object.getOwnPropertyDescriptors
const ElectricCar2 = Object.defineProperties({}, Object.getOwnPropertyDescriptors(Car));
//Print details of ElectricCar2 object's 'discount' property
console.log(Object.getOwnPropertyDescriptor(ElectricCar2, 'discount'));
//prints..
// { get: [Function: get], ??????
// set: [Function: set], ??????
// enumerable: true,
// configurable: true
// }
// Notice that getters and setters are present in the ElectricCar2 object for 'discount' property!
5. 允許在引數尾部新增逗號
在函式引數尾部,現在可以新增逗號了!為什麼要這樣做呢?為了幫助 git , 只有新的改動才發生更改, 詳細見圖。
ECMAScript 2017 (ES 8) — Trailing comma in function paramameter
Tips: 函式尾部也可以新增逗號
6. Async/Await
到目前為止,這個是最重要的一個特性。它幫助我們擺脫非同步函式,地獄回撥的現狀,讓程式碼更加簡潔。
async
關鍵字會讓 JavaScript 以不同的方式去編譯執行程式碼。編譯器在遇到函式內的 await 之後,會暫停。會假定 await
之後,會返回一個 promise
,等待 promise
有返回結果之後, 再有進一步的執行。
在下面這個例子裡,getAmount
方法有兩個非同步的方法, getUser
和 getBankBalance
。我們用 promise
去實現這個功能,通過使用 async await
讓程式碼更加優雅整潔。
6.1 Async 方法返回一個 Promise.
如果你在等待一個非同步方法返回結果,你可以使用 Promise
的 then
方法去捕獲結果。
在下面這個例子裡,我們想在 doubleAndAdd
外部,用 console.log
輸出 doubleAndAdd
的結果。所以我們用 then
語法把結果值傳給 console.log
。
ECMAScript 2017 (ES 8) — Async Await themselves returns Promise
讓 async/await 同時返回
上面一個例子裡,我們呼叫了兩次 await, 所以每呼叫一次,我們需要等一秒鐘(總共兩秒)。我們可以使用 Promise.all
讓 a 和 b 都有返回值之後,同時處理。
ECMAScript 2017 (ES 8) — Using Promise.all to parallelize async/await
async/await 方法的錯誤處理
當使用 async/await 的時候,我們有很多種處理錯誤的方式。
方法一:使用 try 和 catch
ECMAScript 2017 — Use try catch within the async/await function
//Option 1 - Use try catch within the function
async function doubleAndAdd(a, b) {
try {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
} catch (e) {
return NaN; //return something
}
return a + b;
}
//?Usage:
doubleAndAdd('one', 2).then(console.log); // NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
方法二: 給每個 await 新增 catch
//Option 2 - *Catch* errors on every await line
//as each await expression is a Promise in itself
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a).catch(e => console.log('"a" is NaN')); // ?
b = await doubleAfter1Sec(b).catch(e => console.log('"b" is NaN')); // ?
if (!a || !b) {
return NaN;
}
return a + b;
}
//?Usage:
doubleAndAdd('one', 2).then(console.log); // NaN and logs: "a" is NaN
doubleAndAdd(1, 2).then(console.log); // 6
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
方法三: 給完整的 async-await 函式新增 catch
ECMAScript 2017 — Catch the entire async/await function at the end
//Option 3 - Dont do anything but handle outside the function
//since async / await returns a promise, we can catch the whole function's error
async function doubleAndAdd(a, b) {
a = await doubleAfter1Sec(a);
b = await doubleAfter1Sec(b);
return a + b;
}
//?Usage:
doubleAndAdd('one', 2)
.then(console.log)
.catch(console.log); // ???<------- use "catch"
function doubleAfter1Sec(param) {
return new Promise((resolve, reject) => {
setTimeout(function() {
let val = param * 2;
isNaN(val) ? reject(NaN) : resolve(val);
}, 1000);
});
}
到目前為止, ECMAScript 2018 的最終草案,將會在 2018 年6月或者7月推出。以下所有的特性是 Stage-4
中的,會是 ES2018 的一部分。
Shared memory and atomics
這個是對 JS 引擎的核心增強性功能。(This is a huge, pretty advanced feature and is a core enhancement to JS engines.)
帶入多執行緒的概念到 JavaScript 中, 是為了讓 JS 開發者能夠通過管理記憶體,書寫高效能,併發程式。(The main idea is to bring some sort of multi-threading feature to JavaScript so that JS developers can write high-performance, concurrent programs in the future by allowing to manage memory by themselves instead of letting JS engine manage memory.)
新增了一個全域性物件 SharedArrayBuffer
在共享記憶體空間儲存資料。所以這個資料將會在 JS 執行緒和 web-worker
執行緒中分享。
在這之前,如果我們想在主程序和 web-worker 之間分享資料,我們只能通過複製資料, 使用 postMessage 傳送給另一個。
現在的話,就不需要那麼麻煩了,只需要使用 SharedArrayBuffer
, 資料就可以在兩個執行緒中都可用。
共享記憶體就會引起一個 Race Condition
的問題。為了避免這種情況發生,所以有了全域性物件 Atomics
。Atomics
提供了各種方法,可以實現在使用一個數據時,對資料做鎖定。也有一些方法,讓修改資料變得更加安全。
這個特性推薦使用一些庫去實現,但是目前還沒有現成的庫。
如果你對這個有興趣,推薦您幾篇文章。
1. From Workers to Shared Memory — lucasfcosta
2. A cartoon intro to SharedArrayBuffers — Lin Clark
3. Shared memory and atomics — Dr. Axel Rauschmayer
2. 刪除 Tagged Template literal 限制
首頁,我們需要理解一下什麼是 Tagged Template literal
。
在 ES2015+, 有個特性是允許開發者在字串中插入字元變數。如圖:
在標籤文字中, 可以寫一個函式來接收字串字面的硬編碼部分, 例如[‘Hello’,’!’] 以及替換變數, 例如, [ Raja’] , 作為自定義函式的引數(例如 greet) , 並從自定義函式返回任何你想要的。
下面的例子表明, 我們的自定義”標籤”函式問候應用一天的時間, 比如”早上好!” “下午好”等等, 這取決於一天中的時間和字串的字面值, 然後返回一個自定義字串。
Tag function example that shows custom string interpolation
//A "Tag" function returns a custom string literal.
//In this example, greet calls timeGreet() to append Good //Morning/Afternoon/Evening depending on the time of the day.
function greet(hardCodedPartsArray, ...replacementPartsArray) {
console.log(hardCodedPartsArray); //[ 'Hello ', '!' ]
console.log(replacementPartsArray); //[ 'Raja' ]
let str = '';
hardCodedPartsArray.forEach((string, i) => {
if (i < replacementPartsArray.length) {
str += `${string} ${replacementPartsArray[i] || ''}`;
} else {
str += `${string} ${timeGreet()}`; //<-- append Good morning/afternoon/evening here
}
});
return str;
}
//?Usage:
const firstName = 'Raja';
const greetings = greet`Hello ${firstName}!`; //??<-- Tagged literal
console.log(greetings); //'Hello Raja! Good Morning!' ?
function timeGreet() {
const hr = new Date().getHours();
return hr < 12
? 'Good Morning!'
: hr < 18 ? 'Good Afternoon!' : 'Good Evening!';
}
現在我們討論了什麼是”標記”函式, 許多人希望在不同的領域使用這個功能, 比如在終端中使用命令和 HTTP 請求來編寫 uri 等等。
⚠️存在的問題
問題在於, ES2015和 ES2016規範不允許使用像”\u”(unicode) ,”\x”(hexadecimal)這樣的逃逸字元, 除非它們看起來完全像 \u00A9
或 \u {2F804}
或 xA9
。
因此, 如果你有一個內部使用其他域規則的標記函式(如終端規則) , 那麼可能需要使用不像 \u0049
或 \u {@f804}
的標記函式, 那麼您將會得到一個語法錯誤。
在 ES2018, 只要標籤函式返回具有”cooked”屬性(無效字元是”未定義的”)的物件中返回值, 則允許這些看似無效的轉義字元返回值(無效字元是”未定義的”) , 然後是一個”原始”屬性(不管你想要什麼)。
function myTagFunc(str) {
return { "cooked": "undefined", "raw": str.raw[0] }
}
var str = myTagFunc `hi \ubla123abla`; //call myTagFunc
str // { cooked: "undefined", raw: "hi \\unicode" }
3. 正則表示式中的 .
在正則表示式裡,雖然 .
表示一個字元,但目前它不表示像 \n \r \f
這類字元。
例如:
//Before
/first.second/.test('first\nsecond'); //false
這個新增功能,可以讓 . 匹配任意一個字元。但為了相容舊版本, 我們需要加一個 \s
標識,讓正則表示式起作用。
//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true Notice: /s ??
下面是文件API中的內容:
ECMAScript 2018 — Regex dotAll feature allows matching even \n via “.” via /s flag
4. RegExp Named Group Captures
這個特性的增強是從其他語言 Python, Java等學來的,所以稱為命名組。“Named Groups.”。
這個方法允許開發者在正則表示式中(?…) 給不同的組起名稱。使得我們可以輕易的通過名稱獲取到某個組。
4.1 Named group 的基礎用法
在下面的例子中,我們給日期命名了三個不同的組(?) (?) and (?year)。結果物件將包含一致的屬性 group , 包括 year, month。並且
year h會有相對應的值。
ECMAScript 2018 — Regex named groups example
4.2 在 regex 內使用 Named groups
我們可以使用這樣的格式 \k<group name>
去匹配它自己。以下是示例:
ECMAScript 2018 — Regex named groups back referencing via \k
4.3 在 String.prototype.replace 中使用 named groups
在 String.prototype.replace 方法中使用 named groups。所以我們能更快捷的交換詞。
例如,把 “firstName, lastName” 改成 “lastName, firstName”.
ECMAScript 2018 — Using RegEx’s named groups feature in replace function
5. 物件的 Rest 屬性
Rest 操作符(…)三個點允許我們獲取 extract Object properties that are not already extracted.
5.1 通過 Rest 解構你需要的屬性
ECMAScript 2018 — Object destructuring via rest
5.2 更重要的是,你還可以刪除你不需要的選項
ECMAScript 2018 — Object destructuring via rest
6. 物件的 Spread 屬性
Spread 和 Rest 的 三個點是一樣的,但是不同的是你可以用 Spread 去新建或者組合一個新物件。
Tips: Spread 是對齊賦值的右運算子, 而 Rest 是左運算子。
ECMAScript 2018 — Object restructuring via rest
7. RegExp Lookbehind Assertions
這個是對正則表示式的增強,確保在一些字串存在之前,另一些字串存在。before
This is an enhancement to the RegEx that allows us to ensure some string exists immediately before some other string.
你可以使用組(?<=…)
去正向斷定,也可以用 (?<!…)
去取反。
Essentially this will match as long as the -ve assertion passes.
正向斷定: 我們想確保 # 在 winning 之前。(就是#winning),想正則匹配返回 winning。下面是寫法:
反向斷定:匹配一個數字,有 € 字元而沒有 $ 字元在前面的數字。
ECMAScript 2018 — (?<!…)
for negative assertions
8. RegExp Unicode Property Escapes
用正則去匹配 Unicode 字元是很不容易的。像 \w , \W , \d 這種只能匹配英文字元和數字。但是其他語言的字元怎麼辦呢,比如印度語,希臘語?
有了 Unicode Property Escapes 之後,它為每一個字元新增一個 metadata 屬性,用它去分組或者表達各式各樣的符號。
例如 Unicode 資料庫組裡把所有的印度語字元,標識為 Script = Devanagari。還有一個屬性 Script_Extensions, 值也為 Devanagari。
所以我們可以通過搜尋 Script=Devanagari,得到所有的印度語。
Devanagari 可以用於印度的各種語言,如Marathi, Hindi, Sanskrit。
在 ECMAScript 2018 裡, 我們可以使用 \p
和 {Script=Devanagari}
匹配那些所有的印度語字元。也就是說 \p{Script=Devanagari}
這樣就可以匹配。
ECMAScript 2018 — showing \p
//The following matches multiple hindi character
/^\p{Script=Devanagari}+$/u.test('हिन्दी'); //true
//PS:there are 3 hindi characters h
同理,希臘語的語言是 Script_Extensions
和 Script
的值等於 Greek
。也就是用 Script_Extensions=Greek or Script=Greek
這樣就可以匹配所有的希臘語。
也就是說。我們用 \p{Script=Greek}
匹配所有的希臘語。
ECMAScript 2018 — showing \p
//The following matches a single Greek character
/\p{Script_Extensions=Greek}/u.test('π'); // true
進一步說,Unicode 表情庫裡存了各式各樣的布林值,像 Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, and Emoji_Modifier_Base
的值,都等於 true。所以我們想搜 Emoji 等於 ture,就能搜到所有的表情。
我們用 \p{Emoji} ,\Emoji_Modifier
匹配所有的表情。
舉個更加詳盡的例子
ECMAScript 2018 — showing how \p can be used for various emojis
//The following matches an Emoji character
/\p{Emoji}/u.test('❤️'); //true
//The following fails because yellow emojis don't need/have Emoji_Modifier!
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌️'); //false
//The following matches an emoji character\p{Emoji} followed by \p{Emoji_Modifier}
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌?'); //true
//Explaination:
//By default the victory emoji is yellow color.
//If we use a brown, black or other variations of the same emoji, they are considered
//as variations of the original Emoji and are represented using two unicode characters.
//One for the original emoji, followed by another unicode character for the color.
//
//So in the below example, although we only see a single brown victory emoji,
//it actually uses two unicode characters, one for the emoji and another
// for the brown color.
//
//In Unicode database, these colors have Emoji_Modifier property.
//So we need to use both \p{Emoji} and \p{Emoji_Modifier} to properly and
//completely match the brown emoji.
/\p{Emoji}\p{Emoji_Modifier}/u.test('✌?'); //true
我們用大寫的 P 替代 小寫 的 p 去取反。(Lastly, we can use capital “P”(\P ) escape character instead of small p (\p ), to negate the matches.)
參考文獻:
1. ECMAScript 2018 Proposal
2. https://mathiasbynens.be/notes/es-unicode-property-escapes
8. Promise.prototype.finally()
finally()
是 Promise 新增的一個例項方法。意圖是允許在 resolve/reject 之後執行回撥。finally
沒有返回值,始終會被執行。
來看案例:
ECMAScript 2018 — finally() in resolve case
ECMAScript 2018 — finally() in reject case
ECMASCript 2018 — finally() in Error thrown from Promise case
ECMAScript 2018 — Error thrown from within catch case
9. Asynchronous Iteration
這是一個極其好用的新特性。讓我們能夠非常容易的建立非同步迴圈程式碼。
新特性新增了一個 for-await-of
迴圈,允許我們在迴圈過程中,呼叫非同步方法,返回 Promise, 或者 支援 promise 的 Array。
最炫酷的是,迴圈會等待每一個 Promise resolve 之後,進入下次迴圈。
ECMAScript 2018 — Async Iterator via for-await-of