ES6學習筆記3-Set和Map、Promise、Iterator、Generator、async 、Class

Set和Map數據結構

Set

新的數據結構Set相似於數組,可是成員的值都是惟一的,沒有重複的值。Set 自己是一個構造函數,用來生成 Set 數據結構。接受一個數組(或相似數組的對象)做爲參數,用來初始化。git

const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

由於Set的成員都是惟一的,因此可用set去除數組重複成員。向Set加入值的時候,不會發生類型轉換,因此5和"5"是兩個不一樣的值。Set內部判斷兩個值是否不一樣相似於精確相等運算符(===),主要的區別是NaN等於自身,而精確相等運算符認爲NaN不等於自身。Array.from方法能夠將 Set 結構轉爲數組。es6

// 去除數組的重複成員
[...new Set(array)]

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set  // Set(1) {NaN} 這代表,在 Set 內部,兩個NaN是相等。

Set 實例的屬性和方法

Set 實例的屬性

  1. size:返回Set實例的成員總數。
  2. constructor:指向構造函數,默認就是Set函數。

Set 實例的方法

操做方法github

  • add(value):添加某個值,返回Set結構自己。
  • delete(value):刪除某個值,返回一個布爾值,表示刪除是否成功。
  • has(value):返回一個布爾值,表示該值是否爲Set的成員。
  • clear():清除全部成員,沒有返回值。

遍歷操做算法

  • keys():返回鍵名的遍歷器對象。
  • values():返回鍵值的遍歷器對象。
  • entries():返回鍵值對的遍歷器對象。
  • forEach():使用回調函數遍歷每一個成員,沒有返回值。第一個參數是一個處理函數。該函數的參數依次爲鍵值、鍵名、集合自己。第二個參數,表示綁定的this對象。

因爲 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),因此keys方法和values方法的行爲徹底一致。express

Set的遍歷順序就是插入順序。經過該特性,當用Set保存一個回調函數列表,調用時就能保證按照添加順序調用。編程

Set 結構的實例默承認遍歷,它的默認遍歷器生成函數就是它的values方法。擴展運算符(...)可用於 Set 結構。json

Set.prototype[Symbol.iterator] === Set.prototype.values
// true

WeakSet

WeakSet 是一個構造函數,可使用new命令,建立 WeakSet 數據結構。能夠接受一個數組或相似數組的對象做爲參數。(實際上,任何具備 Iterable 接口的對象,均可以做爲 WeakSet 的參數。)該數組的全部成員,都會自動成爲 WeakSet 實例對象的成員。數組

const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

上面的代碼中,a數組的成員成爲 WeakSet 的成員,而不是a數組自己。promise

WeakSet 結構有如下三個方法:數據結構

  • WeakSet.prototype.add(value):向 WeakSet 實例添加一個新成員。
  • WeakSet.prototype.delete(value):清除 WeakSet 實例的指定成員。
  • WeakSet.prototype.has(value):返回一個布爾值,表示某個值是否在 WeakSet 實例之中。

WeakSet的特色

WeakSet 結構與 Set 相似,也是不重複的值的集合。可是,與Set不一樣的是:

  1. WeakSet 的成員只能是對象,而不能是其餘類型的值。
  2. WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,若是其餘對象都再也不引用該對象,那麼垃圾回收機制會自動回收該對象所佔用的內存,不考慮該對象還存在於 WeakSet 之中。
  3. WeakSet 沒有size屬性,不可遍歷。

WeakSet的以上特性決定WeakSet適合臨時存放一組對象,以及存放跟對象綁定的信息。只要這些對象在外部消失,它在 WeakMap 裏面的引用就會自動消失。

Map

JavaScript 的對象(Object),本質上是鍵值對的集合(Hash 結構),可是傳統上只能用字符串看成鍵。
Map 數據結構相似於對象,也是鍵值對的集合,可是「鍵」的範圍不限於字符串,各類類型的值(包括對象)均可以看成鍵。Map做爲構造函數,能夠接受任何具備 Iterator 接口的數據結構做爲參數,好比數組。

//Map構造函數接受數組做爲參數,實際上執行的是下面的算法。
const items = [
  ['name', '張三'],
  ['title', 'Author']
];

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);
  1. 若是對同一個鍵屢次賦值,後面的值將覆蓋前面的值。
  2. 若是讀取一個未知的鍵,則返回undefined。
  3. 只有對同一個對象的引用,Map 結構纔將其視爲同一個鍵,由於Map 的鍵其實是跟內存地址綁定。

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

判斷Map鍵是否相等

  1. Map鍵是對象類型的,內存地址相同才相同。
  2. Map鍵是簡單類型(數字、字符串、布爾值)的,兩個值嚴格相等視爲一個鍵。0和-0是同一個鍵。
  3. Map鍵將NaN和其自身視爲同一個鍵。

Map實例的屬性和方法

Map實例的屬性

  1. size:返回 Map 結構的成員總數。

Map實例的方法

  1. set(key, value):設置鍵名key對應的鍵值爲value,而後返回整個 Map 結構。若是key已經有值,則鍵值會被更新,不然就新生成該鍵。
  2. get(key):讀取key對應的鍵值,若是找不到key,返回undefined。
  3. has(key):返回一個布爾值,表示某個鍵是否在當前 Map 對象之中。
  4. delete(key):刪除某個鍵,返回true。若是刪除失敗,返回false。
  5. clear():清除全部成員,沒有返回值。

遍歷方法

  • keys():返回鍵名的遍歷器。
  • values():返回鍵值的遍歷器。
  • entries():返回全部成員的遍歷器。
  • forEach():使用回調函數遍歷Map的每一個成員。第一個參數是一個處理函數。該函數的參數依次爲鍵值、鍵名、集合自己。第二個參數,表示綁定的this對象。

Map 的遍歷順序就是插入順序。

Map 結構的默認遍歷器接口(Symbol.iterator屬性),就是entries方法。Map 結構轉爲數組結構,比較快速的方法是使用擴展運算符(...)。

WeakMap

WeakMap結構與Map結構相似,也是用於生成鍵值對的集合。可是

  1. WeakMap只接受對象做爲鍵名(null除外),不接受其餘類型的值做爲鍵名。
  2. WeakMap的鍵名所指向的對象,不計入垃圾回收機制。

WeakMap的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內。所以,只要所引用的對象的其餘引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦再也不須要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。

WeakMap結構有助於防止內存泄漏。

WeakMap沒有遍歷操做(即沒有key()、values()和entries()方法),也沒有size屬性,也不支持clear方法。所以,WeakMap只有四個方法可用:get()、set()、has()、delete()。

Promise

Promise對象特色:

  1. Promise對象有三種狀態:Pending(進行中)、Resolved(已完成,又稱 Fulfilled)和Rejected(已失敗)。
  2. 一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。只要這兩種狀況發生,狀態就不會再變了。若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。
  3. 狀態一改變,即調用Promise 對象的 then方法。

缺點:

  1. Promise一旦新建它就會當即執行,沒法中途取消。
  2. 若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
  3. 當處於Pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。

基本用法

Promise對象是一個構造函數,用來生成Promise實例。

var promise = new Promise(
   function(resolve, reject) {
     // ... some code
     if (/* 異步操做成功 */){
         resolve(value);
     } else {
         reject(error);
    } 
});

Promise構造函數接受一個函數做爲參數,該函數在Promise構造函數返回新建對象前被調用,被傳遞resolve和reject函數。resolve和reject函數由JavaScript引擎提供,不用本身部署。若參數函數拋出一個錯誤,那麼該promise 狀態爲rejected。函數的返回值被忽略。

resolve函數:將Promise對象的狀態從「未完成」變爲「成功」(即從Pending變爲Resolved),將傳給resolve函數的參數傳遞出去
reject函數:將Promise對象的狀態從「未完成」變爲「失敗」(即從Pending變爲Rejected),將傳給Promise函數的參數傳遞出去。
簡而言之,若是調用resolve函數和reject函數時帶有參數,那麼它們的參數會被傳遞給回調函數。

resolve函數能夠傳遞一個Promise實例。當傳遞的是一個Promise實例時,其自身狀態無效,其狀態由該Promise實例決定。

var p1 = new Promise(function (resolve, reject) {
  // ...
});

var p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})

上面代碼中p2的resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做。

注意,這時p1的狀態就會傳遞給p2,也就是說,這時p2本身的狀態無效了,由p1的狀態決定p2的狀態若是p1的狀態是Pending,那麼p2的回調函數就會等待p1的狀態改變;若是p1的狀態已是Resolved或者Rejected,那麼p2的回調函數將會馬上執行。

Promise.prototype.then()

the()方法返回一個新的Promise。所以能夠採用鏈式寫法。

promise.then(onFulfilled, onRejected);
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise對象的狀態變爲Resolved時調用,第二個回調函數是Promise對象的狀態變爲Reject時調用。這兩個函數都接受Promise對象傳出的值做爲參數。若省略這兩個參數,或者提供非函數,不會產生任何錯誤。

注意:

  • 若是 onFulfilled 或者 onRejected 拋出一個錯誤,或者返回一個拒絕的 Promise ,then 返回一個 rejected Promise。
  • 若是 onFulfilled 或者 onRejected 返回一個 resolves Promise,或者返回任何其餘值,或者未返回值,then 返回一個 resolved Promise。
  • onFulfilled 或者 onRejected是被異步調用的。異步調用指的是在本輪「事件循環」(event loop)的結束時執行,而不是在下一輪「事件循環」的開始時執行。
getJSON("/posts.json").then(function(json) {
  return json.post; 
}).then(function(post) {
  // ...
});

上面的代碼中第一個回調函數完成之後,會將返回的json.post做爲參數,傳入第二個回調函數。若前一個回調函數返回的是一個Promise對象,這時後一個回調函數,就會等待該Promise對象的狀態發生變化,纔會被調用。

setTimeout(function(){
    console.log("aaa");
});
// using a resolved promise, the 'then' block will be triggered instantly, but its handlers will be triggered asynchronously as demonstrated by the console.logs
var resolvedProm = Promise.resolve(33);

var thenProm = resolvedProm.then(function(value){
    console.log("this gets called after the end of the main stack. the value received and returned is: " + value);
    return value;
});
// instantly logging the value of thenProm
console.log(thenProm);

// using setTimeout we can postpone the execution of a function to the moment the stack is empty
setTimeout(function(){
    console.log(thenProm);
});

//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//this gets called after the end of the main stack. the value received and returned is: 33
//aaa
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 33}

上面代碼中:setTimeout(fn, 0)在下一輪「事件循環」開始時執行,onFulfilled 在本輪「事件循環」結束時執行,console.log(thenProm)則是當即執行,所以最早輸出。

若then中無對應的回調函數,則then返回的新promise將會保持原promise的狀態進行調用。
例如:

function timeout(ms) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, ms);
    });
  }
  timeout(100).then(null, (value) => {
    console.log("aaa");
  }).then((value) => {
    console.log("ccc");
  }, (t) => {
    console.log("ddd");
  });

//ccc

上面代碼中,timeout函數中的 Promise狀態是resolve,可是第一個then中沒有對應的回調函數,所以第一個then返回的是resolve狀態的Promise。因此第二個then立馬被調用,輸出"ccc"。

Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。該方法返回一個新的Promise。

p.catch(onRejected);

p.catch(function(reason) {
   // 拒絕
});

onRejected 拋出一個錯誤,或者返回一個拒絕的 Promise,則catch返回一個 rejected Promise,不然返回一個resolved Promise。

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 處理 getJSON 和 前一個回調函數運行時發生的錯誤
  console.log('發生錯誤!', error);
});

上面代碼中,getJSON方法返回一個 Promise 對象,若是該對象狀態變爲Resolved,則會調用then方法指定的回調函數;若是異步操做拋出錯誤,就會調用catch方法指定的回調函數,處理這個錯誤。另外,then方法指定的回調函數,若是運行中拋出錯誤,也會被catch方法捕獲。

通常來講,不要在then方法裏面定義Reject狀態的回調函數(即then的第二個參數),老是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

由於第二種寫法能夠捕獲前面then方法執行中的錯誤,因此建議老是使用catch方法,而不使用then方法的第二個參數。

重要解析:

var promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise.then(function(value) { console.log(value) });
//ok


var promise = new Promise(function(resolve, reject) {
  resolve('ok');
  setTimeout(function() { throw new Error('test') }, 0)
});
promise.then(function(value) { console.log(value) });
// ok
// Uncaught Error: test

上面代碼中第一個例子中,throw 在resolve語句後面,拋出的錯誤,已經被捕獲並處理。可是Promise 的狀態由於resolve('ok')語句已改變,因此不會再改變。
上面代碼中第二個例子中拋出錯誤時,Promise函數體已經運行結束,因此沒法捕捉到該錯誤,就出現了在console中出現"ok"並拋出異常的現象。
詳見Promise源碼中的tryCallTwo和doResolve函數

Promise.all()

Promise.all(iterable):當在可迭代參數中的全部promises被resolve,或者任一 Promise 被 reject時,返回一個新的promise。
iterable:一個可迭代對象,例如 Array。

狀態斷定

(1)iterable爲空(好比[]),返回一個同步的resolved Promise。
(2)iterable未包含任何的promises(好比[1,2,3]),返回一個異步的resolved Promise。
clipboard.png
(3)iterable中的全部promises都是resolve,返回一個異步的resolved Promise。

以上狀況中,iterable內的全部值將組成一個數組,傳遞給回調函數。

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
}); 

Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});

(4)只要iterable中的promises有一個被rejected,就當即返回一個異步的rejected Promise。此時第一個被reject的實例的返回值,會傳遞給回調函數。

Promise.all([1,2,3, Promise.reject(555)]);
//Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 555}

如何理解返回一個異步的Promise

var p = Promise.all([]); // will be immediately resolved
var p2 = Promise.all([1337, "hi"]); // non-promise values will be ignored, but the evaluation will be done asynchronously
console.log(p);
console.log(p2)
setTimeout(function(){
    console.log('the stack is now empty');
    console.log(p2);
});

// Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: Array(0)}
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
// the stack is now empty
// Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: Array(2)}

Promise.race()

Promise.race(iterable):方法返回一個新的異步的promise,參數iterable中只要有一個promise對象"完成(resolve)"或"失敗(reject)",新的promise就會馬上"完成(resolve)"或者"失敗(reject)",並得到以前那個promise對象的返回值或者錯誤緣由。

var resolvedPromisesArray = [Promise.resolve(33), Promise.resolve(44)];

var p = Promise.race(resolvedPromisesArray);
// immediately logging the value of p
console.log(p);

// using setTimeout we can execute code after the stack is empty
setTimeout(function(){
    console.log('the stack is now empty');
    console.log(p);
});

//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//98
//the stack is now empty
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 33}

若iterable爲空,則返回的promise永遠都是pending狀態。
若iterable裏面包含一個或多個非promise值而且/或者有一個resolved/rejected promise,則新生成的Promise的值爲數組中的能被找到的第一個值。

var foreverPendingPromise = Promise.race([]);
var alreadyResolvedProm = Promise.resolve(666);

var arr = [foreverPendingPromise, alreadyResolvedProm, "non-Promise value"];
var arr2 = [foreverPendingPromise, "non-Promise value", Promise.resolve(666)];
var p = Promise.race(arr);
var p2 = Promise.race(arr2);

console.log(p);
console.log(p2);
setTimeout(function(){
    console.log('the stack is now empty');
    console.log(p);
    console.log(p2);
});

//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
//the stack is now empty
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 666}
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "non-Promise value"}

Promise.resolve()

Promise.resolve返回一個Promise對象。

Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable);

Promise.resolve方法的參數:

  1. 參數是一個Promise實例:Promise.resolve將不作任何修改、原封不動地返回這個實例。
  2. 參數是一個thenable對象:thenable對象指的是具備then方法的對象。Promise.resolve方法將該對象轉爲Promise對象後,就會當即執行thenable對象的then方法。

    let thenable = {
    then: function(resolve, reject) {
      resolve(42);
      }
    };
    
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
    console.log(value);  // 42
    });
    //thenable對象的then方法執行後,對象p1的狀態就變爲resolved,從而當即執行最後那個then方法指定的回調函數,輸出42。
  3. 其餘狀況:Promise.resolve方法返回一個新的Promise對象,狀態爲Resolved。Promise.resolve方法的參數,會同時傳給回調函數。

    var p = Promise.resolve('Hello');
    
    p.then(function (s){
    console.log(s)
    });
    // Hello
    //返回Promise實例的狀態從一輩子成就是Resolved,因此回調函數會當即執行

Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejected。所以,回調函數會當即執行。

Promise.reject(reason);

Promise.reject()方法的參數,會原封不動地做爲返回的新Promise的[[PromiseValue]]值,變成後續方法的參數。

Iterator(遍歷器)

JavaScript原有的表示「集合」的數據結構,主要是數組(Array)和對象(Object),ES6又添加了Map和Set。一個數據結構只要部署了Symbol.iterator屬性,就被視爲具備iterator接口,就能夠用for...of循環遍歷它的成員。也就是說,for...of循環內部調用的是數據結構的Symbol.iterator方法。任何數據結構只要部署了Iterator接口,就稱這種數據結構是」可遍歷的「(iterable)。

Symbol.iterator屬性自己是一個函數,執行這個函數,就會返回一個遍歷器。屬性名Symbol.iterator是一個表達式,返回Symbol對象的iterator屬性,這是一個預約義好的、類型爲Symbol的特殊值,因此要放在方括號內。

遍歷器對象的根本特徵: 具備next方法。每次調用next方法,都會返回一個表明當前成員的信息對象,該對象具備value和done兩個屬性。

內置可迭代對象:String, Array, TypedArray, Map and Set
接受可迭代對象做爲參數的:Map([iterable]), WeakMap([iterable]), Set([iterable])、WeakSet([iterable])、Promise.all(iterable), Promise.race(iterable) 以及 Array.from()。

一個對象若是要有可被for...of循環調用的Iterator接口,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的對象具備該方法也可)。

對於相似數組的對象(存在數值鍵名和length屬性),部署Iterator接口,有一個簡便方法,就是Symbol.iterator方法直接引用數組的Iterator接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

若是Symbol.iterator方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。

調用Iterator接口的場合

  1. 解構賦值:對數組和Set結構進行解構賦值時,會默認調用Symbol.iterator方法。
  2. 擴展運算符:擴展運算符(...)也會調用默認的iterator接口。所以,可經過(...)方便的將部署了Iterator接口的數據接口轉爲數組。

    let arr = [...iterable];
  3. yield*:yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
  4. 任何接受數組做爲參數的場合,都調用了遍歷器接口。

    for...of
     Array.from()
     Map(), Set(), WeakMap(), WeakSet()(好比new Map([['a',1],['b',2]]))
     Promise.all()
     Promise.race()

for...in循環讀取鍵名。for...of循環讀取鍵值,但數組的遍歷器接口只返回具備數字索引的鍵值。

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}
//for...of循環不返回數組arr的foo屬性

Set 和 Map 結構使用for...of循環時:

  1. 遍歷的順序是按照各個成員被添加進數據結構的順序。
  2. Set 結構遍歷時,返回的是一個值,而 Map 結構遍歷時,返回的是一個數組,該數組的兩個成員分別爲當前 Map 成員的鍵名和鍵值。

ES6的數組、Set、Map均有如下方法(返回的都是遍歷器對象,與Object的entries、keys、values方法不一樣,Object返回的均是數組。):

  • entries() 返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數組。對於數組,鍵名就是索引值;對於 Set,鍵名與鍵值相同。Map 結構的 Iterator 接口,默認就是調用entries方法。
  • keys() 返回一個遍歷器對象,用來遍歷全部的鍵名。
  • values() 返回一個遍歷器對象,用來遍歷全部的鍵值。

for...of循環能正確識別字符串中的32位 UTF-16 字符。
可經過Array.from方法將相似數組的對象轉爲數組。

與其餘遍歷語法的比較

  • forEach:沒法中途跳出forEach循環,break命令或return命令都不能奏效。
  • for...in:不只遍歷數字鍵名,還會遍歷手動添加的其餘鍵,甚至包括原型鏈上的鍵。

for...of循環能夠與break、continue和return配合使用,提供了遍歷全部數據結構的統一操做接口。

Generator

Generator 函數是一個普通函數,有如下特徵:

  1. function關鍵字與函數名之間有一個星號。
  2. 函數體內部使用yield表達式,定義不一樣的內部狀態。

調用Generator 函數,就是在函數名後面加上一對圓括號。不過,調用 Generator 函數後,該函數並不執行,而是返回一個遍歷器對象。調用遍歷器對象的next方法,就會返回一個有着value和done兩個屬性的對象。value屬性就是yield表達式或return後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。Generator 函數不能當構造器使用。

function* f() {}
var obj = new f; // throws "TypeError: f is not a constructor"

yield 表達式

遍歷器對象的next方法的運行邏輯:

  1. 遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。
  2. 下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
  3. 若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。
  4. 若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。
function* demo() {
  console.log('Hello' + (yield)); 
  console.log('Hello' + (yield 123)); 
}
var a=demo();
a.next();
//Object {value: undefined, done: false}  第一次運行了yield以後就中止了。
a.next();
//Helloundefined
//Object {value: 123, done: false} 第二次將以前的hello打印,並運行yield 123以後中止。
a.next();
//Helloundefined
//Object {value: undefined, done: true}

yield表達式與return語句:
類似之處:能返回緊跟在語句後面的那個表達式的值。
不一樣之處:每次遇到yield,函數暫停執行,下一次再從該位置後繼續向後執行,即便運行到最後一個yield ,其返回對象的done仍爲false。return語句執行後即表明該遍歷結束,返回對象的done爲true。

function* helloWorldGenerator() {
  yield 'hello';
  return 'ending';
  yield 'world';
}

var hw = helloWorldGenerator();
hw.next();
// Object {value: "hello", done: false}
hw.next();
// Object {value: "ending", done: true}
hw.next();
// Object {value: undefined, done: true}

Generator 函數能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數。但yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。

function* f() {
  console.log('執行了!')
}
var generator = f();
setTimeout(function () {
  generator.next()
}, 2000);

上面代碼中函數f若是是普通函數,在爲變量generator賦值時就會執行。可是,函數f是一個 Generator 函數,就變成只有調用next方法時,函數f纔會執行。

yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。若是用做函數參數或放在賦值表達式的右邊,能夠不加括號。

與 Iterator 接口的關係

任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個遍歷器對象。因爲 Generator 函數就是遍歷器生成函數,所以能夠把 Generator 賦值給對象的Symbol.iterator屬性。

Generator 函數執行後,返回一個遍歷器對象。該對象自己也具備Symbol.iterator屬性,執行後返回自身。

function* gen(){
  // some code
}
var g = gen();
g[Symbol.iterator]() === g
// true

next 方法的參數

yield表達式自己沒有返回值,或者說老是返回undefined。next方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值。注意,因爲next方法的參數表示上一個yield表達式的返回值,因此第一次使用next方法時,不用帶參數。

for...of 循環

for...of循環能夠自動遍歷 Generator 函數時生成的Iterator對象。

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

注意:一旦next方法的返回對象的done屬性爲true,for...of循環就會停止,且不包含該返回對象,因此上面代碼的return語句返回的6,不包括在for...of循環之中。

for...of的本質是一個while循環,通俗的講,就是運行對象的Symbol.iterator方法(即遍歷器生成函數),獲得遍歷器對象,再不停的調用遍歷器對象的next方法運行,直到遍歷結束。相似以下:

var it = foo();
var res = it.next();

while (!res.done){
  // ...
  res = it.next();
}

Generator.prototype.throw()

throw() 方法:向Generator函數內部拋出異常,並恢復生成器的執行,返回帶有 done 及 value 兩個屬性的對象。

gen.throw(exception)

exception:要拋出的異常。

該方法能夠在Generator 函數體外拋出錯誤,而後在 Generator 函數體內捕獲。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b

上面代碼中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被 Generator 函數體內的catch語句捕獲。i第二次拋出錯誤,因爲 Generator 函數內部的catch語句已經執行過了,不會再捕捉到這個錯誤了,因此這個錯誤就被拋出了 Generator 函數體,被函數體外的catch語句捕獲。

throw方法能夠接受一個參數,該參數會被catch語句接收,建議拋出Error對象的實例。遍歷器對象的throw方法和全局的throw命令不同。全局的throw命令只能被該命令外的catch語句捕獲,且不會再繼續try代碼塊裏面剩餘的語句了。

若是 Generator 函數內部沒有部署try...catch代碼塊,那麼throw方法拋出的錯誤,將被外部try...catch代碼塊捕獲。若是 Generator 函數內部和外部,都沒有部署try...catch代碼塊,那麼程序將報錯,直接中斷執行。

var g = function* () {
  while (true) {
    yield;
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 外部捕獲 a

throw方法被捕獲之後,會附帶執行下一條yield表達式。也就是說,會附帶執行一次next方法。

var gen = function* gen(){
  try {
    yield console.log('a');
    console.log('b');
  } catch (e) {
    console.log('錯誤被捕獲');
  }
  yield console.log('c');
  yield console.log('d');
}

var g = gen();
g.next();
//a
//Object {value: undefined, done: false}
g.throw();
//錯誤被捕獲
//c
//Object {value: undefined, done: false}
g.next();
//d
//Object {value: undefined, done: false}

上面的代碼能夠看出,g.throw方法是先拋出異常,再自動執行一次next方法,所以能夠看到沒有打印b,可是打印了c。

Generator 函數體外拋出的錯誤,能夠在函數體內捕獲;反過來,Generator 函數體內拋出的錯誤,也能夠被函數體外的catch捕獲。

一旦 Generator 執行過程當中拋出錯誤,且沒有被內部捕獲,就不會再執行下去了。若是此後還調用next方法,將返回一個value屬性等於undefined、done屬性等於true的對象,即 JavaScript 引擎認爲這個 Generator 已經運行結束了。

Generator.prototype.return()

Generator.prototype.return能夠返回給定的值,而且終結遍歷Generator函數。若該方法被調用時,Generator函數已結束,則Generator函數將保持結束的狀態,可是提供的參數將被設置爲返回對象的value屬性的值。

遍歷器對象調用return方法後,返回值的value屬性就是return方法的參數foo。而且,Generator函數的遍歷就終止了,返回值的done屬性爲true,之後再調用next方法,done屬性老是返回true。若是return方法調用時,不提供參數,則返回值的value屬性爲undefined。

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()      // {value: undefined, done: true}
g.return('111')  //{value: "111", done: true}

上面代碼中,g.return('111')調用時, Generator函數的遍歷已經終止,因此返回的對象的done值仍爲true,可是value值會被設置爲'111'。

若是 Generator 函數內部有try...finally代碼塊,那麼當return方法執行時的語句在 Generator 函數內部的try代碼塊中時,return方法會推遲到finally代碼塊執行完再執行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next();
//Object {value: 1, done: false}
g.return(7);
//Object {value: 7, done: true}
//return執行時還未在try語句塊內,因此返回{value: 7, done: true}並終止遍歷。


function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
    yield 33;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next();
// Object {value: 1, done: false}
g.next();
// Object {value: 2, done: false}
g.return(7);
// Object {value: 4, done: false}
g.next();
// Object {value: 5, done: false}
g.next();
// Object {value: 7, done: true}
//return執行時已在try語句塊內,運行時直接跳至finally語句塊執行,並在該語句塊內的代碼執行完後,因此返回{value: 7, done: true}並終止遍歷。

yield* 表達式

yield* expression :expression 能夠是一個generator 或可迭代對象。yield * 表達式自身的值是當迭代器關閉時返回的值(即,當done時爲true)。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

yield*後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for...of循環。有return語句時,若想遍歷出return返回的值,則須要用var value = yield* iterator的形式獲取return語句的值。

function* foo() {
  yield 'a';
  yield 'b';
  return "mm";
}
function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}
for(var t of bar()){
 console.log(t);
}
//x
//a
//b
//y
//yield* foo()寫法沒法遍歷出foo裏面的「mm」。

function* foo() {
  yield 'a';
  yield 'b';
  return "mm";
}
function* bar() {
  yield 'x';
  yield yield* foo();
  yield 'y';
}
for(var t of bar()){
 console.log(t);
}
//x
//a
//b
//mm
//y
//yield* foo()運行返回的值就是「mm」,因此yield yield* foo()能夠遍歷出「mm」。

任何數據結構只要有 Iterator 接口,就能夠被yield*遍歷。yield*就至關因而使用for...of進行了循環。

做爲對象屬性的Generator函數

若是一個對象的屬性是 Generator 函數,則需在屬性前面加一個星號。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等同於
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator 函數的this

Generator 函數老是返回一個遍歷器,ES6 規定這個遍歷器是 Generator 函數的實例,也繼承了 Generator 函數的prototype對象上的方法。Generator函數不能跟new命令一塊兒用,會報錯。

function* g() {}

g.prototype.hello = function () {
  return 'hi!';
};

let obj = g();
obj.hello() // 'hi!'

clipboard.png

上面代碼能夠看出,obj對象是Generator 函數g的實例。可是,若是把g看成普通的構造函數,並不會生效,由於g返回的老是遍歷器對象,而不是this對象。

function* g() {
  this.a = 11;
}

let obj = g();
obj.a // undefined

上面代碼中,Generator函數g在this對象上面添加了一個屬性a,可是obj對象拿不到這個屬性。

應用場景

  1. 用來處理異步操做,改寫回調函數。即把異步操做寫在yield表達式裏面,異步操做的後續操做放在yield表達式下面。

    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();
    //上面爲經過 Generator 函數部署 Ajax 操做。
  2. 控制流管理
  3. 利用 Generator 函數,在任意對象上部署 Iterator 接口。
  4. 做爲數據結構。

Generator 函數的異步應用

Generator 函數能夠暫停執行和恢復執行,這是它能封裝異步任務的根本緣由。整個 Generator 函數就是一個封裝的異步任務。異步操做須要暫停的地方,都用yield語句註明。

Generator 函數能夠進行數據交換。next返回值的value屬性,是 Generator 函數向外輸出數據;next方法還能夠接受參數,向 Generator 函數體內輸入數據。

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('出錯了');
// 出錯了

上面代碼的最後一行,Generator 函數體外,使用指針對象的throw方法拋出的錯誤,能夠被函數體內的try...catch代碼塊捕獲。這意味着,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對於異步編程無疑是很重要的。

async 函數

async函數返回一個 Promise 對象,可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function add1(x) {
  var a = resolveAfter2Seconds(20);
  var b = resolveAfter2Seconds(30);
  return x + await a + await b;
}

add1(10).then(v => {
  console.log(v);  // prints 60 after 2 seconds.
});

async function add2(x) {
  var a = await resolveAfter2Seconds(20);
  var b = await resolveAfter2Seconds(30);
  return x + a + b;
}

add2(10).then(v => {
  console.log(v);  // prints 60 after 4 seconds.
});

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 () => {};

調用async函數時會返回一個 promise 對象。當這個async函數返回一個值時,promise 的 resolve 方法將會處理這個返回值;當異步函數拋出的是異常或者非法值時,promise 的 reject 方法將處理這個異常值。

async function f() {
  throw new Error('出錯了');
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出錯了

async函數返回的 Promise 對象,必須等到內部全部await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數

await 命令

await expression:會形成異步函數中止執行而且等待 promise 的解決後再恢復執行。若expression是Promise 對象,則返回expression的[[PromiseValue]]值,若expression不是Promise 對象,則直接返回該expression。

async function f2() {
  var y = await 20;
  console.log(y); // 20
}
f2();

await命令後面通常是一個 Promise 對象。若是不是,會被轉成一個當即resolve的 Promise 對象。await命令後面的 Promise 對象若是變爲reject狀態,則會throws異常值,所以reject的參數會被catch方法的回調函數接收到。只要一個await語句後面的 Promise 變爲reject,那麼整個async函數都會中斷執行。

async function f() {
  await Promise.reject('出錯了');
  await Promise.resolve('hello world'); // 第二個await語句是不會執行的
}

錯誤處理

若是await後面的異步操做出錯,那麼等同於async函數返回的 Promise 對象被reject。防止出錯的方法,是將其放在try...catch代碼塊之中。

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出錯了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了

上面代碼中,async函數f執行後,await後面的 Promise 對象會拋出一個錯誤對象,致使catch方法的回調函數被調用,它的參數就是拋出的錯誤對象。

使用注意點

  1. 最好把await命令放在try...catch代碼塊中。由於await命令後面的Promise對象,運行結果多是rejected。

    async function myFunction() {
     try {
       await somethingThatReturnsAPromise();
     } catch (err) {
       console.log(err);
      }
    }
    
     // 另外一種寫法
    
     async function myFunction() {
       await somethingThatReturnsAPromise()
        .catch(function (err) {
          console.log(err);
        };
      }
  2. 多個await命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。同時觸發可使用Promise.all。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      let promises = docs.map((doc) => db.post(doc));
    
      let results = await Promise.all(promises);
      console.log(results);
    }
  3. await命令只能用在async函數之中,若是用在普通函數,就會報錯。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
    
      // 報錯。 由於await用在普通函數之中
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }

async 函數的實現原理

async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裏。

異步遍歷的接口

異步遍歷器的最大的語法特色,就是調用遍歷器的next方法,返回的是一個 Promise 對象。

asyncIterator
  .next()
  .then(
    ({ value, done }) => /* ... */
  );

上面代碼中,asyncIterator是一個異步遍歷器,調用next方法之後,返回一個 Promise 對象。所以,可使用then方法指定,這個 Promise 對象的狀態變爲resolve之後的回調函數。回調函數的參數,則是一個具備value和done兩個屬性的對象,這個跟同步遍歷器是同樣的。

一個對象的同步遍歷器的接口,部署在Symbol.iterator屬性上面。一樣地,對象的異步遍歷器接口,部署在Symbol.asyncIterator屬性上面。無論是什麼樣的對象,只要它的Symbol.asyncIterator屬性有值,就表示應該對它進行異步遍歷。

for await...of

for...of循環用於遍歷同步的 Iterator 接口。新引入的for await...of循環,則是用於遍歷異步的 Iterator 接口。for await...of循環也能夠用於同步遍歷器。

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的循環體。

異步Generator函數

在語法上,異步 Generator 函數就是async函數與 Generator 函數的結合。

async function* readLines(path) {
  let file = await fileOpen(path);

  try {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } finally {
    await file.close();
  }
}

上面代碼中,異步操做前面使用await關鍵字標明,即await後面的操做,應該返回Promise對象。凡是使用yield關鍵字的地方,就是next方法的停下來的地方,它後面的表達式的值(即await file.readLine()的值),會做爲next()返回對象的value屬性。

Class

constructor定義構造方法,this關鍵字表明實例對象。定義「類」的方法的時候,前面不須要加上function這個關鍵字,直接把函數定義放進去了就能夠了。另外,方法之間不須要逗號分隔,加了會報錯。類的數據類型就是函數,類的原型的constructor指向類自身。使用的時候,對類使用new命令。

//定義類
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
  static distance() {
      
   }
}
typeof Point // "function"
Point === Point.prototype.constructor // true

類的通常的方法都定義在類的prototype屬性上面。在類的實例上面調用方法,其實就是調用原型上的方法。類的內部全部定義的方法,都是不可枚舉的。類的靜態方法只能用類來調用,不能用類的實例調用。若是在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。父類的靜態方法,能夠被子類繼承。

class Point {
  constructor(){
    // ...
  }
  toString(){
    // ...
  }
  toValue(){
    // ...
  }
}

// 等同於
Point.prototype = {
  toString(){},
  toValue(){}
};

類的屬性名,能夠採用表達式。

let methodName = "getArea";
class Square{
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}
//Square類的方法名getArea,是從表達式獲得的。

constructor方法

constructor方法是類的默認方法,經過new命令生成對象實例時,自動調用該方法。一個類必須有constructor方法,若是沒有顯式定義,一個空的constructor方法會被默認添加。

constructor方法默認返回實例對象(即this),也能夠指定返回另一個對象。類的構造函數,不使用new是無法調用的,會報錯。這是它跟普通構造函數的一個主要區別,後者不用new也能夠執行。

class Foo {
  constructor() {
    return Object.create(null);
  }
}
new Foo() instanceof Foo
// false

上面代碼中,constructor函數返回一個全新的對象,結果致使實例對象不是Foo類的實例。

類的實例對象

生成類的實例對象的寫法,也是使用new命令。若是忘記加上new,像函數那樣調用Class,將會報錯。類裏面定義的屬性除了定義在this上的,其餘都是定義在原型上的。定義在this上的屬性各實例對象各自有一份。類的全部實例共享一個原型對象。

Class不存在變量提高(hoist),所以先使用,後定義會報錯。

new Foo(); // ReferenceError
class Foo {}

Class表達式

類也可使用表達式的形式定義。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

上面代碼使用表達式定義了一個類。須要注意的是,這個類的名字是MyClass而不是Me,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是一個當即執行的類的實例。

ES6不提供私有方法。

this的指向

類的方法內部若是含有this,它默認指向類的實例。可是,必須很是當心,一旦單獨使用該方法,極可能報錯。注意,若是靜態方法包含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方法而致使報錯。

一個比較簡單的解決方法是,在構造方法中綁定this。

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}

另外一種解決方法是使用箭頭函數。

還有一種解決方法是使用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());

嚴格模式

類和模塊的內部,默認就是嚴格模式,因此不須要使用use strict指定運行模式。只要你的代碼寫在類或模塊之中,就只有嚴格模式可用。

name屬性

本質上,ES6的類只是ES5的構造函數的一層包裝,因此函數的許多特性都被Class繼承,包括name屬性。name屬性老是返回緊跟在class關鍵字後面的類名。

class Point {}
Point.name // "Point"

Class的繼承

Class之間能夠經過extends關鍵字實現繼承。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 調用父類的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}

子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進行加工。若是不調用super方法,子類就得不到this對象。

ES6的繼承實質是先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this。

在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。

若是子類沒有定義constructor方法,如下方法會被默認添加。所以,無論有沒有顯式定義,任何一個子類都有constructor方法。

constructor(...args) {
  super(...args);
}

類的prototype屬性和__proto__屬性

Class同時有prototype屬性和__proto__屬性,所以同時存在兩條繼承鏈。

  1. 子類的__proto__屬性,表示構造函數的繼承,老是指向父類。
  2. 子類prototype屬性的__proto__屬性,老是指向父類的prototype屬性。

類的繼承是按照下面的模式實現的。

class A {
}

class B {
}

Object.setPrototypeOf(B.prototype, A.prototype);
Object.setPrototypeOf(B, A);
const b = new B();

而Object.setPrototypeOf方法的實現以下:

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

所以,就獲得以下結果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同於
B.prototype.__proto__ = A.prototype;

Object.setPrototypeOf(B, A);
// 等同於
B.__proto__ = A;

Extends 的繼承目標

extends關鍵字後面能夠跟多種類型的值。

class B extends A {
}

上面代碼的A,只要是一個有prototype屬性的函數,就能被B繼承。因爲函數都有prototype屬性(除了Function.prototype函數),所以A能夠是任意函數。

class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

上面代碼中,A做爲一個基類(即不存在任何繼承),就是一個普通函數,因此直接繼承Function.prototype。A.prototype是一個對象,因此A.prototype.__proto__指向構造函數(Object)的prototype屬性。

class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

//等同於

class C extends null {
  constructor() { return Object.create(null); }
}

上面代碼中,子類繼承null。

Object.getPrototypeOf()

Object.getPrototypeOf方法能夠用來從子類上獲取父類。所以,可使用這個方法判斷,一個類是否繼承了另外一個類。

Object.getPrototypeOf(ColorPoint) === Point //true

super 關鍵字

super這個關鍵字,既能夠看成函數使用,也能夠看成對象使用。

(一) super做爲函數調用時,表明父類的構造函數,且super()只能用在子類的構造函數之中,用在其餘地方就會報錯。ES6 要求,子類的構造函數必須執行一次super函數。

class A {}

class B extends A {
constructor() {
  super();
}
}

子類B的構造函數之中的super(),表明調用父類的構造函數。super()在這裏至關於A.prototype.constructor.call(this)

(二) super做爲對象時,在普通方法中,指向父類的原型對象(當指向父類的原型對象時,定義在父類實例上的方法或屬性,是沒法經過super調用的。);在靜態方法中,指向父類。

class A {
    p() {
      return 2;
    }
    static m() {
      console.log("父類的m方法被調用")
    }
  }

 class B extends A {
    constructor() {
      super();
      console.log(super.p()); // 2
    }
    static show() {
      super.m();
    }
  }

  let b = new B();
  B.show(); //父類的m方法被調用

上面代碼中,子類B的constructor中的super.p()在普通方法中,指向A.prototype,因此super.p()就至關於A.prototype.p()。子類B的show方法中的super.m()在靜態方法中,因此super.m()就至關於A.m()。

ES6 規定,經過super調用父類的方法時,super會綁定子類的this。

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

上面代碼中,super.print()雖然調用的是A.prototype.print(),可是A.prototype.print()會綁定子類B的this,致使輸出的是2。也就是說,實際上執行的是super.print.call(this)。

經過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類實例的屬性。

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  
 constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

上面代碼中,super.x賦值爲3,這時等同於對this.x賦值爲3。而當讀取super.x的時候,讀的是A.prototype.x,因此返回undefined。

注意,使用super的時候,必須顯式指定是做爲函數、仍是做爲對象使用,不然會報錯。

class A {}
class B extends A {
  constructor() {
    super();
    console.log(super); // 報錯
  }
}
//console.log(super)當中的super,沒法看出是做爲函數使用,仍是做爲對象使用,因此 JavaScript 引擎解析代碼的時候就會報錯。

實例的__proto__屬性

子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。

class Point{

}
class ColorPoint extends Point{
   constructor(){
      super();
  }
}
var p1 = new Point();
var p2 = new ColorPoint();
p2.__proto__.__proto__ === p1.__proto__ // true

原生構造函數的繼承

原生構造函數是指語言內置的構造函數,一般用來生成數據結構。ECMAScript的原生構造函數大體有下面這些。

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

extends關鍵字不只能夠用來繼承類,還能夠用來繼承原生的構造函數。
注意,繼承Object的子類,有一個行爲差別。

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
console.log(o.attr === true);  // false

上面代碼中,NewObj繼承了Object,可是沒法經過super方法向父類Object傳參。這是由於ES6改變了Object構造函數的行爲,一旦發現Object方法不是經過new Object()這種形式調用,ES6規定Object構造函數會忽略參數。

Class的取值函數(getter)和存值函數(setter)

在Class內部可使用get和set關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。存值函數和取值函數是設置在屬性的descriptor對象上的。

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屬性有對應的存值函數和取值函數,所以賦值和讀取行爲都被自定義了。

Class的靜態方法

在一個方法前,加上static關鍵字,則是靜態方法。靜態方法不會被實例繼承,而是直接經過類來調用。所以在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。

class Foo {
  static classMethod() {
    return 'hello';
  }
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,若是靜態方法包含this關鍵字,這個this指的是類,而不是實例。靜態方法能夠與非靜態方法重名。父類的靜態方法,能夠被子類繼承。

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}

Foo.bar() // hello

上面代碼中,靜態方法bar調用了this.baz,這裏的this指的是Foo類,而不是Foo的實例,等同於調用Foo.baz。

Class的靜態屬性和實例屬性

靜態屬性指的是Class自己的屬性,即Class.propname,而不是定義在實例對象(this)上的屬性。由於ES6明確規定,Class內部只有靜態方法,沒有靜態屬性。因此目前只有下面這種寫法。

//爲Foo類定義了一個靜態屬性prop
class Foo {
}

Foo.prop = 1;
Foo.prop // 1

ES7有一個靜態屬性的提案,目前Babel轉碼器支持。這個提案規定:

  1. 類的實例屬性能夠用等式,寫入類的定義之中。

    class MyClass {
      myProp = 42;
    
      constructor() {
        console.log(this.myProp); // 42
      }
    }
  2. 類的靜態屬性只要在上面的實例屬性寫法前面,加上static關鍵字就能夠了。

    // 老寫法
    class Foo {
       // ...
    }
    Foo.prop = 1;
    
    // 新寫法
    class Foo {
      static prop = 1;
    }

類的私有屬性

目前,有一個提案,爲class加了私有屬性。方法是在屬性名以前,使用#表示。#也能夠用來寫私有方法。私有屬性能夠指定初始值,在構造函數執行時進行初始化。

class Point {
  #x;
  constructor(x = 0) {
    #x = +x;
  }
  get x() { return #x }
  set x(value) { #x = +value }
  #sum() { return #x; } 
}

上面代碼中,#x就表示私有屬性x,在Point類以外是讀取不到這個屬性的。還能夠看到,私有屬性與實例的屬性是能夠同名的(好比,#x與get x())。

new.target屬性

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。子類繼承父類時,new.target會返回子類。在函數外部使用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

參考自:ECMAScript 6 入門

相關文章
相關標籤/搜索