一、介紹
現在的網路上已經有各樣關於 ECMAScript 規範介紹和分析的文章,而我自己重新學習一遍這些規範,整理出這麼一份筆記,比較精簡,主要內容涵蓋ES6、ES7、ES8、ES9,後續會增加面試題,框架入門等筆記,歡迎吐槽交流。
這份資料的ES6部分將會參考阮一峰老師的 ECMAScript6入門 ,精簡和整理出快速實用的內容。
另外ES7/ES8/ES9則會從網路綜合參考和整理。
ES全稱ECMAScript:
目前JavaScript使用的ECMAScript版本為ECMAScript-262。
ECMAScript版本 | 釋出時間 | 新增特性 |
---|---|---|
ECMAScript 2009(ES5) | 2009年11月 | 擴充套件了Object、Array、Function的功能等 |
ECMAScript 2015(ES6) | 2015年6月 | 類,模組化,箭頭函式,函式引數預設值等 |
ECMAScript 2016(ES7) | 2016年3月 | includes,指數操作符 |
ECMAScript 2017(ES8) | 2017年6月 | async/await,Object.values(),Object.entries(),St123 ; b['myfun'] => 'hi'ring padding等 |
本文部落格 CuteECMAScript
本文開源地址 CuteECMAScript
個人部落格 ping'anの部落格
二、正文
1. ES6
1.1 let 和 const命令
在ES6中,我們通常實用 let
表示變數,const
表示常量,並且 let
和 const
都是塊級作用域,且在當前作用域有效不能重複宣告。
1.1.1 let 命令
let
命令的用法和 var
相似,但是 let
只在所在程式碼塊內有效。
基礎用法:
{
let a = 1;
let b = 2;
}
複製程式碼
並且 let
有以下特點:
- 不存在變數提升:
在ES6之前,我們var
宣告一個變數一個函式,都會伴隨著變數提升的問題,導致實際開發過程經常出現一些邏輯上的疑惑,按照一般思維習慣,變數都是需要先宣告後使用。
// var
console.log(v1); // undefined
var v1 = 2;
// 由於變數提升 程式碼實際如下
var v1;
console.log(v1)
v1 = 2;
// let
console.log(v2); // ReferenceError
let v2 = 2;
複製程式碼
- 不允許重複宣告:
let
和const
在相同作用域下,都不能重複宣告同一變數,並且不能在函式內重新宣告引數。
// 1. 不能重複宣告同一變數
// 報錯
function f1 (){
let a = 1;
var a = 2;
}
// 報錯
function f2 (){
let a = 1;
let a = 2;
}
// 2. 不能在函式內重新宣告引數
// 報錯
function f3 (a1){
let a1;
}
// 不報錯
function f4 (a2){
{
let a2
}
}
複製程式碼
1.1.2 const 命令
const
宣告一個只讀的常量。
基礎用法:
const PI = 3.1415926;
console.log(PI); // 3.1415926
複製程式碼
注意點:
const
宣告後,無法修改值;
const PI = 3.1415926;
PI = 3;
// TypeError: Assignment to constant variable.
複製程式碼
const
宣告時,必須賦值;
const a ;
// SyntaxError: Missing initializer in const declaration.
複製程式碼
const
宣告的常量,let
不能重複宣告;
const PI = 3.1415926;
let PI = 0;
// Uncaught SyntaxError: Identifier 'PI' has already been declared
複製程式碼
1.2 變數的解構賦值
解構賦值概念:在ES6中,直接從陣列和物件中取值,按照對應位置,賦值給變數的操作。
1.2.1 陣列
基礎用法:
// ES6 之前
let a = 1;
let b = 2;
// ES6 之後
let [a, b] = [1, 2];
複製程式碼
本質上,只要等號兩邊模式一致,左邊變數即可獲取右邊對應位置的值,更多用法:
let [a, [[b], c]] = [1, [[2], 3]];
console.log(a, b, c); // 1, 2, 3
let [ , , c] = [1, 2, 3];
console.log(c); // 3
let [a, , c] = [1, 2, 3];
console.log(a,c); // 1, 3
let [a, ...b] = [1, 2, 3];
console.log(a,b); // 1, [2,3]
let [a, b, ..c.] = [1];
console.log(a, b, c); // 1, undefined, []
複製程式碼
注意點:
- 如果解構不成功,變數的值就等於
undefined
。
let [a] = []; // a => undefined
let [a, b] = [1]; // a => 1 , b => undefined
複製程式碼
- 當左邊模式多於右邊,也可以解構成功。
let [a, b] = [1, 2, 3];
console.log(a, b); // 1, 2
複製程式碼
- 兩邊模式不同,報錯。
let [a] = 1;
let [a] = false;
let [a] = NaN;
let [a] = undefined;
let [a] = null;
let [a] = {};
複製程式碼
指定解構的預設值:
基礎用法:
let [a = 1] = []; // a => 1
let [a, b = 2] = [a]; // a => 1 , b => 2
複製程式碼
特殊情況:
let [a = 1] = [undefined]; // a => 1
let [a = 1] = [null]; // a => null
複製程式碼
右邊模式對應的值,必須嚴格等於undefined
,預設值才能生效,而null
不嚴格等於undefined
。
1.2.2 物件的解構賦值
與陣列解構不同的是,物件解構不需要嚴格按照順序取值,而只要按照變數名去取對應屬性名的值,若取不到對應屬性名的值,則為undefined
。
基礎用法:
let {a, b} = {a:1, b:2}; // a => 1 , b => 2
let {a, b} = {a:2, b:1}; // a => 2 , b => 1
let {a} = {a:3, b:2, c:1};// a => 3
let {a} = {b:2, c:1}; // a => undefined
複製程式碼
注意點:
- 若變數名和屬性名不一致,則需要修改名稱。
let {a:b} = {a:1, c:2};
// error: a is not defined
// b => 1
複製程式碼
物件的解構賦值的內部機制,是先找到同名屬性,然後再賦給對應的變數。真正被賦值的是後者,而不是前者。
上面程式碼中,a
是匹配的模式,b
才是變數。真正被賦值的是變數b
,而不是模式a
。
- 物件解構也支援巢狀解構。
let obj = {
a:[ 1, { b: 2}]
};
let {a, a: [c, {b}]} = obj;
// a=>[1, {b: 2}], b => 2, c => 1
複製程式碼
指定解構的預設值:
let {a=1} = {}; // a => 1
let {a, b=1} = {a:2}; // a => 2, b => 1
let {a:b=3} = {}; // b => 3
let {a:b=3} = {a:4}; // b = >4
// a是模式,b是變數 牢記
let {a=1} = {a:undefined}; // a => 1
let {a=1} = {a:null}; // a => null
// 因為null與undefined不嚴格相等,所以賦值有效
// 導致預設值1不會生效。
複製程式碼
1.2.3 字串的解構賦值
字串的解構賦值中,字串被轉換成了一個類似陣列的物件。 基礎用法:
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length:len} = 'hello';// len => 5
複製程式碼
1.2.4 數值和布林值的解構賦值
解構賦值的規則是,只要等號右邊的值不是物件或陣列,就先將其轉為物件。由於undefined
和null
無法轉為物件,所以對它們進行解構賦值,都會報錯。
// 數值和布林值的包裝物件都有toString屬性
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
複製程式碼
1.2.5 函式引數的解構賦值
基礎用法:
function fun ([a, b]){
return a + b;
}
fun ([1, 2]); // 3
複製程式碼
指定預設值的解構:
function fun ({a=0, b=0} = {}){
return [a, b];
}
fun ({a:1, b:2}); // [1, 2]
fun ({a:1}); // [1, 0]
fun ({}); // [0, 0]
fun (); // [0, 0]
function fun ({a, b} = {a:0, b:0}){
return [a, b];
}
fun ({a:1, b:2}); // [1, 2]
fun ({a:1}); // [1, undefined]
fun ({}); // [undefined, undefined]
fun (); // [0, 0]
複製程式碼
1.2.6 應用
- 交換變數的值:
let a = 1,b = 2;
[a, b] = [b, a]; // a =>2 , b => 1
複製程式碼
- 函式返回多個值:
// 返回一個陣列
function f (){
return [1, 2, 3];
}
let [a, b, c] = f(); // a=>1, b=>2, c=>3
// 返回一個物件
function f (){
return {a:1, b:2};
}
let {a, b} = f(); // a=>1, b=>2
複製程式碼
- 快速對應引數: 快速的將一組引數與變數名對應。
function f([a, b, c]) {...}
f([1, 2, 3]);
function f({a, b, c}) {...}
f({b:2, c:3, a:1});
複製程式碼
- 提取JSON資料:
let json = {
name : 'leo',
age: 18
}
let {name, age} = json;
console.log(name,age); // leo, 18
複製程式碼
- 遍歷Map結構:
const m = new Map();
m.set('a',1);
m.set('b',2);
for (let [k, v] of m){
console.log(k + ' : ' + v);
}
// 獲取鍵名
for (let [k] of m){...}
// 獲取鍵值
for (let [,k] of m){...}
複製程式碼
- 輸入模組的指定方法: 用於按需載入模組中需要用到的方法。
const {log, sin, cos} = require('math');
複製程式碼
1.3 字串的擴充
1.3.1 includes(),startsWith(),endsWith()
在我們判斷字串是否包含另一個字串時,ES6之前,我們只有typeof
方法,ES6之後我們又多了三種方法:
- includes():返回布林值,表示是否找到引數字串。
- startsWith():返回布林值,表示引數字串是否在原字串的頭部。
- endsWith():返回布林值,表示引數字串是否在原字串的尾部。
let a = 'hello leo';
a.startsWith('leo'); // false
a.endsWith('o'); // true
a.includes('lo'); // true
複製程式碼
並且這三個方法都支援第二個引數,表示起始搜尋的位置。
let a = 'hello leo';
a.startsWith('leo',1); // false
a.endsWith('o',5); // true
a.includes('lo',6); // false
複製程式碼
endsWith
是針對前 n
個字元,而其他兩個是針對從第n
個位置直到結束。
1.3.2 repeat()
repeat
方法返回一個新字串,表示將原字串重複n
次。
基礎用法:
'ab'.repeat(3); // 'ababab'
'ab'.repeat(0); // ''
複製程式碼
特殊用法:
- 引數為
小數
,則取整
'ab'.repeat(2.3); // 'abab'
複製程式碼
- 引數為
負數
或Infinity
,則報錯
'ab'.repeat(-1); // RangeError
'ab'.repeat(Infinity); // RangeError
複製程式碼
- 引數為
0到-1的小數
或NaN
,則取0
'ab'.repeat(-0.5); // ''
'ab'.repeat(NaN); // ''
複製程式碼
- 引數為
字串
,則轉成數字
'ab'.repeat('ab'); // ''
'ab'.repeat('3'); // 'ababab'
複製程式碼
1.3.3 padStart(),padEnd()
用於將字串頭部或尾部補全長度,padStart()
為頭部補全,padEnd()
為尾部補全。
這兩個方法接收2個引數,第一個指定字串最小長度,第二個用於補全的字串。
基礎用法 :
'x'.padStart(5, 'ab'); // 'ababx'
'x'.padEnd(5, 'ab'); // 'xabab'
複製程式碼
特殊用法:
- 原字串長度,大於或等於指定最小長度,則返回原字串。
'xyzabc'.padStart(5, 'ab'); // 'xyzabc'
複製程式碼
- 用來補全的字串長度和原字串長度之和,超過指定最小長度,則截去超出部分的補全字串。
'ab'.padStart(5,'012345'); // "012ab"
複製程式碼
- 省略第二個引數,則用
空格
補全。
'x'.padStart(4); // ' x'
'x'.padEnd(4); // 'x '
複製程式碼
1.3.4 模版字串
用於拼接字串,ES6之前:
let a = 'abc' +
'def' +
'ghi';
複製程式碼
ES6之後:
let a = `
abc
def
ghi
`
複製程式碼
拼接變數:
在**反引號(`)**中使用${}
包裹變數或方法。
// ES6之前
let a = 'abc' + v1 + 'def';
// ES6之後
let a = `abc${v1}def`
複製程式碼
1.4 正則的擴充
1.4.1 介紹
在ES5中有兩種情況。
- 引數是字串,則第二個引數為正規表示式的修飾符。
let a = new RegExp('abc', 'i');
// 等價於
let a = /abx/i;
複製程式碼
- 引數是正規表示式,返回一個原表示式的拷貝,且不能有第二個引數,否則報錯。
let a = new RegExp(/abc/i);
//等價於
let a = /abx/i;
let a = new RegExp(/abc/, 'i');
// Uncaught TypeError
複製程式碼
ES6中使用:
第一個引數是正則物件,第二個是指定修飾符,如果第一個引數已經有修飾符,則會被第二個引數覆蓋。
new RegExp(/abc/ig, 'i');
複製程式碼
1.4.2 字串的正則方法
常用的四種方法:match()
、replace()
、search()
和split()
。
1.4.3 u修飾符
新增u
修飾符,是為了處理大於uFFFF
的Unicode字元,即正確處理四個位元組的UTF-16編碼。
/^\uD83D/u.test('\uD83D\uDC2A'); // false
/^\uD83D/.test('\uD83D\uDC2A'); // true
複製程式碼
由於ES5之前不支援四個位元組UTF-16編碼,會識別為兩個字元,導致第二行輸出true
,加入u
修飾符後ES6就會識別為一個字元,所以輸出false
。
注意:
加上u
修飾符後,會改變下面正規表示式的行為:
- (1)點字元
點字元(
.
)在正則中表示除了換行符以外的任意單個字元。對於碼點大於0xFFFF
的Unicode字元,點字元不能識別,必須加上u
修飾符。
var a = "?";
/^.$/.test(a); // false
/^.$/u.test(a); // true
複製程式碼
- (2)Unicode字元表示法
使用ES6新增的大括號表示Unicode字元時,必須在表示式新增
u
修飾符,才能識別大括號。
/\u{61}/.test('a'); // false
/\u{61}/u.test('a'); // true
/\u{20BB7}/u.test('?'); // true
複製程式碼
- (3)量詞
使用
u
修飾符後,所有量詞都會正確識別碼點大於0xFFFF
的 Unicode 字元。
/a{2}/.test('aa'); // true
/a{2}/u.test('aa'); // true
/?{2}/.test('??'); // false
/?{2}/u.test('??'); // true
複製程式碼
- (4)i修飾符
不加
u
修飾符,就無法識別非規範的K
字元。
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
複製程式碼
檢查是否設定u
修飾符:
使用unicode
屬性。
const a = /hello/;
const b = /hello/u;
a.unicode // false
b.unicode // true
複製程式碼
1.4.4 y修飾符
y
修飾符與g
修飾符類似,也是全域性匹配,後一次匹配都是從上一次匹配成功的下一個位置開始。區別在於,g
修飾符只要剩餘位置中存在匹配即可,而y
修飾符是必須從剩餘第一個開始。
var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"] 剩餘 '_aa_a'
r2.exec(s) // null
複製程式碼
lastIndex
屬性:
指定匹配的開始位置:
const a = /a/y;
a.lastIndex = 2; // 從2號位置開始匹配
a.exec('wahaha'); // null
a.lastIndex = 3; // 從3號位置開始匹配
let c = a.exec('wahaha');
c.index; // 3
a.lastIndex; // 4
複製程式碼
返回多個匹配:
一個y
修飾符對match
方法只能返回第一個匹配,與g
修飾符搭配能返回所有匹配。
'a1a2a3'.match(/a\d/y); // ["a1"]
'a1a2a3'.match(/a\d/gy); // ["a1", "a2", "a3"]
複製程式碼
檢查是否使用y
修飾符:
使用sticky
屬性檢查。
const a = /hello\d/y;
a.sticky; // true
複製程式碼
1.4.5 flags屬性
flags
屬性返回所有正規表示式的修飾符。
/abc/ig.flags; // 'gi'
複製程式碼
1.5 數值的擴充
1.5.1 Number.isFinite(), Number.isNaN()
Number.isFinite()
用於檢查一個數值是否是有限的,即不是Infinity
,若引數不是Number
型別,則一律返回false
。
Number.isFinite(10); // true
Number.isFinite(0.5); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('leo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
Number.isFinite(Math.random()); // true
複製程式碼
Number.isNaN()
用於檢查是否是NaN
,若引數不是NaN
,則一律返回false
。
Number.isNaN(NaN); // true
Number.isNaN(10); // false
Number.isNaN('10'); // false
Number.isNaN(true); // false
Number.isNaN(5/NaN); // true
Number.isNaN('true' / 0); // true
Number.isNaN('true' / 'true'); // true
複製程式碼
區別:
與傳統全域性的isFinite()
和isNaN()
方法的區別,傳統的這兩個方法,是先將引數轉換成數值,再判斷。
而ES6新增的這兩個方法則只對數值有效, Number.isFinite()
對於非數值一律返回false
,Number.isNaN()
只有對於NaN
才返回true
,其他一律返回false
。
isFinite(25); // true
isFinite("25"); // true
Number.isFinite(25); // true
Number.isFinite("25"); // false
isNaN(NaN); // true
isNaN("NaN"); // true
Number.isNaN(NaN); // true
Number.isNaN("NaN"); // false
複製程式碼
1.5.2 Number.parseInt(), Number.parseFloat()
這兩個方法與全域性方法parseInt()
和parseFloat()
一致,目的是逐步減少全域性性的方法,讓語言更模組化。
parseInt('12.34'); // 12
parseFloat('123.45#'); // 123.45
Number.parseInt('12.34'); // 12
Number.parseFloat('123.45#'); // 123.45
Number.parseInt === parseInt; // true
Number.parseFloat === parseFloat; // true
複製程式碼
1.5.3 Number.isInteger()
用來判斷一個數值是否是整數,若引數不是數值,則返回false
。
Number.isInteger(10); // true
Number.isInteger(10.0); // true
Number.isInteger(10.1); // false
複製程式碼
1.5.4 Math物件的擴充
ES6新增17個數學相關的靜態方法,只能在Math物件上呼叫。
- Math.trunc:
用來去除小數的小數部分,返回整數部分。
若引數為非數值,則先轉為數值。
若引數為空值或無法擷取整數的值,則返回NaN。
// 正常使用
Math.trunc(1.1); // 1
Math.trunc(1.9); // 1
Math.trunc(-1.1); // -1
Math.trunc(-1.9); // -1
Math.trunc(-0.1234); // -0
// 引數為非數值
Math.trunc('11.22'); // 11
Math.trunc(true); // 1
Math.trunc(false); // 0
Math.trunc(null); // 0
// 引數為空和無法取整
Math.trunc(NaN); // NaN
Math.trunc('leo'); // NaN
Math.trunc(); // NaN
Math.trunc(undefined); // NaN
複製程式碼
ES5實現:
Math.trunc = Math.trunc || function(x){
return x < 0 ? Math.ceil(x) : Math.floor(x);
}
複製程式碼
- Math.sign():
判斷一個數是正數、負數還是零,對於非數值,會先轉成數值。
返回值:- 引數為正數, 返回 +1
- 引數為負數, 返回 -1
- 引數為0, 返回 0
- 引數為-0, 返回 -0
- 引數為其他值, 返回 NaN
Math.sign(-1); // -1
Math.sign(1); // +1
Math.sign(0); // 0
Math.sign(-0); // -0
Math.sign(NaN); // NaN
Math.sign(''); // 0
Math.sign(true); // +1
Math.sign(false);// 0
Math.sign(null); // 0
Math.sign('9'); // +1
Math.sign('leo');// NaN
Math.sign(); // NaN
Math.sign(undefined); // NaN
複製程式碼
ES5實現
Math.sign = Math.sign || function (x){
x = +x;
if (x === 0 || isNaN(x)){
return x;
}
return x > 0 ? 1: -1;
}
複製程式碼
- Math.cbrt():
用來計算一個數的立方根,若引數為非數值則先轉成數值。
Math.cbrt(-1); // -1
Math.cbrt(0); // 0
Math.cbrt(1); // 1
Math.cbrt(2); // 1.2599210498
Math.cbrt('1'); // 1
Math.cbrt('leo'); // NaN
複製程式碼
ES5實現
Math.cbrt = Math.cbrt || function (x){
var a = Math.pow(Math.abs(x), 1/3);
return x < 0 ? -y : y;
}
複製程式碼
- Math.clz32():
用於返回一個數的 32 位無符號整數形式有多少個前導 0。
Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2
複製程式碼
- Math.imul():
用於返回兩個數以 32 位帶符號整數形式相乘的結果,返回的也是一個 32 位的帶符號整數。
Math.imul(2, 4) // 8
Math.imul(-1, 8) // -8
Math.imul(-2, -2) // 4
複製程式碼
- Math.fround():
用來返回一個數的2位單精度浮點數形式。
Math.fround(0) // 0
Math.fround(1) // 1
Math.fround(2 ** 24 - 1) // 16777215
複製程式碼
- Math.hypot():
用來返回所有引數的平方和的平方根。
Math.hypot(3, 4); // 5
Math.hypot(3, 4, 5); // 7.0710678118654755
Math.hypot(); // 0
Math.hypot(NaN); // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5'); // 7.0710678118654755
Math.hypot(-3); // 3
複製程式碼
- Math.expm1():
用來返回ex - 1
,即Math.exp(x) - 1
。
Math.expm1(-1) // -0.6321205588285577
Math.expm1(0) // 0
Math.expm1(1) // 1.718281828459045
複製程式碼
ES5實現
Math.expm1 = Math.expm1 || function(x) {
return Math.exp(x) - 1;
};
複製程式碼
- Math.log1p():
用來返回1 + x
的自然對數,即Math.log(1 + x)
。如果x小於-1
,返回NaN
。
Math.log1p(1) // 0.6931471805599453
Math.log1p(0) // 0
Math.log1p(-1) // -Infinity
Math.log1p(-2) // NaN
複製程式碼
ES5實現
Math.log1p = Math.log1p || function(x) {
return Math.log(1 + x);
};
複製程式碼
- Math.log10():
用來返回以10
為底的x的對數
。如果x小於 0,則返回NaN
。
Math.log10(2) // 0.3010299956639812
Math.log10(1) // 0
Math.log10(0) // -Infinity
Math.log10(-2) // NaN
Math.log10(100000) // 5
複製程式碼
ES5實現
Math.log10 = Math.log10 || function(x) {
return Math.log(x) / Math.LN10;
};
複製程式碼
- Math.log2():
用來返回以2
為底的x的對數
。如果x
小於0
,則返回NaN
。
Math.log2(3) // 1.584962500721156
Math.log2(2) // 1
Math.log2(1) // 0
Math.log2(0) // -Infinity
Math.log2(-2) // NaN
Math.log2(1024) // 10
Math.log2(1 << 29) // 29
複製程式碼
ES5實現
Math.log2 = Math.log2 || function(x) {
return Math.log(x) / Math.LN2;
};
複製程式碼
- 雙曲函式方法:
Math.sinh(x)
返回x的雙曲正弦(hyperbolic sine)Math.cosh(x)
返回x的雙曲餘弦(hyperbolic cosine)Math.tanh(x)
返回x的雙曲正切(hyperbolic tangent)Math.asinh(x)
返回x的反雙曲正弦(inverse hyperbolic sine)Math.acosh(x)
返回x的反雙曲餘弦(inverse hyperbolic cosine)Math.atanh(x)
返回x的反雙曲正切(inverse hyperbolic tangent)
1.5.5 指數運算子
新增的指數運算子(**
):
2 ** 2; // 4
2 ** 3; // 8
2 ** 3 ** 2; // 相當於 2 ** (3 ** 2); 返回 512
複製程式碼
指數運算子(**
)與Math.pow
的實現不相同,對於特別大的運算結果,兩者會有細微的差異。
Math.pow(99, 99)
// 3.697296376497263e+197
99 ** 99
// 3.697296376497268e+197
複製程式碼
1.6 函式的擴充
1.6.1 引數預設值
// ES6 之前
function f(a, b){
b = b || 'leo';
console.log(a, b);
}
// ES6 之後
function f(a, b='leo'){
console.log(a, b);
}
f('hi'); // hi leo
f('hi', 'jack'); // hi jack
f('hi', ''); // hi leo
複製程式碼
注意:
- 引數變數是預設宣告的,不能用
let
和const
再次宣告:
function f (a = 1){
let a = 2; // error
}
複製程式碼
- 使用引數預設值時,引數名不能相同:
function f (a, a, b){ ... }; // 不報錯
function f (a, a, b = 1){ ... }; // 報錯
複製程式碼
與解構賦值預設值結合使用:
function f ({a, b=1}){
console.log(a,b)
};
f({}); // undefined 1
f({a:2}); // 2 1
f({a:2, b:3}); // 2 3
f(); // 報錯
function f ({a, b = 1} = {}){
console.log(a, b)
}
f(); // undefined 1
複製程式碼
尾引數定義預設值:
通常在尾引數定義預設值,便於觀察引數,並且非尾引數無法省略。
function f (a=1,b){
return [a, b];
}
f(); // [1, undefined]
f(2); // [2, undefined]
f(,2); // 報錯
f(undefined, 2); // [1, 2]
function f (a, b=1, c){
return [a, b, c];
}
f(); // [undefined, 1, undefined]
f(1); // [1,1,undefined]
f(1, ,2); // 報錯
f(1,undefined,2); // [1,1,2]
複製程式碼
在給引數傳遞預設值時,傳入undefined
會觸發預設值,傳入null
不會觸發。
function f (a = 1, b = 2){
console.log(a, b);
}
f(undefined, null); // 1 null
複製程式碼
函式的length屬性:
length
屬性將返回,沒有指定預設值的引數數量,並且rest引數不計入length
屬性。
function f1 (a){...};
function f2 (a=1){...};
function f3 (a, b=2){...};
function f4 (...a){...};
function f5 (a,b,...c){...};
f1.length; // 1
f2.length; // 0
f3.length; // 1
f4.length; // 0
f5.length; // 2
複製程式碼
1.6.2 rest 引數
rest
引數形式為(...變數名
),其值為一個陣列,用於獲取函式多餘引數。
function f (a, ...b){
console.log(a, b);
}
f(1,2,3,4); // 1 [2, 3, 4]
複製程式碼
注意:
rest
引數只能放在最後一個,否則報錯:
function f(a, ...b, c){...}; // 報錯
複製程式碼
- 函式的
length
屬性不包含rest
引數。
function f1 (a){...};
function f2 (a,...b){...};
f1(1); // 1
f2(1,2); // 1
複製程式碼
1.6.3 name 屬性
用於返回該函式的函式名。
function f (){...};
f.name; // f
const f = function g(){...};
f.name; // g
複製程式碼
1.6.4 箭頭函式
使用“箭頭”(=>
)定義函式。
基礎使用:
// 有1個引數
let f = v => v;
// 等同於
let f = function (v){return v};
// 有多個引數
let f = (v, i) => {return v + i};
// 等同於
let f = function (v, i){return v + i};
// 沒引數
let f = () => 1;
// 等同於
let f = function (){return 1};
複製程式碼
箭頭函式與變數結構結合使用:
// 正常函式寫法
function f (p) {
return p.a + ':' + p.b;
}
// 箭頭函式寫法
let f = ({a, b}) => a + ':' + b;
複製程式碼
簡化回撥函式:
// 正常函式寫法
[1, 2, 3].map(function (x){
return x * x;
})
// 箭頭函式寫法
[1, 2, 3].map(x => x * x);
複製程式碼
箭頭函式與rest引數結合:
let f = (...n) => n;
f(1, 2, 3); // [1, 2, 3]
複製程式碼
注意點:
- 1.箭頭函式內的
this
總是指向定義時所在的物件,而不是呼叫時。 - 2.箭頭函式不能當做建構函式,即不能用
new
命令,否則報錯。 - 3.箭頭函式不存在
arguments
物件,即不能使用,可以使用rest
引數代替。 - 4.箭頭函式不能使用
yield
命令,即不能用作Generator函式。
不適用場景:
- 1.在定義函式方法,且該方法內部包含
this
。
const obj = {
a:9,
b: () => {
this.a --;
}
}
複製程式碼
上述b
如果是普通函式,函式內部的this
指向obj
,但是如果是箭頭函式,則this
會指向全域性,不是預期結果。
- 2.需要動態
this
時。
let b = document.getElementById('myID');
b.addEventListener('click', ()=>{
this.classList.toggle('on');
})
複製程式碼
上訴按鈕點選會報錯,因為b
監聽的箭頭函式中,this
是全域性物件,若改成普通函式,this
就會指向被點選的按鈕物件。
1.6.5 雙冒號運算子
雙冒號暫時是一個提案,用於解決一些不適用的場合,取代call
、apply
、bind
呼叫。
雙冒號運算子(::
)的左邊是一個物件,右邊是一個函式。該運算子會自動將左邊的物件,作為上下文環境(即this
物件),繫結到右邊函式上。
f::b;
// 等同於
b.bind(f);
f::b(...arguments);
// 等同於
b.apply(f, arguments);
複製程式碼
若雙冒號左邊為空,右邊是一個物件的方法,則等於將該方法繫結到該物件上。
let f = a::a.b;
// 等同於
let f = ::a.b;
複製程式碼
1.7 陣列的擴充
1.7.1 擴充運算子
擴充運算子使用(...
),類似rest
引數的逆運算,將陣列轉為用(,
)分隔的引數序列。
console.log(...[1, 2, 3]); // 1 2 3
console.log(1, ...[2,3], 4); // 1 2 3 4
複製程式碼
擴充運算子主要使用在函式呼叫。
function f (a, b){
console.log(a, b);
}
f(...[1, 2]); // 1 2
function g (a, b, c, d, e){
console.log(a, b, c, d, e);
}
g(0, ...[1, 2], 3, ...[4]); // 0 1 2 3 4
複製程式碼
若擴充運算子後面是個空陣列,則不產生效果。
[...[], 1]; // [1]
複製程式碼
替代apply方法
// ES6之前
function f(a, b, c){...};
var a = [1, 2, 3];
f.apply(null, a);
// ES6之後
function f(a, b, c){...};
let a = [1, 2, 3];
f(...a);
// ES6之前
Math.max.apply(null, [3,2,6]);
// ES6之後
Math.max(...[3,2,6]);
複製程式碼
擴充運算子的運用
- (1)複製陣列:
通常我們直接複製陣列時,只是淺拷貝,如果要實現深拷貝,可以使用擴充運算子。
// 通常情況 淺拷貝
let a1 = [1, 2];
let a2 = a1;
a2[0] = 3;
console.log(a1,a2); // [3,2] [3,2]
// 擴充運算子 深拷貝
let a1 = [1, 2];
let a2 = [...a1];
// let [...a2] = a1; // 作用相同
a2[0] = 3;
console.log(a1,a2); // [1,2] [3,2]
複製程式碼
- (2)合併陣列:
注意,這裡合併陣列,只是淺拷貝。
let a1 = [1,2];
let a2 = [3];
let a3 = [4,5];
// ES5
let a4 = a1.concat(a2, a3);
// ES6
let a5 = [...a1, ...a2, ...a3];
a4[0] === a1[0]; // true
a5[0] === a1[0]; // true
複製程式碼
- (3)與解構賦值結合:
與解構賦值結合生成陣列,但是使用擴充運算子需要放到引數最後一個,否則報錯。
let [a, ...b] = [1, 2, 3, 4];
// a => 1 b => [2,3,4]
let [a, ...b] = [];
// a => undefined b => []
let [a, ...b] = ["abc"];
// a => "abc" b => []
複製程式碼
1.7.2 Array.from()
將 類陣列物件 和 可遍歷的物件,轉換成真正的陣列。
// 類陣列物件
let a = {
'0':'a',
'1':'b',
length:2
}
let arr = Array.from(a);
// 可遍歷的物件
let a = Array.from([1,2,3]);
let b = Array.from({length: 3});
let c = Array.from([1,2,3]).map(x => x * x);
let d = Array.from([1,2,3].map(x => x * x));
複製程式碼
1.7.3 Array.of()
將一組數值,轉換成陣列,彌補Array
方法引數不同導致的差異。
Array.of(1,2,3); // [1,2,3]
Array.of(1).length; // 1
Array(); // []
Array(2); // [,] 1個引數時,為指定陣列長度
Array(1,2,3); // [1,2,3] 多於2個引數,組成新陣列
複製程式碼
1.7.4 find()和findIndex()
find()
方法用於找出第一個符合條件的陣列成員,引數為一個回撥函式,所有成員依次執行該回撥函式,返回第一個返回值為true
的成員,如果沒有一個符合則返回undefined
。
[1,2,3,4,5].find( a => a < 3 ); // 1
複製程式碼
回撥函式接收三個引數,當前值、當前位置和原陣列。
[1,2,3,4,5].find((value, index, arr) => {
// ...
});
複製程式碼
findIndex()
方法與find()
類似,返回第一個符合條件的陣列成員的位置,如果都不符合則返回-1
。
[1,2,3,4].findIndex((v,i,a)=>{
return v>2;
}); // 2
複製程式碼
1.7.5 fill()
用於用指定值填充一個陣列,通常用來初始化空陣列,並抹去陣列中已有的元素。
new Array(3).fill('a'); // ['a','a','a']
[1,2,3].fill('a'); // ['a','a','a']
複製程式碼
並且fill()
的第二個和第三個引數指定填充的起始位置和結束位置。
[1,2,3].fill('a',1,2); // [1, "a", 3]
複製程式碼
1.7.6 entries(),keys(),values()
主要用於遍歷陣列,entries()
對鍵值對遍歷,keys()
對鍵名遍歷,values()
對鍵值遍歷。
for (let i of ['a', 'b'].keys()){
console.log(i)
}
// 0
// 1
for (let e of ['a', 'b'].values()){
console.log(e)
}
// 'a'
// 'b'
for (let e of ['a', 'b'].entries()){
console.log(e)
}
// [0, "a"]
// [1, "b"]
複製程式碼
1.7.7 includes()
用於表示陣列是否包含給定的值,與字串的includes
方法類似。
[1,2,3].includes(2); // true
[1,2,3].includes(4); // false
[1,2,NaN].includes(NaN); // true
複製程式碼
第二個引數為起始位置,預設為0
,如果負數,則表示倒數的位置,如果大於陣列長度,則重置為0
開始。
[1,2,3].includes(3,3); // false
[1,2,3].includes(3,4); // false
[1,2,3].includes(3,-1); // true
[1,2,3].includes(3,-4); // true
複製程式碼
1.7.8 flat(),flatMap()
flat()
用於將陣列一維化,返回一個新陣列,不影響原陣列。
預設一次只一維化一層陣列,若需多層,則傳入一個整數引數指定層數。
若要一維化所有層的陣列,則傳入Infinity
作為引數。
[1, 2, [2,3]].flat(); // [1,2,2,3]
[1,2,[3,[4,[5,6]]]].flat(3); // [1,2,3,4,5,6]
[1,2,[3,[4,[5,6]]]].flat('Infinity'); // [1,2,3,4,5,6]
複製程式碼
flatMap()
是將原陣列每個物件先執行一個函式,在對返回值組成的陣列執行flat()
方法,返回一個新陣列,不改變原陣列。
flatMap()
只能展開一層。
[2, 3, 4].flatMap((x) => [x, x * 2]);
// [2, 4, 3, 6, 4, 8]
複製程式碼
1.8 物件的擴充
1.8.1 屬性的簡潔表示
let a = 'a1';
let b = { a }; // b => { a : 'a1' }
// 等同於
let b = { a : a };
function f(a, b){
return {a, b};
}
// 等同於
function f (a, b){
return {a:a ,b:b};
}
let a = {
fun () {
return 'leo';
}
}
// 等同於
let a = {
fun : function(){
return 'leo';
}
}
複製程式碼
1.8.2 屬性名錶達式
JavaScript
提供2種方法定義物件的屬性。
// 方法1 識別符號作為屬性名
a.f = true;
// 方法2 字串作為屬性名
a['f' + 'un'] = true;
複製程式碼
延伸出來的還有:
let a = 'hi leo';
let b = {
[a]: true,
['a'+'bc']: 123,
['my' + 'fun'] (){
return 'hi';
}
};
// b.a => undefined ; b.abc => 123 ; b.myfun() => 'hi'
// b[a] => true ; b['abc'] => 123 ; b['myfun'] => ƒ ['my' + 'fun'] (){ return 'hi'; }
複製程式碼
注意:
屬性名錶達式不能與簡潔表示法同時使用,否則報錯。
// 報錯
let a1 = 'aa';
let a2 = 'bb';
let b1 = {[a1]};
// 正確
let a1 = 'aa';
let b1 = { [a1] : 'bb'};
複製程式碼
1.8.3 Object.is()
Object.is()
用於比較兩個值是否嚴格相等,在ES5時候只要使用相等運算子(==
)和嚴格相等運算子(===
)就可以做比較,但是它們都有缺點,前者會自動轉換資料型別,後者的NaN
不等於自身,以及+0
等於-0
。
Object.is('a','a'); // true
Object.is({}, {}); // false
// ES5
+0 === -0 ; // true
NaN === NaN; // false
// ES6
Object.is(+0,-0); // false
Object.is(NaN,NaN); // true
複製程式碼
1.8.4 Object.assign()
Object.assign()
方法用於物件的合併,將原物件的所有可列舉屬性複製到目標物件。
基礎用法:
第一個引數是目標物件,後面引數都是源物件。
let a = {a:1};
let b = {b:2};
Object.assign(a,b); // a=> {a:1,b:2}
複製程式碼
注意:
- 若目標物件與源物件有同名屬性,則後面屬性會覆蓋前面屬性。
let a = {a:1, b:2};
let b = {b:3, c:4};
Object.assign(a, b); // a => {a:1, b:3, c:4}
複製程式碼
- 若只有一個引數,則返回該引數。
let a = {a:1};
Object.assign(a) === a; // true
複製程式碼
- 若引數不是物件,則先轉成物件後返回。
typeof Object.assign(2); // 'object'
複製程式碼
- 由於
undefined
或NaN
無法轉成物件,所以做為引數會報錯。
Object.assign(undefined) // 報錯
Object.assign(NaN); // 報錯
複製程式碼
Object.assign()
實現的是淺拷貝。
Object.assign()
拷貝得到的是這個物件的引用。這個物件的任何變化,都會反映到目標物件上面。
let a = {a: {b:1}};
let b = Object.assign({},a);
a.a.b = 2;
console.log(b.a.b); // 2
複製程式碼
- 將陣列當做物件處理,鍵名為陣列下標,鍵值為陣列下標對應的值。
Object.assign([1, 2, 3], [4, 5]); // [4, 5, 3]
複製程式碼
1.9 Symbol
1.9.1 介紹
ES6引入Symbol
作為一種新的原始資料型別,表示獨一無二的值,主要是為了防止屬性名衝突。
ES6之後,JavaScript一共有其中資料型別:Symbol
、undefined
、null
、Boolean
、String
、Number
、Object
。
簡單實用:
let a = Symbol();
typeof a; // "symbol"
複製程式碼
注意:
Symbol
函式不能用new
,會報錯。由於Symbol
是一個原始型別,不是物件,所以不能新增屬性,它是類似於字串的資料型別。Symbol
都是不相等的,即使引數相同。
// 沒有引數
let a1 = Symbol();
let a2 = Symbol();
a1 === a2; // false
// 有引數
let a1 = Symbol('abc');
let a2 = Symbol('abc');
a1 === a2; // false
複製程式碼
Symbol
不能與其他型別的值計算,會報錯。
let a = Symbol('hello');
a + " world!"; // 報錯
`${a} world!`; // 報錯
複製程式碼
Symbol可以顯式轉換為字串:
let a1 = Symbol('hello');
String(a1); // "Symbol(hello)"
a1.toString(); // "Symbol(hello)"
複製程式碼
Symbol可以轉換為布林值,但不能轉為數值:
let a1 = Symbol();
Boolean(a1);
!a1; // false
Number(a1); // TypeError
a1 + 1 ; // TypeError
複製程式碼
1.9.2 Symbol作為屬性名
好處:防止同名屬性,還有防止鍵被改寫或覆蓋。
let a1 = Symbol();
// 寫法1
let b = {};
b[a1] = 'hello';
// 寫法2
let b = {
[a1] : 'hello'
}
// 寫法3
let b = {};
Object.defineProperty(b, a1, {value : 'hello' });
// 3種寫法 結果相同
b[a1]; // 'hello'
複製程式碼
需要注意: Symbol作為物件屬性名時,不能用點運算子,並且必須放在方括號內。
let a = Symbol();
let b = {};
// 不能用點運算
b.a = 'hello';
b[a] ; // undefined
b['a'] ; // 'hello'
// 必須放在方括號內
let c = {
[a] : function (text){
console.log(text);
}
}
c[a]('leo'); // 'leo'
// 上面等價於 更簡潔
let c = {
[a](text){
console.log(text);
}
}
複製程式碼
常常還用於建立一組常量,保證所有值不相等:
let a = {};
a.a1 = {
AAA: Symbol('aaa'),
BBB: Symbol('bbb'),
CCC: Symbol('ccc')
}
複製程式碼
1.9.3 應用:消除魔術字串
魔術字串:指程式碼中多次出現,強耦合的字串或數值,應該避免,而使用含義清晰的變數代替。
function f(a){
if(a == 'leo') {
console.log('hello');
}
}
f('leo'); // 'leo' 為魔術字串
複製程式碼
常使用變數,消除魔術字串:
let obj = {
name: 'leo'
};
function f (a){
if(a == obj.name){
console.log('hello');
}
}
f(obj.name); // 'leo'
複製程式碼
使用Symbol消除強耦合,使得不需關係具體的值:
let obj = {
name: Symbol()
};
function f (a){
if(a == obj.name){
console.log('hello');
}
}
f(obj.name);
複製程式碼
1.9.4 屬性名遍歷
Symbol作為屬性名遍歷,不出現在for...in
、for...of
迴圈,也不被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
let a = Symbol('aa'),b= Symbol('bb');
let obj = {
[a]:'11', [b]:'22'
}
for(let k of Object.values(obj)){console.log(k)}
// 無輸出
let obj = {};
let aa = Symbol('leo');
Object.defineProperty(obj, aa, {value: 'hi'});
for(let k in obj){
console.log(k); // 無輸出
}
Object.getOwnPropertyNames(obj); // []
Object.getOwnPropertySymbols(obj); // [Symbol(leo)]
複製程式碼
Object.getOwnPropertySymbols
方法返回一個陣列,包含當前物件所有用做屬性名的Symbol值。
let a = {};
let a1 = Symbol('a');
let a2 = Symbol('b');
a[a1] = 'hi';
a[a2] = 'oi';
let obj = Object.getOwnPropertySymbols(a);
obj; // [Symbol(a), Symbol(b)]
複製程式碼
另外可以使用Reflect.ownKeys
方法可以返回所有型別的鍵名,包括常規鍵名和 Symbol 鍵名。
let a = {
[Symbol('leo')]: 1,
aa : 2,
bb : 3,
}
Reflect.ownKeys(a); // ['aa', 'bb',Symbol('leo')]
複製程式碼
由於Symbol值作為名稱的屬性不被常規方法遍歷獲取,因此常用於定義物件的一些非私有,且內部使用的方法。
1.9.5 Symbol.for()、Symbol.keyFor()
- Symbol.for()
用於重複使用一個Symbol值,接收一個字串作為引數,若存在用此引數作為名稱的Symbol值,返回這個Symbol,否則新建並返回以這個引數為名稱的Symbol值。
let a = Symbol.for('aaa');
let b = Symbol.for('aaa');
a === b; // true
複製程式碼
Symbol()
和 Symbol.for()
區別:
Symbol.for('aa') === Symbol.for('aa'); // true
Symbol('aa') === Symbol('aa'); // false
複製程式碼
- Symbol.keyFor()
用於返回一個已使用的Symbol型別的key:
let a = Symbol.for('aa');
Symbol.keyFor(a); // 'aa'
let b = Symbol('aa');
Symbol.keyFor(b); // undefined
複製程式碼
1.9.6 內建的Symbol值
ES6提供11個內建的Symbol值,指向語言內部使用的方法:
- 1.Symbol.hasInstance
當其他物件使用instanceof
運算子,判斷是否為該物件的例項時,會呼叫這個方法。比如,foo instanceof Foo
在語言內部,實際呼叫的是Foo[Symbol.hasInstance](foo)
。
class P {
[Symbol.hasInstance](a){
return a instanceof Array;
}
}
[1, 2, 3] instanceof new P(); // true
複製程式碼
P是一個類,new P()會返回一個例項,該例項的Symbol.hasInstance
方法,會在進行instanceof
運算時自動呼叫,判斷左側的運運算元是否為Array
的例項。
- 2.Symbol.isConcatSpreadable
值為布林值,表示該物件用於Array.prototype.concat()
時,是否可以展開。
let a = ['aa','bb'];
['cc','dd'].concat(a, 'ee');
// ['cc', 'dd', 'aa', 'bb', 'ee']
a[Symbol.isConcatSpreadable]; // undefined
let b = ['aa','bb'];
b[Symbol.isConcatSpreadable] = false;
['cc','dd'].concat(b, 'ee');
// ['cc', 'dd',[ 'aa', 'bb'], 'ee']
複製程式碼
- 3.Symbol.species
指向一個建構函式,在建立衍生物件時會使用,使用時需要用get
取值器。
class P extends Array {
static get [Symbol.species](){
return this;
}
}
複製程式碼
解決下面問題:
// 問題: b應該是 Array 的例項,實際上是 P 的例項
class P extends Array{}
let a = new P(1,2,3);
let b = a.map(x => x);
b instanceof Array; // true
b instanceof P; // true
// 解決: 通過使用 Symbol.species
class P extends Array {
static get [Symbol.species]() { return Array; }
}
let a = new P();
let b = a.map(x => x);
b instanceof P; // false
b instanceof Array; // true
複製程式碼
- 4.Symbol.match
當執行str.match(myObject)
,傳入的屬性存在時會呼叫,並返回該方法的返回值。
class P {
[Symbol.match](string){
return 'hello world'.indexOf(string);
}
}
'h'.match(new P()); // 0
複製程式碼
- 5.Symbol.replace
當該物件被
String.prototype.replace
方法呼叫時,會返回該方法的返回值。
let a = {};
a[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(a , 'World') // ["Hello", "World"]
複製程式碼
- 6.Symbol.hasInstance
當該物件被String.prototype.search
方法呼叫時,會返回該方法的返回值。
class P {
constructor(val) {
this.val = val;
}
[Symbol.search](s){
return s.indexOf(this.val);
}
}
'hileo'.search(new P('leo')); // 2
複製程式碼
- 7.Symbol.split
當該物件被String.prototype.split
方法呼叫時,會返回該方法的返回值。
// 重新定義了字串物件的split方法的行為
class P {
constructor(val) {
this.val = val;
}
[Symbol.split](s) {
let i = s.indexOf(this.val);
if(i == -1) return s;
return [
s.substr(0, i),
s.substr(i + this.val.length)
]
}
}
'helloworld'.split(new P('hello')); // ["hello", ""]
'helloworld'.split(new P('world')); // ["", "world"]
'helloworld'.split(new P('leo')); // "helloworld"
複製程式碼
- 8.Symbol.iterator
物件進行for...of
迴圈時,會呼叫Symbol.iterator
方法,返回該物件的預設遍歷器。
class P {
*[Symbol.interator]() {
let i = 0;
while(this[i] !== undefined ) {
yield this[i];
++i;
}
}
}
let a = new P();
a[0] = 1;
a[1] = 2;
for (let k of a){
console.log(k);
}
複製程式碼
- 9.Symbol.toPrimitive
該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值。呼叫時,需要接收一個字串引數,表示當前運算模式,運算模式有:- Number : 此時需要轉換成數值
- String : 此時需要轉換成字串
- Default : 此時可以轉換成數值或字串
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
複製程式碼
- 10.Symbol.toStringTag
在該物件上面呼叫Object.prototype.toString
方法時,如果這個屬性存在,它的返回值會出現在toString
方法返回的字串之中,表示物件的型別。也就是說,這個屬性可以用來定製[object Object
]或[object Array]
中object
後面的那個字串。
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
複製程式碼
- 11.Symbol.unscopables
該物件指定了使用with關鍵字時,哪些屬性會被with環境排除。
// 沒有 unscopables 時
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有 unscopables 時
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
複製程式碼
上面程式碼通過指定Symbol.unscopables
屬性,使得with
語法塊不會在當前作用域尋找foo
屬性,即foo
將指向外層作用域的變數。
1.10 Set和Map資料結構
1.10.1 Set
介紹:
Set
資料結構類似陣列,但所有成員的值唯一。
Set
本身為一個建構函式,用來生成Set
資料結構,使用add
方法來新增新成員。
let a = new Set();
[1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x));
for(let k of a){
console.log(k)
};
// 1 2 3 4 5
複製程式碼
基礎使用:
let a = new Set([1,2,3,3,4]);
[...a]; // [1,2,3,4]
a.size; // 4
// 陣列去重
[...new Set([1,2,3,4,4,4])];// [1,2,3,4]
複製程式碼
注意:
- 向
Set
中新增值的時候,不會型別轉換,即5
和'5'
是不同的。
[...new Set([5,'5'])]; // [5, "5"]
複製程式碼
屬性和方法:
-
屬性:
Set.prototype.constructor
:建構函式,預設就是Set
函式。Set.prototype.size
:返回Set
例項的成員總數。
-
操作方法:
add(value)
:新增某個值,返回 Set 結構本身。delete(value)
:刪除某個值,返回一個布林值,表示刪除是否成功。has(value)
:返回一個布林值,表示該值是否為Set的成員。clear()
:清除所有成員,沒有返回值。
let a = new Set();
a.add(1).add(2); // a => Set(2) {1, 2}
a.has(2); // true
a.has(3); // false
a.delete(2); // true a => Set(1) {1}
a.clear(); // a => Set(0) {}
複製程式碼
陣列去重:
let a = new Set([1,2,3,3,3,3]);
複製程式碼
1.10.2 Set的應用
陣列去重:
// 方法1
[...new Set([1,2,3,4,4,4])]; // [1,2,3,4]
// 方法2
Array.from(new Set([1,2,3,4,4,4])); // [1,2,3,4]
複製程式碼
遍歷和過濾:
let a = new Set([1,2,3,4]);
// map 遍歷操作
let b = new Set([...a].map(x =>x*2));// b => Set(4) {2,4,6,8}
// filter 過濾操作
let c = new Set([...a].filter(x =>(x%2) == 0)); // b => Set(2) {2,4}
複製程式碼
獲取並集、交集和差集:
let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
// 並集
let c1 = new Set([...a, ...b]); // Set {1,2,3,4}
// 交集
let c2 = new Set([...a].filter(x => b.has(x))); // set {2,3}
// 差集
let c3 = new Set([...a].filter(x => !b.has(x))); // set {1}
複製程式碼
- 遍歷方法:
keys()
:返回鍵名的遍歷器。values()
:返回鍵值的遍歷器。entries()
:返回鍵值對的遍歷器。forEach()
:使用回撥函式遍歷每個成員。
Set
遍歷順序是插入順序,當儲存多個回撥函式,只需按照順序呼叫。但由於Set
結構沒有鍵名只有鍵值,所以keys()
和values()
是返回結果相同。
let a = new Set(['a','b','c']);
for(let i of a.keys()){console.log(i)}; // 'a' 'b' 'c'
for(let i of a.values()){console.log(i)}; // 'a' 'b' 'c'
for(let i of a.entries()){console.log(i)};
// ['a','a'] ['b','b'] ['c','c']
複製程式碼
並且 還可以使用for...of
直接遍歷Set
。
let a = new Set(['a','b','c']);
for(let k of a){console.log(k)}; // 'a' 'b' 'c'
複製程式碼
forEach
與陣列相同,對每個成員執行操作,且無返回值。
let a = new Set(['a','b','c']);
a.forEach((v,k) => console.log(k + ' : ' + v));
複製程式碼
1.10.3 Map
由於傳統的JavaScript
物件只能用字串當做鍵,給開發帶來很大限制,ES6增加Map
資料結構,使得各種型別的值(包括物件)都可以作為鍵。
Map
結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。
基礎使用:
let a = new Map();
let b = {name: 'leo' };
a.set(b,'my name'); // 新增值
a.get(b); // 獲取值
a.size; // 獲取總數
a.has(b); // 查詢是否存在
a.delete(b); // 刪除一個值
a.clear(); // 清空所有成員 無返回
複製程式碼
注意:
- 傳入陣列作為引數,指定鍵值對的陣列。
let a = new Map([
['name','leo'],
['age',18]
])
複製程式碼
- 如果對同一個鍵多次賦值,後面的值將覆蓋前面的值。
let a = new Map();
a.set(1,'aaa').set(1,'bbb');
a.get(1); // 'bbb'
複製程式碼
- 如果讀取一個未知的鍵,則返回
undefined
。
new Map().get('abcdef'); // undefined
複製程式碼
- 同樣的值的兩個例項,在 Map 結構中被視為兩個鍵。
let a = new Map();
let a1 = ['aaa'];
let a2 = ['aaa'];
a.set(a1,111).set(a2,222);
a.get(a1); // 111
a.get(a2); // 222
複製程式碼
遍歷方法: Map 的遍歷順序就是插入順序。
keys()
:返回鍵名的遍歷器。values()
:返回鍵值的遍歷器。entries()
:返回所有成員的遍歷器。forEach()
:遍歷 Map 的所有成員。
let a = new Map([
['name','leo'],
['age',18]
])
for (let i of a.keys()){...};
for (let i of a.values()){...};
for (let i of a.entries()){...};
a.forEach((v,k,m)=>{
console.log(`key:${k},value:${v},map:${m}`)
})
複製程式碼
將Map結構轉成陣列結構:
let a = new Map([
['name','leo'],
['age',18]
])
let a1 = [...a.keys()]; // a1 => ["name", "age"]
let a2 = [...a.values()]; // a2 => ["leo", 18]
let a3 = [...a.entries()];// a3 => [['name','leo'], ['age',18]]
複製程式碼
1.10.4 Map與其他資料結構互相轉換
- Map 轉 陣列
let a = new Map().set(true,1).set({f:2},['abc']);
[...a]; // [[true:1], [ {f:2},['abc'] ]]
複製程式碼
- 陣列 轉 Map
let a = [ ['name','leo'], [1, 'hi' ]]
let b = new Map(a);
複製程式碼
- Map 轉 物件
如果所有 Map 的鍵都是字串,它可以無損地轉為物件。
如果有非字串的鍵名,那麼這個鍵名會被轉成字串,再作為物件的鍵名。
function fun(s) {
let obj = Object.create(null);
for (let [k,v] of s) {
obj[k] = v;
}
return obj;
}
const a = new Map().set('yes', true).set('no', false);
fun(a)
// { yes: true, no: false }
複製程式碼
- 物件 轉 Map
function fun(obj) {
let a = new Map();
for (let k of Object.keys(obj)) {
a.set(k, obj[k]);
}
return a;
}
fun({yes: true, no: false})
// Map {"yes" => true, "no" => false}
複製程式碼
- Map 轉 JSON
(1)Map鍵名都是字串,轉為物件JSON:
function fun (s) {
let obj = Object.create(null);
for (let [k,v] of s) {
obj[k] = v;
}
return JSON.stringify(obj)
}
let a = new Map().set('yes', true).set('no', false);
fun(a);
// '{"yes":true,"no":false}'
複製程式碼
(2)Map鍵名有非字串,轉為陣列JSON:
function fun (map) {
return JSON.stringify([...map]);
}
let a = new Map().set(true, 7).set({foo: 3}, ['abc']);
fun(a)
// '[[true,7],[{"foo":3},["abc"]]]'
複製程式碼
- JSON 轉 Map
(1)所有鍵名都是字串:
function fun (s) {
let strMap = new Map();
for (let k of Object.keys(s)) {
strMap.set(k, s[k]);
}
return strMap;
return JSON.parse(strMap);
}
fun('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
複製程式碼
(2)整個 JSON 就是一個陣列,且每個陣列成員本身,又是一個有兩個成員的陣列:
function fun2(s) {
return new Map(JSON.parse(s));
}
fun2('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
複製程式碼
1.11 Proxy
proxy
用於修改某些操作的預設行為,可以理解為一種攔截外界對目標物件訪問的一種機制,從而對外界的訪問進行過濾和修改,即代理某些操作,也稱“代理器”。
1.11.1 基礎使用
proxy
例項化需要傳入兩個引數,target
參數列示所要攔截的目標物件,handler
引數也是一個物件,用來定製攔截行為。
let p = new Proxy(target, handler);
let a = new Proxy({}, {
get: function (target, handler){
return 'leo';
}
})
a.name; // leo
a.age; // leo
a.abcd; // leo
複製程式碼
上述a
例項中,在第二個引數中定義了get
方法,來攔截外界訪問,並且get
方法接收兩個引數,分別是目標物件和所要訪問的屬性,所以不管外部訪問物件中任何屬性都會執行get
方法返回leo
。
注意:
- 只能使用
Proxy
例項的物件才能使用這些操作。 - 如果
handler
沒有設定攔截,則直接返回原物件。
let target = {};
let handler = {};
let p = new Proxy(target, handler);
p.a = 'leo';
target.a; // 'leo'
複製程式碼
同個攔截器函式,設定多個攔截操作:
let p = new Proxy(function(a, b){
return a + b;
},{
get:function(){
return 'get方法';
},
apply:function(){
return 'apply方法';
}
})
複製程式碼
Proxy
支援的13種攔截操作:
13種攔截操作的詳細介紹:開啟阮一峰老師的連結。
-
get(target, propKey, receiver)
: 攔截物件屬性的讀取,比如proxy.foo和proxy['foo']。 -
set(target, propKey, value, receiver)
: 攔截物件屬性的設定,比如proxy.foo = v或proxy['foo'] = v,返回一個布林值。 -
has(target, propKey)
: 攔截propKey in proxy的操作,返回一個布林值。 -
deleteProperty(target, propKey)
: 攔截delete proxy[propKey]的操作,返回一個布林值。 -
ownKeys(target)
: 攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in迴圈,返回一個陣列。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。 -
getOwnPropertyDescriptor(target, propKey)
: 攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述物件。 -
defineProperty(target, propKey, propDesc)
: 攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布林值。 -
preventExtensions(target)
: 攔截Object.preventExtensions(proxy),返回一個布林值。 -
getPrototypeOf(target)
: 攔截Object.getPrototypeOf(proxy),返回一個物件。 -
isExtensible(target)
: 攔截Object.isExtensible(proxy),返回一個布林值。 -
setPrototypeOf(target, proto)
: 攔截Object.setPrototypeOf(proxy, proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。 -
apply(target, object, args)
: 攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 -
construct(target, args)
: 攔截 Proxy 例項作為建構函式呼叫的操作,比如new proxy(...args)。
1.11.2 取消Proxy例項
使用Proxy.revocale
方法取消Proxy
例項。
let a = {};
let b = {};
let {proxy, revoke} = Proxy.revocale(a, b);
proxy.name = 'leo'; // 'leo'
revoeke();
proxy.name; // TypeError: Revoked
複製程式碼
1.11.3 實現 Web服務的客戶端
const service = createWebService('http://le.com/data');
service.employees().than(json =>{
const employees = JSON.parse(json);
})
function createWebService(url){
return new Proxy({}, {
get(target, propKey, receiver{
return () => httpGet(url+'/'+propKey);
})
})
}
複製程式碼
1.12 Promise物件
1.12.1 概念
主要用途:解決非同步程式設計帶來的回撥地獄問題。
把Promise
簡單理解一個容器,存放著某個未來才會結束的事件(通常是一個非同步操作)的結果。通過Promise
物件來獲取非同步操作訊息,處理各種非同步操作。
Promise
物件2特點:
- 物件的狀態不受外界影響。
Promise
物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise
這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。
- 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise物件新增回撥函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
注意,為了行文方便,本章後面的resolve
d統一隻指fulfilled
狀態,不包含rejected
狀態。
Promise
缺點
- 無法取消Promise,一旦新建它就會立即執行,無法中途取消。
- 如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
- 當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
1.12.2 基本使用
Promise
為一個建構函式,需要用new
來例項化。
let p = new Promise(function (resolve, reject){
if(/*非同步操作成功*/){
resolve(value);
} else {
reject(error);
}
})
複製程式碼
Promise
接收一個函式作為引數,該函式兩個引數resolve
和reject
,有JS引擎提供。
resolve
作用是將Promise
的狀態從pending變成resolved,在非同步操作成功時呼叫,返回非同步操作的結果,作為引數傳遞出去。reject
作用是將Promise
的狀態從pending變成rejected,在非同步操作失敗時報錯,作為引數傳遞出去。
Promise
例項生成以後,可以用then
方法分別指定resolved
狀態和rejected
狀態的回撥函式。
p.then(function(val){
// success...
},function(err){
// error...
})
複製程式碼
幾個例子來理解 :
- 當一段時間過後,
Promise
狀態便成為resolved
觸發then
方法繫結的回撥函式。
function timeout (s){
return new Promise((resolve, reject){
setTimeout(result,ms, 'done');
})
}
timeout(100).then(val => {
console.log(val);
})
複製程式碼
Promise
新建後立刻執行。
let p = new Promise(function(resolve, reject){
console.log(1);
resolve();
})
p.then(()=>{
console.log(2);
})
console.log(3);
// 1
// 3
// 2
複製程式碼
非同步載入圖片:
function f(url){
return new Promise(function(resolve, reject){
const img = new Image ();
img.onload = function(){
resolve(img);
}
img.onerror = function(){
reject(new Error(
'Could not load image at ' + url
));
}
img.src = url;
})
}
複製程式碼
resolve
函式和reject
函式的引數為resolve
函式或reject
函式:
p1
的狀態決定了p2
的狀態,所以p2
要等待p1
的結果再執行回撥函式。
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
複製程式碼
呼叫resolve
或reject
不會結束Promise
引數函式的執行,除了return
:
new Promise((resolve, reject){
resolve(1);
console.log(2);
}).then(r => {
console.log(3);
})
// 2
// 1
new Promise((resolve, reject){
return resolve(1);
console.log(2);
})
// 1
複製程式碼
1.12.3 Promise.prototype.then()
作用是為Promise
新增狀態改變時的回撥函式,then
方法的第一個引數是resolved
狀態的回撥函式,第二個引數(可選)是rejected
狀態的回撥函式。
then
方法返回一個新Promise
例項,與原來Promise
例項不同,因此可以使用鏈式寫法,上一個then
的結果作為下一個then
的引數。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
複製程式碼
1.12.4 Promise.prototype.catch()
Promise.prototype.catch
方法是.then(null, rejection)
的別名,用於指定發生錯誤時的回撥函式。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
console.log('發生錯誤!', error);
});
複製程式碼
如果 Promise
狀態已經變成resolved
,再丟擲錯誤是無效的。
const p = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
p
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
複製程式碼
當promise
丟擲一個錯誤,就被catch
方法指定的回撥函式捕獲,下面三種寫法相同。
// 寫法一
const p = new Promise(function(resolve, reject) {
throw new Error('test');
});
p.catch(function(error) {
console.log(error);
});
// Error: test
// 寫法二
const p = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
p.catch(function(error) {
console.log(error);
});
// 寫法三
const p = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
p.catch(function(error) {
console.log(error);
});
複製程式碼
一般來說,不要在then
方法裡面定義Reject
狀態的回撥函式(即then
的第二個引數),總是使用catch
方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
複製程式碼
1.12.5 Promise.prototype.finally()
finally
方法用於指定不管 Promise
物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
複製程式碼
finally
不接收任何引數,與狀態無關,本質上是then
方法的特例。
promise
.finally(() => {
// 語句
});
// 等同於
promise
.then(
result => {
// 語句
return result;
},
error => {
// 語句
throw error;
}
);
複製程式碼
上面程式碼中,如果不使用finally
方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了finally
方法,則只需要寫一次。
finally
方法總是會返回原來的值。
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
複製程式碼
1.12.6 Promise.all()
用於將多個 Promise
例項,包裝成一個新的 Promise
例項,引數可以不是陣列,但必須是Iterator介面,且返回的每個成員都是Promise
例項。
const p = Promise.all([p1, p2, p3]);
複製程式碼
p
的狀態由p1
、p2
、p3
決定,分成兩種情況。
- 只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。
- 只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
// 生成一個Promise物件的陣列
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
複製程式碼
上面程式碼中,promises
是包含 6 個 Promise 例項的陣列,只有這 6 個例項的狀態都變成fulfilled
,或者其中有一個變為rejected
,才會呼叫Promise.all
方法後面的回撥函式。
注意:如果Promise
的引數中定義了catch
方法,則rejected
後不會觸發Promise.all()
的catch
方法,因為引數中的catch
方法執行完後也會變成resolved
,當Promise.all()
方法引數的例項都是resolved
時就會呼叫Promise.all()
的then
方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]
複製程式碼
如果引數裡面都沒有catch方法,就會呼叫Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了
複製程式碼
1.12.7 Promise.race()
與Promise.all
方法類似,也是將多個Promise
例項包裝成一個新的Promise
例項。
const p = Promise.race([p1, p2, p3]);
複製程式碼
與Promise.all
方法區別在於,Promise.race
方法是p1
, p2
, p3
中只要一個引數先改變狀態,就會把這個引數的返回值傳給p
的回撥函式。
1.12.8 Promise.resolve()
將現有物件轉換成 Promise
物件。
const p = Promise.resolve($.ajax('/whatever.json'));
複製程式碼
1.12.9 Promise.reject()
返回一個rejected
狀態的Promise
例項。
const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
複製程式碼
注意,Promise.reject()
方法的引數,會原封不動地作為reject
的理由,變成後續方法的引數。這一點與Promise.resolve
方法不一致。
const thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
複製程式碼
1.13 Iterator和 for...of迴圈
1.13.1 Iterator遍歷器概念
Iterator是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。
Iterator三個作用:
- 為各種資料結構,提供一個統一的、簡便的訪問介面;
- 使得資料結構的成員能夠按某種次序排列;
- Iterator 介面主要供ES6新增的
for...of
消費;
1.13.2 Iterator遍歷過程
- 建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。
- 第一次呼叫指標物件的
next
方法,可以將指標指向資料結構的第一個成員。 - 第二次呼叫指標物件的
next
方法,指標就指向資料結構的第二個成員。 - 不斷呼叫指標物件的
next
方法,直到它指向資料結構的結束位置。
每一次呼叫next
方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含value
和done
兩個屬性的物件。
value
屬性是當前成員的值;done
屬性是一個布林值,表示遍歷是否結束;
模擬next
方法返回值:
let f = function (arr){
var nextIndex = 0;
return {
next:function(){
return nextIndex < arr.length ?
{value: arr[nextIndex++], done: false}:
{value: undefined, done: true}
}
}
}
let a = f(['a', 'b']);
a.next(); // { value: "a", done: false }
a.next(); // { value: "b", done: false }
a.next(); // { value: undefined, done: true }
複製程式碼
1.13.3 預設Iterator介面
若資料可遍歷,即一種資料部署了Iterator介面。
ES6中預設的Iterator介面部署在資料結構的Symbol.iterator
屬性,即如果一個資料結構具有Symbol.iterator
屬性,就可以認為是可遍歷。
Symbol.iterator
屬性本身是函式,是當前資料結構預設的遍歷器生成函式。執行這個函式,就會返回一個遍歷器。至於屬性名Symbol.iterator
,它是一個表示式,返回Symbol
物件的iterator
屬性,這是一個預定義好的、型別為 Symbol 的特殊值,所以要放在方括號內(參見《Symbol》一章)。
原生具有Iterator介面的資料結構有:
- Array
- Map
- Set
- String
- TypedArray
- 函式的 arguments 物件
- NodeList 物件
1.13.4 Iterator使用場景
- (1)解構賦值
對陣列和Set
結構進行解構賦值時,會預設呼叫Symbol.iterator
方法。
let a = new Set().add('a').add('b').add('c');
let [x, y] = a; // x = 'a' y = 'b'
let [a1, ...a2] = a; // a1 = 'a' a2 = ['b','c']
複製程式碼
- (2)擴充套件運算子
擴充套件運算子(...
)也會呼叫預設的 Iterator 介面。
let a = 'hello';
[...a]; // ['h','e','l','l','o']
let a = ['b', 'c'];
['a', ...a, 'd']; // ['a', 'b', 'c', 'd']
複製程式碼
- (2)yield*
yield*
後面跟的是一個可遍歷的結構,它會呼叫該結構的遍歷器介面。
let a = function*(){
yield 1;
yield* [2,3,4];
yield 5;
}
let b = a();
b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // { value: 4, done: false }
b.next() // { value: 5, done: false }
b.next() // { value: undefined, done: true }
複製程式碼
-
(4)其他場合
由於陣列的遍歷會呼叫遍歷器介面,所以任何接受陣列作為引數的場合,其實都呼叫了遍歷器介面。下面是一些例子。 -
for...of
-
Array.from()
-
Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) -
Promise.all()
-
Promise.race()
1.13.5 for...of迴圈
只要資料結構部署了Symbol.iterator
屬性,即具有 iterator 介面,可以用for...of
迴圈遍歷它的成員。也就是說,for...of
迴圈內部呼叫的是資料結構的Symbol.iterato
方法。
使用場景:
for...of
可以使用在陣列,Set
和Map
結構,類陣列物件,Genetator物件和字串。
- 陣列
for...of
迴圈可以代替陣列例項的forEach
方法。
let a = ['a', 'b', 'c'];
for (let k of a){console.log(k)}; // a b c
a.forEach((ele, index)=>{
console.log(ele); // a b c
console.log(index); // 0 1 2
})
複製程式碼
與for...in
對比,for...in
只能獲取物件鍵名,不能直接獲取鍵值,而for...of
允許直接獲取鍵值。
let a = ['a', 'b', 'c'];
for (let k of a){console.log(k)}; // a b c
for (let k in a){console.log(k)}; // 0 1 2
複製程式碼
- Set和Map
可以使用陣列作為變數,如for (let [k,v] of b){...}
。
let a = new Set(['a', 'b', 'c']);
for (let k of a){console.log(k)}; // a b c
let b = new Map();
b.set('name','leo');
b.set('age', 18);
b.set('aaa','bbb');
for (let [k,v] of b){console.log(k + ":" + v)};
// name:leo
// age:18
// aaa:bbb
複製程式碼
- 類陣列物件
// 字串
let a = 'hello';
for (let k of a ){console.log(k)}; // h e l l o
// DOM NodeList物件
let b = document.querySelectorAll('p');
for (let k of b ){
k.classList.add('test');
}
// arguments物件
function f(){
for (let k of arguments){
console.log(k);
}
}
f('a','b'); // a b
複製程式碼
- 物件
普通物件不能直接使用for...of
會報錯,要部署Iterator才能使用。
let a = {a:'aa',b:'bb',c:'cc'};
for (let k in a){console.log(k)}; // a b c
for (let k of a){console>log(k)}; // TypeError
複製程式碼
1.13.6 跳出for...of
使用break
來實現。
for (let k of a){
if(k>100)
break;
console.log(k);
}
複製程式碼
1.14 Generator函式和應用
1.14.1 基本概念
Generator
函式是一種非同步程式設計解決方案。
原理:
執行Genenrator
函式會返回一個遍歷器物件,依次遍歷Generator
函式內部的每一個狀態。
Generator
函式是一個普通函式,有以下兩個特徵:
function
關鍵字與函式名之間有個星號;- 函式體內使用
yield
表示式,定義不同狀態;
通過呼叫next
方法,將指標移向下一個狀態,直到遇到下一個yield
表示式(或return
語句)為止。簡單理解,Generator
函式分段執行,yield
表示式是暫停執行的標記,而next
恢復執行。
function * f (){
yield 'hi';
yield 'leo';
return 'ending';
}
let a = f();
a.next(); // {value: 'hi', done : false}
a.next(); // {value: 'leo', done : false}
a.next(); // {value: 'ending', done : true}
a.next(); // {value: undefined, done : false}
複製程式碼
1.14.2 yield表示式
yield
表示式是暫停標誌,遍歷器物件的next
方法的執行邏輯如下:
- 遇到
yield
就暫停執行,將這個yield
後的表示式的值,作為返回物件的value
屬性值。 - 下次呼叫
next
往下執行,直到遇到下一個yield
。 - 直到函式結束或者
return
為止,並返回return
語句後面表示式的值,作為返回物件的value
屬性值。 - 如果該函式沒有
return
語句,則返回物件的value
為undefined
。
注意:
yield
只能用在Generator
函式裡使用,其他地方使用會報錯。
// 錯誤1
(function(){
yiled 1; // SyntaxError: Unexpected number
})()
// 錯誤2 forEach引數是個普通函式
let a = [1, [[2, 3], 4], [5, 6]];
let f = function * (i){
i.forEach(function(m){
if(typeof m !== 'number'){
yield * f (m);
}else{
yield m;
}
})
}
for (let k of f(a)){
console.log(k)
}
複製程式碼
yield
表示式如果用於另一個表示式之中,必須放在圓括號內。
function * a (){
console.log('a' + yield); // SyntaxErro
console.log('a' + yield 123); // SyntaxErro
console.log('a' + (yield)); // ok
console.log('a' + (yield 123)); // ok
}
複製程式碼
yield
表示式用做函式引數或放在表示式右邊,可以不加括號。
function * a (){
f(yield 'a', yield 'b'); // ok
lei i = yield; // ok
}
複製程式碼
1.14.3 next方法
yield
本身沒有返回值,或者是總返回undefined
,next
方法可帶一個引數,作為上一個yield
表示式的返回值。
function * f (){
for (let k = 0; true; k++){
let a = yield k;
if(a){k = -1};
}
}
let g =f();
g.next(); // {value: 0, done: false}
g.next(); // {value: 1, done: false}
g.next(true); // {value: 0, done: false}
複製程式碼
這一特點,可以讓Generator
函式開始執行之後,可以從外部向內部注入不同值,從而調整函式行為。
function * f(x){
let y = 2 * (yield (x+1));
let z = yield (y/3);
return (x + y + z);
}
let a = f(5);
a.next(); // {value : 6 ,done : false}
a.next(); // {value : NaN ,done : false}
a.next(); // {value : NaN ,done : true}
// NaN因為yeild返回的是物件 和數字計算會NaN
let b = f(5);
b.next(); // {value : 6 ,done : false}
b.next(12); // {value : 8 ,done : false}
b.next(13); // {value : 42 ,done : false}
// x 5 y 24 z 13
複製程式碼
1.14.4 for...of迴圈
for...of
迴圈會自動遍歷,不用呼叫next
方法,需要注意的是,for...of
遇到next
返回值的done
屬性為true
就會終止,return
返回的不包括在for...of
迴圈中。
function * f(){
yield 1;
yield 2;
yield 3;
yield 4;
return 5;
}
for (let k of f()){
console.log(k);
}
// 1 2 3 4 沒有 5
複製程式碼
1.14.5 Generator.prototype.throw()
throw
方法用來向函式外丟擲錯誤,並且在Generator函式體內捕獲。
let f = function * (){
try { yield }
catch (e) { console.log('內部捕獲', e) }
}
let a = f();
a.next();
try{
a.throw('a');
a.throw('b');
}catch(e){
console.log('外部捕獲',e);
}
// 內部捕獲 a
// 外部捕獲 b
複製程式碼
1.14.6 Generator.prototype.return()
return
方法用來返回給定的值,並結束遍歷Generator函式,如果return
方法沒有引數,則返回值的value
屬性為undefined
。
function * f(){
yield 1;
yield 2;
yield 3;
}
let g = f();
g.next(); // {value : 1, done : false}
g.return('leo'); // {value : 'leo', done " true}
g.next(); // {value : undefined, done : true}
複製程式碼
1.14.7 next()/throw()/return()共同點
相同點就是都是用來恢復Generator函式的執行,並且使用不同語句替換yield
表示式。
next()
將yield
表示式替換成一個值。
let f = function * (x,y){
let r = yield x + y;
return r;
}
let g = f(1, 2);
g.next(); // {value : 3, done : false}
g.next(1); // {value : 1, done : true}
// 相當於把 let r = yield x + y;
// 替換成 let r = 1;
複製程式碼
throw()
將yield
表示式替換成一個throw
語句。
g.throw(new Error('報錯')); // Uncaught Error:報錯
// 相當於將 let r = yield x + y
// 替換成 let r = throw(new Error('報錯'));
複製程式碼
next()
將yield
表示式替換成一個return
語句。
g.return(2); // {value: 2, done: true}
// 相當於將 let r = yield x + y
// 替換成 let r = return 2;
複製程式碼
1.14.8 yield* 表示式
用於在一個Generator中執行另一個Generator函式,如果沒有使用yield*
會沒有效果。
function * a(){
yield 1;
yield 2;
}
function * b(){
yield 3;
yield * a();
yield 4;
}
// 等同於
function * b(){
yield 3;
yield 1;
yield 2;
yield 4;
}
for(let k of b()){console.log(k)}
// 3
// 1
// 2
// 4
複製程式碼
1.14.9 應用場景
- 控制流管理
解決回撥地獄:
// 使用前
f1(function(v1){
f2(function(v2){
f3(function(v3){
// ... more and more
})
})
})
// 使用Promise
Promise.resolve(f1)
.then(f2)
.then(f3)
.then(function(v4){
// ...
},function (err){
// ...
}).done();
// 使用Generator
function * f (v1){
try{
let v2 = yield f1(v1);
let v3 = yield f1(v2);
let v4 = yield f1(v3);
// ...
}catch(err){
// console.log(err)
}
}
function g (task){
let obj = task.next(task.value);
// 如果Generator函式未結束,就繼續呼叫
if(!obj.done){
task.value = obj.value;
g(task);
}
}
g( f(initValue) );
複製程式碼
- 非同步程式設計的使用 在真實的非同步任務封裝的情況:
let fetch = require('node-fetch');
function * f(){
let url = 'http://www.baidu.com';
let res = yield fetch(url);
console.log(res.bio);
}
// 執行該函式
let g = f();
let result = g.next();
// 由於fetch返回的是Promise物件,所以用then
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
})
複製程式碼
1.15 Class語法和繼承
1.15.1 介紹
ES6中的class
可以看作只是一個語法糖,絕大部分功能都可以用ES5實現,並且,類和模組的內部,預設就是嚴格模式,所以不需要使用use strict指定執行模式。
// ES5
function P (x,y){
this.x = x;
this.y = y;
}
P.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var a = new P(1, 2);
// ES6
class P {
constructor(x, y){
this.x = x;
this.y = y;
}
toString(){
return '(' + this.x + ', ' + this.y + ')';
}
}
let a = new P(1, 2);
複製程式碼
值得注意:
ES6的類的所有方法都是定義在prototype
屬性上,呼叫類的例項的方法,其實就是呼叫原型上的方法。
class P {
constructor(){ ... }
toString(){ ... }
toNumber(){ ... }
}
// 等同於
P.prototyoe = {
constructor(){ ... },
toString(){ ... },
toNumber(){ ... }
}
let a = new P();
a.constructor === P.prototype.constructor; // true
複製程式碼
類的屬性名可以使用表示式:
let name = 'leo';
class P {
constructor (){ ... }
[name](){ ... }
}
複製程式碼
Class不存在變數提升: ES6中的類不存在變數提升,與ES5完全不同:
new P (); // ReferenceError
class P{...};
複製程式碼
Class的name屬性:
name
屬性總是返回緊跟在class
後的類名。
class P {}
P.name; // 'P'
複製程式碼
1.15.2 constructor()方法
constructor()
是類的預設方法,通過new
例項化時自動呼叫執行,一個類必須有constructor()
方法,否則一個空的constructor()
會預設新增。
constructor()
方法預設返回例項物件(即this
)。
class P { ... }
// 等同於
class P {
constructor(){ ... }
}
複製程式碼
1.15.3 類的例項物件
與ES5一樣,ES6的類必須使用new
命令例項化,否則報錯。
class P { ... }
let a = P (1,2); // 報錯
let b = new P(1, 2); // 正確
複製程式碼
與 ES5 一樣,例項的屬性除非顯式定義在其本身(即定義在this
物件上),否則都是定義在原型上(即定義在class
上)。
class P {
constructor(x, y){
this.x = x;
this.y = y;
}
toString(){
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
// toString是原型物件的屬性(因為定義在Point類上)
複製程式碼
1.15.4 Class表示式
與函式一樣,類也可以使用表示式來定義,使用表示式來作為類的名字,而class
後跟的名字,用來指代當前類,只能再Class內部使用。
let a = class P{
get(){
return P.name;
}
}
let b = new a();
b.get(); // P
P.name; // ReferenceError: P is not defined
複製程式碼
如果類的內部沒用到的話,可以省略P
,也就是可以寫成下面的形式。
let a = class { ... }
複製程式碼
1.15.5 私有方法和私有屬性
由於ES6不提供,只能變通來實現:
- 1.使用命名加以區別,如變數名前新增
_
,但是不保險,外面也可以呼叫到。
class P {
// 公有方法
f1 (x) {
this._x(x);
}
// 私有方法
_x (x){
return this.y = x;
}
}
複製程式碼
- 2.將私有方法移除模組,再在類內部呼叫
call
方法。
class P {
f1 (x){
f2.call(this, x);
}
}
function f2 (x){
return this.y = x;
}
複製程式碼
- 3.使用
Symbol
為私有方法命名。
const a1 = Symbol('a1');
const a2 = Symbol('a2');
export default class P{
// 公有方法
f1 (x){
this[a1](x);
}
// 私有方法
[a1](x){
return this[a2] = x;
}
}
複製程式碼
1.15.6 this指向問題
類內部方法的this
預設指向類的例項,但單獨使用該方法可能報錯,因為this
指向的問題。
class P{
leoDo(thing = 'any'){
this.print(`Leo do ${thing}`)
}
print(text){
console.log(text);
}
}
let a = new P();
let { leoDo } = a;
leoDo(); // TypeError: Cannot read property 'print' of undefined
// 問題出在 單獨使用leoDo時,this指向呼叫的環境,
// 但是leoDo中的this是指向P類的例項,所以報錯
複製程式碼
解決方法:
- 1.在類裡面繫結
this
class P {
constructor(){
this.name = this.name.bind(this);
}
}
複製程式碼
- 2.使用箭頭函式
class P{
constructor(){
this.name = (name = 'leo' )=>{
this.print(`my name is ${name}`)
}
}
}
複製程式碼
1.15.7 Class的getter和setter
使用get
和set
關鍵詞對屬性設定取值函式和存值函式,攔截屬性的存取行為。
class P {
constructor (){ ... }
get f (){
return 'getter';
}
set f (val) {
console.log('setter: ' + val);
}
}
let a = new P();
a.f = 100; // setter : 100
a.f; // getter
複製程式碼
1.15.8 Class的generator方法
只要在方法之前加個(*
)即可。
class P {
constructor (...args){
this.args = args;
}
*[Symbol.iterator](){
for (let arg of this.args){
yield arg;
}
}
}
for (let k of new P('aa', 'bb')){
console.log(k);
}
// 'aa'
// 'bb'
複製程式碼
1.15.9 Class的靜態方法
由於類相當於例項的原型,所有類中定義的方法都會被例項繼承,若不想被繼承,只要加上static
關鍵字,只能通過類來呼叫,即“靜態方法”。
class P (){
static f1 (){ return 'aaa' };
}
P.f1(); // 'aa'
let a = new P();
a.f1(); // TypeError: a.f1 is not a function
複製程式碼
如果靜態方法包含this
關鍵字,則this
指向類,而不是例項。
class P {
static f1 (){
this.f2();
}
static f2 (){
console.log('aaa');
}
f2(){
console.log('bbb');
}
}
P.f2(); // 'aaa'
複製程式碼
並且靜態方法可以被子類繼承,或者super
物件中呼叫。
class P{
static f1(){ return 'leo' };
}
class Q extends P { ... };
Q.f1(); // 'leo'
class R extends P {
static f2(){
return super.f1() + ',too';
}
}
R.f2(); // 'leo , too'
複製程式碼
1.15.10 Class的靜態屬性和例項屬性
ES6中明確規定,Class內部只有靜態方法沒有靜態屬性,所以只能通過下面實現。
// 正確寫法
class P {}
P.a1 = 1;
P.a1; // 1
// 無效寫法
class P {
a1: 2, // 無效
static a1 : 2, // 無效
}
P.a1; // undefined
複製程式碼
新提案來規定例項屬性和靜態屬性的新寫法
- 1.類的例項屬性
類的例項屬性可以用等式,寫入類的定義中。
class P {
prop = 100; // prop為P的例項屬性 可直接讀取
constructor(){
console.log(this.prop); // 100
}
}
複製程式碼
有了新寫法後,就可以不再contructor
方法裡定義。
為了可讀性的目的,對於那些在constructor
裡面已經定義的例項屬性,新寫法允許直接列出。
// 之前寫法:
class RouctCounter extends React.Component {
constructor(prop){
super(prop);
this.state = {
count : 0
}
}
}
// 新寫法
class RouctCounter extends React.Component {
state;
constructor(prop){
super(prop);
this.state = {
count : 0
}
}
}
複製程式碼
- 2.類的靜態屬性
只要在例項屬性前面加上static
關鍵字就可以。
class P {
static prop = 100;
constructor(){console.log(this.prop)}; // 100
}
複製程式碼
新寫法方便靜態屬性的表達。
// old
class P { .... }
P.a = 1;
// new
class P {
static a = 1;
}
複製程式碼
1.15.11 Class的繼承
主要通過extends
關鍵字實現,繼承父類的所有屬性和方法,通過super
關鍵字來新建父類建構函式的this
物件。
class P { ... }
class Q extends P { ... }
class P {
constructor(x, y){
// ...
}
f1 (){ ... }
}
class Q extends P {
constructor(a, b, c){
super(x, y); // 呼叫父類 constructor(x, y)
this.color = color ;
}
f2 (){
return this.color + ' ' + super.f1();
// 呼叫父類的f1()方法
}
}
複製程式碼
子類必須在constructor()
呼叫super()
否則報錯,並且只有super
方法才能呼叫父類例項,還有就是,父類的靜態方法,子類也可以繼承到。
class P {
constructor(x, y){
this.x = x;
this.y = y;
}
static fun(){
console.log('hello leo')
}
}
// 關鍵點1 呼叫super
class Q extends P {
constructor(){ ... }
}
let a = new Q(); // ReferenceError 因為Q沒有呼叫super
// 關鍵點2 呼叫super
class R extends P {
constructor (x, y. z){
this.z = z; // ReferenceError 沒呼叫super不能使用
super(x, y);
this.z = z; // 正確
}
}
// 關鍵點3 子類繼承父類靜態方法
R.hello(); // 'hello leo'
複製程式碼
super關鍵字:
既可以當函式使用,還可以當物件使用。
- 1.當函式呼叫,代表父類的建構函式,但必須執行一次。
class P {... };
class R extends P {
constructor(){
super();
}
}
複製程式碼
- 2.當物件呼叫,指向原型物件,在靜態方法中指向父類。
class P {
f (){ return 2 };
}
class R extends P {
constructor (){
super();
console.log(super.f()); // 2
}
}
let a = new R()
複製程式碼
注意:super
指向父類原型物件,所以定義在父類例項的方法和屬性,是無法通過super
呼叫的,但是通過呼叫super
方法可以把內部this
指向當前例項,就可以訪問到。
class P {
constructor(){
this.a = 1;
}
print(){
console.log(this.a);
}
}
class R extends P {
get f (){
return super.a;
}
}
let b = new R();
b.a; // undefined 因為a是父類P例項的屬性
// 先呼叫super就可以訪問
class Q extends P {
constructor(){
super(); // 將內部this指向當前例項
return super.a;
}
}
let c = new Q();
c.a; // 1
// 情況3
class J extends P {
constructor(){
super();
this.a = 3;
}
g(){
super.print();
}
}
let c = new J();
c.g(); // 3 由於執行了super()後 this指向當前例項
複製程式碼
1.16 Module語法和載入實現
1.16.1 介紹
ES6之前用於JavaScript的模組載入方案,是一些社群提供的,主要有CommonJS
和AMD
兩種,前者用於伺服器,後者用於瀏覽器。
ES6提供了模組的實現,使用export
命令對外暴露介面,使用import
命令輸入其他模組暴露的介面。
// CommonJS模組
let { stat, exists, readFire } = require('fs');
// ES6模組
import { stat, exists, readFire } = from 'fs';
複製程式碼
1.16.2 嚴格模式
ES6模組自動採用嚴格模式,無論模組頭部是否有"use strict"
。
嚴格模式有以下限制:
- 變數必須宣告後再使用
- 函式的引數不能有同名屬性,否則報錯
- 不能使用
with
語句 - 不能對只讀屬性賦值,否則報錯
- 不能使用字首 0 表示八進位制數,否則報錯
- 不能刪除不可刪除的屬性,否則報錯
- 不能刪除變數
delete prop
,會報錯,只能刪除屬性delete * global[prop]
eval
不會在它的外層作用域引入變數eval
和arguments
不能被重新賦值arguments
不會自動反映函式引數的變化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全域性物件 - 不能使用
fn.caller
和fn.arguments
獲取函式呼叫的堆疊 - 增加了保留字(比如
protected
、static
和interface
)
特別是,ES6中頂層this
指向undefined
,即不應該在頂層程式碼使用this
。
1.16.3 export命令
使用export
向模組外暴露介面,可以是方法,也可以是變數。
// 1. 變數
export let a = 'leo';
export let b = 100;
// 還可以
let a = 'leo';
let b = 100;
export {a, b};
// 2. 方法
export function f(a,b){
return a*b;
}
// 還可以
function f1 (){ ... }
function f2 (){ ... }
export {
a1 as f1,
a2 as f2
}
複製程式碼
可以使用as
重新命名函式的對外介面。
特別注意:
export
暴露的必須是介面,不能是值。
// 錯誤
export 1; // 報錯
let a = 1;
export a; // 報錯
// 正確
export let a = 1; // 正確
let a = 1;
export {a}; // 正確
let a = 1;
export { a as b}; // 正確
複製程式碼
暴露方法也是一樣:
// 錯誤
function f(){...};
export f;
// 正確
export function f () {...};
function f(){...};
export {f};
複製程式碼
1.16.4 import命令
載入export
暴露的介面,輸出為變數。
import { a, b } from '/a.js';
function f(){
return a + b;
}
複製程式碼
import
後大括號指定變數名,需要與export
的模組暴露的名稱一致。
也可以使用as
為輸入的變數重新命名。
import { a as leo } from './a.js';
複製程式碼
import
不能直接修改輸入變數的值,因為輸入變數只讀只是個介面,但是如果是個物件,可以修改它的屬性。
// 錯誤
import {a} from './f.js';
a = {}; // 報錯
// 正確
a.foo = 'leo'; // 不報錯
複製程式碼
import
命令具有提升效果,會提升到整個模組頭部最先執行,且多次執行相同import
只會執行一次。
1.16.5 模組的整體載入
當一個模組暴露多個方法和變數,引用時可以用*
整體載入。
// a.js
export function f(){...}
export function g(){...}
// b.js
import * as obj from '/a.js';
console.log(obj.f());
console.log(obj.g());
複製程式碼
但是,不允許執行時改變:
import * as obj from '/a.js';
// 不允許
obj.a = 'leo';
obj.b = function(){...};
複製程式碼
1.16.6 export default 命令
使用export default
命令,為模組指定預設輸出,引用的時候直接指定任意名稱即可。
// a.js
export default function(){console.log('leo')};
// b.js
import leo from './a.js';
leo(); // 'leo'
複製程式碼
export defualt
暴露有函式名的函式時,在呼叫時相當於匿名函式。
// a.js
export default function f(){console.log('leo')};
// 或者
function f(){console.log('leo')};
export default f;
// b.js
import leo from './a.js';
複製程式碼
export defualt
其實是輸出一個名字叫default
的變數,所以後面不能跟變數賦值語句。
// 正確
export let a= 1;
let a = 1;
export defualt a;
// 錯誤
export default let a = 1;
複製程式碼
export default
命令的本質是將後面的值,賦給default
變數,所以可以直接將一個值寫在export default
之後。
// 正確
export default 1;
// 錯誤
export 1;
複製程式碼
1.16.7 export 和 import 複合寫法
常常在先輸入後輸出同一個模組使用,即轉發介面,將兩者寫在一起。
export {a, b} from './leo.js';
// 理解為
import {a, b} from './leo.js';
export {a, b}
複製程式碼
常見的寫法還有:
// 介面改名
export { a as b} from './leo.js';
// 整體輸出
export * from './leo.js';
// 預設介面改名
export { default as a } from './leo.js';
複製程式碼
常常用在模組繼承。
1.16.8 瀏覽器中的載入規則
ES6中,可以在瀏覽器使用<script>
標籤,需要加入type="module"
屬性,並且這些都是非同步載入,避免瀏覽器阻塞,即等到整個頁面渲染完,再執行模組指令碼,等同於開啟了<script>
標籤的defer
屬性。
<script type="module" src="./a.js"></script>
複製程式碼
另外,ES6模組也可以內嵌到網頁,語法與外部載入指令碼一致:
<script type="module">
import a from './a.js';
</script>
複製程式碼
注意點:
- 程式碼是在模組作用域之中執行,而不是在全域性作用域執行。模組內部的頂層變數,外部不可見。
- 模組指令碼自動採用嚴格模式,不管有沒有宣告
use strict
。 - 模組之中,可以使用
import
命令載入其他模組(.js
字尾不可省略,需要提供絕對 UR
L 或相對 UR
L),也可以使用export
命令輸出對外介面。 - 模組之中,頂層的
this
關鍵字返回undefined
,而不是指向window
。也就是說,在模組頂層使用this
關鍵字,是無意義的。 - 同一個模組如果載入多次,將只執行一次。
2. ES7
2.1 Array.prototype.includes()方法
includes()
用於查詢一個值是否在陣列中,如果在返回true
,否則返回false
。
['a', 'b', 'c'].includes('a'); // true
['a', 'b', 'c'].includes('d'); // false
複製程式碼
includes()
方法接收兩個引數,搜尋的內容和開始搜尋的索引,預設值為0,若搜尋值在陣列中則返回true
否則返回false
。
['a', 'b', 'c', 'd'].includes('b'); // true
['a', 'b', 'c', 'd'].includes('b', 1); // true
['a', 'b', 'c', 'd'].includes('b', 2); // false
複製程式碼
與indexOf
方法對比,下面方法效果相同:
['a', 'b', 'c', 'd'].indexOf('b') > -1; // true
['a', 'b', 'c', 'd'].includes('b'); // true
複製程式碼
includes()與indexOf對比:
includes
相比indexOf
更具語義化,includes
返回的是是否存在的具體結果,值為布林值,而indexOf
返回的是搜尋值的下標。includes
相比indexOf
更準確,includes
認為兩個NaN
相等,而indexOf
不會。
let a = [1, NaN, 3];
a.indexOf(NaN); // -1
a.includes(NaN); // true
複製程式碼
另外在判斷+0
與-0
時,includes
和indexOf
的返回相同。
[1, +0, 3, 4].includes(-0); // true
[1, +0, 3, 4].indexOf(-0); // 1
複製程式碼
2.2 指數操作符(**)
基本用法:
let a = 3 ** 2 ; // 9
// 等效於
Math.pow(3, 2); // 9
複製程式碼
**
是一個運算子,也可以滿足類似假髮的操作,如下:
let a = 3;
a **= 2; // 9
複製程式碼
3. ES8
3.1 async函式
3.1.1 介紹
ES8引入async
函式,是為了使非同步操作更加方便,其實它就是Generator函式的語法糖。
async
函式使用起來,只要把Generator函式的(*)號換成async
,yield
換成await
即可。對比如下:
// Generator寫法
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// async await寫法
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
複製程式碼
對比Genenrator有四個優點:
- (1)內建執行器
Generator函式執行需要有執行器,而
async
函式自帶執行器,即async
函式與普通函式一模一樣:
async f();
複製程式碼
- (2)更好的語義
async
和await
,比起星號
和yield
,語義更清楚了。async
表示函式裡有非同步操作,await
表示緊跟在後面的表示式需要等待結果。 - (3)更廣的適用性
yield
命令後面只能是 Thunk 函式或 Promise 物件,而async
函式的await
命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。 - (4)返回值是Promise
async
函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then
方法指定下一步的操作。
進一步說,async
函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await
命令就是內部then
命令的語法糖。
3.1.2 基本用法
async
函式返回一個Promise物件,可以使用then
方法新增回撥函式,函式執行時,遇到await
就會先返回,等到非同步操作完成,在繼續執行。
async function f(item){
let a = await g(item);
let b = await h(item);
return b;
}
f('hello').then(res => {
console.log(res);
})
複製程式碼
async
表明該函式內部有非同步操作,呼叫該函式時,會立即返回一個Promise物件。
另外還有個定時的案例,指定時間後執行:
function f (ms){
return new Promise(res => {
setTimeout(res, ms);
});
}
async function g(val, ms){
await f(ms);
console.log(val);
}
g('leo', 50);
複製程式碼
async
函式還有很多使用形式:
// 函式宣告
async function f (){...}
// 函式表示式
let f = async function (){...}
// 物件的方法
let a = {
async f(){...}
}
a.f().then(...)
// Class的方法
class c {
constructor(){...}
async f(){...}
}
// 箭頭函式
let f = async () => {...}
複製程式碼
3.1.3 返回Promise物件
async
內部return
返回的值會作為then
方法的引數,另外只有async
函式內部的非同步操作執行完,才會執行then
方法指定的回撥函式。
async function f(){
return 'leo';
}
f().then(res => { console.log (res) }); // 'leo'
複製程式碼
async
內部丟擲的錯誤,會被catch
接收。
async function f(){
throw new Error('err');
}
f().then (
v => console.log(v),
e => console.log(e)
)
// Error: err
複製程式碼
3.1.4 await命令
通常await
後面是一個Promise物件,如果不是就返回對應的值。
async function f(){
return await 10;
}
f().then(v => console.log(v)); // 10
複製程式碼
我們常常將async await
和try..catch
一起使用,並且可以放多個await
命令,也是防止非同步操作失敗因為中斷後續非同步操作的情況。
async function f(){
try{
await Promise.reject('err');
}catch(err){ ... }
return await Promise.resolve('leo');
}
f().then(v => console.log(v)); // 'leo'
複製程式碼
3.1.5 使用注意
- (1)
await
命令放在try...catch
程式碼塊中,防止Promise返回rejected
。 - (2)若多個
await
後面的非同步操作不存在繼發關係,最好讓他們同時執行。
// 效率低
let a = await f();
let b = await g();
// 效率高
let [a, b] = await Promise.all([f(), g()]);
// 或者
let a = f();
let b = g();
let a1 = await a();
let b1 = await b();
複製程式碼
- (3)
await
命令只能用在async
函式之中,如果用在普通函式,就會報錯。
// 錯誤
async function f(){
let a = [{}, {}, {}];
a.forEach(v =>{ // 報錯,forEach是普通函式
await post(v);
});
}
// 正確
async function f(){
let a = [{}, {}, {}];
for(let k of a){
await post(k);
}
}
複製程式碼
3.2 Promise.prototype.finally()
finally()
是ES8中Promise新增的一個新標準,用於指定不管Promise物件最後狀態(是fulfilled
還是rejected
)如何,都會執行此操作,並且finally
方法必須寫在最後面,即在then
和catch
方法後面。
// 寫法如下:
promise
.then(res => {...})
.catch(err => {...})
.finally(() => {...})
複製程式碼
finally
方法常用在處理Promise請求後關閉伺服器連線:
server.listen(port)
.then(() => {..})
.finally(server.stop);
複製程式碼
本質上,finally方法是then方法的特例:
promise.finally(() => {...});
// 等同於
promise.then(
result => {
// ...
return result
},
error => {
// ...
throw error
}
)
複製程式碼
3.3 Object.values(),Object.entries()
ES7中新增加的 Object.values()
和Object.entries()
與之前的Object.keys()
類似,返回陣列型別。
回顧下Object.keys()
:
var a = { f1: 'hi', f2: 'leo'};
Object.keys(a); // ['f1', 'f2']
複製程式碼
3.3.1 Object.values()
返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷屬性的鍵值。
let a = { f1: 'hi', f2: 'leo'};
Object.values(a); // ['hi', 'leo']
複製程式碼
如果引數不是物件,則返回空陣列:
Object.values(10); // []
Object.values(true); // []
複製程式碼
3.3.2 Object.entries()
返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷屬性的鍵值對陣列。
let a = { f1: 'hi', f2: 'leo'};
Object.entries(a); // [['f1','hi'], ['f2', 'leo']]
複製程式碼
- 用途1:
遍歷物件屬性。
let a = { f1: 'hi', f2: 'leo'};
for (let [k, v] of Object.entries(a)){
console.log(
`${JSON.stringfy(k)}:${JSON.stringfy(v)}`
)
}
// 'f1':'hi'
// 'f2':'leo'
複製程式碼
- 用途2: 將物件轉為真正的Map結構。
let a = { f1: 'hi', f2: 'leo'};
let map = new Map(Object.entries(a));
複製程式碼
手動實現Object.entries()
方法:
// Generator函式實現:
function* entries(obj){
for (let k of Object.keys(obj)){
yield [k ,obj[k]];
}
}
// 非Generator函式實現:
function entries (obj){
let arr = [];
for(let k of Object.keys(obj)){
arr.push([k, obj[k]]);
}
return arr;
}
複製程式碼
3.4 Object.getOwnPropertyDescriptors()
之前有Object.getOwnPropertyDescriptor
方法會返回某個物件屬性的描述物件,新增的Object.getOwnPropertyDescriptors()
方法,返回指定物件所有自身屬性(非繼承屬性)的描述物件,所有原物件的屬性名都是該物件的屬性名,對應的屬性值就是該屬性的描述物件
let a = {
a1:1,
get f1(){ return 100}
}
Object.getOwnPropetyDescriptors(a);
/*
{
a:{ configurable:true, enumerable:true, value:1, writeable:true}
f1:{ configurable:true, enumerable:true, get:f, set:undefined}
}
*/
複製程式碼
實現原理:
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
複製程式碼
引入這個方法,主要是為了解決Object.assign()
無法正確拷貝get
屬性和set
屬性的問題。
let a = {
set f(v){
console.log(v)
}
}
let b = {};
Object.assign(b, a);
Object.a(b, 'f');
/*
f = {
configurable: true,
enumable: true,
value: undefined,
writeable: true
}
*/
複製程式碼
value
為undefined
是因為Object.assign
方法不會拷貝其中的get
和set
方法,使用getOwnPropertyDescriptors
配合Object.defineProperties
方法來實現正確的拷貝:
let a = {
set f(v){
console.log(v)
}
}
let b = {};
Object.defineProperties(b, Object.getOwnPropertyDescriptors(a));
Object.getOwnPropertyDescriptor(b, 'f')
/*
configurable: true,
enumable: true,
get: undefined,
set: function(){...}
*/
複製程式碼
Object.getOwnPropertyDescriptors
方法的配合Object.create
方法,將物件屬性克隆到一個新物件,實現淺拷貝。
const clone = Object.create(Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
// 或者
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
複製程式碼
3.5 字串填充 padStart和padEnd
用來為字串填充特定字串,並且都有兩個引數:字串目標長度和填充欄位,第二個引數可選,預設空格。
'es8'.padStart(2); // 'es8'
'es8'.padStart(5); // ' es8'
'es8'.padStart(6, 'woof'); // 'wooes8'
'es8'.padStart(14, 'wow'); // 'wowwowwowwoes8'
'es8'.padStart(7, '0'); // '0000es8'
'es8'.padEnd(2); // 'es8'
'es8'.padEnd(5); // 'es8 '
'es8'.padEnd(6, 'woof'); // 'es8woo'
'es8'.padEnd(14, 'wow'); // 'es8wowwowwowwo'
'es8'.padEnd(7, '6'); // 'es86666'
複製程式碼
從上面結果來看,填充函式只有在字元長度小於目標長度時才有效,若字元長度已經等於或小於目標長度時,填充字元不會起作用,而且目標長度如果小於字串本身長度時,字串也不會做截斷處理,只會原樣輸出。
3.6 函式引數列表與呼叫中的尾部逗號
該特性允許我們在定義或者呼叫函式時新增尾部逗號而不報錯:
function es8(var1, var2, var3,) {
// ...
}
es8(10, 20, 30,);
複製程式碼
3.7 共享記憶體與原子操作
當記憶體被共享時,多個執行緒可以併發讀、寫記憶體中相同的資料。原子操作可以確保那些被讀、寫的值都是可預期的,即新的事務是在舊的事務結束之後啟動的,舊的事務在結束之前並不會被中斷。這部分主要介紹了 ES8 中新的建構函式 SharedArrayBuffer
以及擁有許多靜態方法的名稱空間物件 Atomic
。
Atomic
物件類似於 Math
物件,擁有許多靜態方法,所以我們不能把它當做建構函式。 Atomic
物件有如下常用的靜態方法:
- add /sub :為某個指定的value值在某個特定的位置增加或者減去某個值
- and / or /xor :進行位操作
- load :獲取特定位置的值
4. ES9
4.1 物件的擴充運算子
4.1.1 介紹
物件的擴充運算子,即物件的Rest/Spread屬性,可將物件解構賦值用於從一個物件取值,搜鍵值對分配到指定物件上,與陣列的擴充運算子類似:
let {x, y, ...z} = {x:1, y:2, a:3, b:4};
x; // 1
y; // 2
z; // {a:3, b:4}
複製程式碼
物件的解構賦值要求等號右邊必須是個物件,所以如果等號右邊是undefined
或null
就會報錯無法轉成物件。
let {a, ...b} = null; // 執行時報錯
let {a, ...b} = undefined; // 執行時報錯
複製程式碼
解構賦值必須是最後一個引數,否則報錯。
let {...a, b, c} = obj; // 語法錯誤
let {a, ...b, c} = obj; // 語法錯誤
複製程式碼
注意:
- 1.解構賦值是淺拷貝。
let a = {a1: {a2: 'leo'}};
let {...b} = a;
a.a1.a2 = 'leo';
b.a1.a2 = 'leo';
複製程式碼
- 2.擴充運算子的解構賦值,不能複製繼承自原型物件的屬性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3; // { b: 2 }
o3.a; // undefined
複製程式碼
4.1.2 使用場景
- 1.取出引數物件所有可遍歷屬性,拷貝到當前物件中。
let a = { a1:1, a2:2 };
let b = { ...a };
b; // { a1:1, a2:2 }
// 類似Object.assign方法
複製程式碼
- 2.合併兩個物件。
let a = { a1:1, a2:2 };
let b = { b1:11, b2:22 };
let ab = { ...a, ...b }; // {a1: 1, a2: 2, b1: 11, b2: 22}
// 等同於
let ab = Object.assign({}, a, b);
複製程式碼
- 3.將自定義屬性放在擴充運算子後面,覆蓋物件原有的同名屬性。
let a = { a1:1, a2:2, a3:3 };
let r = { ...a, a3:666 };
// r {a1: 1, a2: 2, a3: 666}
// 等同於
let r = { ...a, ...{ a3:666 }};
// r {a1: 1, a2: 2, a3: 666}
// 等同於
let r = Object.assign({}, a, { a3:666 });
// r {a1: 1, a2: 2, a3: 666}
複製程式碼
- 4.將自定義屬性放在擴充運算子前面,就會成為設定新物件的預設值。
let a = { a1:1, a2:2 };
let r = { a3:666, ...a };
// r {a3: 666, a1: 1, a2: 2}
// 等同於
let r = Object.assign({}, {a3:666}, a);
// r {a3: 666, a1: 1, a2: 2}
// 等同於
let r = Object.assign({a3:666}, a);
// r {a3: 666, a1: 1, a2: 2}
複製程式碼
- 5.擴充運算子後面可以使用表示式。
let a = {
...(x>1? {a:!:{}),
b:2
}
複製程式碼
- 6.擴充運算子後面如果是個空物件,則沒有任何效果。
{...{}, a:1}; // {a:1}
複製程式碼
- 7.若引數是
null
或undefined
則忽略且不報錯。
let a = { ...null, ...undefined }; // 不報錯
複製程式碼
- 8.若有取值函式
get
則會執行。
// 不會列印 因為f屬性只是定義 而不沒執行
let a = {
...a1,
get f(){console.log(1)}
}
// 會列印 因為f執行了
let a = {
...a1,
...{
get f(){console.log(1)}
}
}
複製程式碼
4.2 正規表示式 s 修飾符
在正規表示式中,點(.
)可以表示任意單個字元,除了兩個:用u
修飾符解決四個位元組的UTF-16字元,另一個是行終止符。
終止符即表示一行的結束,如下四個字元屬於“行終止符”:
- U+000A 換行符(\n)
- U+000D 回車符(\r)
- U+2028 行分隔符(line separator)
- U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')
// false
複製程式碼
上面程式碼中,因為.
不匹配\n
,所以正規表示式返回false
。
換個醒,可以匹配任意單個字元:
/foo[^]bar/.test('foo\nbar')
// true
複製程式碼
ES9引入s
修飾符,使得.
可以匹配任意單個字元:
/foo.bar/s.test('foo\nbar') // true
複製程式碼
這被稱為dotAll
模式,即點(dot
)代表一切字元。所以,正規表示式還引入了一個dotAll
屬性,返回一個布林值,表示該正規表示式是否處在dotAll
模式。
const re = /foo.bar/s;
// 另一種寫法
// const re = new RegExp('foo.bar', 's');
re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'
複製程式碼
/s
修飾符和多行修飾符/m
不衝突,兩者一起使用的情況下,.
匹配所有字元,而^
和$
匹配每一行的行首和行尾。
4.3 非同步遍歷器
在前面ES6章節中,介紹了Iterator介面,而ES6引入了“非同步遍歷器”,是為非同步操作提供原生的遍歷器介面,即value
和done
這兩個屬性都是非同步產生的。
4.3.1 非同步遍歷的介面
通過呼叫遍歷器的next
方法,返回一個Promise物件。
a.next().then(
({value, done}) => {
//...
}
)
複製程式碼
上述a
為非同步遍歷器,呼叫next
後返回一個Promise物件,再呼叫then
方法就可以指定Promise物件狀態變為resolve
後執行的回撥函式,引數為value
和done
兩個屬性的物件,與同步遍歷器一致。
與同步遍歷器一樣,非同步遍歷器介面也是部署在Symbol.asyncIterator
屬性上,只要有這個屬性,就都可以非同步遍歷。
let a = createAsyncIterable(['a', 'b']);
//createAsyncIterable方法用於構建一個iterator介面
let b = a[Symbol.asyncInterator]();
b.next().then( result1 => {
console.log(result1); // {value: 'a', done:false}
return b.next();
}).then( result2 => {
console.log(result2); // {value: 'b', done:false}
return b.next();
}).then( result3 => {
console.log(result3); // {value: undefined, done:true}
})
複製程式碼
另外next
方法返回的是一個Promise物件,所以可以放在await
命令後。
async function f(){
let a = createAsyncIterable(['a', 'b']);
let b = a[Symbol.asyncInterator]();
console.log(await b.next());// {value: 'a', done:false}
console.log(await b.next());// {value: 'b', done:false}
console.log(await b.next());// {value: undefined, done:true}
}
複製程式碼
還有一種情況,使用Promise.all
方法,將所有的next
按順序連續呼叫:
let a = createAsyncIterable(['a', 'b']);
let b = a[Symbol.asyncInterator]();
let {{value:v1}, {value:v2}} = await Promise.all([
b.next(), b.next()
])
複製程式碼
也可以一次呼叫所有next
方法,再用await
最後一步操作。
async function f(){
let write = openFile('aaa.txt');
write.next('hi');
write.next('leo');
await write.return();
}
f();
複製程式碼
4.3.2 for await...of
for...of
用於遍歷同步的Iterator介面,而ES8引入for await...of
遍歷非同步的Iterator介面。
async function f(){
for await(let a of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
複製程式碼
上面程式碼,createAsyncIterable()
返回一個擁有非同步遍歷器介面的物件,for...of
自動呼叫這個物件的next
方法,得到一個Promise物件,await
用來處理這個Promise,一但resolve
就把得到的值x
傳到for...of
裡面。
用途
直接把部署了asyncIteable操作的非同步介面放入這個迴圈。
let a = '';
async function f(){
for await (let b of req) {
a += b;
}
let c = JSON.parse(a);
console.log('leo', c);
}
複製程式碼
當next
返回的Promise物件被reject
,for await...of
就會保錯,用try...catch
捕獲。
async function f(){
try{
for await (let a of iterableObj()){
console.log(a);
}
}catch(e){
console.error(e);
}
}
複製程式碼
注意,for await...of
迴圈也可以用於同步遍歷器。
(async function () {
for await (let a of ['a', 'b']) {
console.log(a);
}
})();
// a
// b
複製程式碼
4.3.3 非同步Generator函式
就像 Generator 函式返回一個同步遍歷器物件一樣,非同步 Generator 函式的作用,是返回一個非同步遍歷器物件。
在語法上,非同步 Generator 函式就是async
函式與 Generator 函式的結合。
async function* f() {
yield 'hi';
}
const a = f();
a.next().then(x => console.log(x));
// { value: 'hello', done: false }
複製程式碼
設計非同步遍歷器的目的之一,就是為了讓Generator函式能用同一套介面處理同步和非同步操作。
// 同步Generator函式
function * f(iterable, fun){
let a = iterabl[Symbol.iterator]();
while(true){
let {val, done} = a.next();
if(done) break;
yield fun(val);
}
}
// 非同步Generator函式
async function * f(iterable, fun){
let a = iterabl[Symbol.iterator]();
while(true){
let {val, done} = await a.next();
if(done) break;
yield fun(val);
}
}
複製程式碼
同步和非同步Generator函式相同點:在yield
時用next
方法停下,將後面表示式的值作為next()
返回物件的value
。
在非同步Generator函式中,同時使用await
和yield
,簡單樣理解,await
命令用於將外部操作產生的值輸入函式內部,yield
命令用於將函式內部的值輸出。
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
複製程式碼
非同步 Generator 函式可以與for await...of
迴圈結合起來使用。
async function* f(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
複製程式碼
4.3.4 yield* 語句
yield*
語句跟一個非同步遍歷器。
async function * f(){
yield 'a';
yield 'b';
return 'leo';
}
async function * g(){
const a = yield* f(); // a => 'leo'
}
複製程式碼
與同步 Generator 函式一樣,for await...of
迴圈會展開yield*
。
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
複製程式碼
5. 知識補充
5.1 塊級作用域
通常指一個函式內部,或者一個程式碼塊內部。
比如:
function fun1 () {
// 塊級作用域
if (true) {
// 塊級作用域
}
}
複製程式碼
缺點: 1.導致內層變數覆蓋外層變數
var a1 = new Date();
function f1 (){
console.log(a1); // undefined
if (false) {
var a1 = 'hello'
}
}
複製程式碼
輸出 undefined
是因為 if
內的 a1
變數宣告的變數提升,導致內部的 a1
覆蓋外部的 a1
,所以輸出為 undefined
。
2.變數的全域性汙染
var a = 'hello';
for (var i = 0; i< a.length; i++) {
//...
}
console.log(i); // 5
複製程式碼
迴圈結束後,變數 i
的值依然存在,造成變數的全域性汙染。
5.2 ES5/6對陣列空位的處理
陣列的空位不是undefined
,而是沒有任何值,陣列的undefined
也是有值。
0 in [undefined,undefined,undefined] // true
0 in [,,,] // false
複製程式碼
ES5對空位的處理:
forEach()
,filter()
,reduce()
,every()
和some()
都會跳過空位。map()
會跳過空位,但會保留這個值。join()
和toString()
會將空位視為undefined
,而undefined
和null
會被處理成空字串。
[,'a'].forEach((x,i) => console.log(i)); // 1
['a',,'b'].filter(x => true); // ['a','b']
[,'a'].every(x => x==='a'); // true
[1,,2].reduce((x,y) => x+y); // 3
[,'a'].some(x => x !== 'a'); // false
[,'a'].map(x => 1); // [,1]
[,'a',undefined,null].join('#'); // "#a##"
[,'a',undefined,null].toString(); // ",a,,"
複製程式碼
ES6對空位的處理:
將空位視為正常值,轉成undefined
。
Array.from(['a',,'b']);// [ "a", undefined, "b" ]
[...['a',,'b']]; // [ "a", undefined, "b" ]
//copyWithin() 會連空位一起拷貝。
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
//fill()會將空位視為正常的陣列位置。
new Array(3).fill('a') // ["a","a","a"]
//for...of迴圈也會遍歷空位。
let arr = [, ,];
for (let i of arr) {
console.log(1);
} // 1 1
複製程式碼
entries()
、keys()
、values()
、find()
和findIndex()
會將空位處理成undefined
。
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
[...[,'a'].keys()] // [0,1]
[...[,'a'].values()] // [undefined,"a"]
[,'a'].find(x => true) // undefined
[,'a'].findIndex(x => true) // 0
複製程式碼
由於空位的處理規則非常不統一,所以建議避免出現空位。