????????
一、ES6 新特性(2015)
1. let和const
在ES6中,新增了let和const關鍵字,其中 let 主要用來聲明變量,而 const 通常用來聲明常量。let、const相對于var關鍵字有以下特點:
特性 | var | let | const |
---|---|---|---|
變量提升 | ?? | × | × |
全局變量 | ?? | × | × |
重復聲明 | ?? | × | × |
重新賦值 | ?? | ?? | × |
暫時性死區 | × | ?? | ?? |
塊作用域 | × | ?? | ?? |
只聲明不初始化 | ?? | ?? | × |
這里主要介紹其中的四點:
(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);?//?輸出結果:1const?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?initializationlet?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 nulllet?{?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?//?輸出結果:truelet?{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);??//?輸出結果:1const?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 操作符的執行步驟如下:
-
創建一個對象
-
將構造函數的作用域賦給新對象(也就是將對象的__proto__屬性指向構造函數的prototype屬性)
-
指向構造函數中的代碼,構造函數中的this指向該對象(也就是為這個對象添加屬性和方法)
-
返回新的對象
實際上第二步就是將函數中的this指向該對象。但是由于箭頭函數時沒有自己的this的,且this指向外層的執行環境,且不能改變指向,所以不能當做構造函數使用。
?
(4)不綁定 arguments
箭頭函數沒有自己的arguments對象。在箭頭函數中訪問arguments實際上獲得的是它外層函數的arguments值。
6. 擴展運算符
擴展運算符:... ?就像是rest參數的逆運算,將一個數組轉為用逗號分隔的參數序列,對數組進行解包。
spread 擴展運算符有以下用途:
(1)將數組轉化為用逗號分隔的參數序列:
function??test(a,b,c){console.log(a);?//?1console.log(b);?//?2console.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進行多人協作開發時修改同一個函數減少不必要的行變更。
?
(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進行多人協作開發時修改同一個函數減少不必要的行變更。
?
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?+?2max1?===?max2???//?true
有了BigInt之后,這個問題就不復存在了:
let?max?=?BigInt(Number.MAX_SAFE_INTEGER);let?max1?=?max?+?1n
let?max2?=?max?+?2nmax1?===?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. at()
at()
?是一個數組方法,用于通過給定索引來獲取數組元素。當給定索引為正時,這種新方法與使用括號表示法訪問具有相同的行為。當給出負整數索引時,就會從數組的最后一項開始檢索:
const?array?=?[0,1,2,3,4,5];console.log(array[array.length-1]);??//?5
console.log(array.at(-1));??//?5console.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.jsimport?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.jsimport?{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);?//?undefinedsetTimeout(()?=>?{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.jsconst?resp?=?await?fetch('https://jsonplaceholder.typicode.com/users');const?users?=?resp.json();export?{?users};//?usingAwait.jsimport?{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');
}
九、ES14 新特性(2023)
1. 從尾到頭搜索數組
概述
在 JavaScript 中,通過?find()
?和?findIndex()
??查找數組中的值是一種常見做法。不過,這些方法從數組的開始進行遍歷:
?
const?array?=?[{v:?1},?{v:?2},?{v:?3},?{v:?4},?{v:?5}];array.find(elem?=>?elem.v?>?3);?//?{v:?4}
array.findIndex(elem?=>?elem.v?>?3);?//?3
如果要從數組的末尾開始遍歷,就必須反轉數組并使用上述方法。這樣做就需要一個額外的數組操作。findLast()
?和?findLastIndex()
?的就解決了這一問題。提出這兩個方法的一個重要原因就是:語義。
使用
它們的用法和find()
、findIndex()
類似,唯一不同的是它們是?從后向前?遍歷數組,這兩個方法適用于數組和類數組。
-
findLast()
?會返回第一個查找到的元素,如果沒有找到,就會返回?undefined
; -
findLastIndex()
?會返回第一個查找到的元素的索引。如果沒有找到,就會返回 -1;
const?array?=?[{v:?1},?{v:?2},?{v:?3},?{v:?4},?{v:?5}];array.findLast(elem?=>?elem.v?>?3);?//?{v:?5}
array.findLastIndex(elem?=>?elem.v?>?3);?//?4
array.findLastIndex(elem?=>?elem.v?>?5);?//?undefined
2. 通過副本更改數組
通過副本更改數組的方法有四個:
-
Array.prototype.toReversed()
-
Array.prototype.toSorted()
-
Array.prototype.toSpliced()
-
Array.prototype.with()
我們知道,大多數的數組方法都是非破壞性的,也就是不會改變原數組,比如?filter()
?方法:
const?arr?=?['a',?'b',?'b',?'a'];
const?result?=?arr.filter(x?=>?x?!==?'b');
console.log(result);?//?['a',?'a']
當然,也有一些是破壞性的方法,它們在執行時會改變原數組,比如?sort()
?方法:
const?arr?=?['c',?'a',?'b'];
const?result?=?arr.sort();
console.log(result);?//?['a',?'b',?'c']
在數組的方法中,下面的方法是具有破壞性的:
-
reverse()
-
sort()
-
splice()
如果想要這些數組方法應用于數組而不改變它,可以使用下面任意一種形式:
const?sorted1?=?arr.slice().sort();
const?sorted2?=?[...arr].sort();
const?sorted3?=?Array.from(arr).sort();
可以看到,我們首先需要創建數組的副本,再對這個副本進行修改。因此就引入了這三個方法的非破壞性版本,因此不需要手動創建副本再進行操作:
-
reverse()
?的非破壞性版本:toReversed()
-
sort()
?非破壞性版本:toSorted(compareFn)
-
splice()
?非破壞性版本:toSpliced(start, deleteCount, ...items)
這些函數屬性引入到了?Array.prototype
:
-
Array.prototype.toReversed()
?-> Array -
Array.prototype.toSorted(compareFn)
?-> Array -
Array.prototype.toSpliced(start, deleteCount, ...items)
?-> Array -
Array.prototype.with(index, value)
?-> Array
除此之外,還有了一個新的非破壞性方法:with()
。該方法會以非破壞性的方式替換給定?index
?處的數組元素,即?arr[index]=value
?的非破壞性版本。
所有這些方法都將保持目標數組不變,并返回它的副本并執行更改。這些方法適用于數組,也適用于類型化數組,即以下類的實例:
-
Int8Array
-
Uint8Array
-
Uint8ClampedArray
-
Int16Array
-
Uint16Array
-
Int32Array
-
Uint32Array
-
Float32Array
-
Float64Array
-
BigInt64Array
-
BigUint64Array
TypedArray是一種通用的固定長度緩沖區類型,允許讀取緩沖區中的二進制數據。其在WEBGL規范中被引入用于解決Javascript處理二進制數據的問題。類型化數組也是數組,只不過其元素被設置為特定類型的值。
類型化數組的核心就是一個名為 ArrayBuffer 的類型。每個ArrayBuffer對象表示的只是內存中指定的字節數,但不會指定這些字節用于保存什么類型的數據。通過ArrayBuffer能做的就是為了將來使用而分配一定數量的字節。
這些方法也適用于元組,元組相當于不可變的數組。它們擁有數組的所有方法——除了破壞性的方法。因此,將后者的非破壞性版本添加到數組對元組是有幫助的,這意味著我們可以使用相同的方法來非破壞性地更改數組和元組。
Array.prototype.toReversed()
toReversed()
?是?reverse()
?方法的非破壞性版本:
const?arr?=?['a',?'b',?'c'];
const?result?=?arr.toReversed();
console.log(result);?//?['c',?'b',?'a']
console.log(arr);????//?['a',?'b',?'c']
Array.prototype.toSorted()
toSorted()
?是?sort()
?方法的非破壞性版本:
const?arr?=?['c',?'a',?'b'];
const?result?=?arr.toSorted();
console.log(result);??//?['a',?'b',?'c']
console.log(arr);?????//?['c',?'a',?'b']
Array.prototype.toSpliced()
splice()
?方法比其他幾種方法都復雜,其使用形式:splice(start, deleteCount, ...items)
。該方法會從從?start
?索引處開始刪除?deleteCount
個元素,然后在?start
?索引處開始插入item
?中的元素,最后返回已經刪除的元素。
toSpliced
?是?splice()
?方法的非破壞性版本,它會返回更新后的數組,原數組不會變化,并且無法再得到已經刪除的元素:
const?arr?=?['a',?'b',?'c',?'d'];
const?result?=?arr.toSpliced(1,?2,?'X');
console.log(result);?//?['a',?'X',?'d']
console.log(arr);????//?['a',?'b',?'c',?'d']
Array.prototype.with()
.with()
方法的使用形式:.with(index, value)
,它是?arr[index] = value
?的非破壞性版本:
const?arr?=?['a',?'b',?'c'];
const?result?=?arr.with(1,?'X');
console.log(result);??//?['a',?'X',?'c']
console.log(arr);?????//?['a',?'b',?'c']