ECMAScript 是 JAVAScript 要實現的一個語言標準,通常縮寫為 ES。自從 ES6 之后 JavaScript 多出了很多新特性,當開始學習這些新特性時,不可避免的會看到這些術語:“ES6、ES7、ES8、ECMAScript 2018、ECMAScript 2019...” 等等很多。很多時候讓人困惑或混淆,例如 ES6 其實等價于 ES2015,這個 ES2015 代表的是當時發表的年份,ES2016 發布的稱為 ES7,依次類推,ES2023 可以稱為 ES14 了~
本文根據最新已完成的提案,按照時間倒序列出 ES2013 ~ ES2016 之間新增的語言特性。
ES2023
ES2023 新特性目前有兩條:Array find from last、Hashbang Grammar,也都處于 stage 4 階段,預計 2023 年發布。
從數組末尾查找元素
新增兩個方法: .findLast()、.findLastIndex()? 從數組的最后一個元素開始查找,可以同 find()、findIndex() 做一個對比。
const arr = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];
// find vs findLast
console.log(arr.find(n n.value % 2 === 1)); // { value: 1 }
console.log(arr.findLast(n n.value % 2 === 1)); // { value: 3 }
// findIndex vs findLastIndex
console.log(arr.findIndex(n n.value % 2 === 1)); // 0
console.log(arr.findLastIndex(n n.value % 2 === 1)); // 2
Hashbang 語法
如下所示,在 index.js 腳本文件里編寫 JS 代碼,如果要正確的執行,需要在控制臺輸入 node index.js。
console.log("JavaScript");
如果直接執行 ./index.js 腳本文件會得到以下報錯:
$ ./index.js
./index.js: line 1: syntax error near unexpected token `"JavaScript"'
./index.js: line 1: `console.log("JavaScript");'
很正常,因為我們并沒有指定使用何種解釋器來執行上述腳本文件。Hashbang 語法是用來指定腳本文件的解釋器是什么,語法規則是在腳本文件頭部增加一行代碼:#!/usr/bin/env node。
// #!/usr/bin/env node
console.log("JavaScript");
注意,還需修改腳本文件的權限 chmod +x index.js?,否則執行會報 permission denied: ./index.js 錯誤。
ES2022
Class Fields
允許在類最外層聲明類成員,參考 https://github.com/tc39/proposal-class-fields。
class Person {
name = 'Tom'
}
私有化類成員:支持私有實例、私有靜態類型字段、私有方法。
class Person {
#privateField1 = 'private field 1'; // 私有字段賦初值
#privateField2; // 默認 undefined
static #privateStaticField3 = 'private static field 3'
constructor(value) {
this.#privateField2 = value; // 實例化時為私有字段賦值
}
#toString() {
console.log(this.#privateField1, this.#privateField2, InstPrivateClass.#privateStaticField3);
}
print() {
this.#toString()
}
}
const p = new Person('private field 2')
p.print()
私有字段檢查
使用 in 操作符檢測某一實例是否包含要檢測的私有字段。
class Person {
#name = 'Ergonomic brand checks for Private Fields';
static check(obj) {
return #name in obj;
}
}
Top-level await
以前 await 必須隨著 async 一起出現,只有在 async 函數內才可用。當需要在一些文件頂部進行初始化的場景中使用時就有不支持了,頂級 await 可以解決這個問題,但它僅支持 ES Modules。
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
正則新增 /d 修飾符
以前的正則表達式支持 3 個修飾符:/i(忽略大小寫)、/g(全局匹配)、/m(多行),當執行正則表達式的 exec() 方法時,新增一個 /d 修飾符,它會返回一個 indices 屬性,包含了匹配元素的開始、結束位置索引。
const str = 'ECMAScript_JavaScript'
const regexp = /sc/igd // 忽略大小全局匹配并返回匹配元素的開始、結束位置索引
console.log(regexp.exec(str).indices) // [ 4, 6 ]
console.log(regexp.exec(str).indices) // [ 15, 17 ]
.at() 操作符
根據指定索引獲取數組元素,不同的是它支持傳遞負數,例如 -1 獲取最后一個元素。
const arr = ['a', 'b', 'c']
console.log(arr.at(0));
console.log(arr.at(-1)); // 等價于 arr[arr.length - 1]
Object.hasOwn()
Object.hasOwn()? 提供了一種更安全的方法來檢查對象是否具有自己的屬性,適用于檢查所有的對象。Object.prototype.hasOwnProperty()? 方法遇到 obj = null這種情況會報錯,參見以下示例:
const person = Object.create({ name: 'Tom' })
person.age = 18;
console.log(Object.hasOwn(person, 'name')); // false
console.log(Object.hasOwn(person, 'age')); // true
// 遇到這種情況 hasOwnProperty 會報錯
const p1 = null
console.log(p1.hasOwnProperty('name')); // TypeError: Cannot read properties of null (reading 'hasOwnProperty')
Error Cause
Error Cause 是由阿里巴巴提出的一個提案,為 Error 構造函數增加了一個 options,可設置 cause 的值為任意一個有效的 JavaScript 值。
例如,自定義錯誤 message,將錯誤原因賦給 cause 屬性,傳遞給下一個捕獲的地方。
try {
await fetch().catch(err {
throw new Error('Request failed', { cause: err })
})
} catch (e) {
console.log(e.message);
console.log(e.cause);
}
Class Static Block
類的靜態初始化塊是在類中為靜態成員提供了一個用于做初始化工作的代碼塊。
class C {
static x = 'x';
static y;
static z;
static {
try {
const obj = doSomethingWith(this.x);
this.y = obj.y;
this.z = obj.z;
} catch (err) {
this.y = 'y is error';
this.z = 'z is error';
}
}
}
ES2021
String.prototype.replaceAll
replaceAll()? 用于替換正則表達式或字符串的所有匹配項,之前的 replace() 只會匹配一個。
console.log('JavaScript'.replaceAll('a', 'b')); // JbvbScript
Promise.any
Promise.any() 接收一個 Promise 數組做為參數,返回第一個執行成功的 Promise,如果全部執行失敗將返回一個新的異常類型 AggregateError,錯誤信息會以對象數組的形式放入其中。
const delay = (value, ms) => new Promise((resolve, reject) => setTimeout(() resolve(value), ms));
const promises = [
delay('a', 3000),
delay('b', 2000),
delay('c', 4000),
];
Promise.any(promises)
.then(res console.log(res)) // b
.catch(err console.error(err.name, err.message, err.errors)) // 全部失敗時返回:AggregateError All promises were rejected [ 'a', 'b', 'c' ]
數字分隔符
數字分隔符可以讓大數字看起來也容易理解。
const budget = 1_000_000_000_000;
console.log(budget === 10 ** 12); // true
邏輯賦值運算符
結合了邏輯運算符 &&、||、??? 和邏輯表達式 =。
// "Or Or Equals" (or, the Mallet operator :wink:)
a ||= b; // a || (a = b);
// "And And Equals"
a &&= b; // a && (a = b);
// "QQ Equals"
a ??= b; // a ?? (a = b);
WeakRefs
能夠拿到一個對象的弱引用,而不會阻止該對象被垃圾回收。例如 ref 弱引用了 obj,盡管持有了 obj 對象的引用,但也不會阻止垃圾回收 obj 這個對象,如果是強引用則不行。 參考 https://github.com/tc39/proposal-weakrefs
const obj = { a: 1 };
const ref = new WeakRef(obj)
console.log(ref.deref());
ES2020
matchAll - 匹配所有
String.prototype.matchAll()? 會返回正則匹配的所有字符串及其位置,相比于 String.prototype.match() 返回的信息更詳細。
const str = 'JavaScript'
const regexp = /a/g
console.log([...str.matchAll(regexp)]);
// Output:
[
[ 'a', index: 1, input: 'JavaScript', groups: undefined ],
[ 'a', index: 3, input: 'JavaScript', groups: undefined ]
]
模塊新特性
- import 動態導入
動態導入意思是當你需要該模塊時才會進行加載,返回的是一個 Promise? 對象。只有在 ES Modules 模塊規范下才支持。
// index-a.mjs
export default {
hello () {
console.log(`hello JavaScript`);
}
}
// index-b.mjs
import('./index-a.mjs').then(module {
module.default.hello(); // hello JavaScript
})
- import.meta
import.meta? 指當前模塊的元數據。一個廣泛支持的屬性是 import.meta.url,以字符串形式輸出當前模塊的文件路徑。
BigInt
BigInt 是新增的一種描述數據的類型,用來表示任意大的整數。因為原先的 JavaScript Number 類型能夠表示的最大整數位 Math.pow(2, 53) - 1,一旦超出就會出現精度丟失問題。詳情可參考筆者之前的這篇文章 JavaScript 浮點數之迷下:大數危機 https://github.com/qufei1993/blog/issues/10。
9007199254740995 // 會出現精度丟失
9007199254740995n // BigInt 表示方式一
BigInt('9007199254740995') // BigInt 表示方式二
Promise.allSettled
Promise.allSettled() 會等待所有的 Promise 對象都結束后在返回結果。
const delay = (value, ms, isReject) => new Promise((resolve, reject) => setTimeout(() isReject ? reject(new Error(value)) : resolve(value), ms));
const promises = [
delay('a', 3000),
delay('b', 2000, true),
];
Promise.allSettled(promises)
.then(res console.log(res))
// Output:
[
{ status: 'fulfilled', value: 'a' },
{
status: 'rejected',
reason: Error: b
at Timeout._onTimeout (/index.js:1:108)
at listOnTimeout (node:internal/timers:564:17)
at process.processTimers (node:internal/timers:507:7)
}
]
全局對象
JavaScript 可以運行在不同的環境,瀏覽器為 window、Node.js 為 global。為了能夠統一全局環境變量,引入了 globalThis。
window === globalThis // 瀏覽器環境
global === globalThis // Node.js 環境
for-in 機制
ECMA-262 規范沒有規定 for (a in b) ... 的遍歷順序,部分原因是所有引擎都有自己特殊的實現,現在很難就 for-in 完整順序規范達成一致,但規范了一些供參考的實現行為,詳情參考 this list of interop semantics。
可選鏈
可選鏈是一個很好的語法,使用 ?. 表示,能避免一些常見類型錯誤。
const obj = null;
obj.a // TypeError: Cannot read properties of null (reading 'a')
obj?.a // 使用可選鏈之后就不會報錯了,會輸出 undefined
空值合并
空值合并語法使用 ??? 表示,和 ||? 這個語法類似,不同的是 ?? 有明確的規定,只有當左側的值為 null 或 undefined 時才會返回右側的值,例如,左側是 0 也會認為是合法的。
const a = 0
a || 1 // 1
a ?? 1 // 0
ES2019
可選的 catch 參數
try {
throw new Error('this is not a valid')
} catch {
console.error(`error...`);
}
Symbol.prototype.description
創建 Symbol 對象時可以傳入一個描述做為參數。如下所示,使用 symbol.description 可方便的獲取到這個描述。
const symbol = Symbol('Hello World')
symbol.description
函數的 toString() 方法
函數也可以執行 toString() 方法,它會返回定義的函數體代碼,包含注釋。
const fn = (a, b) => {
// return a + b value
const c = a + b;
return c;
}
console.log(fn.toString());
Object.fromEntries
Object.fromEntries()? 方法會把鍵值對列表轉換為對象。同 Object.entries() 相反。
const arr = [ [ 'name', 'foo' ], [ 'age', 18 ] ];
const obj = Object.fromEntries(arr);
console.log(obj); // { name: 'foo', age: 18 }
console.log(Object.entries(obj)); // [ [ 'name', 'foo' ], [ 'age', 18 ] ]
消除前后空格
ES2019 之前有一個 trim()? 方法會默認消除前后空格。新增的 trimStart()、trimEnd() 方法分別用來指定消除前面、后面空格。
' JavaScript '.trim() // 'JavaScript'
' JavaScript '.trimStart() // 'JavaScript '
' JavaScript '.trimEnd() // ' JavaScript'
數組 flat()、flatMap()
flat(depth)? 可以實現數組扁平化,傳入的 depth 參數表示需要扁平化的數組層級。
[['a'], ['b', 'bb'], [['c']]].flat(2) // [ 'a', 'b', 'bb', 'c' ]
flatMap()? 方法是 map()? 和 flat() 方法的結合,該方法只能展開一維數組。
[['a'], ['b', 'bb'], [['c']]].flatMap(x x) // [ 'a', 'b', 'bb', [ 'c' ] ]
JSON 超集
ES2019 之前 JSON 字符串中不支持 u2028(行分隔符)、u2029(段落分隔符) 字符,否則 JSON.parse() 會報錯,現在給予了支持。
const json = '"u2028"';
JSON.parse(json);
JSON.stringify() 加強格式轉化
防止 JSON.stringify 返回格式錯誤的 Unicode 字符串,參考 https://2ality.com/2019/01/well-formed-stringify.html
ES2018
異步迭代
異步迭代在 Node.js 中用的會多些,使用 for-await-of 遍歷異步數據。例如使用 MongoDB 查詢數據返回值默認為一個游標對象,避免了一次性把數據讀入應用內存,詳情參考 https://github.com/qufei1993/blog/issues/31。
const userCursor = userCollection.find();
for await (const data of userCursor) { ... }
Promise.finally
Promise.finally 能保證無論執行成功或失敗都一定被執行,可以用來做一些清理工作。
const connection = { open: () Promise.resolve() }
connection
.open()
.then()
.catch()
.finally(() {
console.log('clear connection');
})
新的正則表達式功能
- 正則命名組捕獲
正則命名組捕獲使用符號 ?<name> 表示,對匹配到的正則結果按名稱訪問。
const regexp = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/u;
const result = regexp.exec('2023-01-01');
console.log(result.groups); // { year: '2023', month: '01', day: '01' }
- 正則 Lookbehind 斷言:https://github.com/tc39/proposal-regexp-lookbehind
- 正則表達式dotAll模式:https://github.com/tc39/proposal-regexp-dotall-flag
- 正則表達式 Unicode 轉義:https://github.com/tc39/proposal-regexp-unicode-property-escapes
Rest/Spread 屬性
Rest 參數語法使用 ... 表示,會將所有未明確的參數表示為一個數組。
const fn = (a, ...rest) => {
console.log(a, rest); // 1 [ 2, 3 ]
}
fn(1, 2, 3);
展開操作符(Spread)也使用 ... 表示,將一個數組內容轉換為參數傳遞給函數。
const fn = (a, ...rest) => {
console.log(a, rest); // 1 [ 2, 3 ]
}
fn(...[1, 2, 3]);
展開操作符另一個常用的場景是用來做對象的淺拷貝。
const obj = { a: 1 }
const newObj = { ...obj, b: 2 }
解除模版字符串限制 - Lifting template literal restriction
“Lifting template literal restriction” 翻譯過來為 “解除模版字符串限制”,這個要結合 ES6 中的 “帶標簽的模版字符串” 來理解。
以下代碼執行時,解析器會去查找有效的轉義序列,例如 Unicode? 字符以 "u"? 開頭,例如 u00A9?,以下 "unicode"? 是一個非法的 Unicode? 字符,在之前就會得到一個 SyntaxError: malformed Unicode character escape sequence? 錯誤。ES2018 中解除了這個限制,當遇到不合法的字符時也會正常執行,得到的是一個 undefined?,通過 raw 屬性還是可以取到原始字符串。
function latex(strings, ...exps) {
console.log(strings); // [ undefined ]
console.log(strings.raw); // [ 'latex \unicode' ]
}
latex`latex unicode`;
ES2017
Object.values/Object.entries
Object.values()? 返回一個對象的所有值,同 Object.keys() 相反。
const obj = { name: 'Tom', age: 18 }
console.log(Object.values(obj)); // [ 'Tom', 18 ]
Object.entries() 返回一個對象的鍵值對。
const obj = { name: 'Tom', age: 18 }
for (const [key, value] of Object.entries(obj)) {
console.log(key, value);
}
// Output
// name Tom
// age 18
字符串補全
兩個字符串補全方法 .padStart()、.padEnd() 分別在字符串的頭部、尾部進行按目標長度和指定字符進行填充。
console.log('a'.padStart(5, '1')); // 1111a
console.log('a'.padEnd(5, '2')); // a2222
async/await
異步函數 async/await 現在開發必備了,無需多講。
async function fn() { ... }
try {
await fn();
} catch (err) {}
Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptors() 方法用來獲取一個對象的所有自身屬性的描述符。
const obj = {
name: 'Tom',
run: () ``,
};
console.log(Object.getOwnPropertyDescriptors(obj));
// {
// name: {
// value: 'Tom',
// writable: true,
// enumerable: true,
// configurable: true
// },
// run: {
// value: [Function: run],
// writable: true,
// enumerable: true,
// configurable: true
// }
// }
參數列表支持尾逗號
支持在函數聲明及調用時末尾增加逗號而不報 SyntaxError 錯誤。
function add(a, b,) {
return a + b;
}
add(1, 2,)
共享內存和原子 - Shared memory and atomics
Shared memory and atomics,“共享內存和原子” 又名 “共享數組緩沖區”,可以在主線程和多個工作線程間共享對象的字節,能更快的在多個工作線程間共享數據、除 postMessage() 多了一種數據傳輸方式。
多線程間的內存共享會帶來一個后端常遇見的問題 “競態條件”,提案提供了全局變量 Atomics 來解決這個問題。詳情參考 ES proposal: Shared memory and atomics
ES2016
Array.prototype.includes
當判斷一個數組中是否包含指定值時,使用 includes() 會很實用。
['a', 'b'].includes('a') // true
求冪運算符
**? 是求冪運算符,左側是基數、右側是指數。等價于之前的 Math.pow() 函數。
2 ** 3 // 8
Math.pow(2, 3) // 8
最后關于 ES2015,也就是常說的 ES6 更新的內容是比較多的,不在這里贅述,推薦一個資料給有需要的朋友 https://es6.ruanyifeng.com/。
Reference
- Finished Proposals
- https://exploringjs.com/impatient-js/ch_new-javascript-features.html#source-of-this-chapter