es6學習筆記

hello芳芳發表於2024-03-20

11.15 星期三
學習地址:ECMAScript 6 入門 http://es6.ruanyifeng.com/ 阮一峰
下載node js,Node JS環境搭建及sublime Text 3配置Node Js環境,新增前端外掛。
一、es6簡介:
二者關係:ECMAScript 和 JavaScript 的關係是,前者是後者的規格,後者是前者的一種實現(另外的 ECMAScript 方言還有 Jscript 和 ActionScript)。日常場合,這兩個詞是可以互換的。
ES6 既是一個歷史名詞,也是一個泛指,含義是 5.1 版以後的 JavaScript 的下一代標準,涵蓋了 ES2015、ES2016、ES2017 等等,而 ES2015 則是正式名稱,特指該年釋出的正式版本的語言標準。本書中提到 ES6 的地方,一般是指 ES2015 標準,但有時也是泛指“下一代 JavaScript 語言”。

二、let和const命令:

let:
1、不存在提升變數:所宣告的變數一定要在宣告後使用,否則報錯。
2、暫時性死區:在程式碼塊內,使用let命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。
只要塊級作用域記憶體在let命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。在let宣告變數a前,對變數a賦值會報錯
如果一個變數根本沒有被宣告,使用typeof反而不會報錯。
例如:typeof x; let x; //報錯
typeof y;//undefined
let a=a;//報錯
var b=b;//不報錯
常見的死區:function a(x=y,y=2){};a();//y還沒宣告,x不能使用y;
3、不允許重複宣告:
4、塊級作用域:ES5 只有全域性作用域和函式作用域。let實際上為 JavaScript 新增了塊級作用域,只要let相同名字的變數不在同一層作用域就可以
5、塊級作用域與函式宣告:
ES5 規定,函式只能在頂層作用域和函式作用域之中宣告,不能在塊級作用域宣告。
ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。
ES6 規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。
(1)允許在塊級作用域內宣告函式。
(2)函式宣告類似於var,即會提升到全域性作用域或函式作用域的頭部。
(3)同時,函式宣告還會提升到所在的塊級作用域的頭部。
注意,上面三條規則只對 ES6 的瀏覽器實現有效,其他環境的實現不用遵守,還是將塊級作用域的函式宣告當作let處理。
6、do表示式:在塊級作用域之前加上do,使它變為do表示式,然後就會返回內部最後執行的表示式的值。
例如:{let t = f(); t = t * t + 1;} //沒有返回值,無法得到t的值;
let x = do { let t = f(); t * t + 1;}; //變數x會得到整個塊級作用域的返回值(t * t + 1)。

var:
1、存在提升變數:var命令會發生”變數提升“現象,即變數可以在宣告之前使用,值為undefined。

const:
1、宣告一個只讀的常量。一旦宣告,必須立即初始化,常量的值就不能改變。
2、不存在提升變數
3、只在所宣告的作用域裡有效
4、存在暫時性死區
5、不可重複宣告
本質:const實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址不得改動。
但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,
儲存的只是一個指標,const只能保證這個指標是固定的,至於它指向的資料結構是不是可變的,就完全不能控制了。
(可以給物件加屬性,給陣列加元素,但是不能重新指向新的地址)
將物件凍結,應該使用Object.freeze方法。const foo = Object.freeze({});//不能給該物件新增屬性,否則報錯;

宣告方法:
ES5 只有兩種宣告變數的方法:var命令和function命令。
ES6 有6種宣告方法:var 、function、let、const、import和class。

頂層物件:在瀏覽器環境指的是window物件,在 Node 指的是global物件。ES5 之中,頂層物件的屬性與全域性變數是等價的。
var命令和function命令宣告的全域性變數,依舊是頂層物件的屬性;
另一方面規定,let命令、const命令、class命令宣告的全域性變數,不屬於頂層物件的屬性。

global物件:
1、瀏覽器裡面,頂層物件是window,但 Node 和 Web Worker 沒有window。
2、瀏覽器和 Web Worker 裡面,self也指向頂層物件,但是 Node 沒有self。
3、Node 裡面,頂層物件是global,但其他環境都不支援。

this:
全域性環境中,this會返回頂層物件。但是,Node 模組和 ES6 模組中,this返回的是當前模組。
函式里面的this,如果函式不是作為物件的方法執行,而是單純作為函式執行,this會指向頂層物件。但是,嚴格模式下,這時this會返回undefined。
不管是嚴格模式,還是普通模式,new Function('return this')(),總是會返回全域性物件。但是,如果瀏覽器用了 CSP(Content SecurityPolicy,內容安全政策),那麼eval、new Function這些方法都可能無法使用。

11。16 星期四:

三、變數的解構賦值:
1、基本用法:
形如let [a, b, c] = [1, 2, 3];//從陣列中提取值,按照對應位置,對變數賦值。
如果對應不上,就預設為undefined;兩邊都要是陣列的形式。否則解構失敗
2、解構允許使用預設值:例如:let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
如果一個陣列成員不嚴格等於undefined,預設值是不會生效的。
例如:let [x = 1] = [null]; x // null 如果一個陣列成員是null,預設值就不會生效,因為null不嚴格等於undefined。
3、物件的解構賦值:解構不僅可以用於陣列,還可以用於物件。
例如:let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
陣列的元素是按次序排列的,變數的取值由它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值。
4、物件的解構也可以指定預設值。
5、字串的解構賦值:
6、字串的解構賦值:解構賦值的規則是,只要等號右邊的值不是物件或陣列,就先將其轉為物件。
由於undefined和null無法轉為物件,所以對它們進行解構賦值,都會報錯。
7、函式引數的解構賦值:例如:function add([x, y]){ return x + y;}; add([1, 2]); // 3
8、以下三種解構賦值不得使用圓括號:(1)變數宣告語句
(2)函式引數
(3)賦值語句的模式
9、可以使用圓括號的情況:賦值語句的非模式部分,可以使用圓括號。
例如:[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確
10、用途:(1)交換變數的值:let x = 1;let y = 2;[x, y] = [y, x];
(2)從函式返回多個值:function example() {return [1, 2, 3];} ; let [a, b, c] = example();
(3)函式引數的定義:function f([x, y, z]) { ... };f([1, 2, 3]);
(4)提取 JSON 資料:let jsonData = {id: 42,status: "OK",data: [867, 5309]}; let { id, status, data: number } = jsonData;
(5)函式引數的預設值
(6)遍歷 Map 結構:const map = new Map(); map.set('first', 'hello'); map.set('second', 'world');
for (let [key, value] of map) { console.log(key + " is " + value);}
// first is hello // second is world
(7)輸入模組的指定方法


11.17 星期五
四、字串的擴充套件:
1、字元的 Unicode 表示法:JavaScript 允許採用\uxxxx形式表示一個字元,其中xxxx表示字元的 Unicode 碼點。
只要將碼點放入大括號,就能正確解讀該字元。"\u{20BB7}"// "��"
以下有6種方法表示:
(1)'\z' === 'z' // true
(2)'\172' === 'z' // true
(3)'\x7A' === 'z' // true
(4)'\u007A' === 'z' // true
(5)'\u{7A}' === 'z' // true
(6)"\u{41}\u{42}\u{43}"// "ABC"
2、codePointAt():能夠正確處理 4 個位元組儲存的字元,返回一個字元的碼點。
3、String.fromCodePoint():用於從碼點返回對應字元
4、字串的遍歷器介面:字串可以被for...of迴圈遍歷。
5、at():可以識別 Unicode 編號大於0xFFFF的字元,返回正確的字元。
6、normalize():用來將字元的不同表示方法統一為同樣的形式,這稱為 Unicode 正規化。
7、includes(), startsWith(), endsWith():傳統上,JavaScript 只有indexOf方法,可以用來確定一個字串是否包含在另一個字串中。
includes():返回布林值,表示是否找到了引數字串。
startsWith():返回布林值,表示引數字串是否在原字串的頭部。
endsWith():返回布林值,表示引數字串是否在原字串的尾部。
8、repeat(n):返回一個新字串,表示將原字串重複n次;引數是負數或者Infinity,會報錯。引數NaN等同於 0。
9、padStart(),padEnd():原字串的長度,等於或大於指定的最小長度,則返回原字串。
省略第二個引數,預設使用空格補全長度。
方法(長度,‘補全的內容’)
padStart()用於頭部補全,'x'.padStart(5, 'ab') // 'ababx' 一共5個字元,x前面以ab補全
padEnd()用於尾部補全。'x'.padEnd(5, 'ab') // 'xabab' 一共5個字元,x後面以ab補全
10、模板字串:用反引號(`)標識。它可以當作普通字串使用,也可以用來定義多行字串,或者在字串中嵌入變數。
例如:原來:
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
現在:
`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`
模板字串中嵌入變數,需要將變數名寫在${變數名|函式名()}之中。
模板使用<%...%>放置 JavaScript 程式碼,使用<%= ... %>輸出 JavaScript 表示式。
11、例項:模板編譯
12、標籤模板:模板字串的功能,不僅僅是上面這些。它可以緊跟在一個函式名後面,該函式將被呼叫來處理這個模板字串。
這被稱為“標籤模板”功能(tagged template)。
標籤模板其實不是模板,而是函式呼叫的一種特殊形式。“標籤”指的就是函式,緊跟在後面的模板字串就是它的引數。
alert`123`
// 等同於
alert(123)
例如:
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同於
tag(['Hello ', ' world ', ''], 15, 50);
上面程式碼中,模板字串前面有一個標識名tag,它是一個函式。整個表示式的返回值,就是tag函式處理模板字串後的返回值。
tag函式的第一個引數是一個陣列,該陣列的成員是模板字串中那些沒有變數替換的部分,也就是說,變數替換隻發生在陣列的第一個成員與第二個成員之間、第二個成員與第三個成員之間,以此類推。
13、String.raw():String.raw方法可以作為處理模板字串的基本方法,它會將所有變數替換,而且對斜槓進行轉義,方便下一步作為字串來使用。
String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'

// 等同於
String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
14、模板字串的限制:標籤模板裡面,可以內嵌其他語言。但是,模板字串預設會將字串轉義,導致無法嵌入其他語言。

五、正則的擴充套件:
1、RegExp 建構函式:
在 ES5 中,RegExp建構函式的引數有兩種情況:
第一種情況是,引數是字串,這時第二個參數列示正規表示式的修飾符(flag)。
var regex = new RegExp('xyz', 'i');
第二種情況是,引數是一個正則表示式,這時會返回一個原有正規表示式的複製。
var regex = new RegExp(/xyz/i);
// 它們都等價於
var regex = /xyz/i;
在ES6中:
如果RegExp建構函式第一個引數是一個正則物件,那麼可以使用第二個引數指定修飾符。
而且,返回的正規表示式會忽略原有的正規表示式的修飾符,只使用新指定的修飾符。
new RegExp(/abc/ig, 'i').flags // 在es5中報錯
new RegExp(/abc/ig, 'i').flags //原有正則物件的修飾符是ig,它會被第二個引數i覆蓋。
2、字串的正則方法:
String.prototype.match 呼叫 RegExp.prototype[Symbol.match]
String.prototype.replace 呼叫 RegExp.prototype[Symbol.replace]
String.prototype.search 呼叫 RegExp.prototype[Symbol.search]
String.prototype.split 呼叫 RegExp.prototype[Symbol.split]
3、u 修飾符:
ES6 對正規表示式新增了u修飾符,含義為“Unicode 模式”,用來正確處理大於\uFFFF的 Unicode 字元。也就是說,會正確處理四個位元組的 UTF-16 編碼。
(1)點字元:點(.)字元在正規表示式中,含義是除了換行符以外的任意單個字元。對於碼點大於0xFFFF的 Unicode 字元,點字元不能識別,必須加上u修飾符。
(2)Unicode 字元表示法:ES6 新增了使用大括號表示 Unicode 字元,這種表示法在正規表示式中必須加上u修飾符,才能識別當中的大括號,否則會被解讀為量詞。
(3)量詞:使用u修飾符後,所有量詞都會正確識別碼點大於0xFFFF的 Unicode 字元。
(4)預定義模式:u修飾符也影響到預定義模式,能否正確識別碼點大於0xFFFF的 Unicode 字元。
(5)i 修飾符:有些 Unicode 字元的編碼不同,但是字型很相近,比如,\u004B與\u212A都是大寫的K。
4、y 修飾符:與g修飾符類似,也是全域性匹配,後一次匹配都從上一次匹配成功的下一個位置開始,,g修飾符只要剩餘位置中匹配就可,二y修飾符確保匹配必須從剩餘的第一個位置開始,這就是“粘連”的涵義。
5、sticky 屬性:與y修飾符匹配,ES6的正則物件多了sticky屬性,表示是否設定了y修飾符;
6、flags 屬性:返回正規表示式的修飾符
7、s 修飾符:dotAll 模式
8、後行斷言
9、Unicode 屬性類
10具名組匹配

六、數值的擴充套件:
1、二進位制和八進位制表示法:分別用字首0b和0o表示;
2、Number.isFinite():用來檢查一個數值是否非無窮
3、Number.NaN()檢查一個值是否為NaN
4、es6將parseInt(),和parseFloat()一直到Number物件,行為保持不變;
5、Number.isInteger()來判斷一個值是否為整數。
6、Number.EPSILON:是一個可以接受的誤差範圍;
7、安全整數和Number.isSafeInteger():判斷一個整數是否落在這個範圍之內;

Math物件的擴充套件:
1、Math。trunc():去除一個數的小數部分;
2、Math.sign()判斷一個數到底是正數、負數、還是零;引數為正數,返回+1;引數為負數,返回-1;引數為0,返回0;引數為-0返回-0;其他值,返回NaN;
3、Math.cbrt()技術一個數的立方根
4、Math.clz32()整數使用32位二進位制形式表示,返回一個32位無符號整數形式有多少個前導0;
5、Math.imul(引數1,引數2)返回兩個數一32位帶符號整數形式相乘的結果,返回也是一個32位的帶符號整數;
6、Math.fround()返回一個數的單精度浮點數形式;
7、Math.hypot()返回所有引數的平方和的平方根;
es6新增了4個對數相關方法:
1、Math.expm1()返回ex-1,即Math.exp(x)-1
2、Math.log1p()返回1+x的自然對數,即Math.log(1+x),如果x小於-1,返回NaN;
3、Math.log10()返回以10為底的x的對數,如果x小於0,則返回NaN;
4、Math.log2()返回以2為底數的x的對數,如果x小於0,則返回NaN;
es新增6個三角函式方法。
Math.sinh\cosh\tanh\asinh\acosh\atanh(x):返回x的雙曲正弦\雙曲餘弦\雙曲正弦\反雙曲正弦\反雙曲餘弦\反雙曲正切;


11.21 星期二
七、函式的擴充套件:
1、函式引數的預設值:
ES6 之前,不能直接為函式的引數指定預設值,只能採用變通的方法。
ES6 允許為函式的引數設定預設值,即直接寫在引數定義的後面。
引數變數是預設宣告的,所以不能用let或const再次宣告。
使用引數預設值時,函式不能有同名引數。
引數預設值不是傳值的,而是每次都重新計算預設值表示式的值。也就是說,引數預設值是惰性求值的。

1、與解構賦值預設值結合使用
引數預設值可以與解構賦值的預設值,結合起來使用。
2、引數預設值的位置
通常情況下,定義了預設值的引數,應該是函式的尾引數。
當引數對應undefined,結果觸發了預設值,引數等於null,就沒有觸發預設值。
3、函式的 length 屬性
指定了預設值以後,函式的length屬性,將返回沒有指定預設值的引數個數。也就是說,指定了預設值後,length屬性將失真。
如果設定了預設值的引數不是尾引數,那麼length屬性也不再計入後面的引數了。例如:(function (a = 0, b, c) {}).length // 0
2、rest 引數:
ES6 引入 rest 引數(形式為...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。
rest 引數搭配的變數是一個陣列,該變數將多餘的引數放入陣列中。
1、arguments物件不是陣列,而是一個類似陣列的物件。所以為了使用陣列的方法,必須使用Array.prototype.slice.call先將其轉為陣列。
2、rest 引數就不存在這個問題,它就是一個真正的陣列,陣列特有的方法都可以使用。
注意,rest 引數之後不能再有其他引數(即只能是最後一個引數),否則會報錯。
函式的length屬性,不包括 rest 引數。
3、嚴格模式:
從 ES5 開始,函式內部可以設定為嚴格模式。
ES2016 做了一點修改,規定只要函式引數使用了/預設值/、/解構賦值/、或者/擴充套件運算子/,那麼函式內部就不能顯式設定為嚴格模式,否則會報錯。
第一種是設定全域性性的嚴格模式,這是合法的。
'use strict';
function doSomething(a, b = a) {
// code
}
第二種是把函式包在一個無引數的立即執行函式里面。
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
4、name 屬性:
函式的name屬性,返回該函式的函式名。例如:function foo() {} ;foo.name // "foo"
1、將一個匿名函式賦值給一個變數,ES5 的name屬性,會返回空字串,而 ES6 的name屬性會返回實際的函式名。
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
2、將一個具名函式賦值給一個變數,則 ES5 和 ES6 的name屬性都返回這個具名函式原本的名字。
Function建構函式返回的函式例項,name屬性的值為anonymous。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
3、Function建構函式返回的函式例項,name屬性的值為anonymous。
(new Function).name // "anonymous"
4、bind返回的函式,name屬性值會加上bound字首。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
5、箭頭函式:var 函式名=(引數s)=>返回值
var f = v => v;等同於 var f = function(v) { return v;};
1、如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。
2、如果箭頭函式的程式碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return語句返回。
var sum = (num1, num2) => { return num1 + num2; }
3、所以如果箭頭函式直接返回一個物件,必須在物件外面加上括號,否則會報錯。
箭頭函式有幾個使用注意點。

(1)函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件。

(2)不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。

(3)不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。

(4)不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。
箭頭函式里面根本沒有自己的this,而是引用外層的this。
6、雙冒號運算子:箭頭函式可以繫結this物件,大大減少了顯式繫結this物件的寫法(call、apply、bind)。但是,箭頭函式並不適用於所有場合,所以現在有一個提案,提出了“函式繫結”(function bind)運算子,用來取代call、apply、bind呼叫。
函式繫結運算子是並排的兩個冒號(::),雙冒號左邊是一個物件,右邊是一個函式。
例如:foo::bar;// 等同於bar.bind(foo);
1、如果雙冒號左邊為空,右邊是一個物件的方法,則等於將該方法繫結在該物件上面。
2、雙冒號運算子的運算結果,還是一個物件,因此可以採用鏈式寫法。
7、尾呼叫最佳化:某個函式的最後一步是呼叫另一個函式。
例如:function f(x){ return g(x);}//一定要有一個return,不加return 就預設為return undefined,就不屬於尾呼叫;
1、尾呼叫最佳化:只保留內層函式的呼叫幀。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫幀只有一項,這將大大節省記憶體。
2、尾遞迴:函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。
3、遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生“棧溢位”錯誤(stack overflow)。
但對於尾遞迴來說,由於只存在一個呼叫幀,所以永遠不會發生“棧溢位”錯誤。
ES6 中只要使用尾遞迴,就不會發生棧溢位,相對節省記憶體。
4、ES6 的尾呼叫最佳化只在嚴格模式下開啟,正常模式是無效的。
這是因為在正常模式下,函式內部有兩個變數,可以跟蹤函式的呼叫棧。嚴格模式則不可以使用這兩個變數,否則報錯;
func.arguments:返回撥用時函式的引數。
func.caller:返回撥用當前函式的那個函式。
8、函式引數的尾逗號:
ES2017 允許函式的最後一個引數有尾逗號(trailing comma)。
這樣的規定也使得,函式引數與陣列和物件的尾逗號規則,保持一致了。
9、catch 語句的引數:
1、傳統:寫法是catch語句必須帶有引數,用來接收try程式碼塊丟擲的錯誤。
try {
// ···
} catch (error) {
// ···
}
2、新的寫法允許省略catch後面的引數,而不報錯。
try {
// ···
} catch {
// ···
}


八、陣列的擴充套件:
1、Array.from()用於將兩類物件轉為真正的陣列;類似陣列的物件,和可遍歷的物件;
用法:Array.from(物件名\長度,對每個引數進行處理)
Array.from(【1、2、3】,(x)=>x*x);//1、4、9、
2、Array.of()將一組值轉換為陣列;Array.of(3, 11, 8) // [3,11,8]
3、Array.prototyp.copyWithin(targer從該位開始替換,start=從第n位開始讀取資料,end=到第n為停止讀取)
4、find()找出第一個符合條件的陣列成員,它的引數是一個回撥引數,所有陣列成員依次執行回撥函式,知道找出第一個返回值為true的成員;
例如:【1、2、3、4】.find((n)=>n<0)
5、findIndex()返回第一個符合條件的陣列成員的位置,若所有成員都不符合條件,則返回-1;
例如:【1、5、10、15】.findIndex(func(value,index,arr){return value>9;})//2
6、fill(引數,起位置,止位置),將引數填充整個陣列;
7、es6提供三個新方法:
1、entries()返回鍵對值
2、keys()返回鍵名
3、values()返回鍵值
這三個都可以用在for……of裡
8、includs()返回布林值,表示某數是否包含給定的值;
9、陣列的空位:陣列的空位指,陣列的某一個位置沒有任何值。比如,Array建構函式返回的陣列都是空位。
Array(3) // [, , ,]Array(3)返回一個具有 3 個空位的陣列。
空位不是undefined,一個位置的值等於undefined,依然是有值的。空位是沒有任何值,in運算子可以說明這一點。
0 in [undefined, undefined, undefined] // true 0 號位置是有值的,
0 in [, , ,] // false 0 號位置沒有值。
ES5 對空位的處理,已經很不一致了,大多數情況下會忽略空位:
1、forEach(), filter(), every() 和some()都會跳過空位。
2、map()會跳過空位,但會保留這個值
3、join()和toString()會將空位視為undefined,而undefined和null會被處理成空字串。

ES6 則是明確將空位轉為undefined。
1、Array.from方法會將陣列的空位,轉為undefined,也就是說,這個方法不會忽略空位。
2、擴充套件運算子(...)也會將空位轉為undefined。
3、copyWithin()會連空位一起複製。
4、fill()會將空位視為正常的陣列位置。
5、for...of迴圈也會遍歷空位。
6、entries()、keys()、values()、find()和findIndex()會將空位處理成undefined。
由於空位的處理規則非常不統一,所以建議避免出現空位。

11.22 星期三
九、物件的擴充套件:
1、屬性的簡潔表示法:
ES6 允許直接寫入變數和函式,作為物件的屬性和方法。
ES6 允許在物件之中,直接寫變數。這時,屬性名為變數名, 屬性值為變數的值。
屬性的賦值器(setter)和取值器(getter),事實上也是採用這種寫法。
2、屬性名錶達式:
avaScript 定義物件的屬性,有兩種方法。
// 方法一 obj.foo = true;用識別符號作為屬性名
// 方法二 obj['a' + 'bc'] = 123;用表示式作為屬性名,這時要將表示式放在方括號之內。
表示式還可以用於定義方法名。
屬性名錶達式如果是一個物件,預設情況下會自動將物件轉為字串
3、方法的 name 屬性:函式的name屬性,返回函式名。物件方法也是函式,因此也有name屬性。
1、物件的方法使用了取值函式(getter)和存值函式(setter),則name屬性不是在該方法上面,
而是該方法的屬性的描述物件的get和set屬性上面,返回值是方法名前加上get和set。
const obj = {
get foo() {},
set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
2、有兩種特殊情況:
bind方法創造的函式,name屬性返回bound加上原函式的名字;
Function建構函式創造的函式,name屬性返回anonymous。
3、如果物件的方法是一個 Symbol 值,那麼name屬性返回的是這個 Symbol 值的描述。
4、Object.is():
1、用來比較兩個值是否嚴格相等,與嚴格比較運算子(===)的行為基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
2、不同之處只有兩個:一是+0不等於-0,二是NaN等於自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
5、Object.assign():用於物件的合併,將源物件(source)的所有可列舉屬性,複製到目標物件(target)。
例如:
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
Object.assign方法的第一個引數是目標物件,後面的引數都是源物件。
注意:
如果目標物件與源物件有同名屬性,或多個源物件有同名屬性,則後面的屬性會覆蓋前面的屬性。
如果只有一個引數,Object.assign會直接返回該引數。
如果該引數不是物件,則會先轉成物件,然後返回。
由於undefined和null無法轉成物件,所以如果它們作為引數,就會報錯。
如果undefined和null不在首引數,就不會報錯。
其他型別的值(即數值、字串和布林值)不在首引數,也不會報錯。但是,除了字串會以陣列形式,複製入目標物件,其他值都不會產生效果。
Object.assign複製的屬性是有限制的,只複製源物件的自身屬性(不複製繼承屬性),也不複製不可列舉的屬性(enumerable: false)。
屬性名為 Symbol 值的屬性,也會被Object.assign複製。

(1)淺複製
Object.assign方法實行的是淺複製,而不是深複製。
也就是說,如果源物件某個屬性的值是物件,那麼目標物件複製得到的是這個物件的引用。
(2)同名屬性的替換
對於這種巢狀的物件,一旦遇到同名屬性,Object.assign的處理方法是替換,而不是新增。
(3)陣列的處理
Object.assign可以用來處理陣列,但是會把陣列視為物件。Object.assign([1, 2, 3], [4, 5])// [4, 5, 3]
(4)取值函式的處理
Object.assign只能進行值的複製,如果要複製的值是一個取值函式,那麼將求值後再複製。//就是返回函式的最終結果——值;

常見用途:
(1)為物件新增屬性
(2)為物件新增方法
(3)克隆物件
(4)合併多個物件
(5)為屬性指定預設值

6、屬性的可列舉性和遍歷:
可列舉性
物件的每個屬性都有一個描述物件(Descriptor),用來控制該屬性的行為。Object.getOwnPropertyDescriptor方法可以獲取該屬性的描述物件。
描述物件的enumerable屬性,稱為”可列舉性“,如果該屬性為false,就表示某些操作會忽略當前屬性
1、目前,有四個操作會忽略enumerable為false的屬性。
for...in迴圈:只遍歷物件自身的和繼承的可列舉的屬性。
Object.keys():返回物件自身的所有可列舉的屬性的鍵名。
JSON.stringify():只序列化物件自身的可列舉的屬性。
Object.assign(): 忽略enumerable為false的屬性,只複製物件自身的可列舉的屬性。
2、ES6 規定,所有 Class 的原型的方法都是不可列舉的。
3、操作中引入繼承的屬性會讓問題複雜化,大多數時候,我們只關心物件自身的屬性。所以,儘量不要用for...in迴圈,而用Object.keys()代替。
4、屬性的遍歷
ES6 一共有 5 種方法可以遍歷物件的屬性。
(1)for...in
for...in迴圈遍歷物件自身的和繼承的可列舉屬性(不含 Symbol 屬性)。
(2)Object.keys(obj)
Object.keys返回一個陣列,包括物件自身的(不含繼承的)所有可列舉屬性(不含 Symbol 屬性)的鍵名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一個陣列,包含物件自身的所有屬性(不含 Symbol 屬性,但是包括不可列舉屬性)的鍵名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一個陣列,包含物件自身的所有 Symbol 屬性的鍵名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一個陣列,包含物件自身的所有鍵名,不管鍵名是 Symbol 或字串,也不管是否可列舉。
以上的 5 種方法遍歷物件的鍵名,都遵守同樣的屬性遍歷的次序規則。
首先遍歷所有數值鍵,按照數值升序排列。
其次遍歷所有字串鍵,按照加入時間升序排列。
最後遍歷所有 Symbol 鍵,按照加入時間升序排列。
7、Object.getOwnPropertyDescriptors():返回某個物件屬性的描述物件(descriptor)。
1、ES2017 引入了Object.getOwnPropertyDescriptors方法,返回指定物件所有自身屬性(非繼承屬性)的描述物件。
2、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、Object.getOwnPropertyDescriptors方法可以實現一個物件繼承另一個物件。
8、__proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()
1、__proto__屬性(前後各兩個下劃線),用來讀取或設定當前物件的prototype物件。目前,所有瀏覽器(包括 IE11)都部署了這個屬性。
// es6 的寫法
const obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es5 的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };
2、Object.setPrototypeOf方法的作用與__proto__相同,用來設定一個物件的prototype物件,返回引數物件本身。
它是 ES6 正式推薦的設定原型物件的方法。
// 格式
Object.setPrototypeOf(object繼承, prototype遺產)

// 用法
const o = Object.setPrototypeOf({}, null);
3、Object.getPrototypeOf(obj);讀取一個物件的原型物件。
9、super 關鍵字
this關鍵字總是指向函式所在的當前物件,ES6 又新增了另一個類似的關鍵字super,指向當前物件的原型物件。
const proto = {
foo: 'hello'
};

const obj = {
find() {
return super.foo;
}
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
注意,super關鍵字表示原型物件時,只能用在物件的方法之中,用在其他地方都會報錯。
第一種寫法是super用在屬性裡面,第二種和第三種寫法是super用在一個函式里面,然後賦值給foo屬性。
目前,只有物件方法的簡寫法可以讓 JavaScript 引擎確認,定義的是物件的方法。
const obj = {foo: super.foo}// 報錯
const obj = {foo: () => super.foo}// 報錯
const obj = {foo: function () {return super.foo}}// 報錯
對於 JavaScript 引擎來說,這裡的super都沒有用在物件的方法之中。
JavaScript 引擎內部,super.foo等同於Object.getPrototypeOf(this).foo(屬性)或Object.getPrototypeOf(this).foo.call(this)(方法)。
10、Object.keys(),Object.values(),Object.entries():
1、Object.keys():ES5 引入了Object.keys方法,返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名。
例如:var obj = { foo: 'bar', baz: 42 };Object.keys(obj)// ["foo", "baz"]
ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作為遍歷一個物件的補充手段,供for...of迴圈使用。
2、Object.values:方法返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值。
例如:const obj = { foo: 'bar', baz: 42 };Object.values(obj)// ["bar", 42]
注意:Object.values會過濾屬性名為 Symbol 值的屬性。
如果Object.values方法的引數是一個字串,會返回各個字元組成的一個陣列。
3、Object.entries方法:返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值對陣列。
例如:const obj = { foo: 'bar', baz: 42 };Object.entries(obj)// [ ["foo", "bar"], ["baz", 42] ]
注意:Object.entries的基本用途是遍歷物件的屬性。
Object.entries方法的另一個用處是,將物件轉為真正的Map結構。
11、物件的擴充套件運算子:
擴充套件運算子(...)。
const [a, ...b] = [1, 2, 3];
a // 1
b // [2, 3]
(1)解構賦值:物件的解構賦值用於從一個物件取值,相當於將所有可遍歷的、但尚未被讀取的屬性,分配到指定的物件上面。所有的鍵和它們的值,都會複製到新物件上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
(2)擴充套件運算子(...)用於取出引數物件的所有可遍歷屬性,複製到當前物件之中。
let z = { a: 3, b: 4 };let n = { ...z }; n // { a: 3, b: 4 }
1、這等同於使用Object.assign方法。let aClone = { ...a };// 等同於 let aClone = Object.assign({}, a);
2、擴充套件運算子可以用於合併兩個物件。let ab = { ...a, ...b };// 等同於let ab = Object.assign({}, a, b);
3、與陣列的擴充套件運算子一樣,物件的擴充套件運算子後面可以跟表示式。const obj = {...(x > 1 ? {a: 1} : {}),b: 2,};
4、如果擴充套件運算子後面是一個空物件,則沒有任何效果。{...{}, a: 1}// { a: 1 }
5、如果擴充套件運算子的引數是null或undefined,這兩個值會被忽略,不會報錯。
6、擴充套件運算子的引數物件之中,如果有取值函式get,這個函式是會執行的
12、Null 傳導運算子:
const firstName = message?.body?.user?.firstName || 'default';
上面程式碼有三個?.運算子,只要其中一個返回null或undefined,就不再往下運算,而是返回undefined。
“Null 傳導運算子”有四種用法:
obj?.prop // 讀取物件屬性
obj?.[expr] // 同上
func?.(...args) // 函式或物件方法的呼叫
new C?.(...args) // 建構函式的呼叫
例如:
// 如果 a 是 null 或 undefined, 返回 undefined
// 否則返回 a.b.c().d
a?.b.c().d
// 如果 a 是 null 或 undefined,下面的語句不產生任何效果
// 否則執行 a.b = 42
a?.b = 42
// 如果 a 是 null 或 undefined,下面的語句不產生任何效果
delete a?.b

12.2 星期四
十、Symbol
1、概述:ES6 引入了一種新的原始資料型別Symbol,表示獨一無二的值。
它是 JavaScript 語言的第七種資料型別,前六種是:undefined、null、布林值(Boolean)、字串(String)、數值(Number)、物件(Object)。
Symbol保證每個屬性的名字都是獨一無二的,從根本上防止屬性名的衝突

Symbol 值透過Symbol函式生成。這就是說,物件的屬性名現在可以有兩種型別,一種是原來就有的字串,另一種就是新增的 Symbol 型別。凡是屬性名屬於 Symbol 型別,就都是獨一無二的,可以保證不會與其他屬性名產生衝突。
注意:Symbol函式前不能使用new命令,否則會報錯。這是因為生成的 Symbol 是一個原始型別的值,不是物件。
也就是說,由於 Symbol 值不是物件,所以不能新增屬性。基本上,它是一種類似於字串的資料型別。
Symbol函式可以接受一個字串作為引數,表示對 Symbol 例項的描述,主要是為了在控制檯顯示,或者轉為字串時,比較容易區分。
let s1 = Symbol('foo');s1 // Symbol(foo)
s1.toString() // "Symbol(foo)"
1、Symbol 值不能與其他型別的值進行運算,會報錯。
2、Symbol 值可以顯式轉為字串。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
3、Symbol 值也可以轉為布林值,但是不能轉為數值。
Boolean(sym) // true
!sym // false
Number(sym) // TypeError
sym + 2 // TypeError
2、作為屬性名的 Symbol:
由於每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為識別符號,用於物件的屬性名,就能保證不會出現同名的屬性。
這對於一個物件由多個模組構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋。
1、Symbol 值作為物件屬性名時,不能用點運算子。
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
因為點運算子後面總是字串,所以不會讀取mySymbol作為標識名所指代的那個值,導致a的屬性名實際上是一個字串,而不是一個 Symbol 值。
使用 Symbol 值定義屬性時,Symbol 值必須放在方括號之中。
2、採用增強的物件寫法,上面程式碼的obj物件可以寫得更簡潔一些。
let obj = {
[s](arg) { ... }
};
3、常量使用 Symbol 值最大的好處,就是其他任何值都不可能有相同的值了,因此可以保證上面的switch語句會按設計的方式工作。
還有一點需要注意,Symbol 值作為屬性名時,該屬性還是公開屬性,不是私有屬性。
3、例項:消除魔術字串:
魔術字串指的是,在程式碼之中多次出現、與程式碼形成強耦合的某一個具體的字串或者數值。
1、把Triangle寫成shapeType物件的triangle屬性,這樣就消除了強耦合。
如果仔細分析,可以發現shapeType.triangle等於哪個值並不重要,只要確保不會跟其他shapeType屬性的值衝突即可。
因此,這裡就很適合改用 Symbol 值。
const shapeType = {
triangle: Symbol()
};
4、屬性名的遍歷:
1、Symbol 作為屬性名,該屬性不會出現在for...in、for...of迴圈中,也不會被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,可以獲取指定物件的所有 Symbol 屬性名。
2、Object.getOwnPropertySymbols方法返回一個陣列,成員是當前物件的所有用作屬性名的 Symbol 值。
Object.getOwnPropertySymbols(物件名);//返回該物件的symbol值
3、for (let i in obj) {
console.log(i); // 無輸出
}
Object.getOwnPropertyNames(obj)
// []
4、另一個新的 API,Reflect.ownKeys方法可以返回所有型別的鍵名,包括常規鍵名和 Symbol 鍵名。
Reflect.ownKeys(物件名)// 返回所有型別的鍵名,包括常規鍵名和 Symbol 鍵名。 ["屬性名1", "屬性名2", Symbol(symbol名)]
5、Object.keys(x)、Object.getOwnPropertyNames(x)都無法獲取它,可以造成了一種非私有的內部方法的效果。
5、Symbol.for(),Symbol.keyFor():
1、symbol。for()重新使用同一個 Symbol 值,Symbol.for方法可以做到這一點。它接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字串為名稱的 Symbol 值。
2、symbol.for()與symbol()的區別:
1、都會生成新的 Symbol。它們的區別是,前者會被登記在全域性環境中供搜尋,後者不會。
2、Symbol.for()不會每次呼叫就返回一個新的 Symbol 型別的值,而是會先檢查給定的key是否已經存在,如果不存在才會新建一個值。
3、如果你呼叫Symbol.for("cat")30 次,每次都會返回同一個 Symbol 值,但是呼叫Symbol("cat")30 次,會返回 30 個不同的 Symbol 值。
4、Symbol()寫法沒有登記機制,所以每次呼叫都會返回一個不同的值。Symbol.keyFor方法返回一個已登記的 Symbol 型別值的key。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
上面程式碼中,變數s2屬於未登記的 Symbol 值,所以返回undefined。
5、Symbol.for為 Symbol 值登記的名字,是全域性環境的,可以在不同的 iframe 或 service worker 中取到同一個值。
6、例項:模組的 Singleton 模式:
Singleton 模式指的是呼叫一個類,任何時候返回的都是同一個例項。
對於 Node 來說,模組檔案可以看成是一個類。
7、內建的 Symbol 值:
1、Symbol.hasInstance
物件的Symbol.hasInstance屬性,指向一個內部方法。當其他物件使用instanceof運算子,判斷是否為該物件的例項時,會呼叫這個方法。比如,foo instanceof Foo在語言內部,實際呼叫的是Foo[Symbol.hasInstance](foo)。
例如:class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}

[1, 2, 3] instanceof new MyClass() // true
上面程式碼中,MyClass是一個類,new MyClass()會返回一個例項。該例項的Symbol.hasInstance方法,會在進行instanceof運算時自動呼叫,判斷左側的運運算元是否為Array的例項。
2、Symbol.isConcatSpreadable
物件的Symbol.isConcatSpreadable屬性等於一個布林值,表示該物件用於Array.prototype.concat()時,是否可以展開。
例如:let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
上面程式碼說明,陣列的預設行為是可以展開,Symbol.isConcatSpreadable預設等於undefined。該屬性等於true時,也有展開的效果。
類似陣列的物件正好相反,預設不展開。它的Symbol.isConcatSpreadable屬性設為true,才可以展開。
Symbol.isConcatSpreadable屬性也可以定義在類裡面。
注意,Symbol.isConcatSpreadable的位置差異
3、Symbol.species
物件的Symbol.species屬性,指向當前物件的建構函式。創造例項時,預設會呼叫這個方法,即使用這個屬性返回的函式當作建構函式,來創造新的例項物件。
class MyArray extends Array {
// 覆蓋父類 Array 的建構函式
static get [Symbol.species]() { return Array; }
}
上面程式碼中,子類MyArray繼承了父類Array。建立MyArray的例項物件時,本來會呼叫它自己的建構函式(本例中被省略了),但是由於定義了Symbol.species屬性,所以會使用這個屬性返回的的函式,建立MyArray的例項。
這個例子也說明,定義Symbol.species屬性要採用get讀取器。預設的Symbol.species屬性等同於下面的寫法。
static get [Symbol.species]() {
return this;
}
4、Symbol.match
物件的Symbol.match屬性,指向一個函式。當執行str.match(myObject)時,如果該屬性存在,會呼叫它,返回該方法的返回值。
String.prototype.match(regexp)
// 等同於
regexp[Symbol.match](this)

class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}

'e'.match(new MyMatcher()) // 1
5、Symbol.replace
物件的Symbol.replace屬性,指向一個方法,當該物件被String.prototype.replace方法呼叫時,會返回該方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同於
searchValue[Symbol.replace](this, replaceValue)
例如:
const x = {};
x[Symbol.replace] = (...s) => console.log(s);

'Hello'.replace(x, 'World') // ["Hello", "World"]
Symbol.replace方法會收到兩個引數,第一個引數是replace方法正在作用的物件,上面例子是Hello,第二個引數是替換後的值,上面例子是World。
6、Symbol.search
物件的Symbol.search屬性,指向一個方法,當該物件被String.prototype.search方法呼叫時,會返回該方法的返回值。
String.prototype.search(regexp)
// 等同於
regexp[Symbol.search](this)
6、Symbol.split
物件的Symbol.split屬性,指向一個方法,當該物件被String.prototype.split方法呼叫時,會返回該方法的返回值。
String.prototype.split(separator, limit)
// 等同於
separator[Symbol.split](this, limit)
7、Symbol.iterator
物件的Symbol.iterator屬性,指向該物件的預設遍歷器方法。
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]
8、Symbol.toPrimitive
物件的Symbol.toPrimitive屬性,指向一個方法。該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值。

Symbol.toPrimitive被呼叫時,會接受一個字串引數,表示當前運算的模式,一共有三種模式。

Number:該場合需要轉成數值
String:該場合需要轉成字串
Default:該場合可以轉成數值,也可以轉成字串
9、Symbol.toStringTag
物件的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]"
10、Symbol.unscopables
物件的Symbol.unscopables屬性,指向一個物件。該物件指定了使用with關鍵字時,哪些屬性會被with環境排除。

十一、Set 和 Map 資料結構
1、Set:
1、基本用法:
ES6 提供了新的資料結構 Set。它類似於陣列,但是成員的值都是唯一的,沒有重複的值。Set 本身是一個建構函式,用來生成 Set 資料結構。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4 程式碼透過add方法向 Set 結構加入成員,結果表明 Set 結構不會新增重複的值。
[...new Set(array)] // 去除陣列的重複成員
2、向 Set 加入值的時候,不會發生型別轉換,所以5和"5"是兩個不同的值。Set 內部判斷兩個值是否不同,使用的演算法叫做“Same-value equality”,它類似於精確相等運算子(===),主要的區別是NaN等於自身,而精確相等運算子認為NaN不等於自身。
3、在 Set 內部,兩個NaN是相等。
4、兩個物件總是不相等的。{}和{}不相等
5、Set 例項的屬性和方法
Set 結構的例項有以下屬性。
Set.prototype.constructor:建構函式,預設就是Set函式。
Set.prototype.size:返回Set例項的成員總數。
Set 例項的方法分為兩大類:操作方法(用於運算元據)和遍歷方法(用於遍歷成員)。
四個操作方法。
add(value):新增某個值,返回 Set 結構本身。
delete(value):刪除某個值,返回一個布林值,表示刪除是否成功。
has(value):返回一個布林值,表示該值是否為Set的成員。
clear():清除所有成員,沒有返回值。
6、Array.from方法可以將 Set 結構轉為陣列。
7、遍歷操作
Set 結構的例項有四個遍歷方法,可以用於遍歷成員。
keys():返回鍵名的遍歷器
values():返回鍵值的遍歷器
entries():返回鍵值對的遍歷器
forEach():使用回撥函式遍歷每個成員
需要特別指出的是,Set的遍歷順序就是插入順序。這個特性有時非常有用,比如使用 Set 儲存一個回撥函式列表,呼叫時就能保證按照新增順序呼叫。
(1)keys(),values(),entries():返回的都是遍歷器物件。由於 Set 結構沒有鍵名,只有鍵值,所以keys方法和values方法的行為完全一致。
entries方法返回的遍歷器,同時包括鍵名和鍵值,所以每次輸出一個陣列,它的兩個成員完全相等。
Set 結構的例項預設可遍歷,它的預設遍歷器生成函式就是它的values方法。可以直接用for...of迴圈遍歷 Set。
Set.prototype[Symbol.iterator] === Set.prototype.values// true
(2)forEach()
Set 結構的例項與陣列一樣,也擁有forEach方法,用於對每個成員執行某種操作,沒有返回值。
forEach方法還可以有第二個引數,表示繫結處理函式內部的this物件。
(3)遍歷的應用
擴充套件運算子(...)內部使用for...of迴圈,所以也可以用於 Set 結構。
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
擴充套件運算子和 Set 結構相結合,就可以去除陣列的重複成員。

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
使用 Set 可以很容易地實現並集(Union)、交集(Intersect)和差集(Difference)。
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 並集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
2、WeakSet:
WeakSet 結構與 Set 類似,也是不重複的值的集合。但是,它與 Set 有兩個區別。首先,WeakSet 的成員只能是物件,而不能是其他型別的值。
其次,WeakSet 中的物件都是弱引用,即垃圾回收機制不考慮 WeakSet 對該物件的引用,也就是說,如果其他物件都不再引用該物件,那麼垃圾回收機制會自動回收該物件所佔用的記憶體,不考慮該物件還存在於 WeakSet 之中。
1、語法
WeakSet 是一個建構函式,可以使用new命令,建立 WeakSet 資料結構。
const ws = new WeakSet();//作為建構函式,WeakSet 可以接受一個陣列或類似陣列的物件作為引數。陣列的成員只能是物件。
2、WeakSet 結構有以下三個方法。
WeakSet.prototype.add(value):向 WeakSet 例項新增一個新成員。
WeakSet.prototype.delete(value):清除 WeakSet 例項的指定成員。
WeakSet.prototype.has(value):返回一個布林值,表示某個值是否在
3、WeakSet 沒有size屬性,沒有辦法遍歷它的成員。
4、WeakSet 不能遍歷,是因為成員都是弱引用,隨時可能消失,遍歷機制無法保證成員的存在,很可能剛剛遍歷結束,成員就取不到了。WeakSet 的一個用處,是儲存 DOM 節點,而不用擔心這些節點從文件移除時,會引發記憶體洩漏
3、Map:
1、含義和基本用法
JavaScript 的物件(Object),本質上是鍵值對的集合(Hash 結構),但是傳統上只能用字串當作鍵。這給它的使用帶來了很大的限制。
它類似於物件,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。也就是說,Object 結構提供了“字串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。
2、鍵值對”的資料結構,Map 比 Object 更合適。
3、任何具有 Iterator 介面、且每個成員都是一個雙元素的陣列的資料結構都可以當作Map建構函式的引數。這就是說,Set和Map都可以用來生成新的 Map。
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
上面程式碼中,我們分別使用 Set 物件和 Map 物件,當作Map建構函式的引數,結果都生成了新的 Map 物件。
4、對同一個鍵多次賦值,後面的值將覆蓋前面的值。
5、如果讀取一個未知的鍵,則返回undefined。
6、注意,只有對同一個物件的引用,Map 結構才將其視為同一個鍵。這一點要非常小心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面程式碼的set和get方法,表面是針對同一個鍵,但實際上這是兩個值,記憶體地址是不一樣的,因此get方法無法讀取該鍵,返回undefined。
7、同理,同樣的值的兩個例項,在 Map 結構中被視為兩個鍵。
8、Map 的鍵實際上是跟記憶體地址繫結的,只要記憶體地址不一樣,就視為兩個鍵。這就解決了同名屬性碰撞(clash)的問題,我們擴充套件別人的庫的時候,如果使用物件作為鍵名,就不用擔心自己的屬性與原作者的屬性同名。
9、若 Map 的鍵是一個簡單型別的值(數字、字串、布林值),則只要兩個值嚴格相等,Map 將其視為一個鍵,比如0和-0就是一個鍵,布林值true和字串true則是兩個不同的鍵。另外,undefined和null也是兩個不同的鍵。雖然NaN不嚴格相等於自身,但 Map 將其視為同一個鍵。
10、例項的屬性和操作方法
(1)size 屬性size屬性返回 Map 結構的成員總數。
(2)set(key, value)set方法設定鍵名key對應的鍵值為value,然後返回整個 Map 結構。如果key已經有值,則鍵值會被更新,否則就新生成該鍵。
set方法返回的是當前的Map物件,因此可以採用鏈式寫法。
(3)get(key)get方法讀取key對應的鍵值,如果找不到key,返回undefined。
(4)has(key)has方法返回一個布林值,表示某個鍵是否在當前 Map 物件之中。
(5)delete(key)delete方法刪除某個鍵,返回true。如果刪除失敗,返回false。
(6)clear()clear方法清除所有成員,沒有返回值。
11、遍歷方法
Map 結構原生提供三個遍歷器生成函式和一個遍歷方法。
keys():返回鍵名的遍歷器。
values():返回鍵值的遍歷器。
entries():返回所有成員的遍歷器。
forEach():遍歷 Map 的所有成員。
需要特別注意的是,Map 的遍歷順序就是插入順序。
map[Symbol.iterator] === map.entries
12、Map 結構轉為陣列結構,比較快速的方法是使用擴充套件運算子(...)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]// [1, 2, 3]
[...map.values()]// ['one', 'two', 'three']
[...map.entries()]// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]// [[1,'one'], [2, 'two'], [3, 'three']]
13、與其他資料結構的互相轉換
(1)Map 轉為陣列,Map 轉為陣列最方便的方法,就是使用擴充套件運算子(...)。
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)陣列 轉為 Map,將陣列傳入 Map 建構函式,就可以轉為 Map。new Map([ [true, 7],[{foo: 3}, ['abc']] ])
(3)Map 轉為物件,如果所有 Map 的鍵都是字串,它可以轉為物件。
(4)物件轉為 Map
(5)Map 轉為 JSON,Map 轉為 JSON 要區分兩種情況。一種情況是,Map 的鍵名都是字串,這時可以選擇轉為物件 JSON。
另一種情況是,Map 的鍵名有非字串,這時可以選擇轉為陣列 JSON。
(6)JSON 轉為 MapJSON 轉為 Map,正常情況下,所有鍵名都是字串。
4、WeakMap:WeakMap結構與Map結構類似,也是用於生成鍵值對的集合。
1、區別有兩點。
首先,WeakMap只接受物件作為鍵名(null除外),不接受其他型別的值作為鍵名。
其次,WeakMap的鍵名所指向的物件,不計入垃圾回收機制。
2、只要所引用的物件的其他引用都被清除,垃圾回收機制就會釋放該物件所佔用的記憶體。也就是說,一旦不再需要,WeakMap 裡面的鍵名物件和所對應的鍵值對會自動消失,不用手動刪除引用。
基本上,如果你要往物件上新增資料,又不想干擾垃圾回收機制,就可以使用 WeakMap。
WeakMap的專用場合就是,它的鍵所對應的物件,可能會在將來消失。WeakMap結構有助於防止記憶體洩漏。
注意,WeakMap 弱引用的只是鍵名,而不是鍵值。鍵值依然是正常引用。
3、WeakMap 的語法
WeakMap 與 Map 在 API 上的區別主要是兩個,一是沒有遍歷操作(即沒有key()、values()和entries()方法),也沒有size屬性。
二是無法清空,即不支援clear方法。因此,WeakMap只有四個方法可用:get()、set()、has()、delete()。
4、WeakMap 的用途:WeakMap 應用的典型場合就是 DOM 節點作為鍵名。

12.7:星期四、
十二、Proxy
1、概述:Proxy 用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“超程式設計”(meta programming),即對程式語言進行程式設計。
ES6 原生提供 Proxy 建構函式,用來生成 Proxy 例項。
var proxy = new Proxy(target, handler);
new Proxy()表示生成一個Proxy例項,target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。
如果handler沒有設定任何攔截,那就等同於直接通向原物件。
Proxy 支援的攔截操作一覽,一共 13 種。
1、get(target, propKey, receiver):攔截物件屬性的讀取,比如proxy.foo和proxy['foo']。
2、set(target, propKey, value, receiver):攔截物件屬性的設定,比如proxy.foo = v或proxy['foo'] = v,返回一個布林值。
3、has(target, propKey):攔截propKey in proxy的操作,返回一個布林值。
4、deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布林值。
5、ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一個陣列。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。
6、getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述物件。
7、defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布林值。
8、preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布林值。
9、getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個物件。
10、isExtensible(target):攔截Object.isExtensible(proxy),返回一個布林值。
11、setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。
12、apply(target, object, args):攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
13、construct(target, args):攔截 Proxy 例項作為建構函式呼叫的操作,比如new proxy(...args)。
2、Proxy 例項的方法:
1、get(目標物件,屬性名,例項本身)
get方法用於攔截某個屬性的讀取操作,可以接受三個引數,依次為目標物件、屬性名和 proxy 例項本身(即this關鍵字指向的那個物件),其中最後一個引數可選。
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 丟擲一個錯誤
get方法可以繼承。
get方法的第三個引數receiver,總是為當前的 Proxy 例項。
如果一個屬性不可配置(configurable)和不可寫(writable),則該屬性不能被代理,透過 Proxy 物件訪問該屬性會報錯。
2、set(目標物件,屬性名,屬性值, Proxy 例項本身)
set方法用來攔截某個屬性的賦值操作,可以接受四個引數,依次為目標物件、屬性名、屬性值和 Proxy 例項本身,其中最後一個引數可選。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 對於滿足條件的 age 屬性以及其他屬性,直接儲存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
結合get和set方法,就可以做到防止這些內部屬性被外部讀寫。
3、apply(目標物件,目標物件的上下文物件(this)和目標物件的引數陣列)apply方法攔截函式的呼叫、call和apply操作。
apply方法可以接受三個引數,分別是目標物件、目標物件的上下文物件(this)和目標物件的引數陣列。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
上面程式碼中,變數p是 Proxy 的例項,當它作為函式呼叫時(p()),就會被apply方法攔截,返回一個字串。
4、has()has方法用來攔截HasProperty操作,即判斷物件是否具有某個屬性時,這個方法會生效。典型的操作就是in運算子。
雖然for...in迴圈也用到了in運算子,但是has攔截對for...in迴圈不生效。
let stu1 = {name: '張三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}

let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);

'score' in oproxy1
// 張三 不及格
// false

'score' in oproxy2
// true

for (let a in oproxy1) {
console.log(oproxy1[a]);
}
// 張三
// 59

for (let b in oproxy2) {
console.log(oproxy2[b]);
}
// 李四
// 99
上面程式碼中,has攔截只對in運算子生效,對for...in迴圈不生效,導致不符合要求的屬性沒有被排除在for...in迴圈之外。
5、construct(目標物件、構建函式的引數物件)construct方法用於攔截new命令,下面是攔截物件的寫法。
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
例如:
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };// construct方法返回的必須是一個物件,否則會報錯。
}
});

(new p(1)).value
// "called: 1"
// 10
6、deleteProperty()
deleteProperty方法用於攔截delete操作,如果這個方法丟擲錯誤或者返回false,當前屬性就無法被delete命令刪除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
上面程式碼中,deleteProperty方法攔截了delete運算子,刪除第一個字元為下劃線的屬性會報錯。
注意,目標物件自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯。
7、defineProperty()defineProperty方法攔截了Object.defineProperty操作。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar'
// TypeError: proxy defineProperty handler returned false for property '"foo"'
上面程式碼中,defineProperty方法返回false,導致新增新屬性會丟擲錯誤。

注意,如果目標物件不可擴充套件(extensible),則defineProperty不能增加目標物件上不存在的屬性,否則會報錯。另外,如果目標物件的某個屬性不可寫(writable)或不可配置(configurable),則defineProperty方法不得改變這兩個設定。
8、getOwnPropertyDescriptor()getOwnPropertyDescriptor方法攔截Object.getOwnPropertyDescriptor(),返回一個屬性描述物件或者undefined。
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
上面程式碼中,handler.getOwnPropertyDescriptor方法對於第一個字元為下劃線的屬性名會返回undefined。
9、getPrototypeOf()
getPrototypeOf方法主要用來攔截獲取物件原型。具體來說,攔截下面這些操作。
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
下面是一個例子。
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
上面程式碼中,getPrototypeOf方法攔截Object.getPrototypeOf(),返回proto物件。
注意,getPrototypeOf方法的返回值必須是物件或者null,否則報錯。另外,如果目標物件不可擴充套件(extensible), getPrototypeOf方法必須返回目標物件的原型物件。
10、isExtensible()
isExtensible方法攔截Object.isExtensible操作。
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
上面程式碼設定了isExtensible方法,在呼叫Object.isExtensible時會輸出called。
注意,該方法只能返回布林值,否則返回值會被自動轉為布林值。
11、ownKeys()ownKeys方法用來攔截物件自身屬性的讀取操作。具體來說,攔截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
下面是攔截Object.keys()的例子。
let target = {
a: 1,
b: 2,
c: 3
};

let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'a' ]
上面程式碼攔截了對於target物件的Object.keys()操作,只返回a、b、c三個屬性之中的a屬性。
12、preventExtensions()
preventExtensions方法攔截Object.preventExtensions()。該方法必須返回一個布林值,否則會被自動轉為布林值。
這個方法有一個限制,只有目標物件不可擴充套件時(即Object.isExtensible(proxy)為false),proxy.preventExtensions才能返回true,否則會報錯。
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p) // 報錯
上面程式碼中,proxy.preventExtensions方法返回true,但這時Object.isExtensible(proxy)會返回true,因此報錯。
為了防止出現這個問題,通常要在proxy.preventExtensions方法裡面,呼叫一次Object.preventExtensions。
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(p)
// "called"
// true
13、setPrototypeOf()setPrototypeOf方法主要用來攔截Object.setPrototypeOf方法。
下面是一個例子。
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
上面程式碼中,只要修改target的原型物件,就會報錯。
注意,該方法只能返回布林值,否則會被自動轉為布林值。另外,如果目標物件不可擴充套件(extensible),setPrototypeOf方法不得改變目標物件的原型。
3、Proxy.revocable():
Proxy.revocable方法返回一個可取消的 Proxy 例項。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable方法返回一個物件,該物件的proxy屬性是Proxy例項,revoke屬性是一個函式,可以取消Proxy例項。上面程式碼中,當執行revoke函式之後,再訪問Proxy例項,就會丟擲一個錯誤。
Proxy.revocable的一個使用場景是,目標物件不允許直接訪問,必須透過代理訪問,一旦訪問結束,就收回代理權,不允許再次訪問。
4、this 問題:
雖然 Proxy 可以代理針對目標物件的訪問,但它不是目標物件的透明代理,即不做任何攔截的情況下,也無法保證與目標物件的行為一致。
主要原因就是在 Proxy 代理的情況下,目標物件內部的this關鍵字會指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
上面程式碼中,一旦proxy代理target.m,後者內部的this就是指向proxy,而不是target。
5、例項:Web 服務的客戶端

12。11星期一、
十三、reflect
1、概述:
Reflect物件與Proxy物件一樣,也是 ES6 為了操作物件而提供的新 API。Reflect物件的設計目的有這樣幾個。

(1) 將Object物件的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect物件上。現階段,某些方法同時在Object和Reflect物件上部署,未來的新方法將只部署在Reflect物件上。也就是說,從Reflect物件上可以拿到語言內部的方法。

(2) 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會丟擲一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}// 老寫法

if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
} // 新寫法
(3) 讓Object操作都變成函式行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函式行為。
'assign' in Object // true // 老寫法
Reflect.has(Object, 'assign') // true// 新寫法
(4)Reflect物件的方法與Proxy物件的方法一一對應,只要是Proxy物件的方法,就能在Reflect物件上找到對應的方法。這就讓Proxy物件可以方便地呼叫對應的Reflect方法,完成預設行為,作為修改行為的基礎。也就是說,不管Proxy怎麼修改預設行為,你總可以在Reflect上獲取預設行為。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
上面程式碼中,Proxy方法攔截target物件的屬性賦值行為。它採用Reflect.set方法將值賦值給物件的屬性,確保完成原有的行為,然後再部署額外的功能。
有了Reflect物件以後,很多操作會更易讀。
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 // 老寫法
Reflect.apply(Math.floor, undefined, [1.75]) // 1 // 新寫法
2、靜態方法:
Reflect物件一共有 13 個靜態方法。
1、Reflect.apply(target, thisArg, args):
Reflect.apply方法等同於Function.prototype.apply.call(func, thisArg, args),用於繫結this物件後執行給定函式。
一般來說,如果要繫結一個函式的this物件,可以這樣寫fn.apply(obj, args),但是如果函式定義了自己的apply方法,就只能寫成Function.prototype.apply.call(fn, obj, args),採用Reflect物件可以簡化這種操作。
const ages = [11, 33, 12, 54, 18, 96];

// 舊寫法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);

// 新寫法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);
2、Reflect.construct(target, args)
Reflect.construct方法等同於new target(...args),這提供了一種不使用new,來呼叫建構函式的方法。
function Greeting(name) {
this.name = name;
}
const instance = new Greeting('張三');// new 的寫法
const instance = Reflect.construct(Greeting, ['張三']);// Reflect.construct 的寫法
3、Reflect.get(target, name, receiver)
Reflect.get方法查詢並返回target物件的name屬性,如果沒有該屬性,則返回undefined。
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}

Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3

var myReceiverObject = {
foo: 4,
bar: 4,
};

Reflect.get(myObject, 'baz', myReceiverObject) // 8如果name屬性部署了讀取函式(getter),則讀取函式的this繫結receiver。
如果第一個引數不是物件,Reflect.get方法會報錯。
4、Reflect.set(target, name, value, receiver)
Reflect.set方法設定target物件的name屬性等於value。
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
myObject.foo // 1
Reflect.set(myObject, 'foo', 2);
myObject.foo // 2
Reflect.set(myObject, 'bar', 3)
myObject.foo // 3
如果第一個引數不是物件,Reflect.set會報錯。
如果name屬性設定了賦值函式,則賦值函式的this繫結receiver。
5、Reflect.defineProperty(target, name, desc)
Reflect.defineProperty方法基本等同於Object.defineProperty,用來為物件定義屬性。未來,後者會被逐漸廢除,請從現在開始就使用Reflect.defineProperty代替它。
function MyDate() {
/*…*/
}

// 舊寫法
Object.defineProperty(MyDate, 'now', {
value: () => Date.now()
});

// 新寫法
Reflect.defineProperty(MyDate, 'now', {
value: () => Date.now()
});
如果Reflect.defineProperty的第一個引數不是物件,就會丟擲錯誤,比如Reflect.defineProperty(1, 'foo')。
6、Reflect.deleteProperty(target, name):
Reflect.deleteProperty方法等同於delete obj[name],用於刪除物件的屬性。
const myObj = { foo: 'bar' };
delete myObj.foo;// 舊寫法
Reflect.deleteProperty(myObj, 'foo');// 新寫法
該方法返回一個布林值。如果刪除成功,或者被刪除的屬性不存在,返回true;刪除失敗,被刪除的屬性依然存在,返回false。
7、Reflect.has(target, name)
Reflect.has方法對應name in obj裡面的in運算子。
var myObject = {
foo: 1,
};
'foo' in myObject // true // 舊寫法
Reflect.has(myObject, 'foo') // true// 新寫法
如果第一個引數不是物件,Reflect.has和in運算子都會報錯。
8、Reflect.ownKeys(target):
Reflect.ownKeys方法用於返回物件的所有屬性,基本等同於Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};

// 舊寫法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']

Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]

// 新寫法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]
9、Reflect.isExtensible(target):
Reflect.isExtensible方法對應Object.isExtensible,返回一個布林值,表示當前物件是否可擴充套件。
const myObject = {};
Object.isExtensible(myObject) // true// 舊寫法
Reflect.isExtensible(myObject) // true// 新寫法
如果引數不是物件,Object.isExtensible會返回false,因為非物件本來就是不可擴充套件的,而Reflect.isExtensible會報錯。
10、Reflect.preventExtensions(target)
Reflect.preventExtensions對應Object.preventExtensions方法,用於讓一個物件變為不可擴充套件。它返回一個布林值,表示是否操作成功。
var myObject = {};
Object.preventExtensions(myObject) // Object {}// 舊寫法
Reflect.preventExtensions(myObject) // true// 新寫法
如果引數不是物件,Object.preventExtensions在 ES5 環境報錯,在 ES6 環境返回傳入的引數,而Reflect.preventExtensions會報錯。
11、Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getOwnPropertyDescriptor基本等同於Object.getOwnPropertyDescriptor,用於得到指定屬性的描述物件,將來會替代掉後者。
var myObject = {};
Object.defineProperty(myObject, 'hidden', {
value: true,
enumerable: false,
});
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');// 舊寫法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden'); // 新寫法
Reflect.getOwnPropertyDescriptor和Object.getOwnPropertyDescriptor的一個區別是,如果第一個引數不是物件,Object.getOwnPropertyDescriptor(1, 'foo')不報錯,返回undefined,而Reflect.getOwnPropertyDescriptor(1, 'foo')會丟擲錯誤,表示引數非法。
12、Reflect.getPrototypeOf(target):
Reflect.getPrototypeOf(obj)
Reflect.getPrototypeOf方法用於讀取物件的__proto__屬性,對應Object.getPrototypeOf(obj)。
const myObj = new FancyThing();
Object.getPrototypeOf(myObj) === FancyThing.prototype;// 舊寫法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;// 新寫法
Reflect.getPrototypeOf和Object.getPrototypeOf的一個區別是,如果引數不是物件,Object.getPrototypeOf會將這個引數轉為物件,然後再執行,而Reflect.getPrototypeOf會報錯。
Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 報錯
13、Reflect.setPrototypeOf(target, prototype)
Reflect.setPrototypeOf方法用於設定物件的__proto__屬性,返回第一個引數物件,對應Object.setPrototypeOf(obj, newProto)。
const myObj = new FancyThing();
Object.setPrototypeOf(myObj, OtherThing.prototype);// 舊寫法
Reflect.setPrototypeOf(myObj, OtherThing.prototype);// 新寫法
如果第一個引數不是物件,Object.setPrototypeOf會返回第一個引數本身,而Reflect.setPrototypeOf會報錯。
如果第一個引數是undefined或null,Object.setPrototypeOf和Reflect.setPrototypeOf都會報錯。

上面這些方法的作用,大部分與Object物件的同名方法的作用都是相同的,而且它與Proxy物件的方法是一一對應的。
3、例項:使用 Proxy 實現觀察者模式

12.12 星期二
十四、Promise 物件
1、Promise 的含義
Promise 是非同步程式設計的一種解決方案,所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

Promise物件有以下兩個特點。
(1)物件的狀態不受外界影響。Promise物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。

(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise物件新增回撥函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。

有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易。
Promise也有一些缺點。首先,無法取消Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。第三,當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
如果某些事件不斷地反覆發生,一般來說,使用 Stream 模式是比部署Promise更好的選擇。
2、基本用法:
下面程式碼創造了一個Promise例項。
const promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise建構函式接受一個函式作為引數,該函式的兩個引數分別是resolve和reject。它們是兩個函式,由 JavaScript 引擎提供,不用自己部署。
resolve函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise例項生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函式。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then方法可以接受兩個回撥函式作為引數。第一個回撥函式是Promise物件的狀態變為resolved時呼叫,第二個回撥函式是Promise物件的狀態變為rejected時呼叫。其中,第二個函式是可選的,不一定要提供。這兩個函式都接受Promise物件傳出的值作為引數。
面是一個用Promise物件實現的 Ajax 操作的例子。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();

});

return promise;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯了', error);
});
上面程式碼中,getJSON是對 XMLHttpRequest 物件的封裝,用於發出一個針對 JSON 資料的 HTTP 請求,並且返回一個Promise物件。需要注意的是,在getJSON內部,resolve函式和reject函式呼叫時,都帶有引數。
如果呼叫resolve函式和reject函式時帶有引數,那麼它們的引數會被傳遞給回撥函式。reject函式的引數通常是Error物件的例項,表示丟擲的錯誤;resolve函式的引數除了正常的值以外,還可能是另一個 Promise 例項,
一般來說,呼叫resolve或reject以後,Promise 的使命就完成了,後繼操作應該放到then方法裡面,而不應該直接寫在resolve或reject的後面。所以,最好在它們前面加上return語句,這樣就不會有意外。

new Promise((resolve, reject) => {
return resolve(1);
// 後面的語句不會執行
console.log(2);
})
3、Promise.prototype.then():
Promise 例項具有then方法,也就是說,then方法是定義在原型物件Promise.prototype上的。它的作用是為 Promise 例項新增狀態改變時的回撥函式。前面說過,then方法的第一個引數是resolved狀態的回撥函式,第二個引數(可選)是rejected狀態的回撥函式。

then方法返回的是一個新的Promise例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。

getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的程式碼使用then方法,依次指定了兩個回撥函式。第一個回撥函式完成以後,會將返回結果作為引數,傳入第二個回撥函式。
4、Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。

getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
console.log('發生錯誤!', error);
});
上面程式碼中,getJSON方法返回一個 Promise 物件,如果該物件狀態變為resolved,則會呼叫then方法指定的回撥函式;如果非同步操作丟擲錯誤,狀態就會變為rejected,就會呼叫catch方法指定的回撥函式,處理這個錯誤。另外,then方法指定的回撥函式,如果執行中丟擲錯誤,也會被catch方法捕獲。
// 寫法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});

// 寫法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
比較上面兩種寫法,可以發現reject方法的作用,等同於丟擲錯誤。
如果 Promise 狀態已經變成resolved,再丟擲錯誤是無效的。
Promise 在resolve語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。

Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。
5、Promise.all():
Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.all([p1, p2, p3]);
上面程式碼中,Promise.all方法接受一個陣列作為引數,p1、p2、p3都是 Promise例項
p的狀態由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
6、Promise.race():
Promise.race方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.race([p1, p2, p3]);
上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。
Promise.race方法的引數與Promise.all方法一樣,如果不是 Promise 例項,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。
如果指定時間內沒有獲得結果,就將 Promise 的狀態變為reject,否則變為resolve。

const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));
上面程式碼中,如果 5 秒之內fetch方法無法返回結果,變數p的狀態就會變為rejected,從而觸發catch方法指定的回撥函式。
7、Promise.resolve():
有時需要將現有物件轉為 Promise 物件,Promise.resolve方法就起到這個作用。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面程式碼將 jQuery 生成的deferred物件,轉為一個新的 Promise 物件。
Promise.resolve等價於下面的寫法。
Promise.resolve('foo')// 等價於 new Promise(resolve => resolve('foo'))
Promise.resolve方法的引數分成四種情況。
(1)引數是一個 Promise 例項:如果引數是 Promise 例項,那麼Promise.resolve將不做任何修改、原封不動地返回這個例項。
(2)引數是一個thenable物件:thenable物件指的是具有then方法的物件,比如下面這個物件。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve方法會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then方法。
(3)引數不是具有then方法的物件,或根本就不是物件:
如果引數是一個原始值,或者是一個不具有then方法的物件,則Promise.resolve方法返回一個新的 Promise 物件,狀態為resolved。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
(4)不帶有任何引數: Promise.resolve方法允許呼叫時不帶引數,直接返回一個resolved狀態的 Promise 物件。
所以,如果希望得到一個 Promise 物件,比較方便的方法就是直接呼叫Promise.resolve方法。
const p = Promise.resolve();

p.then(function () {
// ...
});
上面程式碼的變數p就是一個 Promise 物件。
需要注意的是,立即resolve的 Promise 物件,是在本輪“事件迴圈”(event loop)的結束時,而不是在下一輪“事件迴圈”的開始時。
8、Promise.reject():Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。
const p = Promise.reject('出錯了');// 等同於 const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
上面程式碼生成一個 Promise 物件的例項p,狀態為rejected,回撥函式會立即執行。
注意,Promise.reject()方法的引數,會原封不動地作為reject的理由,變成後續方法的引數。這一點與Promise.resolve方法不一致。
9、兩個有用的附加方法:
1、done()
Promise 物件的回撥鏈,不管以then方法或catch方法結尾,要是最後一個方法丟擲錯誤,都有可能無法捕捉到(因為 Promise 內部的錯誤不會冒泡到全域性)。因此,我們可以提供一個done方法,總是處於回撥鏈的尾端,保證丟擲任何可能出現的錯誤。
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done();
它的實現程式碼相當簡單。

Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function (reason) {
// 丟擲一個全域性錯誤
setTimeout(() => { throw reason }, 0);
});
};
從上面程式碼可見,done方法的使用,可以像then方法那樣用,提供fulfilled和rejected狀態的回撥函式,也可以不提供任何引數。但不管怎樣,done都會捕捉到任何可能出現的錯誤,並向全域性丟擲。
2、finally():finally方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。
它與done方法的最大區別,它接受一個普通的回撥函式作為引數,該函式不管怎樣都必須執行。
下面是一個例子,伺服器使用 Promise 處理請求,然後使用finally方法關掉伺服器。
server.listen(0)
.then(function () {
// run test
})
.finally(server.stop);
它的實現也很簡單。
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
上面程式碼中,不管前面的 Promise 是fulfilled還是rejected,都會執行回撥函式callback。
10、應用
載入圖片:
我們可以將圖片的載入寫成一個Promise,一旦載入完成,Promise的狀態就發生變化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
11、Promise.try():
實際開發中,經常遇到一種情況:不知道或者不想區分,函式f是同步函式還是非同步操作,但是想用 Promise 來處理它。
因為這樣就可以不管f是否包含非同步操作,都用then方法指定下一步流程,用catch方法處理f丟擲的錯誤。一般就會採用下面的寫法。
Promise.resolve().then(f)
上面的寫法有一個缺點,就是如果f是同步函式,那麼它會在本輪事件迴圈的末尾執行。

const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now
上面程式碼中,函式f是同步的,但是用 Promise 包裝了以後,就變成非同步執行了。

那麼有沒有一種方法,讓同步函式同步執行,非同步函式非同步執行,並且讓它們具有統一的 API 呢?回答是可以的,並且還有兩種寫法。
第一種寫法是用async函式來寫。
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
上面程式碼中,第二行是一個立即執行的匿名函式,會立即執行裡面的async函式,因此如果f是同步的,就會得到同步的結果;如果f是非同步的,就可以用then指定下一步,就像下面的寫法。

(async () => f())()
.then(...)
需要注意的是,async () => f()會吃掉f()丟擲的錯誤。所以,如果想捕獲錯誤,要使用promise.catch方法。

(async () => f())()
.then(...)
.catch(...)
第二種寫法是使用new Promise()。

const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
上面程式碼也是使用立即執行的匿名函式,執行new Promise()。這種情況下,同步函式也是同步執行的。

鑑於這是一個很常見的需求,所以現在有一個提案,提供Promise.try方法替代上面的寫法。

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
事實上,Promise.try存在已久,Promise 庫Bluebird、Q和when,早就提供了這個方法。

由於Promise.try為所有操作提供了統一的處理機制,所以如果想用then方法管理流程,最好都用Promise.try包裝一下。這樣有許多好處,其中一點就是可以更好地管理異常。

function getUsername(userId) {
return database.users.get({id: userId})
.then(function(user) {
return user.name;
});
}
上面程式碼中,database.users.get()返回一個 Promise 物件,如果丟擲非同步錯誤,可以用catch方法捕獲,就像下面這樣寫。

database.users.get({id: userId})
.then(...)
.catch(...)
但是database.users.get()可能還會丟擲同步錯誤(比如資料庫連線錯誤,具體要看實現方法),這時你就不得不用try...catch去捕獲。

try {
database.users.get({id: userId})
.then(...)
.catch(...)
} catch (e) {
// ...
}
上面這樣的寫法就很笨拙了,這時就可以統一用promise.catch()捕獲所有同步和非同步的錯誤。

Promise.try(database.users.get({id: userId}))
.then(...)
.catch(...)
事實上,Promise.try就是模擬try程式碼塊,就像promise.catch模擬的是catch程式碼塊。

十五、Iterator 和 for...of 迴圈
1、Iterator(遍歷器)的概念:
遍歷器(Iterator)就是這樣一種機制。它是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。
Iterator 的作用有三個:一是為各種資料結構,提供一個統一的、簡便的訪問介面;
二是使得資料結構的成員能夠按某種次序排列;
三是 ES6 創造了一種新的遍歷命令for...of迴圈,Iterator 介面主要供for...of消費。
Iterator 的遍歷過程是這樣的。
(1)建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。
(2)第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員。
(3)第二次呼叫指標物件的next方法,指標就指向資料結構的第二個成員。
(4)不斷呼叫指標物件的next方法,直到它指向資料結構的結束位置。
每一次呼叫next方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含value和done兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
2、預設 Iterator 介面
Iterator 介面的目的,就是為所有資料結構,提供了一種統一的訪問機制,即for...of迴圈(詳見下文)。當使用for...of迴圈遍歷某種資料結構時,該迴圈會自動去尋找 Iterator 介面。
一種資料結構只要部署了 Iterator 介面,我們就稱這種資料結構是”可遍歷的“(iterable)。
ES6 規定,預設的 Iterator 介面部署在資料結構的Symbol.iterator屬性,或者說,一個資料結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函式,就是當前資料結構預設的遍歷器生成函式。執行這個函式,就會返回一個遍歷器。至於屬性名Symbol.iterator,它是一個表示式,返回Symbol物件的iterator屬性,這是一個預定義好的、型別為 Symbol 的特殊值,所以要放在方括號內(參見 Symbol 一章)。
原生具備 Iterator 介面的資料結構如下。、Array、MapSet、String、TypedArray、函式的 arguments 物件、NodeList 物件
對於原生部署 Iterator 介面的資料結構,不用自己寫遍歷器生成函式,for...of迴圈會自動遍歷它們。除此之外,其他資料結構(主要是物件)的 Iterator 介面,都需要自己在Symbol.iterator屬性上面部署,這樣才會被for...of迴圈遍歷。
一個物件如果要具備可被for...of迴圈呼叫的 Iterator 介面,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的物件具有該方法也可)。
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}

[Symbol.iterator]() { return this; }

next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
上面程式碼是一個類部署 Iterator 介面的寫法。Symbol.iterator屬性對應一個函式,執行後返回當前物件的遍歷器物件。
注意,普通物件部署陣列的Symbol.iterator方法,並無效果。
如果Symbol.iterator方法對應的不是遍歷器生成函式(即會返回一個遍歷器物件),解釋引擎將會報錯。
3、呼叫 Iterator 介面的場合:
有一些場合會預設呼叫 Iterator 介面(即Symbol.iterator方法),除了下文會介紹的for...of迴圈,還有幾個別的場合。
(1)解構賦值:對陣列和 Set 結構進行解構賦值時,會預設呼叫Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;// x='a'; y='b'
let [first, ...rest] = set;// first='a'; rest=['b','c'];
(2)擴充套件運算子:擴充套件運算子(...)也會呼叫預設的 Iterator 介面。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd'] // ['a', 'b', 'c', 'd']
上面程式碼的擴充套件運算子內部就呼叫 Iterator 介面。
實際上,這提供了一種簡便機制,可以將任何部署了 Iterator 介面的資料結構,轉為陣列。也就是說,只要某個資料結構部署了 Iterator 介面,就可以對它使用擴充套件運算子,將其轉為陣列。
let arr = [...iterable];
(3)yield*:yield*後面跟的是一個可遍歷的結構,它會呼叫該結構的遍歷器介面。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
(4)其他場合;由於陣列的遍歷會呼叫遍歷器介面,所以任何接受陣列作為引數的場合,其實都呼叫了遍歷器介面。下面是一些例子。
for...of
Array.from()
Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
Promise.all()
Promise.race()
4、字串的 Iterator 介面:字串是一個類似陣列的物件,也原生具有 Iterator 介面。
var someString = "hi";
typeof someString[Symbol.iterator]// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
上面程式碼中,呼叫Symbol.iterator方法返回一個遍歷器物件,在這個遍歷器上可以呼叫 next 方法,實現對於字串的遍歷。
可以覆蓋原生的Symbol.iterator方法,達到修改遍歷器行為的目的。
5、Iterator 介面與 Generator 函式
6、遍歷器物件的 return(),throw():
遍歷器物件除了具有next方法,還可以具有return方法和throw方法。如果你自己寫遍歷器物件生成函式,那麼next方法是必須部署的,return方法和throw方法是否部署是可選的。
return方法的使用場合是,如果for...of迴圈提前退出(通常是因為出錯,或者有break語句或continue語句),就會呼叫return方法。如果一個物件在完成遍歷前,需要清理或釋放資源,就可以部署return方法。
function readLinesSync(file) {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
},
};
}
上面程式碼中,函式readLinesSync接受一個檔案物件作為引數,返回一個遍歷器物件,其中除了next方法,還部署了return方法。下面的三種情況,都會觸發執行return方法。
// 情況一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情況二
for (let line of readLinesSync(fileName)) {
console.log(line);
continue;
}
// 情況三
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
上面程式碼中,情況一輸出檔案的第一行以後,就會執行return方法,關閉這個檔案;情況二輸出所有行以後,執行return方法,關閉該檔案;情況三會在執行return方法關閉檔案之後,再丟擲錯誤。
注意,return方法必須返回一個物件,這是 Generator 規格決定的。
7、for...of 迴圈
一個資料結構只要部署了Symbol.iterator屬性,就被視為具有 iterator 介面,就可以用for...of迴圈遍歷它的成員。
也就是說,for...of迴圈內部呼叫的是資料結構的Symbol.iterator方法。
for...of迴圈可以使用的範圍包括陣列、Set 和 Map 結構、某些類似陣列的物件(比如arguments物件、DOM NodeList 物件)、後文的 Generator 物件,以及字串。
1、陣列原生具備iterator介面(即預設部署了Symbol.iterator屬性),for...of迴圈本質上就是呼叫這個介面產生的遍歷器,
for...of迴圈呼叫遍歷器介面,陣列的遍歷器介面只返回具有數字索引的屬性。這一點跟for...in迴圈也不一樣。
2、Set 和 Map 結構也原生具有 Iterator 介面,可以直接使用for...of迴圈。
3、計算生成的資料結構
有些資料結構是在現有資料結構的基礎上,計算生成的。比如,ES6 的陣列、Set、Map 都部署了以下三個方法,呼叫後都返回遍歷器物件。
entries() 返回一個遍歷器物件,用來遍歷[鍵名, 鍵值]組成的陣列。對於陣列,鍵名就是索引值;
對於 Set,鍵名與鍵值相同。Map 結構的 Iterator 介面,預設就是呼叫entries方法。
keys() 返回一個遍歷器物件,用來遍歷所有的鍵名。
values() 返回一個遍歷器物件,用來遍歷所有的鍵值。
這三個方法呼叫後生成的遍歷器物件,所遍歷的都是計算生成的資料結構。
4、類似陣列的物件:類似陣列的物件包括好幾類。下面是for...of迴圈用於字串、DOM NodeList 物件、arguments物件的例子。
5、物件:對於普通的物件,for...of結構不能直接使用,會報錯,必須部署了 Iterator 介面後才能使用。但是,這樣情況下,for...in迴圈依然可以用來遍歷鍵名。
對於普通的物件,for...in迴圈可以遍歷鍵名,for...of迴圈會報錯。
一種解決方法是,使用Object.keys方法將物件的鍵名生成一個陣列,然後遍歷這個陣列。
與其他遍歷語法的比較:
1、以陣列為例,JavaScript 提供多種遍歷語法。最原始的寫法就是for迴圈。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
這種寫法比較麻煩,因此陣列提供內建的forEach方法。
myArray.forEach(function (value) {
console.log(value);
});
這種寫法的問題在於,無法中途跳出forEach迴圈,break命令或return命令都不能奏效。

2、for...in迴圈可以遍歷陣列的鍵名。
for (var index in myArray) {
console.log(myArray[index]);
}
for...in迴圈有幾個缺點。
陣列的鍵名是數字,但是for...in迴圈是以字串作為鍵名“0”、“1”、“2”等等。
for...in迴圈不僅遍歷數字鍵名,還會遍歷手動新增的其他鍵,甚至包括原型鏈上的鍵。
某些情況下,for...in迴圈會以任意順序遍歷鍵名。
總之,for...in迴圈主要是為遍歷物件而設計的,不適用於遍歷陣列。
3、for...of迴圈相比上面幾種做法,有一些顯著的優點。
for (let value of myArray) {
console.log(value);
}
有著同for...in一樣的簡潔語法,但是沒有for...in那些缺點。
不同於forEach方法,它可以與break、continue和return配合使用。
提供了遍歷所有資料結構的統一操作介面。
下面是一個使用 break 語句,跳出for...of迴圈的例子。
for (var n of fibonacci) {
if (n > 1000)
break;
console.log(n);
}
上面的例子,會輸出斐波納契數列小於等於 1000 的項。如果當前項大於 1000,就會使用break語句跳出for...of迴圈。

十六、Generator 函式的語法
1、簡介:
1.1Generator 函式是 ES6 提供的一種非同步程式設計解決方案,語法行為與傳統函式完全不同。
Generator 函式有多種理解角度。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。
執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。
形式上,Generator 函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號;二是,函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。
Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。
呼叫 Generator 函式,返回一個遍歷器物件,代表 Generator 函式的內部指標。以後,每次呼叫遍歷器物件的next方法,就會返回一個有著value和done兩個屬性的物件。value屬性表示當前的內部狀態的值,是yield表示式後面那個表示式的值;done屬性是一個布林值,表示是否遍歷結束。
4種寫法:function * foo(x, y) { ··· }、function *foo(x, y) { ··· }、function* foo(x, y) { ··· }、function*foo(x, y) { ··· }
1.2yield 表示式:
由於 Generator 函式返回的遍歷器物件,只有呼叫next方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函式。
yield表示式就是暫停標誌。
1.2.1遍歷器物件的next方法的執行邏輯如下:
(1)遇到yield表示式,就暫停執行後面的操作,並將緊跟在yield後面的那個表示式的值,作為返回的物件的value屬性值。
(2)下一次呼叫next方法時,再繼續往下執行,直到遇到下一個yield表示式。
(3)如果沒有再遇到新的yield表示式,就一直執行到函式結束,直到return語句為止,並將return語句後面的表示式的值,作為返回的物件的value屬性值。
(4)如果該函式沒有return語句,則返回的物件的value屬性值為undefined。
yield表示式只能用在 Generator 函式里面,用在其他地方都會報錯。
yield表示式如果用在另一個表示式之中,必須放在圓括號裡面。
yield表示式用作函式引數或放在賦值表示式的右邊,可以不加括號。
1.3、與 Iterator 介面的關係:任意一個物件的Symbol.iterator方法,等於該物件的遍歷器生成函式,呼叫該函式會返回該物件的一個遍歷器物件。
由於 Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]
上面程式碼中,Generator 函式賦值給Symbol.iterator屬性,從而使得myIterable物件具有了 Iterator 介面,可以被...運算子遍歷了
Generator 函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator屬性,執行後返回自身。
function* gen(){
// some code
}

var g = gen();

g[Symbol.iterator]() === g
// true
上面程式碼中,gen是一個 Generator 函式,呼叫它會生成一個遍歷器物件g。它的Symbol.iterator屬性,也是一個遍歷器物件生成函式,執行後返回它自己。
2、next 方法的引數:yield表示式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值。
例子1:
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面程式碼先定義了一個可以無限執行的 Generator 函式f,如果next方法沒有引數,每次執行到yield表示式,變數reset的值總是undefined。當next方法帶一個引數true時,變數reset就被重置為這個引數(即true),因此i會等於-1,下一輪迴圈就會從-1開始遞增。
例子2:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
上面程式碼中,第二次執行next方法的時候不帶引數,導致 y 的值等於2 * undefined(即NaN),除以 3 以後還是NaN,因此返回物件的value屬性也等於NaN。第三次執行Next方法的時候不帶引數,所以z等於undefined,返回物件的value屬性等於5 + NaN + undefined,即NaN。

如果向next方法提供引數,返回結果就完全不一樣了。上面程式碼第一次呼叫b的next方法時,返回x+1的值6;第二次呼叫next方法,將上一次yield表示式的值設為12,因此y等於24,返回y / 3的值8;第三次呼叫next方法,將上一次yield表示式的值設為13,因此z等於13,這時x等於5,y等於24,所以return語句的值等於42。
3、for...of 迴圈:
for...of迴圈可以自動遍歷 Generator 函式時生成的Iterator物件,且此時不再需要呼叫next方法。
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;//一旦next方法的返回物件的done屬性為true,for...of迴圈就會中止,且不包含該返回物件
return 6;}//return語句返回的6,不包括在for...of迴圈之中。
for (let v of foo()) { console.log(v);}// 1 2 3 4 5使用for...of語句時不需要使用next方法。
原生的 JavaScript 物件沒有遍歷介面,無法使用for...of迴圈,透過 Generator 函式為它加上這個介面,就可以用了。
除了for...of迴圈以外,擴充套件運算子(...)、解構賦值和Array.from方法內部呼叫的,都是遍歷器介面。這意味著,它們都可以將 Generator 函式返回的 Iterator 物件,作為引數。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}

[...numbers()] // [1, 2]// 擴充套件運算子
Array.from(numbers()) // [1, 2]// Array.from 方法
let [x, y] = numbers();// 解構賦值
x // 1
y // 2

for (let n of numbers()) {
console.log(n)
}// for...of 迴圈
// 1
// 2
4、Generator.prototype.throw():Generator 函式返回的遍歷器物件,都有一個throw方法,可以在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};

var i = g();
i.next();
//i.throw(new Error('出錯了!'));// Error: 出錯了!(…)throw方法可以接受一個引數,該引數會被catch語句接收,建議丟擲Error物件的例項。
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
上面程式碼中,遍歷器物件i連續丟擲兩個錯誤。第一個錯誤被 Generator 函式體內的catch語句捕獲。i第二次丟擲錯誤,由於 Generator 函式內部的catch語句已經執行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被丟擲了 Generator 函式體,被函式體外的catch語句捕獲。
5、Generator.prototype.return():Generator 函式返回的遍歷器物件,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函式。
function* gen() {
yield 1;
yield 2;
yield 3;
}

var g = gen();

g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
g.return() //undefined如果return方法呼叫時,不提供引數,則返回值的value屬性為undefined。
上面程式碼中,遍歷器物件g呼叫return方法後,返回值的value屬性就是return方法的引數foo。並且,Generator 函式的遍歷就終止了,返回值的done屬性為true,以後再呼叫next方法,done屬性總是返回true。
如果 Generator 函式內部有try...finally程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行。
6、next()、throw()、return() 的共同點:next()、throw()、return()這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator
函式恢復執行,並且使用不同的語句替換yield表示式。
1、next()是將yield表示式替換成一個值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相當於將 let result = yield x + y
// 替換成 let result = 1;
上面程式碼中,第二個next(1)方法就相當於將yield表示式替換成一個值1。如果next方法沒有引數,就相當於替換成undefined。
2、throw()是將yield表示式替換成一個throw語句。
gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當於將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));
3、return()是將yield表示式替換成一個return語句。
gen.return(2); // Object {value: 2, done: true}
// 相當於將 let result = yield x + y
// 替換成 let result = return 2;
7、yield* 表示式:
如果在 Generator 函式內部,呼叫另一個 Generator 函式,預設情況下是沒有效果的。
function* bar() {
yield 'x';
foo();//這是一個generator函式,直接呼叫沒有效果
yield 'y';
}
for (let v of bar()){
console.log(v);
}// "x"// "y"
我們可以這樣寫:
function* bar() {
yield 'x';
yield* foo();//前面加一個yield* generator函式名,如果是yield foo()返回的是物件
yield 'y';
}
或者:
function* bar() {
yield 'x';
for (let v of foo()) {//遍歷generator函式
yield v;
}
yield 'y';
}
如果yield表示式後面跟的是一個遍歷器物件,需要在yield表示式後面加上星號,表明它返回的是一個遍歷器物件。這被稱為yield*表示式。
任何資料結構只要有 Iterator 介面,就可以被yield*遍歷。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
上面程式碼中,yield表示式返回整個字串,yield*語句返回單個字元。因為字串具有 Iterator 介面,所以被yield*遍歷。
8、作為物件屬性的 Generator 函式:
如果一個物件的屬性是 Generator 函式,可以簡寫成下面的形式。
let obj = {
* myGeneratorMethod() {//等同於myGeneratorMethod: function* ()
···//表示這個屬性是一個 Generator 函式。
}
};
9、Generator 函式的this:
生成一個空物件,使用call方法繫結 Generator 函式內部的this。這樣,建構函式呼叫以後,這個空物件就是 Generator 函式的例項物件了。
function* F() {
this.a = 1;//如果let obj = F();obj.a返回是 undefined
yield this.b = 2;//直接new F()報錯;因為F不是建構函式。
yield this.c = 3;
}
var obj = {};//生成一個空物件,
var f = F.call(obj);//使用call方法繫結 Generator 函式內部的this。
f.next(); // Object {value: 2, done: false}//這個物件執行三次next方法(因為F內部有兩個yield表示式),完成 F 內部所有程式碼的執行。
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
執行的是遍歷器物件f,但是生成的物件例項是obj
或者:
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);//將這兩個物件統一,將obj換成F.prototype。
//或者
再將F改成建構函式,就可以對它執行new命令了。

function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}

function F() {//將F改成建構函式,就可以對它執行new命令
return gen.call(gen.prototype);
}

var f = new F();
輸入下面的程式碼,同樣有效
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
上面程式碼中,首先是F內部的this物件繫結obj物件,然後呼叫它,返回一個 Iterator 物件。這個物件執行三次next方法(因為F內部有兩個yield表示式),完成 F 內部所有程式碼的執行。這時,所有內部屬性都繫結在obj物件上了,因此obj物件也就成了F的例項。
10、含義:
10.1、Generator 與狀態機
Generator 是實現狀態機的最佳結構。比如,下面的clock函式就是一個狀態機。
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
上面程式碼的clock函式一共有兩種狀態(Tick和Tock),每執行一次,就改變一次狀態。這個函式如果用 Generator 實現,就是下面這樣。

var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
11、應用:Generator 可以暫停函式執行,返回任意表示式的值。這種特點使得 Generator 有多種應用場景。
(1)非同步操作的同步化表達
Generator 函式的暫停執行的效果,意味著可以把非同步操作寫在yield表示式裡面,等到呼叫next方法時再往後執行。這實際上等同於不需要寫回撥函式了,因為非同步操作的後續操作可以放在yield表示式下面,反正要等到呼叫next方法時再執行。所以,Generator 函式的一個重要實際意義就是用來處理非同步操作,改寫回撥函式。
Ajax 是典型的非同步操作,透過 Generator 函式部署 Ajax 操作,可以用同步的方式表達。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}

function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}

var it = main();
it.next();
上面程式碼的main函式,就是透過 Ajax 操作獲取資料。可以看到,除了多了一個yield,它幾乎與同步操作的寫法完全一樣。注意,makeAjaxCall函式中的next方法,必須加上response引數,因為yield表示式,本身是沒有值的,總是等於undefined。
(2)控制流管理
如果有一個多步操作非常耗時,採用回撥函式,可能會寫成下面這樣。

step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
採用 Promise 改寫上面的程式碼。

Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
上面程式碼已經把回撥函式,改成了直線執行的形式,但是加入了大量 Promise 的語法。Generator 函式可以進一步改善程式碼執行流程。

function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
(3)部署 Iterator 介面
利用 Generator 函式,可以在任意物件上部署 Iterator 介面。

function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}

// foo 3
// bar 7
上述程式碼中,myObj是一個普通物件,透過iterEntries函式,就有了 Iterator 介面。也就是說,可以在任意物件上部署next方法。
(4)作為資料結構
Generator 可以看作是資料結構,更確切地說,可以看作是一個陣列結構,因為 Generator 函式可以返回一系列的值,這意味著它可以對任意表示式,提供類似陣列的介面。

function *doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
上面程式碼就是依次返回三個函式,但是由於使用了 Generator 函式,導致可以像處理陣列那樣,處理這三個返回的函式。

12.14星期四:
十六、Generator 函式的非同步應用
1、傳統方法:
ES6 誕生以前,非同步程式設計的方法,大概有下面四種。回撥函式、事件監聽、釋出/訂閱、Promise 物件
Generator 函式將 JavaScript 非同步程式設計帶入了一個全新的階段。
2、基本概念:
2.1、非同步
所謂"非同步",簡單說就是一個任務不是連續完成的,可以理解成該任務被人為分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
2.2、回撥函式
JavaScript 語言對非同步程式設計的實現,就是回撥函式。所謂回撥函式,就是把任務的第二段單獨寫在一個函式里面,等到重新執行這個任務的時候,就直接呼叫這個函式。回撥函式的英語名字callback,直譯過來就是"重新呼叫"。
回撥函式的第一個引數,必須是錯誤物件err(如果沒有錯誤,該引數就是null):原因是執行分成兩段,第一段執行完以後,任務所在的上下文環境就已經結束了。在這以後丟擲的錯誤,原來的上下文環境已經無法捕捉,只能當作引數,傳入第二段。
2.3、Promise
回撥函式本身並沒有問題,它的問題出現在多個回撥函式巢狀。假定讀取A檔案之後,再讀取B檔案,程式碼如下。
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
不難想象,如果依次讀取兩個以上的檔案,就會出現多重巢狀。程式碼不是縱向發展,而是橫向發展,很快就會亂成一團,無法管理。因為多個非同步操作形成了強耦合,只要有一個操作需要修改,它的上層回撥函式和下層回撥函式,可能都要跟著修改。這種情況就稱為"回撥函式地獄"(callback hell)。
Promise 物件就是為了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,允許將回撥函式的巢狀,改成鏈式呼叫。採用 Promise,連續讀取多個檔案,寫法如下。
var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
上面程式碼中,我使用了fs-readfile-promise模組,它的作用就是返回一個 Promise 版本的readFile函式。Promise 提供then方法載入回撥函式,catch方法捕捉執行過程中丟擲的錯誤。
3、Generator 函式:
3.1、協程:傳統的程式語言,早有非同步程式設計的解決方案(其實是多工的解決方案)。其中有一種叫做"協程"(coroutine),意思是多個執行緒互相協作,完成非同步任務。
協程有點像函式,又有點像執行緒。它的執行流程大致如下:
第一步,協程A開始執行。
第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
第三步,(一段時間後)協程B交還執行權。
第四步,協程A恢復執行。
例如:讀取檔案的協程寫法如下。
function* asyncJob() {
// ...其他程式碼
var f = yield readFile(fileA);
// ...其他程式碼
}
函式asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield命令是非同步兩個階段的分界線。
協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是程式碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。
3.2、協程的 Generator 函式實現
Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)。
整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield語句註明
例如:Generator 函式的執行方法如下。
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面程式碼中,呼叫 Generator 函式,會返回一個內部指標(即遍歷器)g。這是 Generator 函式不同於普通函式的另一個地方,即執行它不會返回結果,返回的是指標物件。呼叫指標g的next方法,會移動內部指標(即執行非同步任務的第一段),指向第一個遇到的yield語句,上例是執行到x + 2為止。
換言之,next方法的作用是分階段執行Generator函式。每次呼叫next方法,會返回一個物件,表示當前階段的資訊(value屬性和done屬性)。value屬性是yield語句後面表示式的值,表示當前階段的值;done屬性是一個布林值,表示 Generator 函式是否執行完畢,即是否還有下一個階段。
3.3、Generator 函式的資料交換和錯誤處理
Generator 函式可以暫停執行和恢復執行,這是它能封裝非同步任務的根本原因。除此之外,它還有兩個特性,使它可以作為非同步程式設計的完整解決方案:函式體內外的資料交換和錯誤處理機制。
next返回值的 value 屬性,是 Generator 函式向外輸出資料;next方法還可以接受引數,向 Generator 函式體內輸入資料。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面程式碼中,第一next方法的value屬性,返回表示式x + 2的值3。第二個next方法帶有引數2,這個引數可以傳入 Generator 函式,作為上個階段非同步任務的返回結果,被函式體內的變數y接收。因此,這一步的value屬性,返回的就是2(變數y的值)。
Generator 函式內部還可以部署錯誤處理程式碼,捕獲函式體外丟擲的錯誤。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}

var g = gen(1);
g.next();
g.throw('出錯了');//出錯的程式碼與處理錯誤的程式碼,實現了時間和空間上的分離,這對於非同步程式設計無疑是很重要的。
// 出錯了
3.4、非同步任務的封裝
下面看看如何使用 Generator 函式,執行一個真實的非同步任務。
var fetch = require('node-fetch');

function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
上面程式碼中,Generator 函式封裝了一個非同步操作,該操作先讀取一個遠端介面,然後從 JSON 格式的資料解析資訊。就像前面說過的,這段程式碼非常像同步操作,除了加上了yield命令。
執行這段程式碼的方法如下。
var g = gen();
var result = g.next();

result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面程式碼中,首先執行 Generator 函式,獲取遍歷器物件,然後使用next方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。

可以看到,雖然 Generator 函式將非同步操作表示得很簡潔,但是流程管理卻不方便(即何時執行第一階段、何時執行第二階段)。
4、Thunk 函式:
4.1、Thunk 函式是自動執行 Generator 函式的一種方法。
f(x + 5) 傳值呼叫時,等同於f(6)
f(x + 5) 傳名呼叫時,等同於(x + 5) * 2
4.2、Thunk 函式的含義:編譯器的“傳名呼叫”實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體。這個臨時函式就叫做 Thunk 函式。
function f(m) { return m * 2;}
f(x + 5);
// 等同於
var thunk = function () { return x + 5;};
function f(thunk) { return thunk() * 2;}
函式 f 的引數x + 5被一個函式替換了。凡是用到原引數的地方,對Thunk函式求值即可。
這就是 Thunk 函式的定義,它是“傳名呼叫”的一種實現策略,用來替換某個表示式。
4.3、JavaScript 語言的 Thunk 函式
JavaScript 語言是傳值呼叫,它的 Thunk 函式含義有所不同。在 JavaScript 語言中,Thunk 函式替換的不是表示式,而是多引數函式,將其替換成一個只接受回撥函式作為引數的單引數函式。
// 正常版本的readFile(多引數版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(單引數版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);
上面程式碼中,fs模組的readFile方法是一個多引數函式,兩個引數分別為檔名和回撥函式。經過轉換器處理,它變成了一個單引數函式,只接受回撥函式作為引數。這個單引數版本,就叫做 Thunk 函式。
任何函式,只要引數有回撥函式,就能寫成 Thunk 函式的形式。下面是一個簡單的 Thunk 函式轉換器。
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};

// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
使用上面的轉換器,生成fs.readFile的 Thunk 函式。

var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
下面是另一個完整的例子。

function f(a, cb) {
cb(a);
}
const ft = Thunk(f);

ft(1)(console.log) // 1
4.4、Thunkify 模組
生產環境的轉換器,建議使用 Thunkify 模組。
首先是安裝。$ npm install thunkify
使用方式如下:
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
4.5、Generator 函式的流程管理: ES6 有了 Generator 函式,Thunk 函式現在可以用於 Generator 函式的自動流程管理。
4.6、Thunk 函式的自動流程管理:Thunk 函式真正的威力,在於可以自動執行 Generator 函式。下面就是一個基於 Thunk 函式的 Generator 執行器。
5、co 模組:
5.1、基本用法
co 模組是著名程式設計師 TJ Holowaychuk 於 2013 年 6 月釋出的一個小工具,用於 Generator 函式的自動執行。
下面是一個 Generator 函式,用於依次讀取兩個檔案。
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co 模組可以讓你不用編寫 Generator 函式的執行器。

var co = require('co');
co(gen);
上面程式碼中,Generator 函式只要傳入co函式,就會自動執行。

co函式返回一個Promise物件,因此可以用then方法新增回撥函式。

co(gen).then(function (){
console.log('Generator 函式執行完成');
});
上面程式碼中,等到 Generator 函式執行結束,就會輸出一行提示。
5.2、co 模組的原理
為什麼 co 可以自動執行 Generator 函式?前面說過,Generator 就是一個非同步操作的容器。它的自動執行需要一種機制,當非同步操作有了結果,能夠自動交回執行權。
兩種方法可以做到這一點。
(1)回撥函式。將非同步操作包裝成 Thunk 函式,在回撥函式里面交回執行權。
(2)Promise 物件。將非同步操作包裝成 Promise 物件,用then方法交回執行權。
co 模組其實就是將兩種自動執行器(Thunk 函式和 Promise 物件),包裝成一個模組。使用 co 的前提條件是,Generator 函式的yield命令後面,只能是 Thunk 函式或 Promise 物件。如果陣列或物件的成員,全部都是 Promise 物件,也可以使用 co,詳見後文的例子。
5.3、co 模組的原始碼:co 就是上面那個自動執行器的擴充套件,它的原始碼只有幾十行,非常簡單。
首先,co 函式接受 Generator 函式作為引數,返回一個 Promise 物件。
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
});
}
需要進行判斷
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);//在返回的 Promise 物件裡面,co 先檢查引數gen是否為 Generator 函式。如果是,就執行該函式,得到一個內部指標物件;
if (!gen || typeof gen.next !== 'function') return resolve(gen);如果不是就返回,並將 Promise 物件的狀態改為resolved。
});
}
接著,co 將 Generator 函式的內部指標物件的next方法,包裝成onFulfilled函式。這主要是為了能夠捕捉丟擲的錯誤。
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);

onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}
最後,就是關鍵的next函式,它會反覆呼叫自身。

function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "'
+ String(ret.value)
+ '"'
)
);
}
上面程式碼中,next函式的內部程式碼,一共只有四行命令。
第一行,檢查當前是否為 Generator 函式的最後一步,如果是就返回。
第二行,確保每一步的返回值,是 Promise 物件。
第三行,使用then方法,為返回值加上回撥函式,然後透過onFulfilled函式再次呼叫next函式。
第四行,在引數不符合要求的情況下(引數非 Thunk 函式和 Promise 物件),將 Promise 物件的狀態改為rejected,從而終止執行。
5.4、處理併發的非同步操作
co 支援併發的非同步操作,即允許某些操作同時進行,等到它們全部完成,才進行下一步。

這時,要把併發的操作都放在陣列或物件裡面,跟在yield語句後面。

// 陣列的寫法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);

// 物件的寫法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
下面是另一個例子。

co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});

function* somethingAsync(x) {
// do something async
return y
}
上面的程式碼允許併發三個somethingAsync非同步操作,等到它們全部完成,才會進行下一步。
5.6、例項:處理 Stream
Node 提供 Stream 模式讀寫資料,特點是一次只處理資料的一部分,資料分成一塊塊依次處理,就好像“資料流”一樣。這對於處理大規模資料非常有利。Stream 模式使用 EventEmitter API,會釋放三個事件:data事件:下一塊資料塊已經準備好了;end事件:整個“資料流”處理“完了;error事件:發生錯誤。
使用Promise.race()函式,可以判斷這三個事件之中哪一個最先發生,只有當data事件最先發生時,才進入下一個資料塊的處理。從而,我們可以透過一個while迴圈,完成所有資料的讀取。
const co = require('co');
const fs = require('fs');

const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;

co(function*() {
while(true) {
const res = yield Promise.race([
new Promise(resolve => stream.once('data', resolve)),
new Promise(resolve => stream.once('end', resolve)),
new Promise((resolve, reject) => stream.once('error', reject))
]);
if (!res) {
break;
}
stream.removeAllListeners('data');
stream.removeAllListeners('end');
stream.removeAllListeners('error');
valjeanCount += (res.toString().match(/valjean/ig) || []).length;
}
console.log('count:', valjeanCount); // count: 1120
});
上面程式碼採用 Stream 模式讀取《悲慘世界》的文字檔案,對於每個資料塊都使用stream.once方法,在data、end、error三個事件上新增一次性回撥函式。變數res只有在data事件發生時才有值,然後累加每個資料塊之中valjean這個詞出現的次數。

12.15星期五
十八、async 函式
1、含義:是 Generator 函式的語法糖。
1.1、例如:有一個 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函式,就是下面這樣。

const asyncReadFile = async function () {//async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await。
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};

1.2、async函式對 Generator 函式的改進,體現在以下四點。
(1)內建執行器。
Generator 函式的執行必須靠執行器,所以才有了co模組,而async函式自帶執行器。也就是說,async函式的執行,與普通函式一模一樣,只要一行。
asyncReadFile();
上面的程式碼呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。這完全不像 Generator 函式,需要呼叫next方法,或者用co模組,才能真正執行,得到最後結果。
(2)更好的語義。
async和await,比起星號和yield,語義更清楚了。async表示函式里有非同步操作,await表示緊跟在後面的表示式需要等待結果。
(3)更廣的適用性。
co模組約定,yield命令後面只能是 Thunk 函式或 Promise 物件,而async函式的await命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時等同於同步操作)。
(4)返回值是 Promise。
async函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then方法指定下一步的操作。

進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。
2、基本用法:async函式返回一個 Promise 物件,可以使用then方法新增回撥函式。當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
2.1、async 函式有多種使用形式。
// 函式宣告:async function foo() {}
// 函式表示式:const foo = async function () {};
// 物件的方法:let obj = { async foo() {} };obj.foo().then(...)
// Class 的方法:
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭頭函式:const foo = async () => {};
3、語法:async函式的語法規則總體上比較簡單,難點是錯誤處理機制。
3.1、返回 Promise 物件:async函式返回一個 Promise 物件。
async函式內部return語句返回的值,會成為then方法回撥函式的引數。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面程式碼中,函式f內部return命令返回的值,會被then方法回撥函式接收到。
async函式內部丟擲錯誤,會導致返回的 Promise 物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。
async function f() {
throw new Error('出錯了');
}

f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出錯了
3.2、Promise 物件的狀態變化
async函式返回的 Promise 物件,必須等到內部所有await命令後面的 Promise 物件執行完,才會發生狀態改變,除非遇到return語句或者丟擲錯誤。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。
例如:下面是一個例子。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
上面程式碼中,函式getTitle內部有三個操作:抓取網頁、取出文字、匹配頁面標題。只有這三個操作全部完成,才會執行then方法裡面的console.log。
3.3、await 命令:await命令後面是一個 Promise 物件。如果不是,會被轉成一個立即resolve的 Promise 物件。
async function f() {
return await 123;
}

f().then(v => console.log(v))// 123,await命令的引數是數值123,它被轉成 Promise 物件,並立即resolve。
await命令後面的 Promise 物件如果變為reject狀態,則reject的引數會被catch方法的回撥函式接收到。
async function f() {
await Promise.reject('出錯了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
注意,上面程式碼中,await語句前面沒有return,但是reject方法的引數依然傳入了catch方法的回撥函式。這裡如果在await前面加上return,效果是一樣的。
只要一個await語句後面的 Promise 變為reject,那麼整個async函式都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行,因為第一個await語句狀態變成了reject。
}

有時,我們希望即使前一個非同步操作失敗,也不要中斷後面的非同步操作。
第一種方法:這時可以將第一個await放在try...catch結構裡面,這樣不管這個非同步操作是否成功,第二個await都會執行。
async function f() {
try {
await Promise.reject('出錯了');
} catch(e) {
}
return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))// hello world
第二種方法是await後面的 Promise 物件再跟一個catch方法,處理前面可能出現的錯誤。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出錯了
// hello world
3.3、錯誤處理:如果await後面的非同步操作出錯,那麼等同於async函式返回的 Promise 物件被reject。
防止出錯的方法,也是將其放在try...catch程式碼塊之中。
第一種:如果有多個await命令,可以統一放在try...catch結構中。
第二種:實現多次重複嘗試。for(){try{…… break}catch(err){}}//如果await操作成功,就會使用break語句退出迴圈;如果失敗,會被catch語句捕捉,然後進入下一輪迴圈。
3.4、使用注意點
第一點,前面已經說過,await命令後面的Promise物件,執行結果可能是rejected,所以最好把await命令放在try...catch程式碼塊中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}

// 另一種寫法

async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
第二點,多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
第三點,await命令只能用在async函式之中,如果用在普通函式,就會報錯。
4、async 函式的實現原理:就是將 Generator 函式和自動執行器,包裝在一個函式里。
async function fn(args) { // ...} 等同於 function fn(args) { return spawn(function* () { // ... });}
所有的async函式都可以寫成上面的第二種形式,其中的spawn函式就是自動執行器。
5、與其他非同步處理方法的比較:
5.1、首先是 Promise 的寫法。
function chainAnimationsPromise(elem, animations) {
// 變數ret用來儲存上一個動畫的返回值
let ret = null;
// 新建一個空的Promise
let p = Promise.resolve();
// 使用then方法,新增所有動畫
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
// 返回一個部署了錯誤捕捉機制的Promise
return p.catch(function(e) {
/* 忽略錯誤,繼續執行 */
}).then(function() {
return ret;
});
}
雖然 Promise 的寫法比回撥函式的寫法大大改進,但是一眼看上去,程式碼完全都是 Promise 的 API(then、catch等等),操作本身的語義反而不容易看出來。
5.2、接著是 Generator 函式的寫法。
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret;
});
}
上面程式碼使用 Generator 函式遍歷了每個動畫,語義比 Promise 寫法更清晰,使用者定義的操作全部都出現在spawn函式的內部。這個寫法的問題在於,必須有一個任務執行器,自動執行 Generator 函式,上面程式碼的spawn函式就是自動執行器,它返回一個 Promise 物件,而且必須保證yield語句後面的表示式,必須返回一個 Promise。
5.3、最後是 async 函式的寫法。
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略錯誤,繼續執行 */
}
return ret;
}
可以看到 Async 函式的實現最簡潔,最符合語義,幾乎沒有語義不相關的程式碼。它將 Generator 寫法中的自動執行器,改在語言層面提供,不暴露給使用者,因此程式碼量最少。如果使用 Generator 寫法,自動執行器需要使用者自己提供。
6、例項:按順序完成非同步操作
7、非同步遍歷器:Iterator 介面是一種資料遍歷的協議,只要呼叫遍歷器物件的next方法,就會得到一個物件,表示當前遍歷指標所在的那個位置的資訊。next方法返回的物件的結構是{value, done},其中value表示當前的資料的值,done是一個布林值,表示遍歷是否結束。
7.1、非同步遍歷的介面
非同步遍歷器的最大的語法特點,就是呼叫遍歷器的next方法,返回的是一個 Promise 物件。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
上面程式碼中,asyncIterator是一個非同步遍歷器,呼叫next方法以後,返回一個 Promise 物件。因此,可以使用then方法指定,這個 Promise 物件的狀態變為resolve以後的回撥函式。回撥函式的引數,則是一個具有value和done兩個屬性的物件,這個跟同步遍歷器是一樣的。
物件的非同步遍歷器介面,部署在Symbol.asyncIterator屬性上面。不管是什麼樣的物件,只要它的Symbol.asyncIterator屬性有值,就表示應該對它進行非同步遍歷。
7.2、for await...of:前面介紹過,for...of迴圈用於遍歷同步的 Iterator 介面。新引入的for await...of迴圈,則是用於遍歷非同步的 Iterator 介面。
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}// a// b
上面程式碼中,createAsyncIterable()返回一個非同步遍歷器,for...of迴圈自動呼叫這個遍歷器的next方法,會得到一個 Promise 物件。await用來處理這個 Promise 物件,一旦resolve,就把得到的值(x)傳入for...of的迴圈體。for await...of迴圈的一個用途,是部署了 asyncIterable 操作的非同步介面,可以直接放入這個迴圈。
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
上面程式碼中,req是一個 asyncIterable 物件,用來非同步讀取資料。可以看到,使用for await...of迴圈以後,程式碼會非常簡潔。
如果next方法返回的 Promise 物件被reject,for await...of就會報錯,要用try...catch捕捉。
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
注意,for await...of迴圈也可以用於同步遍歷器。
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();// a// b
7.3、非同步 Generator 函式:就像 Generator 函式返回一個同步遍歷器物件一樣,非同步 Generator 函式的作用,是返回一個非同步遍歷器物件。
在語法上,非同步 Generator 函式就是async函式與 Generator 函式的結合。
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
上面程式碼中,gen是一個非同步 Generator 函式,執行後返回一個非同步 Iterator 物件。對該物件呼叫next方法,返回一個 Promise 物件。
非同步遍歷器的設計目的之一,就是 Generator 函式處理同步操作和非同步操作時,能夠使用同一套介面。
// 同步 Generator 函式
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 非同步 Generator 函式
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
const {value, done} = await iter.next();
if (done) break;
yield func(value);
}
}
上面程式碼中,可以看到有了非同步遍歷器以後,同步 Generator 函式和非同步 Generator 函式的寫法基本上是一致的。
定義的非同步 Generator 函式的用法如下。
(async function () {
for await (const line of readLines(filePath)) {
console.log(line);
}
})()
非同步 Generator 函式可以與for await...of迴圈結合起來使用。

async function* prefixLines(asyncIterable) {
for await (const line of asyncIterable) {
yield '> ' + line;
}
}
非同步 Generator 函式的返回值是一個非同步 Iterator,即每次呼叫它的next方法,會返回一個 Promise 物件,也就是說,跟在yield命令後面的,應該是一個 Promise 物件。
async function* asyncGenerator() {
console.log('Start');
const result = await doSomethingAsync(); // (A)
yield 'Result: '+ result; // (B)
console.log('Done');
}

const ag = asyncGenerator();
ag.next().then({value, done} => {
// ...
})
上面程式碼中,ag是asyncGenerator函式返回的非同步 Iterator 物件。呼叫ag.next()以後,asyncGenerator函式內部的執行順序如下。

列印出Start。
await命令返回一個 Promise 物件,但是程式不會停在這裡,繼續往下執行。
程式在B處暫停執行,yield命令立刻返回一個 Promise 物件,該物件就是ag.next()的返回值。
A處await命令後面的那個 Promise 物件 resolved,產生的值放入result變數。
B處的 Promise 物件 resolved,then方法指定的回撥函式開始執行,該函式的引數是一個物件,value的值是表示式'Result: ' + result的值,done屬性的值是false。
7.4、yield* 語句
yield*語句也可以跟一個非同步遍歷器。
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}

async function* gen2() {
// result 最終會等於 2
const result = yield* gen1();
}
上面程式碼中,gen2函式里面的result變數,最後的值是2。

與同步 Generator 函式一樣,for await...of迴圈會展開yield*。

(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();// a// b

12.18 星期一
十九、Class 的基本語法
1、簡介:
JavaScript 語言中,生成例項物件的傳統方法是透過建構函式
ES6 的class可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法而已。
ES6 的類,完全可以看作建構函式的另一種寫法。
class Point { function aa(){console.log('stuff');}}//等同於 Point.prototype = {aa() {console.log('stuff')}};
//類的所有方法都定義在類的prototype屬性上面。
typeof Point // "function"
Point === Point.prototype.constructor // true
var b = new Point();//使用的時候,也是直接對類使用new命令,跟建構函式的用法完全一致。
Point.aa() // "stuff"
b.aa === Ponit.prototype.aa // true在類的例項上面呼叫方法,其實就是呼叫原型上的方法。
上面程式碼表明,類的資料型別就是函式,類本身就指向建構函式。
由於類的方法都定義在prototype物件上面,所以類的新方法可以新增在prototype物件上面。Object.assign方法可以很方便地一次向類新增多個方法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
Point.prototype.constructor === Point // true ,prototype物件的constructor屬性,直接指向“類”的本身,這與 ES5 的行為是一致的。
另外,類的內部所有定義的方法,都是不可列舉的(non-enumerable)。
採用 ES5 的寫法(Point.prototype.方法(){……}),這樣的方法就是可列舉的。
2、嚴格模式:
類和模組的內部,預設就是嚴格模式,所以不需要使用use strict指定執行模式。只要你的程式碼寫在類或模組之中,就只有嚴格模式可用。
考慮到未來所有的程式碼,其實都是執行在模組之中,所以 ES6 實際上把整個語言升級到了嚴格模式。
3、constructor 方法:
constructor方法是類的預設方法,透過new命令生成物件例項時,自動呼叫該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被預設新增。
class Point {} 等同於 class Point { constructor() {}}
上面程式碼中,定義了一個空的類Point,JavaScript 引擎會自動為它新增一個空的constructor方法。
constructor方法預設返回例項物件(即this),完全可以指定返回另外一個物件。
class Foo {
constructor() {
return Object.create(null);
}
}

new Foo() instanceof Foo
// false
上面程式碼中,constructor函式返回一個全新的物件,結果導致例項物件不是Foo類的例項。
類必須使用new呼叫,否則會報錯。這是它跟普通建構函式的一個主要區別,後者不用new也可以執行。
4、類的例項物件:
與 ES5 一樣,類的所有例項共享一個原型物件。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__//true
p1.__proto__.printName = function () { return 'Oops' };//在p1的原型上新增了一個printName方法,由於p1的原型就是p2的原型,因此p2也可以呼叫這個方法。
使用例項的__proto__屬性改寫原型,必須相當謹慎,不推薦使用,因為這會改變“類”的原始定義,影響到所有例項。
再來一個例子:
class Point {
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
point.hasOwnProperty('y') // true ,x和y都是例項物件point自身的屬性(因為定義在this變數上),所以hasOwnProperty方法返回true,
point.hasOwnProperty('toString') // false,而toString是原型物件的屬性(因為定義在Point類上),所以hasOwnProperty方法返回false。
point.__proto__.hasOwnProperty('toString') // true,point的原型有string方法
5、Class 表示式:
與函式一樣,類也可以使用表示式的形式定義。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};注意這個類的名字是MyClass而不是Me,Me只在 Class 的內部程式碼可用,指代當前類。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面程式碼表示,Me只在 Class 內部有定義。
如果類的內部沒用到的話,可以省略Me,也就是可以寫成下面的形式。
const MyClass = class { /* ... */ };
採用 Class 表示式,可以寫出立即執行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}('張三');
person.sayName(); // "張三"
上面程式碼中,person是一個立即執行的類的例項。
6、不存在變數提升:
類不存在變數提升(hoist),這一點與 ES5 完全不同。
new Foo(); // ReferenceError
class Foo {}
7、私有方法:
私有方法是常見需求,但 ES6 不提供,只能透過變通方法模擬實現。
7.1、一種做法是在命名上加以區別。這種命名是不保險的,在類的外部,還是可以呼叫到這個方法。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
7.2、另一種方法就是索性將私有方法移出模組,因為模組內部的所有方法都是對外可見的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
7.3、還有一種方法是利用Symbol值的唯一性,將私有方法的名字命名為一個Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{

// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
上面程式碼中,bar和snaf都是Symbol值,導致第三方無法獲取到它們,因此達到了私有方法和私有屬性的效果。
8、私有屬性:
8.1、與私有方法一樣,ES6 不支援私有屬性。目前,有一個提案,為class加了私有屬性。方法是在屬性名之前,使用#表示。
class Point {
#x;
constructor(x = 0) {
#x = +x; // 寫成 this.#x 亦可
}
get x() { return #x }
set x(value) { #x = +value }
}
上面程式碼中,#x就表示私有屬性x,在Point類之外是讀取不到這個屬性的。還可以看到,私有屬性與例項的屬性是可以同名的(比如,#x與get x())。
8.2、私有屬性可以指定初始值,在建構函式執行時進行初始化。
class Point {
#x = 0;
constructor() {
#x; // 0
}
}
要引入一個新的字首#表示私有屬性,而沒有采用private關鍵字,是因為 JavaScript 是一門動態語言,使用獨立的符號似乎是唯一的可靠方法,能夠準確地區分一種屬性是否為私有屬性。
8.3、它也可以用來寫私有方法。
class Foo {
#a;
#b;
#sum() { return #a + #b; }
printSum() { console.log(#sum()); }
constructor(a, b) { #a = a; #b = b; }
}
9、this 的指向:
類的方法內部如果含有this,它預設指向類的例項。但是,必須非常小心,一旦單獨使用該方法,很可能報錯。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}

print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
上面程式碼中,printName方法中的this,預設指向Logger類的例項。但是,如果將這個方法提取出來單獨使用,this會指向該方法執行時所在的環境,因為找不到print方法而導致報錯。
9.1、一個比較簡單的解決方法是,在構造方法中繫結this,這樣就不會找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}

// ...
}
9.2、另一種解決方法是使用箭頭函式。
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}

// ...
}
9.3、還有一種解決方法是使用Proxy,獲取方法的時候,自動繫結this。
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());
10、name 屬性:
class Point {}
Point.name // "Point"
name屬性總是返回緊跟在class關鍵字後面的類名。
11、Class 的取值函式(getter)和存值函式(setter):
與 ES5 一樣,在“類”的內部可以使用get和set關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為。
11.1、 class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
上面程式碼中,prop屬性有對應的存值函式和取值函式,因此賦值和讀取行為都被自定義了。

11.2、存值函式和取值函式是設定在屬性的 Descriptor 物件上的。
class CustomHTMLElement {
constructor(element) {
this.element = element;
}

get html() {
return this.element.innerHTML;
}

set html(value) {
this.element.innerHTML = value;
}
}

var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html"
);

"get" in descriptor // true
"set" in descriptor // true
上面程式碼中,存值函式和取值函式是定義在html屬性的描述物件上面,這與 ES5 完全一致。
12、Class 的 Generator 方法:
如果某個方法之前加上星號(*),就表示該方法是一個 Generator 函式。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}

for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
上面程式碼中,Foo類的Symbol.iterator方法前有一個星號,表示該方法是一個 Generator 函式。Symbol.iterator方法返回一個Foo類的預設遍歷器,for...of迴圈會自動呼叫這個遍歷器。
13、Class 的靜態方法:
13.1、類相當於例項的原型,所有在類中定義的方法,都會被例項繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被例項繼承,而是直接透過類來呼叫,這就稱為“靜態方法”。
class Foo {
static classMethod() {//Foo類的classMethod方法前有static關鍵字,表明該方法是一個靜態方法
return 'hello';
}
}
Foo.classMethod() // 'hello'可以直接在Foo類上呼叫(Foo.classMethod())
var foo = new Foo();
foo.classMethod()// TypeError: foo.classMethod is not a function
如果在例項上呼叫靜態方法,會丟擲一個錯誤,表示不存在該方法。

13.2、如果靜態方法包含this關鍵字,這個this指的是類,而不是例項。
class Foo {
static bar () {
this.baz();//靜態方法bar呼叫了this.baz,這裡的this指的是Foo類,而不是Foo的例項
}
static baz () {
console.log('hello');
}
baz () {//靜態方法可以與非靜態方法重名。
console.log('world');
}
}
Foo.bar() // hello 等同於呼叫Foo.baz。
父類的靜態方法,可以被子類繼承。父類Foo有一個靜態方法,子類Bar可以呼叫這個方法。
靜態方法也是可以從super物件上呼叫的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
14、Class 的靜態屬性和例項屬性:
靜態屬性指的是 Class 本身的屬性,即Class.propName,而不是定義在例項物件(this)上的屬性。
class Foo {}
Foo.prop = 1;
Foo.prop // 1
上面的寫法為Foo類定義了一個靜態屬性prop。
目前,只有這種寫法可行,因為 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。
class Foo {
prop: 2// 寫法一
static prop: 2// 寫法二 }
Foo.prop // undefined// 以下兩種寫法都無效
目前有一個靜態屬性的提案,對例項屬性和靜態屬性都規定了新的寫法。
(1)類的例項屬性(需要用到等式)
class MyClass {
myProp = 42;//類的例項屬性可以用等式,寫入類的定義之中。
constructor() {
console.log(this.myProp); // 42
}
}
上面程式碼中,myProp就是MyClass的例項屬性。在MyClass的例項上,可以讀取這個屬性。
以前,我們定義例項屬性,只能寫在類的constructor方法裡面。
class ReactCounter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0//構造方法constructor裡面,定義了this.state屬性。
};
}
}
有了新的寫法以後,可以不在constructor方法裡面定義。
class ReactCounter extends React.Component {
state = {
count: 0
};
}
這種寫法比以前更清晰。
為了可讀性的目的,對於那些在constructor裡面已經定義的例項屬性,新寫法允許直接列出。
class ReactCounter extends React.Component {
state;
constructor(props) {
super(props);
this.state = {
count: 0
};
}
}
(2)類的靜態屬性
類的靜態屬性只要在上面的例項屬性寫法前面,加上static關鍵字就可以了。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
同樣的,這個新寫法大大方便了靜態屬性的表達。
class Foo { // ...}
Foo.prop = 1;// 老寫法
class Foo { static prop = 1;}// 新寫法
上面程式碼中,老寫法的靜態屬性定義在類的外部。整個類生成以後,再生成靜態屬性。這樣讓人很容易忽略這個靜態屬性,也不符合相關程式碼應該放在一起的程式碼組織原則。另外,新寫法是顯式宣告(declarative),而不是賦值處理,語義更好。
15、new.target 屬性:
new是從建構函式生成例項物件的命令。ES6 為new命令引入了一個new.target屬性,該屬性一般用在建構函式之中,返回new命令作用於的那個建構函式。如果建構函式不是透過new命令呼叫的,new.target會返回undefined,因此這個屬性可以用來確定建構函式是怎麼呼叫的。

function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必須使用 new 命令生成例項');
}
}

// 另一種寫法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必須使用 new 命令生成例項');
}
}

var person = new Person('張三'); // 正確
var notAPerson = Person.call(person, '張三'); // 報錯
上面程式碼確保建構函式只能透過new命令呼叫。

Class 內部呼叫new.target,返回當前 Class。

class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}

var obj = new Rectangle(3, 4); // 輸出 true
需要注意的是,子類繼承父類時,new.target會返回子類。

class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}

class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}

var obj = new Square(3); // 輸出 false
上面程式碼中,new.target會返回子類。

利用這個特點,可以寫出不能獨立使用、必須繼承後才能使用的類。

class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本類不能例項化');
}
}
}

class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}

var x = new Shape(); // 報錯
var y = new Rectangle(3, 4); // 正確
上面程式碼中,Shape類不能被例項化,只能用於繼承。

注意,在函式外部,使用new.target會報錯。

12.18 星期一
二十、Class 的繼承
1、簡介:
Class 可以透過extends關鍵字實現繼承,這比 ES5 的透過修改原型鏈實現繼承,要清晰和方便很多。
class Point {}
class ColorPoint extends Point {}//定義了一個ColorPoint類,該類透過extends關鍵字,繼承了Point類的所有屬性和方法。
super關鍵字,它在這裡表示父類的建構函式,用來新建父類的this物件。
子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類沒有自己的this物件,而是繼承父類的this物件,然後對其進行加工。如果不呼叫super方法,子類就得不到this物件。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
2、Object.getPrototypeOf():可以用來從子類上獲取父類。
Object.getPrototypeOf(ColorPoint) === Point// true可以判斷,一個類是否繼承了另一個類。
3、super 關鍵字
4、類的 prototype 屬性和__proto__屬性
5、原生建構函式的繼承:
原生建構函式是指語言內建的建構函式,通常用來生成資料結構。ECMAScript 的原生建構函式大致有下面這些:
Boolean()、Number()、String()、Array()、Date()、Function()、RegExp()、Error()、Object()
6、Mixin 模式的實現:
Mixin 指的是多個物件合成一個新的物件,新物件具有各個組成成員的介面。它的最簡單實現如下。
const a = { a: 'a'};
const b = { b: 'b'};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
上面程式碼中,c物件是a物件和b物件的合成,具有兩者的介面。
下面是一個更完備的實現,將多個類的介面“混入”(mix in)另一個類。
function mix(...mixins) {
class Mix {}

for (let mixin of mixins) {
copyProperties(Mix, mixin); // 複製例項屬性
copyProperties(Mix.prototype, mixin.prototype); // 複製原型屬性
}

return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面程式碼的mix函式,可以將多個物件合成為一個類。使用的時候,只要繼承這個類即可。

class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}

相關文章