你需要知道的ES6—ES13開發技巧!

語言: CN / TW / HK

大家好,我是 CUGGZ。

ECMAScript 是 JavaScript 的標準與規範,JavaScript 是 ECMAScript 標準的實現和擴充套件。今天就來看看 ECMAScript 各版本有哪些實用開發技巧吧!

一、ES6 新特性(2015)

1、let和const

在ES6中,新增了let和const關鍵字,其中 let 主要用來宣告變數,而 const 通常用來宣告常量。let、const相對於var關鍵字有以下特點:

特性

var

let

const

變數提升

:heavy_check_mark:

×

×

全域性變數

:heavy_check_mark:

×

×

重複宣告

:heavy_check_mark:

×

×

重新賦值

:heavy_check_mark:

:heavy_check_mark:

×

暫時性死區

×

:heavy_check_mark:

:heavy_check_mark:

塊作用域

×

:heavy_check_mark:

:heavy_check_mark:

只宣告不初始化

:heavy_check_mark:

:heavy_check_mark:

×

這裡主要介紹其中的四點:

(1)重新賦值

const 關鍵字宣告的變數是“不可修改”的。其實,const 保證的並不是變數的值不能改動,而是變數指向的那個記憶體地址不能改動。對於基本型別的資料(數值、字串、布林值),其值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於引用型別的資料(主要是物件和陣列),變數指向資料的記憶體地址,儲存的只是一個指標,const只能保證這個指標是不變的,至於它指向的資料結構就不可控制了。

(2)塊級作用域

在引入let和const之前是不存在塊級作用域的說法的,這也就導致了很多問題,比如內層變數會覆蓋外層的同名變數:

var a = 1;
if (true) {
  var a = 2;
}
console.log(a); // 輸出結果:2

迴圈變數會洩漏為全域性變數:

var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
  console.log(arr[i]);  // 輸出結果:1  2  3
}
console.log(i); // 輸出結果:3

而通過let和const定義的變數存在塊級作用域,就不會產生上述問題:

let a = 1;
if (true) {
  let a = 2;
}
console.log(a); // 輸出結果:1
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);  // 輸出結果:1  2  3
}
console.log(i); // Uncaught ReferenceError: i is not defined

(3)變數提升

我們知道,在ES6之前是存在變數提升的,所謂的變數提升就是變數可以在宣告之前使用:

console.log(a); // 輸出結果:undefined
var a = 1;

變數提升的本質是JavaScript引擎在執行程式碼之前會對程式碼進行編譯分析,這個階段會將檢測到的變數和函式宣告新增到 JavaScript 引擎中名為 Lexical Environment 的記憶體中,並賦予一個初始化值 undefined。然後再進入程式碼執行階段。所以在程式碼執行之前,JS 引擎就已經知道宣告的變數和函式。

這種現象就不太符合我們的直覺,所以在ES6中,let和const關鍵字限制了變數提升,let 定義的變數新增到 Lexical Environment 後不再進行初始化為 undefined 操作,JS 引擎只會在執行到詞法宣告和賦值時才進行初始化。而在變數建立到真正初始化之間的時間跨度內,它們無法訪問或使用,ES6 將其稱之為暫時性死區:

// 暫時性死區 開始
a = "hello";     //  Uncaught ReferenceError: Cannot access 'a' before initialization
let a;   
//  暫時性死區 結束
console.log(a);  // undefined

(4)重複宣告

在ES6之前,var關鍵字宣告的變數對於一個作用域內變數的重複宣告是沒有限制的,甚至可以宣告與引數同名變數,以下兩個函式都不會報錯:

function funcA() {
  var a = 1;
  var a = 2;
}
function funcB(args) {
  var args = 1; 
}

而let修復了這種不嚴謹的設計:

function funcA() {
  let a = 1;
  let a = 2;  // Uncaught SyntaxError: Identifier 'a' has already been declared
}
function funcB(args) {
  let args = 1;  // Uncaught SyntaxError: Identifier 'args' has already been declared
}

現在我們專案中已經完全放棄了var,而使用let來定義變數,使用const來定義常量。在ESlint開啟瞭如下規則:

"no-var": 0;

2、解構賦值

ES6中還引入瞭解構賦值的概念,解構賦值遵循“模式匹配”,即只要等號兩邊的模式相等,左邊的變數就會被賦予對應的值。不同型別資料的解構方式不同,下面就分別來看看不同型別資料的解構方式。

平時在開發中,我主要會用到物件的解構賦值,比如在React中解構porps值等,使用解構賦值來獲取父元件傳來的值;在React Hooks中的useState使用到了陣列的解構賦值。

(1)陣列解構

具有 Iterator 介面的資料結構,都可以採用陣列形式的解構賦值。

const [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo, bar, baz) // 輸出結果:1  2  3

這裡,ES6實現了對陣列的結構,並依次賦值變數foo、bar、baz。陣列的解構賦值按照位置將值與變數對應。

陣列還可以實現不完全解構,只解構部分內容:

const [x, y] = [1, 2, 3];   // 提取前兩個值
const [, y, z] = [1, 2, 3]  // 提取後兩個值
const [x, , z] = [1, 2, 3]  // 提取第一三個值

如果解構時對應的位置沒有值就會將變數賦值為undefined:

const [x, y, z] = [1, 2]; 
console.log(z)  // 輸出結果:undefined

陣列解構賦值可以使用rest操作符來捕獲剩餘項:

const [x, ...y] = [1, 2, 3];   
console.log(x);  // 輸出結果:1
console.log(y);  // 輸出結果:[2, 3]

在解構時還支援使用預設值,當對應的值為undefined時才會使用預設值:

const [x, y, z = 3] = [1, 2]; 
console.log(z)  // 輸出結果:3

(2)物件解構

物件的解構賦值的本質其實是先找到同名的屬性,在賦值給對應的變數:

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
console.log(foo, bar); // 輸出結果:aaa  bbb

需要注意的是,在JavaScript中,物件的屬性是沒有順序的。所以,在解構賦值時,變數必須與屬性同名才能去取到值。

物件的解構賦值也是支援預設值的,當定義的變數在物件中不存在時,其預設值才會生效:

let { foo, bar, baz = 'ccc'} = { foo: 'aaa', bar: 'bbb', baz: null };
console.log(foo, bar, baz); // 輸出結果:aaa  bbb  null
let { foo, bar, baz = 'ccc'} = { foo: 'aaa', bar: 'bbb' };
console.log(foo, bar, baz); // 輸出結果:aaa  bbb  ccc

可以看到,只有定義的變數是嚴格的===undefined時,它的預設值才會生效。

除此之外,我們還需要注意,不能給已宣告的變數進行賦值,因為當缺少 let、const、var 關鍵詞時,將會把 {baz} 理解為程式碼塊從而導致語法錯誤,所以下面程式碼會報錯:

let baz;
{ baz } = { foo: 'aaa', bar: 'bbb', baz: 'ccc' };

可以使用括號包裹整個解構賦值語句來解決上述問題:

let baz;
({ baz } = { foo: 'aaa', bar: 'bbb', baz: 'ccc' });
console.log(baz)

在物件的解構賦值中,可以將現有物件的方法賦值給某個變數,比如:

let { log, sin, cos } = Math;
log(12)  // 輸出結果:2.4849066497880004
sin(1)   // 輸出結果:0.8414709848078965
cos(1)   // 輸出結果:0.5403023058681398

(3)其他解構賦值

剩下的幾種解構賦值,目前我在專案中應用的較少,來簡單看一下。

  • 字串解構

字串解構規則:只要等號右邊的值不是物件或陣列,就先將其轉為類陣列物件,在進行解構:

const [a, b, c, d, e] = 'hello';
console.log(a, b, c, d, e)  // 輸出結果:h e l l o

類陣列物件有 length 屬性,因此可以給這個屬性進行解構賦值:

let {length} = 'hello';    // 輸出結果:5

由於字串都是一個常量,所以我們通常是知道它的值是什麼的,所以很少會使用變數的解構賦值。

  • 數值和布林值解構賦值

對數值和布林值進行解構時,它們將會先被轉為物件,然後再應用解構語法:

let {toString: s} = 123;
s === Number.prototype.toString // 輸出結果:true
let {toString: s} = true;
s === Boolean.prototype.toString // 輸出結果:true

注意null和undefined不能轉換為物件,所以如果右邊是這兩個值,就會報錯。

  • 函式引數解構賦值

函式引數表面上是一個數組,在傳入引數的那一刻,就會被解構為x和y。

function add([x, y]){
  return x + y;
}
add([1, 2]);   // 3

除此之外,我們還可以解構函式的返回值:

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

3、模板字串

傳統的JavaScript語言中,輸出模板經常使用的是字串拼接的形式,這樣寫相當繁瑣,在ES6中引入了模板字串的概念來解決以上問題。

模板字串是增強版的字串,用反引號``來標識,他可以用來定義單行字串,也可以定義多行字串,或者在字串中嵌入變數。

// 字串中嵌入變數
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
// 字串中呼叫函式
` ${fn()}

在平時的開發中,除了上面程式碼中的應用,很多地方會用到模板字串,比如拼接一個DOM串,在Emotion/styled中定義DOM結構等,都會用到模板字串。不過在模板字串中定義DOM元素就不會有程式碼提示了。

在使用模板字串時,需要注意以下幾點:

  • 如果在字串中使用反引號,需要使用\來轉義。
  • 如果在多行字串中有空格和縮排,那麼它們都會被保留在輸出中。
  • 模板字串中嵌入變數,需要將變數名寫在${}之中。
  • 模板字串中可以放任意的表示式,也可以進行運算,以及引用物件的屬性,甚至可以呼叫函式。
  • 如果模板字元中的變數沒有宣告,會報錯。

4、函式預設引數

在ES6之前,函式是不支援預設引數的,ES6實現了對此的支援,並且只有不傳入引數時才會觸發預設值:

function getPoint(x = 0, y = 0) {
  console.log(x, y);
}
getPoint(1, 2);   // 1  2
getPoint()        // 0  0 
getPoint(1)       // 1  0

當使用函式預設值時,需要注意以下幾點:

(1)函式length屬性值

函式length屬性通常用來表示函式引數的個數,當引入函式預設值之後,length表示的就是第一個有預設值引數之前的普通引數個數:

const funcA = function(x, y) {};
console.log(funcA.length);  // 輸出結果:2 
const funcB = function(x, y = 1) {};
console.log(funcB.length);  // 輸出結果:1
const funcC = function(x = 1, y) {};
console.log(funcC.length);  // 輸出結果 0

(2)引數作用域

當給函式的引數設定了預設值之後,引數在被初始化時將形成一個獨立作用域,初始化完成後作用域消解:

let x = 1;
function func(x, y = x) {
  console.log(y);
}
func(2);

這裡最終會打印出2。在函式呼叫時,引數 x, y 將形成一個獨立的作用域,所以引數中的y會等於第一個引數中的x,而不是上面定義的1。

5、箭頭函式

ES6中引入了箭頭函式,用來簡化函式的定義:

const counter = (x, y) => x + y;

相對於普通函式,箭頭函式有以下特點:

(1)更加簡潔

  • 如果沒有引數,就直接寫一個空括號即可。
  • 如果只有一個引數,可以省去引數的括號。
  • 如果有多個引數,用逗號分割。
  • 如果函式體的返回值只有一句,可以省略大括號。
// 1. 不傳入引數
const funcA = () => console.log('funcA');
// 等價於
const funcA = function() {
  console.log('funcA');
} 
// 2. 傳入引數
const funcB = (x, y) => x + y;
// 等價於
const funcB = function(x, y) {
  return x + y;
} 
// 3. 單個引數的簡化
const funcC = (x) => x;
// 對於單個引數,可以去掉 (),簡化為
const funcC = x => x;
// 等價於
const funcC = function(x) {
  return x;
}
// 4. 上述程式碼函式體只有單條語句,如果有多條,需要使用 {}
const funcD = (x, y) => { console.log(x, y); return x + y; }
// 等價於
const funcD = function(x, y) {
  console.log(x, y);
  return x + y;
}

(2)不繫結 this

箭頭函式不會建立自己的this, 所以它沒有自己的this,它只會在自己作用域的上一層繼承this。所以箭頭函式中this的指向在它在定義時已經確定了,之後不會改變。

var id = 'GLOBAL';
var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }
};
obj.a();    // 'OBJ'
obj.b();    // 'GLOBAL'
new obj.a()  // undefined
new obj.b()  // Uncaught TypeError: obj.b is not a constructor

物件obj的方法b是使用箭頭函式定義的,這個函式中的this就永遠指向它定義時所處的全域性執行環境中的this,即便這個函式是作為物件obj的方法呼叫,this依舊指向Window物件。需要注意,定義物件的大括號{}是無法形成一個單獨的執行環境的,它依舊是處於全域性執行環境中。

同樣,使用call()、apply()、bind()等方法也不能改變箭頭函式中this的指向:

var id = 'Global';
let fun1 = () => {
    console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'

(3)不可作為建構函式

建構函式 new 操作符的執行步驟如下:

  1. 建立一個物件。
  2. 將建構函式的作用域賦給新物件(也就是將物件的__proto__屬性指向建構函式的prototype屬性)。
  3. 指向建構函式中的程式碼,建構函式中的this指向該物件(也就是為這個物件新增屬性和方法)。
  4. 返回新的物件。

實際上第二步就是將函式中的this指向該物件。但是由於箭頭函式時沒有自己的this的,且this指向外層的執行環境,且不能改變指向,所以不能當做建構函式使用。

(4)不繫結 arguments

箭頭函式沒有自己的arguments物件。在箭頭函式中訪問arguments實際上獲得的是它外層函式的arguments值。

6、擴充套件運算子

擴充套件運算子:...  就像是rest引數的逆運算,將一個數組轉為用逗號分隔的引數序列,對陣列進行解包。

spread 擴充套件運算子有以下用途:

(1)將陣列轉化為用逗號分隔的引數序列:

function  test(a,b,c){
    console.log(a); // 1
    console.log(b); // 2
    console.log(c); // 3
}
var arr = [1, 2, 3];
test(...arr);

(2)將一個數組拼接到另一個數組:

var arr1 = [1, 2, 3,4];
var arr2 = [...arr1, 4, 5, 6];
console.log(arr2);  // [1, 2, 3, 4, 4, 5, 6]

(3)將字串轉為逗號分隔的陣列:

var str='JavaScript';
var arr= [...str];
console.log(arr); // ["J", "a", "v", "a", "S", "c", "r", "i", "p", "t"]

7、Symbol

ES6中引入了一個新的基本資料型別Symbol,表示獨一無二的值。它是一種類似於字串的資料型別,它的特點如下:

  • Symbol的值是唯一的,用來解決命名衝突的問題。
  • Symbol值不能與其他型別資料進行運算。
  • Symbol定義的物件屬性不能使用for...in​遍歷迴圈,但是可以使用Reflect.ownKeys 來獲取物件的所有鍵名。
let s1 = Symbol();
console.log(typeof s1); // "symbol"
let s2 = Symbol('hello');
let s3 = Symbol('hello');
console.log(s2 === s3); // false

基於以上特性,Symbol 屬性型別比較適合用於兩類場景中:常量值和物件屬性。

(1)避免常量值重複

getValue 函式會根據傳入字串引數 key 執行對應程式碼邏輯:

function getValue(key) {
  switch(key){
    case 'A':
      ...
    case 'B':
      ...
  }
}
getValue('B');

這段程式碼對呼叫者而言非常不友好,因為程式碼中使用了魔術字串(Magic string,指的是在程式碼之中多次出現、與程式碼形成強耦合的某一個具體的字串或者數值),導致呼叫 getValue 函式時需要檢視函式程式碼才能找到引數 key 的可選值。所以可以將引數 key 的值以常量的方式宣告:

const KEY = {
  alibaba: 'A',
  baidu: 'B',
}
function getValue(key) {
  switch(key){
    case KEY.alibaba:
      ...
    case KEY.baidu:
      ...
  }
}
getValue(KEY.baidu);

但這樣也並非完美,假設現在要在 KEY 常量中加入一個 key,根據對應的規則,很有可能會出現值重複的情況:

const KEY = {
  alibaba: 'A',
  baidu: 'B',
  tencent: 'B'
}

這就會出現問題:

getValue(KEY.baidu) // 等同於 getValue(KEY.tencent)

所以在這種場景下更適合使用 Symbol,不需要關心值本身,只關心值的唯一性:

const KEY = {
  alibaba: Symbol(),
  baidu: Symbol(),
  tencent: Symbol()
}

(2)避免物件屬性覆蓋

函式 fn 需要對傳入的物件引數新增一個臨時屬性 user,但可能該物件引數中已經有這個屬性了,如果直接賦值就會覆蓋之前的值。此時就可以使用 Symbol 來避免這個問題。建立一個 Symbol 資料型別的變數,然後將該變數作為物件引數的屬性進行賦值和讀取,這樣就能避免覆蓋的情況:

function fn(o) { // {user: {id: xx, name: yy}}
  const s = Symbol()
  o[s] = 'zzz'
}

8、集合 Set

ES6提供了新的資料結構Set(集合)。它類似於陣列,但是成員的值都是唯一的,集合實現了iterator介面,所以可以使用擴充套件運算子和 for…of 進行遍歷。

Set的屬性和方法:

屬性和方法

概述

size

返回集合的元素個數

add

增加一個新的元素,返回當前的集合

delete

刪除元素,返回布林值

has

檢查集合中是否包含某元素,返回布林值

clear

清空集合,返回undefined

//建立一個空集合
let s = new Set();
//建立一個非空集合
let s1 = new Set([1,2,3,1,2,3]);
//返回集合的元素個數
console.log(s1.size);       // 3
//新增新元素
console.log(s1.add(4));     // {1,2,3,4}
//刪除元素
console.log(s1.delete(1));  //true
//檢測是否存在某個值
console.log(s1.has(2));     // true
//清空集合
console.log(s1.clear());    //undefined

由於集合中元素的唯一性,所以在實際應用中,可以使用set來實現陣列去重:

let arr = [1,2,3,2,1]
Array.from(new Set(arr))  // {1, 2, 3}

這裡使用了Array.form()方法來將陣列集合轉化為陣列。

可以通過set來求兩個陣列的交集和並集:

// 模擬求交集 
let intersection = new Set([...set1].filter(x => set2.has(x)));
// 模擬求差集
let difference = new Set([...set1].filter(x => !set2.has(x)));

用以下方法可以進行陣列與集合的相互轉化:

// Set集合轉化為陣列
const arr = [...mySet]
const arr = Array.from(mySet)

// 陣列轉化為Set集合
const mySet = new Set(arr)

9、Map

ES6提供了Map資料結構,它類似於物件,也是鍵值隊的集合,但是它的鍵值的範圍不限於字串,可以是任何型別(包括物件)的值,也就是說, Object 結構提供了“ 字串—值” 的對應, Map 結構提供了“ 值—值” 的對應, 是一種更完善的 Hash 結構實現。如果需要“ 鍵值對” 的資料結構, Map 比 Object 更合適。Map也實現了iterator介面,所以可以使用擴充套件運算子和 for…of 進行遍歷。

Map的屬性和方法:

屬性和方法

概述

size

返回Map的元素個數

set

增加一個新的元素,返回當前的Map

get

返回鍵名物件的鍵值

has

檢查Map中是否包含某元素,返回布林值

clear

清空Map,返回undefined

//建立一個空 map
let m = new Map();
//建立一個非空 map
let m2 = new Map([
 ['name', 'hello'],
]);
//獲取對映元素的個數
console.log(m2.size);          // 1
//新增對映值
console.log(m2.set('age', 6)); // {"name" => "hello", "age" => 6}
//獲取對映值
console.log(m2.get('age'));    // 6
//檢測是否有該對映
console.log(m2.has('age'));    // true
//清除
console.log(m2.clear());       // undefined

需要注意, 只有對同一個物件的引用, Map 結構才將其視為同一個鍵:

let map = new Map(); 
map.set(['a'], 555); 
map.get(['a']) // undefined

上面程式碼的set和get方法, 表面是針對同一個鍵, 但實際上這是兩個值, 記憶體地址是不一樣的, 因此get方法無法讀取該鍵, 所以會返回undefined。

由上可知, Map 的鍵實際上是跟記憶體地址繫結的, 只要記憶體地址不一樣, 就視為兩個鍵。這就解決了同名屬性碰撞( clash) 的問題,在擴充套件庫時, 如果使用物件作為鍵名, 就不用擔心自己的屬性與原來的屬性同名。

如果 Map 的鍵是一個簡單型別的值( 數字、 字串、 布林值), 則只要兩個值嚴格相等, Map 將其視為一個鍵, 包括0和 - 0。另外, 雖然NaN不嚴格相等於自身, 但 Map 將其視為同一個鍵。

let map = new Map(); 
map.set(NaN, 123); 
map.get(NaN) // 123 
map.set(-0, 123); 
map.get(+0) // 123

10、模組化

ES6中首次引入模組化開發規範ES Module,讓Javascript首次支援原生模組化開發。ES Module把一個檔案當作一個模組,每個模組有自己的獨立作用域,那如何把每個模組聯絡起來呢?核心點就是模組的匯入與匯出。

(1)export 匯出模組

  • 正常匯出:
// 方式一
export var first = 'test';
export function func() {
    return true;
}
// 方式二
var first = 'test';
var second = 'test';
function func() {
    return true;
}
export {first, second, func};
  • as關鍵字:
var first = 'test';
export {first as second};

as關鍵字可以重新命名暴露出的變數或方法,經過重新命名後同一變數可以多次暴露出去。

  • export default

export default會匯出預設輸出,即使用者不需要知道模組中輸出的名字,在匯入的時候為其指定任意名字。

// 匯出
export default function () {
  console.log('foo');
}
// 匯入
import customName from './export-default';

注意:匯入預設模組時不需要大括號,匯出預設的變數或方法可以有名字,但是對外無效。export default只能使用一次。

(2)import 匯入模組

  • 正常匯入:
import {firstName, lastName, year} from './profile';
複製程式碼

匯入模組位置可以是相對路徑也可以是絕對路徑,.js可以省略,如果不帶路徑只是模組名,則需要通過配置檔案告訴引擎查詢的位置。

  • as關鍵字:
import { lastName as surname } from './profile';

import 命令會被提升到模組頭部,所以寫的位置不是那麼重要,但是不能使用表示式和變數來進行匯入。

  • 載入整個模組(無輸出)
import 'lodash'; //僅僅是載入而已,無法使用
  • 載入整個模組(有輸出)
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));

注意:import * 會忽略default輸出

(3)匯入匯出複合用法

  • 先匯入後匯出
export { foo, bar } from 'my_module';
// 等同於
import { foo, bar } from 'my_module';
export { foo, boo};
  • 整體先匯入再輸出以及default
// 整體輸出
export * from 'my_module';
// 匯出default,正如前面所說,export default 其實匯出的是default變數
export { default } from 'foo';
// 具名介面改default
export { es6 as default } from './someModule';

(4)模組的繼承

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}

注意:export * 會忽略default。

11、字串方法

(1)includes()

includes():該方法用於判斷字串是否包含指定的子字串。如果找到匹配的字串則返回 true,否則返回 false。該方法的語法如下:

string.includes(searchvalue, start)

該方法有兩個引數:

  • searchvalue:必需,要查詢的字串。
  • start:可選,設定從那個位置開始查詢,預設為 0。
let str = 'Hello world!';
str.includes('o')  // 輸出結果:true
str.includes('z')  // 輸出結果:false
str.includes('e', 2)  // 輸出結果:false

(2)startsWith()

startsWith():該方法用於檢測字串是否以指定的子字串開始。如果是以指定的子字串開頭返回 true,否則 false。其語法和上面的includes()方法一樣。

let str = 'Hello world!';
str.startsWith('Hello') // 輸出結果:true
str.startsWith('Helle') // 輸出結果:false
str.startsWith('wo', 6) // 輸出結果:true

(3)endsWith()

endsWith():該方法用來判斷當前字串是否是以指定的子字串結尾。如果傳入的子字串在搜尋字串的末尾則返回 true,否則將返回 false。其語法如下:

string.endsWith(searchvalue, length)

該方法有兩個引數:

  • searchvalue:必需,要搜尋的子字串。
  • length:設定字串的長度,預設值為原始字串長度 string.length。
let str = 'Hello world!';
str.endsWith('!')       // 輸出結果:true
str.endsWith('llo')     // 輸出結果:false
str.endsWith('llo', 5)  // 輸出結果:true

可以看到,當第二個引數設定為5時,就會從字串的前5個字元中進行檢索,所以會返回true。

(4)repeat()

repeat() 方法返回一個新字串,表示將原字串重複n次:

'x'.repeat(3)     // 輸出結果:"xxx"
'hello'.repeat(2) // 輸出結果:"hellohello"
'na'.repeat(0)    // 輸出結果:""

如果引數是小數,會向下取整:

'na'.repeat(2.9) // 輸出結果:"nana"

如果引數是負數或者Infinity,會報錯:

'na'.repeat(Infinity)   // RangeError
'na'.repeat(-1)         // RangeError

如果引數是 0 到-1 之間的小數,則等同於 0,這是因為會先進行取整運算。0 到-1 之間的小數,取整以後等於-0,repeat視同為 0。

'na'.repeat(-0.9)   // 輸出結果:""

如果引數是NaN,就等同於 0:

'na'.repeat(NaN)    // 輸出結果:""

如果repeat的引數是字串,則會先轉換成數字。

'na'.repeat('na')   // 輸出結果:""
'na'.repeat('3')    // 輸出結果:"nanana"

12、陣列方法

(1)reduce()

reduce() 方法對陣列中的每個元素執行一個reducer函式(升序執行),將其結果彙總為單個返回值。其使用語法如下:

arr.reduce(callback,[initialValue])

reduce 為陣列中的每一個元素依次執行回撥函式,不包括陣列中被刪除或從未被賦值的元素,接受四個引數:初始值(或者上一次回撥函式的返回值),當前元素值,當前索引,呼叫 reduce 的陣列。

(1) callback (執行陣列中每個值的函式,包含四個引數。)

  • previousValue (上一次呼叫回撥返回的值,或者是提供的初始值(initialValue))。
  • currentValue (陣列中當前被處理的元素)。
  • index (當前元素在陣列中的索引)。
  • array (呼叫 reduce 的陣列)。

(2) initialValue (作為第一次呼叫 callback 的第一個引數。)

let arr = [1, 2, 3, 4]
let sum = arr.reduce((prev, cur, index, arr) => {
    console.log(prev, cur, index);
    return prev + cur;
})
console.log(arr, sum);

輸出結果如下:

1 2 1
3 3 2
6 4 3
[1, 2, 3, 4] 10

再來加一個初始值看看:

let arr = [1, 2, 3, 4]
let sum = arr.reduce((prev, cur, index, arr) => {
    console.log(prev, cur, index);
    return prev + cur;
}, 5)
console.log(arr, sum);

輸出結果如下:

5 1 0
6 2 1
8 3 2
11 4 3
[1, 2, 3, 4] 15

通過上面例子,可以得出結論:如果沒有提供initialValue,reduce 會從索引1的地方開始執行 callback 方法,跳過第一個索引。如果提供initialValue,從索引0開始。

注意,該方法如果新增初始值,就會改變原陣列,將這個初始值放在陣列的最後一位。

(2)filter()

filter()方法用於過濾陣列,滿足條件的元素會被返回。它的引數是一個回撥函式,所有陣列元素依次執行該函式,返回結果為true的元素會被返回。該方法會返回一個新的陣列,不會改變原陣列。

let arr = [1, 2, 3, 4, 5]
arr.filter(item => item > 2) 
// 結果:[3, 4, 5]

可以使用filter()方法來移除陣列中的undefined、null、NAN等值。

let arr = [1, undefined, 2, null, 3, false, '', 4, 0]
arr.filter(Boolean)
// 結果:[1, 2, 3, 4]

(3)Array.from

Array.from 的設計初衷是快速基於其他物件建立新陣列,準確來說就是從一個類似陣列的可迭代物件中建立一個新的陣列例項。其實,只要一個物件有迭代器,Array.from 就能把它變成一個數組(注意:該方法會返回一個的陣列,不會改變原物件)。

從語法上看,Array.from 有 3 個引數:

  • 類似陣列的物件,必選。
  • 加工函式,新生成的陣列會經過該函式的加工再返回。
  • this 作用域,表示加工函式執行時 this 的值。

這三個引數裡面第一個引數是必選的,後兩個引數都是可選的:

var obj = {0: 'a', 1: 'b', 2:'c', length: 3};
Array.from(obj, function(value, index){
  console.log(value, index, this, arguments.length);
  return value.repeat(3);   //必須指定返回值,否則返回 undefined
}, obj);

結果如圖:

以上結果表明,通過 Array.from 這個方法可以自定義加工函式的處理方式,從而返回想要得到的值;如果不確定返回值,則會返回 undefined,最終生成的是一個包含若干個 undefined 元素的空陣列。

實際上,如果這裡不指定 this,加工函式就可以是一個箭頭函式。上述程式碼可以簡寫為以下形式。

Array.from(obj, (value) => value.repeat(3));
//  控制檯列印 (3) ["aaa", "bbb", "ccc"]

除了上述 obj 物件以外,擁有迭代器的物件還包括 String、Set、Map 等,Array.from 都可以進行處理:

// String
Array.from('abc');                             // ["a", "b", "c"]
// Set
Array.from(new Set(['abc', 'def']));           // ["abc", "def"]
// Map
Array.from(new Map([[1, 'ab'], [2, 'de']]));   // [[1, 'ab'], [2, 'de']]

(1)fill()

使用fill()方法可以向一個已有陣列中插入全部或部分相同的值,開始索引用於指定開始填充的位置,它是可選的。如果不提供結束索引,則一直填充到陣列末尾。如果是負值,則將從負值加上陣列的長度而得到的值開始。該方法的語法如下:

array.fill(value, start, end)

其引數如下:

  • value:必需。填充的值。
  • start:可選。開始填充位置。
  • end:可選。停止填充位置 (預設為 array .length)。

使用示例如下:

const arr = [0, 0, 0, 0, 0];
// 用5填充整個陣列
arr.fill(5);
console.log(arr); // [5, 5, 5, 5, 5]
arr.fill(0);      // 重置
// 用5填充索引大於等於3的元素
arr.fill(5, 3);
console.log(arr); // [0, 0, 0, 5, 5]
arr.fill(0);      // 重置
// 用5填充索引大於等於1且小於等於3的元素
arr.fill(5, 3);
console.log(arr); // [0, 5, 5, 0, 0]
arr.fill(0);      // 重置
// 用5填充索引大於等於-1的元素
arr.fill(5, -1);
console.log(arr); // [0, 0, 0, 0, 5]
arr.fill(0);      // 重置

二、ES7 新特性(2016)

1、Array.includes()

includes() 方法用來判斷一個數組是否包含一個指定的值,如果包含則返回 true,否則返回false。該方法不會改變原陣列。其語法如下:

arr.includes(searchElement, fromIndex)

該方法有兩個引數:

  • searchElement:必須,需要查詢的元素值。
  • fromIndex:可選,從fromIndex 索引處開始查詢目標值。如果為負值,則按升序從 array.length + fromIndex 的索引開始搜 (即使從末尾開始往前跳 fromIndex 的絕對值個索引,然後往後搜尋)。預設為 0。
[1, 2, 3].includes(2);  //  true
[1, 2, 3].includes(4);  //  false
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

在 ES7 之前,通常使用 indexOf 來判斷陣列中是否包含某個指定值。但 indexOf 在語義上不夠明確直觀,同時 indexOf 內部使用 === 來判等,所以存在對 NaN 的誤判,includes 則修復了這個問題:

[1, 2, NaN].indexOf(NaN);   // -1
[1, 2, NaN].includes(NaN);  //  true

注意:使用includes()比較字串和字元時區分大小寫。

2、指數操作符

ES7 還引入了指數操作符 ,用來更為方便的進行指數計算,它與 Math.pow() 等效:

Math.pow(2, 10));  // 1024
2**10;           // 1024

三、ES8 新特性(2017)

1、padStart()和padEnd()

padStart()和padEnd()方法用於補齊字串的長度。如果某個字串不夠指定長度,會在頭部或尾部補全。

(1)padStart()

padStart()用於頭部補全。該方法有兩個引數,其中第一個引數是一個數字,表示字串補齊之後的長度;第二個引數是用來補全的字串。

如果原字串的長度,等於或大於指定的最小長度,則返回原字串:

'x'.padStart(1, 'ab') // 'x'

如果用來補全的字串與原字串,兩者的長度之和超過了指定的最小長度,則會截去超出位數的補全字串:

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

如果省略第二個引數,預設使用空格補全長度:

'x'.padStart(4, 'ab') // 'a   '

padStart()的常見用途是為數值補全指定位數,筆者最近做的一個需求就是將返回的頁數補齊為三位,比如第1頁就顯示為001,就可以使用該方法來操作:

"1".padStart(3, '0')   // 輸出結果: '001'
"15".padStart(3, '0')  // 輸出結果: '015'

(2)padEnd()

padEnd()用於尾部補全。該方法也是接收兩個引數,第一個引數是字串補全生效的最大長度,第二個引數是用來補全的字串:

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

2、Object.values()和Object.entries()

在ES5中就引入了Object.keys方法,在ES8中引入了跟Object.keys配套的Object.values和Object.entries,作為遍歷一個物件的補充手段,供for...of迴圈使用。它們都用來遍歷物件,它會返回一個由給定物件的自身可列舉屬性(不含繼承的和Symbol屬性)組成的陣列,陣列元素的排列順序和正常迴圈遍歷該物件時返回的順序一致,這個三個元素返回的值分別如下:

  • Object.keys():返回包含物件鍵名的陣列。
  • Object.values():返回包含物件鍵值的陣列。
  • Object.entries():返回包含物件鍵名和鍵值的陣列。
let obj = { 
  id: 1, 
  name: 'hello', 
  age: 18 
};
console.log(Object.keys(obj));   // 輸出結果: ['id', 'name', 'age']
console.log(Object.values(obj)); // 輸出結果: [1, 'hello', 18]
console.log(Object.entries(obj));   // 輸出結果: [['id', 1], ['name', 'hello'], ['age', 18]

注意:

  • Object.keys()方法返回的陣列中的值都是字串,也就是說不是字串的key值會轉化為字串。
  • 結果陣列中的屬性值都是物件本身 可列舉的屬性 ,不包括繼承來的屬性。

3、函式擴充套件

ES2017 規定函式的引數列表的結尾可以為逗號:

function person( name, age, sex, ) {}

該特性的主要作用是方便使用git進行多人協作開發時修改同一個函式減少不必要的行變更。

4、Object.values

之前可以通過 Object.keys 來獲取一個物件所有的 key。在ES8中提供了 Object.values 來獲取物件所有的 value 值:

const person = {
  name: "zhangsan",
  age: 18,
  height: 188,
};

console.log(Object.values(person)); // ['zhangsan', 18, 188]

四、ES9 新特性(2018)

1、for await…of

for await...of方法被稱為非同步迭代器,該方法是主要用來遍歷非同步物件。

for await...of 語句會在非同步或者同步可迭代物件上建立一個迭代迴圈,包括 String,Array,類陣列,Map, Set和自定義的非同步或者同步可迭代物件。這個語句只能在 async function內使用:

function Gen (time) {
  return new Promise((resolve,reject) => {
    setTimeout(function () {
       resolve(time)
    },time)
  })
}
async function test () {
   let arr = [Gen(2000),Gen(100),Gen(3000)]
   for await (let item of arr) {
      console.log(Date.now(),item)
   }
}
test()

輸出結果:

2、Promise.finally

ES2018 為 Promise 添加了 finally() 方法,表示無論 Promise 例項最終成功或失敗都會執行的方法:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const one = '1';
    reject(one);
  }, 1000);
});
promise
  .then(() => console.log('success'))
  .catch(() => console.log('fail'))
  .finally(() => console.log('finally'))

finally() 函式不接受引數,finally() 內部通常不知道 promise 例項的執行結果,所以通常在 finally() 方法內執行的是與 promise 狀態無關的操作。

3、物件的擴充套件運算子

在ES6中就引入了擴充套件運算子,但是它只能作用於陣列,ES2018中的擴充套件運算子可以作用於物件:

(1)將元素組織成物件

const obj = {a: 1, b: 2, c: 3};
const {a, ...rest} = obj;
console.log(rest);    // 輸出 {b: 2, c: 3}
(function({a, ...obj}) {
  console.log(obj);    // 輸出 {b: 2, c: 3}
}({a: 1, b: 2, c: 3}));

(2)將物件擴充套件為元素

const obj = {a: 1, b: 2, c: 3};
const newObj ={...obj, d: 4};
console.log(newObj);  // 輸出 {a: 1, b: 2, c: 3, d: 4}

(3)可以用來合併物件

const obj1 = {a: 1, b:2};
const obj2 = {c: 3, d:4};
const mergedObj = {...obj1, ...obj2};
console.log(mergedObj);  // 輸出 {a: 1, b: 2, c: 3, d: 4}

4、物件的 Rest

在物件的解構中,除了已經指定的屬性之外,rest將會拷貝物件其他的所有可列舉屬性:

const obj = {foo: 1, bar: 2, baz: 3};
const {foo, ...rest} = obj;
console.log(rest); // {bar: 2, baz: 3}

如果用在函式引數中,rest 表示所有剩下的引數:

function func({param1, ...rest}) {
    return rest;
}
console.log(func({param1:1, b:2, c:3, d:4}))  // {b: 2, c: 3, d: 4}

注意,在物件字面量中,rest運算子只能放在物件的最頂層,並且只能使用一次,要放在最後:

const {...rest, foo} = obj; // Uncaught SyntaxError: Rest element must be last element
const {foo, ...rest1, ...rest2} = obj; // Uncaught SyntaxError: Rest element must be last element

五、ES10 新特性(2019)

1、trimStart() 和 trimEnd()

在ES10之前,JavaScript提供了trim()方法,用於移除字串首尾空白符。在ES9中提出了trimStart()和trimEnd() 方法用於移除字串首尾的頭尾空白符,空白符包括:空格、製表符 tab、換行符等其他空白符等。

(1)trimStart()

trimStart() 方法的的行為與trim()一致,不過會返回一個從原始字串的開頭刪除了空白的新字串,不會修改原始字串:

const s = '  abc  ';
s.trimStart()   // "abc  "

(2)trimStart()

trimEnd() 方法的的行為與trim()一致,不過會返回一個從原始字串的結尾刪除了空白的新字串,不會修改原始字串:

const s = '  abc  ';

s.trimEnd()   // "  abc"

注意,這兩個方法都不適用於null、undefined、Number型別。

2. flat()和flatMap()

(1)flat()

在ES2019中,flat()方法用於建立並返回一個新陣列,這個新陣列包含與它呼叫flat()的陣列相同的元素,只不過其中任何本身也是陣列的元素會被打平填充到返回的陣列中:

[1, [2, 3]].flat()        // [1, 2, 3]
[1, [2, [3, 4]]].flat()   // [1, 2, [3, 4]]

在不傳引數時,flat()預設只會打平一級巢狀,如果想要打平更多的層級,就需要傳給flat()一個數值引數,這個引數表示要打平的層級數:

[1, [2, [3, 4]]].flat(2)  // [1, 2, 3, 4]

如果陣列中存在空項,會直接跳過:

[1, [2, , 3]].flat());    //  [1, 2, 3]

如果傳入的引數小於等於0,就會返回原陣列:

[1, [2, [3, [4, 5]]]].flat(0);    //  [1, [2, [3, [4, 5]]]]
[1, [2, [3, [4, 5]]]].flat(-10);  //  [1, [2, [3, [4, 5]]]]

(2)flatMap()

flatMap()方法使用對映函式對映每個元素,然後將結果壓縮成一個新陣列。它與 map 和連著深度值為1的 flat 幾乎相同,但 flatMap 通常在合併成一種方法的效率稍微高一些。該方法會返回一個新的陣列,其中每個元素都是回撥函式的結果,並且結構深度 depth 值為1。

[1, 2, 3, 4].flatMap(x => x * 2);      //  [2, 4, 6, 8]
[1, 2, 3, 4].flatMap(x => [x * 2]);    //  [2, 4, 6, 8]
[1, 2, 3, 4].flatMap(x => [[x * 2]]);  //  [[2], [4], [6], [8]]
[1, 2, 3, 4].map(x => [x * 2]);        //  [[2], [4], [6], [8]]

3、Object.fromEntries()

Object.fromEntries()方法可以把鍵值對列表轉換為一個物件。該方法相當於 Object.entries() 方法的逆過程。Object.entries()方法返回一個給定物件自身可列舉屬性的鍵值對陣列,而Object.fromEntries() 方法把鍵值對列表轉換為一個物件。

const object = { key1: 'value1', key2: 'value2' }
const array = Object.entries(object)  // [ ["key1", "value1"], ["key2", "value2"] ]
Object.fromEntries(array)             // { key1: 'value1', key2: 'value2' }

使用該方法主要有以下兩個用途:

(1)將陣列轉成物件

const entries = [
  ['foo', 'bar'],
  ['baz', 42]
]
Object.fromEntries(entries)  //  { foo: "bar", baz: 42 }

(2)將 Map 轉成物件

const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
])
Object.fromEntries(entries)  //  { foo: "bar", baz: 42 }

4、Symbol描述

通過 Symbol() 建立符號時,可以通過引數提供字串作為描述:

let dog = Symbol("dog");  // dog 為描述

在 ES2019 之前,獲取一個 Symbol 值的描述需要通過 String 方法 或 toString 方法:

String(dog);              // "Symbol(dog)" 
dog.toString();           // "Symbol(dog)"

ES2019 補充了屬性 description,用來直接訪問 描述

dog.description;  // dog

5、toString()

ES2019 對函式的 toString() 方法進行了擴充套件,以前這個方法只會輸出函式程式碼,但會省略註釋和空格。ES2019 的 toString()則會保留註釋、空格等,即輸出的是原始程式碼:

function sayHi() {
  /* dog */
  console.log('wangwang');
}
sayHi.toString();  // 將輸出和上面一樣的原始程式碼

6、catch

在 ES2019 以前,catch 會帶有引數,但是很多時候 catch 塊是多餘的。而現在可以不帶引數:

// ES2019 之前
try {
   ...
} catch(error) {
   ...
}
// ES2019 之後
try {
   ...
} catch {
   ...
}

六、ES11 新特性(2020)

1、BigInt

在 JavaScript 中,數值型別 Number 是 64 位浮點數,所以計算精度和表示範圍都有一定限制。ES2020 新增了 BigInt 資料型別,這也是 JavaScript 引入的第八種基本型別。BigInt 可以表示任意大的整數。其語法如下:

BigInt(value);

其中 value 是建立物件的數值。可以是字串或者整數。

在 JavaScript 中,Number 基本型別可以精確表示的最大整數是253。因此早期會有這樣的問題:

let max = Number.MAX_SAFE_INTEGER;    // 最大安全整數
let max1 = max + 1
let max2 = max + 2
max1 === max2   // true

有了BigInt之後,這個問題就不復存在了:

let max = BigInt(Number.MAX_SAFE_INTEGER);
let max1 = max + 1n
let max2 = max + 2n
max1 === max2   // false

可以通過typeof操作符來判斷變數是否為BigInt型別(返回字串"bigint"):

typeof 1n === 'bigint'; // true 
typeof BigInt('1') === 'bigint'; // true

還可以通過Object.prototype.toString方法來判斷變數是否為BigInt型別(返回字串"[object BigInt]"):

Object.prototype.toString.call(10n) === '[object BigInt]';    // true

注意,BigInt 和 Number 不是嚴格相等的,但是寬鬆相等:

10n === 10 // false 
10n == 10  // true

Number 和 BigInt 可以進行比較:

1n < 2;    // true 
2n > 1;    // true 
2 > 2;     // false 
2n > 2;    // false 
2n >= 2;   // true

2、空值合併運算子(??)

在編寫程式碼時,如果某個屬性不為 null 和 undefined,那麼就獲取該屬性,如果該屬性為 null 或 undefined,則取一個預設值:

const name = dogName ? dogName : 'default';

可以通過 || 來簡化:

const name =  dogName || 'default';

但是 || 的寫法存在一定的缺陷,當 dogName 為 0 或 false 的時候也會走到 default 的邏輯。所以 ES2020 引入了 ?? 運算子。只有 ?? 左邊為 null 或 undefined時才返回右邊的值:

const dogName = false; 
const name =  dogName ?? 'default';  // name = false;

3、可選鏈操作符(?.)

在開發過程中,我們經常需要獲取深層次屬性,例如 system.user.addr.province.name。但在獲取 name 這個屬性前需要一步步的判斷前面的屬性是否存在,否則並會報錯:

const name = (system && system.user && system.user.addr && system.user.addr.province && system.user.addr.province.name) || 'default';

為了簡化上述過程,ES2020 引入了「鏈判斷運算子」?.,可選鏈操作符( ?. )允許讀取位於連線物件鏈深處的屬性的值,而不必明確驗證鏈中的每個引用是否有效。?. 操作符的功能類似於 . 鏈式操作符,不同之處在於,在引用為null 或 undefined 的情況下不會引起錯誤,該表示式短路返回值是 undefined。與函式呼叫一起使用時,如果給定的函式不存在,則返回 undefined。

const name = system?.user?.addr?.province?.name || 'default';

當嘗試訪問可能不存在的物件屬性時,可選鏈操作符將會使表示式更短、更簡明。在探索一個物件的內容時,如果不能確定哪些屬性必定存在,可選鏈操作符也是很有幫助的。

可選鏈有以下三種形式:

a?.[x]
// 等同於
a == null ? undefined : a[x]
a?.b()
// 等同於
a == null ? undefined : a.b()
a?.()
// 等同於
a == null ? undefined : a()

在使用TypeScript開發時,這個操作符可以解決很多問題。

4、Promise.allSettled

Promise.allSettled 的引數接受一個 Promise 的陣列,返回一個新的 Promise。唯一的不同在於,執行完之後不會失敗,也就是說當 Promise.allSettled 全部處理完成後,我們可以拿到每個 Promise 的狀態,而不管其是否處理成功。

下面使用 allSettled 實現的一段程式碼:

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回結果:
// [
//    { status: 'fulfilled', value: 2 },
//    { status: 'rejected', reason: -1 }
// ]

可以看到,Promise.allSettled 最後返回的是一個數組,記錄傳進來的引數中每個 Promise 的返回值,這就是和 all 方法不太一樣的地方。你也可以根據 all 方法提供的業務場景的程式碼進行改造,其實也能知道多個請求發出去之後,Promise 最後返回的是每個引數的最終狀態。

5、String.matchAll()

matchAll() 是新增的字串方法,它返回一個包含所有匹配正則表示式的結果及分組捕獲組的迭代器。因為返回的是遍歷器,所以通常使用for...of迴圈取出。

for (const match of 'abcabc'.matchAll(/a/g)) {
    console.log(match)
}
//["a", index: 0, input: "abcabc", groups: undefined]
//["a", index: 3, input: "abcabc", groups: undefined]

需要注意,該方法的第一個引數是一個正則表示式物件,如果傳的引數不是一個正則表示式物件,則會隱式地使用 new RegExp(obj) 將其轉換為一個 RegExp 。另外,RegExp必須是設定了全域性模式g的形式,否則會丟擲異常 TypeError。

七、ES12 新特性(2021)

1、String.replaceAll()

replaceAll()方法會返回一個全新的字串,所有符合匹配規則的字元都將被替換掉,替換規則可以是字串或者正則表示式。

let string = 'hello world, hello ES12'
string.replace(/hello/g,'hi')    // hi world, hi ES12
string.replaceAll('hello','hi')  // hi world, hi ES12

注意的是,replaceAll 在使用正則表示式的時候,如果非全域性匹配(/g),會丟擲異常:

let string = 'hello world, hello ES12'
string.replaceAll(/hello/,'hi') 
// Uncaught TypeError: String.prototype.replaceAll called with a non-global

2、數字分隔符

數字分隔符可以在數字之間建立視覺化分隔符,通過 _下劃線來分割數字,使數字更具可讀性,可以放在數字內的任何地方:

const money = 1_000_000_000
//等價於
const money = 1000000000

該新特性同樣支援在八進位制數中使用:

const number = 0o123_456
//等價於
const number = 0o123456

3、Promise.any

Promise.any是是 ES2021 新增的特性,它接收一個 Promise 可迭代物件(例如陣列),只要其中的一個 promise 成功,就返回那個已經成功的 promise 如果可迭代物件中沒有一個 promise 成功(即所有的 promises 都失敗/拒絕),就返回一個失敗的 promise 和 AggregateError 型別的例項,它是 Error 的一個子類,用於把單一的錯誤集合在一起

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.resolve('result'),
]
Promise.any(promises).then((value) => {
  console.log('value: ', value)
}).catch((err) => {
  console.log('err: ', err)
})
// 輸出結果:value:  result

如果所有傳入的 promises 都失敗:

const promises = [
  Promise.reject('ERROR A'),
  Promise.reject('ERROR B'),
  Promise.reject('ERROR C'),
]
Promise.any(promises).then((value) => {
  console.log('value:', value)
}).catch((err) => {
  console.log('err:', err)
  console.log(err.message)
  console.log(err.name)
  console.log(err.errors)
})

輸出結果:

err:AggregateError: All promises were rejected
All promises were rejected
AggregateError
["ERROR A", "ERROR B", "ERROR C"]

4、邏輯賦值操作符

ES12中新增了幾個邏輯賦值操作符,可以用來簡化一些表示式:

// 等同於 a = a || b
a ||= b;
// 等同於 c = c && d
c &&= d;
// 等同於 e = e ?? f
e ??= f;

八、ES13 新特性(2022)

1、Object.hasOwn()

在ES2022之前,可以使用 Object.prototype.hasOwnProperty() 來檢查一個屬性是否屬於物件。

Object.hasOwn 特性是一種更簡潔、更可靠的檢查屬性是否直接設定在物件上的方法:

const example = {
  property: '123'
};
console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property'));

2、Array.at()

at() 是一個數組方法,用於通過給定索引來獲取陣列元素。當給定索引為正時,這種新方法與使用括號表示法訪問具有相同的行為。當給出負整數索引時,就會從陣列的最後一項開始檢索:

const array = [0,1,2,3,4,5];
console.log(array[array.length-1]);  // 5
console.log(array.at(-1));  // 5
console.log(array[array.lenght-2]);  // 4
console.log(array.at(-2));  // 4

除了陣列,字串也可以使用at()方法進行索引:

const str = "hello world";
console.log(str[str.length - 1]);  // d
console.log(str.at(-1));  // d

3、error.cause

在 ECMAScript 2022 規範中,new Error() 中可以指定導致它的原因:

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        // ···
      } catch (error) {
        throw new Error(
          `While processing ${filePath}`,
          {cause: error}
        );
      }
    });
}

4、Top-level Await

在ES2017中,引入了 async 函式和 await 關鍵字,以簡化 Promise 的使用,但是 await 關鍵字只能在 async 函式內部使用。嘗試在非同步函式之外使用 await 就會報錯:SyntaxError - SyntaxError: await is only valid in async function。

頂層 await 允許我們在 async 函式外面使用 await 關鍵字。它允許模組充當大型非同步函式,通過頂層 await,這些 ECMAScript 模組可以等待資源載入。這樣其他匯入這些模組的模組在執行程式碼之前要等待資源載入完再去執行。

由於 await 僅在 async 函式中可用,因此模組可以通過將程式碼包裝在 async 函式中來在程式碼中包含 await:

// a.js
  import fetch  from "node-fetch";
  let users;
  export const fetchUsers = async () => {
    const resp = await fetch('https://jsonplaceholder.typicode.com/users');
    users =  resp.json();
  }
  fetchUsers();
  export { users };
  // usingAwait.js
  import {users} from './a.js';
  console.log('users: ', users);
  console.log('usingAwait module');

我們還可以立即呼叫頂層async函式(IIAFE):

import fetch  from "node-fetch";
  (async () => {
    const resp = await fetch('https://jsonplaceholder.typicode.com/users');
    users = resp.json();
  })();
  export { users };

這樣會有一個缺點,直接匯入的 users 是 undefined,需要在非同步執行完成之後才能訪問它:

// usingAwait.js
import {users} from './a.js';
console.log('users:', users); // undefined
setTimeout(() => {
  console.log('users:', users);
}, 100);
console.log('usingAwait module');

當然,這種方法並不安全,因為如果非同步函式執行花費的時間超過100毫秒, 它就不會起作用了,users 仍然是 undefined。

另一個方法是匯出一個 promise,讓匯入模組知道資料已經準備好了:

//a.js
import fetch  from "node-fetch";
export default (async () => {
  const resp = await fetch('https://jsonplaceholder.typicode.com/users');
  users = resp.json();
})();
export { users };
//usingAwait.js
import promise, {users} from './a.js';
promise.then(() => { 
  console.log('usingAwait module');
  setTimeout(() => console.log('users:', users), 100); 
});

雖然這種方法似乎是給出了預期的結果,但是有一定的侷限性:匯入模組必須瞭解這種模式才能正確使用它。

而頂層await就可以解決這些問題:

// a.js
  const resp = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = resp.json();
  export { users};
  // usingAwait.js
  import {users} from './a.mjs';
  console.log(users);
  console.log('usingAwait module');

頂級 await 在以下場景中將非常有用:

  • 動態載入模組:
const strings = await import(`/i18n/${navigator.language}`);
  • 資源初始化:
const connection = await dbConnector();
  • 依賴回退:
let translations;
try {
  translations = await import('https://app.fr.json');
} catch {
  translations = await import('https://fallback.en.json');
}