es6摘要

 

 

1、let、constjavascript

一、塊級做用域css

二、不存在變量提高(變量必須在聲明後使用)html

三、暫時性死區java

(1)只要塊級做用域內存在let、const命令,只要一進入當前做用域,所要使用的變量就已經存在了,可是不可獲取,只有等到聲明變量的那一行代碼出現,才能夠獲取和使用該變量。凡是在聲明以前就使用這些變量,就會報錯。node

(2)「暫時性死區」也意味着typeof再也不是一個百分之百安全的操做。git

四、不容許重複聲明程序員

五、const聲明一個只讀的常量。一旦聲明,常量的值就不能改變es6

六、let、const聲明的全局變量,不屬於頂層對象(瀏覽器windows、node環境global)的屬性github

2、變量的解構賦值ajax

一、值本質屬於‘模式匹配’,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的,若是解構不成功,變量的值就等於undefined;解構賦值的規則是,只要等號右邊的值不是對象或數組,就先將其轉爲對象

二、數組的解構賦值

    let [a, b, c] = [1, 2, 3];// a: 1, b: 2, c: 3

    let [a, [b], c] = [1, [2], 3]; // a: 1, b: 2, c: 3

    let [, , c] = [1, 2, 3]; // c: 3

    let [a, ...tail] = [1, 2, 3, 4]; // a: 1, tail: [2, 3, 4]

    let [a, b, ...tail] = [1]; // a: 1, b: undefined, tail = []

    let [a, b, c] = 1; // 報錯 (等號右邊自己或者轉爲對象以後不是可遍歷的結構,即不具有Iterator接口)

    let [a = 1] = []; // a: 1

    let [a = 1] = [null]; // a: null (只有一個數組成員嚴格等於undefined時,默認值纔會生效)

    function* fibs() {
      let a = 0;
      let b = 1;
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }

    let [first, second, third, fourth, fifth, sixth] = fibs();
    // first: 0, second: 1, third: 1, fourth: 2, fifth: 3, sixth: 5

    // 若是默認值時一個表達式,那麼這個表達式時惰性求值的,即只在用到時纔會執行表達式求值

    const f = () => 1; let [a = f()] = [1]; // a: 1, f不會執行

    // 默認值能夠引用解構賦值的其餘變量,但該變量必須已經聲明

    let [a = 1, b = a] = [] // a: 1, b: 1

三、對象的解構賦值

(1)變量必須與屬性同名才能取到正確的值

let {a, b} = {b: 1, a: 2} // a: 2, b:1

(2)若是屬性名與變量名不一致,必須寫成下面這樣

let {a: c, b} = {a: 1, b: 2, c: 3} // c: 1, b: 2
// 對象的解構賦值是下面形式的簡寫
let {a: c, b: d} = {a: 1, b: 2} // c: 1, d: 2, a、b是匹配模式,c、d纔是變量
// 對象解構賦值的內部機制,是先找到同名屬性,而後再賦給對應的變量,真正被賦值的是後者,而不是前者

// 解構也能夠用於嵌套解構的對象
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
// x: "Hello", y: "World", p是匹配模式

// 對象的解構也能夠指定默認值, 默認值生效的條件是,對象的屬性值嚴格等於undefined
let {x = 3} = {}; // x: 3
let { message: msg = 'Something went wrong' } = {}; // msg: 'Something went wrong'

// 若是解構模式是嵌套的對象,並且子對象所在的父屬性不存在,那麼將會報錯
let {foo: {bar}} = {baz: 'baz'}; // 報錯 (let _tmp = {baz: 'baz'};_tmp.foo.bar // 報錯)

// 若是要將一個已經聲明的變量用於解構賦值,必須很是當心
let x; {x} = {x: 1};
// 報錯,由於javascrip引擎會將{x}理解成一個代碼塊,從而發生語法錯誤,只有不將大括號寫在行首,
// 避免 JavaScript 將其解釋爲代碼塊,才能解決這個問題。
let x; ({x} = {x: 1}); // x: 1

// 因爲數組本質是特殊的對象,所以能夠對數組進行對象屬性的解構。
let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; // first: 1, last: 3

四、字符串的解構賦值

(1)原理:解構賦值時,字符串被轉換成了一個相似數組的對象

let [a, b, c] = '12345'; // a: 1, b: 2, c: 3
let {length: a} = '12345'; // a: 5

五、數值和布爾值的解構賦值(先將其轉換成對象)

let {toString: toString} = 123 // toString: Number.prototype.toString
let {toString: toString} = true // toString: Boolean.prototype.toString

六、undefinednull的解構賦值

(1)因爲undefinednull不能轉換成對象,因此對它們進行解構賦值,都會報錯

let { prop: x } = undefined; // 報錯
let { prop: y } = null; // 報錯

七、函數參數的解構賦值

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

add([1, 2]); // 3

function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

七、圓括號的問題

(1)對於編譯器來講,一個式子究竟是模式,仍是表達式,沒有辦法從一開始就知道,必須解析到(或解析不到)等號才能知道。ES6 的規則是,只要有可能致使解構的歧義,就不得使用圓括號。

(2)能夠使用圓括號的狀況只有一種:賦值語句的非模式部分

let b, d;
[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確

(3)不能使用圓括號的狀況

   a、變量聲明語句

// 所有報錯
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

b、函數參數,函數參數也屬於變量聲明,所以不能帶有圓括號。

c、賦值語句的模式

// 所有報錯
({ p: a }) = { p: 42 };
([a]) = [5];
// 上面代碼將整個模式放在圓括號之中,致使報錯

// 報錯
[({ p: a }), { x: c }] = [{}, {}];
// 上面代碼將一部分模式放在圓括號之中,致使報錯。

八、用途

(1)交換變量值

let x = 1;
let y = 2;

[x, y] = [y, x];

(2)從函數返回多個值

// 返回一個數組

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

// 返回一個對象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

(3)函數參數的定義

// 參數是一組有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 參數是一組無次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

(4)提取JSON數據

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

(5)函數參數的默認值

指定參數的默認值,就避免了在函數體內部再寫var foo = config.foo || 'default foo';這樣的語句。

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

(6)遍歷Map結構

任何部署了 Iterator 接口的對象,均可以用for...of循環遍歷。Map 結構原生支持 Iterator 接口,配合變量的解構賦值,獲取鍵名和鍵值就很是方便

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)輸入模塊的指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");

3、字符串擴展

一、includes():返回布爾值,表示是否找到了參數字符串。

二、startsWith():返回布爾值,表示參數字符串是否在原字符串的頭部

三、endsWith():返回布爾值,表示參數字符串是否在原字符串的尾部。

四、repeat方法返回一個新字符串,表示將原字符串重複n

五、padStart、padEnd,若是某個字符串不夠指定長度,會在頭部或尾部補全。padStart()用於頭部補全,padEnd()用於尾部補全。

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

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

若是原字符串的長度,等於或大於指定的最小長度,則返回原字符串。

'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

若是省略第二個參數,默認使用空格補全長度。 

'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x

六、模板字符串 `abc${var}`

七、標籤模板  alert`123`

4、數字的擴展

一、0b:二進制;0o:八進制;0x:16進制,Number()轉換成十進制

二、Number.isFinite,Number.isNaN ,它們與傳統的全局方法isFinite()isNaN()的區別在於,傳統方法先調用Number()將非數值的值轉爲數值,再進行判斷,而這兩個新方法只對數值有效,Number.isNaN('aaa') // false,isNaN('aaa') // true ,Number.isFinite('25') // false,isFinite('25') // true

三、Number.parseInt(), Number.parseFloat(),ES6 將全局方法parseInt()parseFloat(),移植到Number對象上面,行爲徹底保持不變。這樣作的目的,是逐步減小全局性方法,使得語言逐步模塊化。

四、Number.isInteger() 用來判斷一個數值是否爲整數,若是參數不是數值,Number.isInteger返回false

五、Number.EPSILON:2 的 -52 次方,JavaScript 可以表示的最小精度。偏差若是小於這個值,就能夠認爲已經沒有意義了,即不存在偏差了

六、安全整數和 Number.isSafeInteger(),avaScript 可以準確表示的整數範圍在-2^532^53之間(不含兩個端點),超過這個範圍,沒法精確表示這個值。ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER這兩個常量,用來表示這個範圍的上下限。Number.isSafeInteger()則是用來判斷一個整數是否落在這個範圍以內。

5、函數的擴展

一、尾調用:是指某個函數的最後一步是調用另外一個函數。

二、尾調用優化:函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。等到B運行結束,將結果返回到AB的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就能夠了。注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」。

三、尾遞歸:遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀,因此永遠不會發生「棧溢出」錯誤。

// 階乘函數

// 非尾遞歸
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

// 尾遞歸
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

四、ES6 的尾調用優化只在嚴格模式下開啓,正常模式是無效的。(ES6 的尾調用優化只在嚴格模式下開啓,正常模式是無效的。)

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000) // 100001

上面代碼中,tco函數是尾遞歸優化的實現,它的奧妙就在於狀態變量active。默認狀況下,這個變量是不激活的。一旦進入尾遞歸優化的過程,這個變量就激活了。而後,每一輪遞歸sum返回的都是undefined,因此就避免了遞歸執行;而accumulated數組存放每一輪sum執行的參數,老是有值的,這就保證了accumulator函數內部的while循環老是會執行。這樣就很巧妙地將「遞歸」改爲了「循環」,然後一輪的參數會取代前一輪的參數,保證了調用棧只有一層。

6、數組的擴展

一、擴展運算符:擴展運算符(spread)是三個點(...),將一個數組轉爲用逗號分隔的參數序列

2.、Aarray.from:Array.from方法用於將兩類對象轉爲真正的數組:相似數組的對象(array-like object)和可遍歷(iterable)的對象(包括 ES6 新增的數據結構 Set 和 Map)。

三、Array.of:將一組值,轉換爲數組

Array.of(3, 11, 8) // [3,11,8]

四、數組實例的 copyWithin:Array.prototype.copyWith(target, start = 0, end = this.length)

五、數組實例的 find() 和 findIndex():數組實例的find方法,用於找出第一個符合條件的數組成員;數組實例的findIndex方法的用法與find方法很是相似,返回第一個符合條件的數組成員的位置,若是全部成員都不符合條件,則返回-1;這兩個方法均可以接受第二個參數,用來綁定回調函數的this對象;另外,這兩個方法均可以發現NaN,彌補了數組的indexOf方法的不足

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

六、 數組實例的 fill() :fill方法使用給定值,填充一個數組

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

 fill方法還能夠接受第二個和第三個參數,用於指定填充的起始位置和結束位置。

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

 七、數組實例的 entries(),keys() 和 values():ES6 提供三個新的方法——entries()keys()values()——用於遍歷數組。它們都返回一個遍歷器對象(Iterator),能夠用for...of循環進行遍歷,惟一的區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()是對鍵值對的遍歷。

八、數組實例的 includes():Array.prototype.includes方法返回一個布爾值,表示某個數組是否包含給定的值;該方法的第二個參數表示搜索的起始位置,默認爲0。若是第二個參數爲負數,則表示倒數的位置,若是這時它大於數組長度(好比第二個參數爲-4,但數組長度爲3),則會重置爲從0開始。

九、數組的空位

Array(3) // [, , ,]

 

上面代碼中,Array(3)返回一個具備 3 個空位的數組。

注意,空位不是undefined,一個位置的值等於undefined,依然是有值的。空位是沒有任何值,in運算符能夠說明這一點。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // fals

 ES6 明確將空位轉爲undefined

Array.from(['a',,'b'])
// [ "a", undefined, "b" ]

6、對象擴展 

一、屬性名錶達式:ES6 容許字面量定義對象時,用表達式做爲對象的屬性名,即把表達式放在方括號內。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

表達式還能夠用於定義方法名

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

二、Object.is() 比較兩個值是否嚴格相等,與嚴格比較運算符(===)的行爲基本一致。不一樣之處只有兩個:一是+0不等於-0,二是NaN等於自身。

三、屬性的可枚舉性和遍歷

(1)可枚舉性:對象的每一個屬性都有一個描述對象(Descriptor),用來控制該屬性的行爲。Object.getOwnPropertyDescriptor方法能夠獲取該屬性的描述對象,ES6 規定,全部 Class 的原型的方法都是不可枚舉的,目前,有四個操做會忽略enumerablefalse的屬性。

  • for...in循環:只遍歷對象自身的和繼承的可枚舉的屬性。
  • Object.keys():返回對象自身的全部可枚舉的屬性的鍵名。
  • JSON.stringify():只串行化對象自身的可枚舉的屬性。
  • Object.assign(): 忽略enumerablefalse的屬性,只拷貝對象自身的可枚舉的屬性。 

(2)屬性的遍歷 :ES6 一共有 5 種方法能夠遍歷對象的屬性。

for...in:for...in循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)。

Object.keys(obj):Object.keys返回一個數組,包括對象自身的(不含繼承的)全部可枚舉屬性(不含 Symbol 屬性)的鍵名。

Object.getOwnPropertyNames(obj):bject.getOwnPropertyNames返回一個數組,包含對象自身的全部屬性(不含 Symbol 屬性,可是包括不可枚舉屬性)的鍵名。

Object.getOwnPropertySymbols(obj):Object.getOwnPropertySymbols返回一個數組,包含對象自身的全部 Symbol 屬性的鍵名。

Reflect.ownKeys(obj):Reflect.ownKeys返回一個數組,包含對象自身的全部鍵名,無論鍵名是 Symbol 或字符串,也無論是否可枚舉。

以上的 5 種方法遍歷對象的鍵名,都遵照一樣的屬性遍歷的次序規則。

  • 首先遍歷全部數值鍵,按照數值升序排列。
  • 其次遍歷全部字符串鍵,按照加入時間升序排列。
  • 最後遍歷全部 Symbol 鍵,按照加入時間升序排列。

四、Object.getOwnPropertyDescriptors() :前面說過,Object.getOwnPropertyDescriptor方法會返回某個對象屬性的描述對象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors方法,返回指定對象全部自身屬性(非繼承屬性)的描述對象。

五、__proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()

六、super:指向當前對象的原型對象

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

 注意,super關鍵字表示原型對象時,只能用在對象的方法之中,用在其餘地方都會報錯。目前,只有對象方法的簡寫法可讓 JavaScript 引擎確認,定義的是對象的方法。

七、Object.keys(),Object.values(),Object.entries()

八、解構賦值:擴展運算符的解構賦值,不能複製繼承自原型對象的屬性。

7、Symbol:ES6 引入了一種新的原始數據類型Symbol,表示獨一無二的值。

注意,Symbol函數前不能使用new命令,不然會報錯。這是由於生成的 Symbol 是一個原始類型的值,不是對象。也就是說,因爲 Symbol 值不是對象,因此不能添加屬性。基本上,它是一種相似於字符串的數據類型。

若是 Symbol 的參數是一個對象,就會調用該對象的toString方法,將其轉爲字符串,而後才生成一個 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
sym // Symbol(abc)

注意,Symbol函數的參數只是表示對當前 Symbol 值的描述,所以相同參數的Symbol函數的返回值是不相等的。

// 沒有參數的狀況
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有參數的狀況
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

 Symbol 值不能與其餘類型的值進行運算,會報錯。可是,Symbol 值能夠顯式轉爲字符串

let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

 另外,Symbol 值也能夠轉爲布爾值,可是不能轉爲數值。

let sym = Symbol();
Boolean(sym) // true
!sym  // false

if (sym) {
  // ...
}

Number(sym) // TypeError
sym + 2 // TypeError

 Symbol 做爲屬性名,該屬性不會出如今for...infor...of循環中,也不會被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。可是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,能夠獲取指定對象的全部 Symbol 屬性名。

一、Symbol.for()、Symbol.keyFor()

有時,咱們但願從新使用同一個 Symbol 值,Symbol.for方法能夠作到這一點。它接受一個字符串做爲參數,而後搜索有沒有以該參數做爲名稱的 Symbol 值。若是有,就返回這個 Symbol 值,不然就新建並返回一個以該字符串爲名稱的 Symbol 值。

Symbol.for()Symbol()這兩種寫法,都會生成新的 Symbol。它們的區別是,前者會被登記在全局環境中供搜索,後者不會。Symbol.for()不會每次調用就返回一個新的 Symbol 類型的值,而是會先檢查給定的key是否已經存在,若是不存在纔會新建一個值。好比,若是你調用Symbol.for("cat")30 次,每次都會返回同一個 Symbol 值,可是調用Symbol("cat")30 次,會返回 30 個不一樣的 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

須要注意的是,Symbol.for爲 Symbol 值登記的名字,是全局環境的,能夠在不一樣的 iframe 或 service worker 中取到同一個值。

iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true

二、內置的Symbol值 :除了定義本身使用的 Symbol 值之外,ES6 還提供了 11 個內置的 Symbol 值,指向語言內部使用的方法。

Symbol.hasInstance § 

對象的Symbol.hasInstance屬性,指向一個內部方法。當其餘對象使用instanceof運算符,判斷是否爲該對象的實例時,會調用這個方法。好比,foo instanceof Foo在語言內部,實際調用的是Foo[Symbol.hasInstance](foo)

8、Set和Map數據結構

一、ES6 提供了新的數據結構 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

二、Set 函數能夠接受一個數組(或者具備 iterable 接口的其餘數據結構)做爲參數,用來初始化。

三、

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

向 Set 加入值的時候,不會發生類型轉換,因此5"5"是兩個不一樣的值。Set 內部判斷兩個值是否不一樣,使用的算法叫作「Same-value-zero equality」,它相似於精確相等運算符(===),主要的區別是NaN等於自身,而精確相等運算符認爲NaN不等於自身。 

四、Set實例的屬性和方法

Set 結構的實例有如下屬性

  • Set.prototype.constructor:構造函數,默認就是Set函數。
  • Set.prototype.size:返回Set實例的成員總數。

Set 實例的方法分爲兩大類:操做方法(用於操做數據)和遍歷方法(用於遍歷成員)。下面先介紹四個操做方法。

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

Set 結構的實例有四個遍歷方法,能夠用於遍歷成員

  • keys():返回鍵名的遍歷器
  • values():返回鍵值的遍歷器
  • entries():返回鍵值對的遍歷器
  • forEach():使用回調函數遍歷每一個成員
  • keys方法、values方法、entries方法返回的都是遍歷器對象(詳見《Iterator 對象》一章)。因爲 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),因此keys方法和values方法的行爲徹底一致。
  • let set = new Set(['red', 'green', 'blue']);
    
    for (let item of set.keys()) {
      console.log(item);
    }
    // red
    // green
    // blue
    
    for (let item of set.values()) {
      console.log(item);
    }
    // red
    // green
    // blue
    
    for (let item of set.entries()) {
      console.log(item);
    }
    // ["red", "red"]
    // ["green", "green"]
    // ["blue", "blue"]

        Set 結構的實例默承認遍歷,它的默認遍歷器生成函數就是它的values方法。

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

這意味着,能夠省略values方法,直接用for...of循環遍歷 Set。

let set = new Set(['red', 'green', 'blue']);

for (let x of set) {
  console.log(x);
}
// red
// green
// blue

forEach() Set 結構的實例與數組同樣,也擁有forEach方法,用於對每一個成員執行某種操做,沒有返回值。

set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

上面代碼說明,forEach方法的參數就是一個處理函數。該函數的參數與數組的forEach一致,依次爲鍵值、鍵名、集合自己(上例省略了該參數)。這裏須要注意,Set 結構的鍵名就是鍵值(二者是同一個值),所以第一個參數與第二個參數的值永遠都是同樣的。

另外,forEach方法還能夠有第二個參數,表示綁定處理函數內部的this對象。

五、Array.from方法能夠將 Set 結構轉爲數組。

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

這就提供了去除數組重複成員的另外一種方法。

function dedupe(array) {
  return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 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]

並且,數組的mapfilter方法也能夠間接用於 Set 了。

let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set結構:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set結構:{2, 4}

所以使用 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}

若是想在遍歷操做中,同步改變原來的 Set 結構,目前沒有直接的方法,但有兩種變通方法。一種是利用原 Set 結構映射出一個新的結構,而後賦值給原來的 Set 結構;另外一種是利用Array.from方法。

// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6

上面代碼提供了兩種方法,直接在遍歷操做中改變原來的 Set 結構。

六、WeakSet

WeakSet 結構與 Set 相似,也是不重複的值的集合。可是,它與 Set 有兩個區別。

首先,WeakSet 的成員只能是對象,而不能是其餘類型的值。其次,WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,若是其餘對象都再也不引用該對象,那麼垃圾回收機制會自動回收該對象所佔用的內存,不考慮該對象還存在於 WeakSet 之中。

這是由於垃圾回收機制依賴引用計數,若是一個值的引用次數不爲0,垃圾回收機制就不會釋放這塊內存。結束使用該值以後,有時會忘記取消引用,致使內存沒法釋放,進而可能會引起內存泄漏。WeakSet 裏面的引用,都不計入垃圾回收機制,因此就不存在這個問題。所以,WeakSet 適合臨時存放一組對象,以及存放跟對象綁定的信息。只要這些對象在外部消失,它在 WeakSet 裏面的引用就會自動消失。

因爲上面這個特色,WeakSet 的成員是不適合引用的,由於它會隨時消失。另外,因爲 WeakSet 內部有多少個成員,取決於垃圾回收機制有沒有運行,運行先後極可能成員個數是不同的,而垃圾回收機制什麼時候運行是不可預測的,所以 ES6 規定 WeakSet 不可遍歷。

WeakSet 沒有size屬性,沒有辦法遍歷它的成員。WeakSet 不能遍歷,是由於成員都是弱引用,隨時可能消失,遍歷機制沒法保證成員的存在,極可能剛剛遍歷結束,成員就取不到了。WeakSet 的一個用處,是儲存 DOM 節點,而不用擔憂這些節點從文檔移除時,會引起內存泄漏。

const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在Foo的實例上調用!');
    }
  }
}

上面代碼保證了Foo的實例方法,只能在Foo的實例上調用。這裏使用 WeakSet 的好處是,foos對實例的引用,不會被計入內存回收機制,因此刪除實例的時候,不用考慮foos,也不會出現內存泄漏。

七、Map

JavaScript 的對象(Object),本質上是鍵值對的集合(Hash 結構),可是傳統上只能用字符串看成鍵。這給它的使用帶來了很大的限制。爲了解決這個問題,ES6 提供了 Map 數據結構。它相似於對象,也是鍵值對的集合,可是「鍵」的範圍不限於字符串,各類類型的值(包括對象)均可以看成鍵。也就是說,Object 結構提供了「字符串—值」的對應,Map 結構提供了「值—值」的對應,是一種更完善的 Hash 結構實現。若是你須要「鍵值對」的數據結構,Map 比 Object 更合適。

做爲構造函數,Map 也能夠接受一個數組做爲參數。該數組的成員是一個個表示鍵值對的數組。

const map = new Map([
  ['name', '張三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"

 Map構造函數接受數組做爲參數,實際上執行的是下面的算法。

const items = [
  ['name', '張三'],
  ['title', 'Author']
];

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);

 事實上,不只僅是數組,任何具備 Iterator 接口、且每一個成員都是一個雙元素的數組的數據結構(詳見《Iterator》一章)均可以看成Map構造函數的參數。這就是說,SetMap均可以用來生成新的 Map。Map 的鍵其實是跟內存地址綁定的,只要內存地址不同,就視爲兩個鍵。這就解決了同名屬性碰撞(clash)的問題,咱們擴展別人的庫的時候,若是使用對象做爲鍵名,就不用擔憂本身的屬性與原做者的屬性同名。

Map 結構的實例有如下屬性和操做方法。

size屬性返回 Map 結構的成員總數。

set(key, value)set方法設置鍵名key對應的鍵值爲value,而後返回整個 Map 結構。若是key已經有值,則鍵值會被更新,不然就新生成該鍵。

const m = new Map();

m.set('edition', 6)        // 鍵是字符串
m.set(262, 'standard')     // 鍵是數值
m.set(undefined, 'nah')    // 鍵是 undefined

set方法返回的是當前的Map對象,所以能夠採用鏈式寫法。

let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

get(key) 

get方法讀取key對應的鍵值,若是找不到key,返回undefined

const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 鍵是函數

m.get(hello)  // Hello ES6!

has(key) as方法返回一個布爾值,表示某個鍵是否在當前 Map 對象之中

const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true

delete(key) delete方法刪除某個鍵,返回true。若是刪除失敗,返回false

const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false

clear() clear方法清除全部成員,沒有返回值。

let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

Map 結構原生提供三個遍歷器生成函數和一個遍歷方法。

  • keys():返回鍵名的遍歷器。
  • values():返回鍵值的遍歷器。
  • entries():返回全部成員的遍歷器。
  • forEach():遍歷 Map 的全部成員。

 須要特別注意的是,Map 的遍歷順序就是插入順序。

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同於使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

上面代碼最後的那個例子,表示 Map 結構的默認遍歷器接口(Symbol.iterator屬性),就是entries方法。

map[Symbol.iterator] === map.entries
// true

 

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']]

結合數組的map方法、filter方法,能夠實現 Map 的遍歷和過濾(Map 自己沒有mapfilter方法)。

const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

const map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 產生 Map 結構 {1 => 'a', 2 => 'b'}

const map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
    );
// 產生 Map 結構 {2 => '_a', 4 => '_b', 6 => '_c'}

此外,Map 還有一個forEach方法,與數組的forEach方法相似,也能夠實現遍歷。

map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});

forEach方法還能夠接受第二個參數,用來綁定this

const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);

上面代碼中,forEach方法的回調函數的this,就指向reporter

 八、Map與其餘數據結構的互相轉換

(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']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }

(3)Map 轉爲對象

若是全部 Map 的鍵都是字符串,它能夠無損地轉爲對象。

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

若是有非字符串的鍵名,那麼這個鍵名會被轉成字符串,再做爲對象的鍵名。

(4)對象轉爲 Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}

(5)Map 轉爲 JSON

Map 轉爲 JSON 要區分兩種狀況。一種狀況是,Map 的鍵名都是字符串,這時能夠選擇轉爲對象 JSON。

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另外一種狀況是,Map 的鍵名有非字符串,這時能夠選擇轉爲數組 JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

(6)JSON 轉爲 Map

JSON 轉爲 Map,正常狀況下,全部鍵名都是字符串。

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

可是,有一種特殊狀況,整個 JSON 就是一個數組,且每一個數組成員自己,又是一個有兩個成員的數組。這時,它能夠一一對應地轉爲 Map。這每每是 Map 轉爲數組 JSON 的逆操做。

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}

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

// WeakMap 能夠使用 set 方法添加成員
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也能夠接受一個數組,
// 做爲構造函數的參數
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

 

WeakMapMap的區別有兩點。

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

WeakMap的設計目的在於,有時咱們想在某個對象上面存放一些數據,可是這會造成對於這個對象的引用。請看下面的例子。

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

上面代碼中,e1e2是兩個對象,咱們經過arr數組對這兩個對象添加一些文字說明。這就造成了arre1e2的引用。

一旦再也不須要這兩個對象,咱們就必須手動刪除這個引用,不然垃圾回收機制就不會釋放e1e2佔用的內存。

// 不須要 e1 和 e2 的時候
// 必須手動刪除引用
arr [0] = null;
arr [1] = null;

上面這樣的寫法顯然很不方便。一旦忘了寫,就會形成內存泄露。WeakMap 就是爲了解決這個問題而誕生的,它的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內。所以,只要所引用的對象的其餘引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦再也不須要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。基本上,若是你要往對象上添加數據,又不想幹擾垃圾回收機制,就能夠使用 WeakMap。一個典型應用場景是,在網頁的 DOM 元素上添加數據,就能夠使用WeakMap結構。當該 DOM 元素被清除,其所對應的WeakMap記錄就會自動被移除。

const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"

 

上面代碼中,先新建一個 Weakmap 實例。而後,將一個 DOM 節點做爲鍵名存入該實例,並將一些附加信息做爲鍵值,一塊兒存放在 WeakMap 裏面。這時,WeakMap 裏面對element的引用就是弱引用,不會被計入垃圾回收機制。

也就是說,上面的 DOM 節點對象的引用計數是1,而不是2。這時,一旦消除對該節點的引用,它佔用的內存就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。

總之,WeakMap的專用場合就是,它的鍵所對應的對象,可能會在未來消失。WeakMap結構有助於防止內存泄漏。

注意,WeakMap 弱引用的只是鍵名,而不是鍵值。鍵值依然是正常引用。

const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}

上面代碼中,鍵值obj是正常引用。因此,即便在 WeakMap 外部消除了obj的引用,WeakMap 內部的引用依然存在。

WeakMap 與 Map 在 API 上的區別主要是兩個,一是沒有遍歷操做(即沒有keys()values()entries()方法),也沒有size屬性。由於沒有辦法列出全部鍵名,某個鍵名是否存在徹底不可預測,跟垃圾回收機制是否運行相關。這一刻能夠取到鍵名,下一刻垃圾回收機制忽然運行了,這個鍵名就沒了,爲了防止出現不肯定性,就統一規定不能取到鍵名。二是沒法清空,即不支持clear方法。所以,WeakMap只有四個方法可用:get()set()has()delete()

const wm = new WeakMap();

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined

WeakMap 的用途

前文說過,WeakMap 應用的典型場合就是 DOM 節點做爲鍵名。下面是一個例子。

let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();

myWeakmap.set(myElement, {timesClicked: 0});

myElement.addEventListener('click', function() {
  let logoData = myWeakmap.get(myElement);
  logoData.timesClicked++;
}, false);

上面代碼中,myElement是一個 DOM 節點,每當發生click事件,就更新一下狀態。咱們將這個狀態做爲鍵值放在 WeakMap 裏,對應的鍵名就是myElement。一旦這個 DOM 節點刪除,該狀態就會自動消失,不存在內存泄漏風險。

 

WeakMap 的另外一個用處是部署私有屬性。

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

const c = new Countdown(2, () => console.log('DONE'));

c.dec()
c.dec()
// DONE

上面代碼中,Countdown類的兩個內部屬性_counter_action,是實例的弱引用,因此若是刪除實例,它們也就隨之消失,不會形成內存泄漏

8、Proxy

Proxy 用於修改某些操做的默認行爲,等同於在語言層面作出修改,因此屬於一種「元編程」(meta programming),即對編程語言進行編程。

Proxy 能夠理解成,在目標對象以前架設一層「攔截」,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裏表示由它來「代理」某些操做,能夠譯爲「代理器」。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

上面代碼對一個空對象架設了一層攔截,重定義了屬性的讀取(get)和設置(set)行爲。這裏暫時先不解釋具體的語法,只看運行結果。對設置了攔截行爲的對象obj,去讀寫它的屬性,就會獲得下面的結果。

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

上面代碼說明,Proxy 實際上重載(overload)了點運算符,即用本身的定義覆蓋了語言的原始定義。

ES6 原生提供 Proxy 構造函數,用來生成 Proxy 實例。

var proxy = new Proxy(target, handler);

Proxy 對象的全部用法,都是上面這種形式,不一樣的只是handler參數的寫法。其中,new Proxy()表示生成一個Proxy實例,target參數表示所要攔截的目標對象,handler參數也是一個對象,用來定製攔截行爲。

下面是另外一個攔截讀取屬性行爲的例子。

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

上面代碼中,做爲構造函數,Proxy接受兩個參數。第一個參數是所要代理的目標對象(上例是一個空對象),即若是沒有Proxy的介入,操做原來要訪問的就是這個對象;第二個參數是一個配置對象,對於每個被代理的操做,須要提供一個對應的處理函數,該函數將攔截對應的操做。好比,上面代碼中,配置對象有一個get方法,用來攔截對目標對象屬性的訪問請求。get方法的兩個參數分別是目標對象和所要訪問的屬性。能夠看到,因爲攔截函數老是返回35,因此訪問任何屬性都獲得35

注意,要使得Proxy起做用,必須針對Proxy實例(上例是proxy對象)進行操做,而不是針對目標對象(上例是空對象)進行操做。

若是handler沒有設置任何攔截,那就等同於直接通向原對象。

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

上面代碼中,handler是一個空對象,沒有任何攔截效果,訪問proxy就等同於訪問target

一個技巧是將 Proxy 對象,設置到object.proxy屬性,從而能夠在object對象上調用。

var object = { proxy: new Proxy(target, handler) };

Proxy 實例也能夠做爲其餘對象的原型對象。

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

let obj = Object.create(proxy);
obj.time // 35

上面代碼中,proxy對象是obj對象的原型,obj對象自己並無time屬性,因此根據原型鏈,會在proxy對象上讀取該屬性,致使被攔截。

同一個攔截器函數,能夠設置攔截多個操做。

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

對於能夠設置、但沒有設置攔截的操做,則直接落在目標對象上,按照原先的方式產生結果。

下面是 Proxy 支持的攔截操做一覽,一共 13 種。

  • get(target, propKey, receiver):攔截對象屬性的讀取,好比proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):攔截對象屬性的設置,好比proxy.foo = vproxy['foo'] = v,返回一個布爾值。
  • has(target, propKey):攔截propKey in proxy的操做,返回一個布爾值。
  • deleteProperty(target, propKey):攔截delete proxy[propKey]的操做,返回一個布爾值。
  • ownKeys(target):攔截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循環,返回一個數組。該方法返回目標對象全部自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
  • getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
  • defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一個布爾值。
  • preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
  • getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
  • isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。
  • setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。若是目標對象是函數,那麼還有兩種額外操做能夠攔截。
  • apply(target, object, args):攔截 Proxy 實例做爲函數調用的操做,好比proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):攔截 Proxy 實例做爲構造函數調用的操做,好比new proxy(...args)

 9、Reflect

Reflect對象與Proxy對象同樣,也是 ES6 爲了操做對象而提供的新 API。Reflect對象的設計目的有這樣幾個。

(1) 將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty),放到Reflect對象上。現階段,某些方法同時在ObjectReflect對象上部署,將來的新方法將只部署在Reflect對象上。也就是說,從Reflect對象上能夠拿到語言內部的方法。

(2) 修改某些Object方法的返回結果,讓其變得更合理。好比,Object.defineProperty(obj, name, desc)在沒法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false

// 老寫法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

(3) 讓Object操做都變成函數行爲。某些Object操做是命令式,好比name in objdelete 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方法將值賦值給對象的屬性,確保完成原有的行爲,而後再部署額外的功能。

下面是另外一個例子。

var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});

上面代碼中,每個Proxy對象的攔截操做(getdeletehas),內部都調用對應的Reflect方法,保證原生行爲可以正常執行。添加的工做,就是將每個操做輸出一行日誌。

有了Reflect對象之後,不少操做會更易讀。

// 老寫法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1

// 新寫法
Reflect.apply(Math.floor, undefined, [1.75]) // 1

實例:使用 Proxy 實現觀察者模式 

觀察者模式(Observer mode)指的是函數自動觀察數據對象,一旦對象有變化,函數就會自動執行。

const person = observable({
  name: '張三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 輸出
// 李四, 20

上面代碼中,數據對象person是觀察目標,函數print是觀察者。一旦數據對象發生變化,print就會自動執行。

下面,使用 Proxy 寫一個觀察者模式的最簡單實現,即實現observableobserve這兩個函數。思路是observable函數返回一個原始對象的 Proxy 代理,攔截賦值操做,觸發充當觀察者的各個函數。

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

上面代碼中,先定義了一個Set集合,全部觀察者函數都放進這個集合。而後,observable函數返回原始對象的代理,攔截賦值操做。攔截函數set之中,會自動執行全部觀察者。

10、Promise

一、Promise 的含義

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。

所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理。

Promise對象有如下兩個特色。

(1)對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是「承諾」,表示其餘手段沒法改變。

(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型)。若是改變已經發生了,你再對Promise對象添加回調函數,也會當即獲得這個結果。這與事件(Event)徹底不一樣,事件的特色是,若是你錯過了它,再去監聽,是得不到結果的。

注意,爲了行文方便,本章後面的resolved統一隻指fulfilled狀態,不包含rejected狀態。

二、基本用法

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

下面代碼創造了一個Promise實例。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 異步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolvereject。它們是兩個函數,由 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對象的簡單例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

上面代碼中,timeout方法返回一個Promise實例,表示一段時間之後纔會發生的結果。過了指定的時間(ms參數)之後,Promise實例的狀態變爲resolved,就會觸發then方法綁定的回調函數。

Promise 新建後就會當即執行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代碼中,Promise 新建後當即執行,因此首先輸出的是Promise。而後,then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行,因此resolved最後輸出。

下面是異步加載圖片的例子。

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

上面代碼中,使用Promise包裝了一個圖片加載的異步操做。若是加載成功,就調用resolve方法,不然就調用reject方法。

下面是一個用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 實例,好比像下面這樣。

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

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

上面代碼中,p1p2都是 Promise 的實例,可是p2resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做。

注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。若是p1的狀態是pending,那麼p2的回調函數就會等待p1的狀態改變;若是p1的狀態已是resolved或者rejected,那麼p2的回調函數將會馬上執行。

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

上面代碼中,p1是一個 Promise,3 秒以後變爲rejectedp2的狀態在 1 秒以後改變,resolve方法返回的是p1。因爲p2返回的是另外一個 Promise,致使p2本身的狀態無效了,由p1的狀態決定p2的狀態。因此,後面的then語句都變成針對後者(p1)。又過了 2 秒,p1變爲rejected,致使觸發catch方法指定的回調函數。

注意,調用resolvereject並不會終結 Promise 的參數函數的執行。

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

上面代碼中,調用resolve(1)之後,後面的console.log(2)仍是會執行,而且會首先打印出來。這是由於當即 resolved 的 Promise 是在本輪事件循環的末尾執行,老是晚於本輪循環的同步任務。

通常來講,調用resolvereject之後,Promise 的使命就完成了,後繼操做應該放到then方法裏面,而不該該直接寫在resolvereject的後面。因此,最好在它們前面加上return語句,這樣就不會有意外。

new Promise((resolve, reject) => {
  return resolve(1);
  // 後面的語句不會執行
  console.log(2);
})

有了Promise對象,就能夠將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操做更加容易。

Promise也有一些缺點。首先,沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。其次,若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。第三,當處於pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。

若是某些事件不斷地反覆發生,通常來講,使用 Stream 模式是比部署Promise更好的選擇。

四、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方法捕獲。

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// 等同於
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));

下面是一個例子。

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

上面代碼中,promise拋出一個錯誤,就被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,再拋出錯誤是無效的。

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

上面代碼中,Promise 在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。由於 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。

Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 處理前面三個Promise產生的錯誤
});

上面代碼中,一共有三個 Promise 對象:一個由getJSON產生,兩個由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方法執行中的錯誤,也更接近同步的寫法(try/catch)。所以,建議老是使用catch方法,而不使用then方法的第二個參數。

跟傳統的try/catch代碼塊不一樣的是,若是沒有使用catch方法指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會報錯,由於x沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

上面代碼中,someAsyncThing函數產生的 Promise 對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示ReferenceError: x is not defined,可是不會退出進程、終止腳本執行,2 秒以後仍是會輸出123。這就是說,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是「Promise 會吃掉錯誤」。

這個腳本放在服務器執行,退出碼就是0(即表示執行成功)。不過,Node 有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤,上面的腳本會觸發這個事件的監聽函數,能夠在監聽函數裏面拋出錯誤。

process.on('unhandledRejection', function (err, p) {
  throw err;
});

上面代碼中,unhandledRejection事件的監聽函數有兩個參數,第一個是錯誤對象,第二個是報錯的 Promise 實例,它能夠用來了解發生錯誤的環境信息。

注意,Node 有計劃在將來廢除unhandledRejection事件。若是 Promise 內部有未捕獲的錯誤,會直接終止進程,而且進程的退出碼不爲 0。

再看下面的例子。

const 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

上面代碼中,Promise 指定在下一輪「事件循環」再拋出錯誤。到了那個時候,Promise 的運行已經結束了,因此這個錯誤是在 Promise 函數體外拋出的,會冒泡到最外層,成了未捕獲的錯誤。

通常老是建議,Promise 對象後面要跟catch方法,這樣能夠處理 Promise 內部發生的錯誤。catch方法返回的仍是一個 Promise 對象,所以後面還能夠接着調用then方法。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會報錯,由於x沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

上面代碼運行完catch方法指定的回調函數,會接着運行後面那個then方法指定的回調函數。若是沒有報錯,則會跳過catch方法。

Promise.resolve()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// carry on

上面的代碼由於沒有報錯,跳過了catch方法,直接執行後面的then方法。此時,要是then方法裏面報錯,就與前面的catch無關了。

catch方法之中,還能再拋出錯誤。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行會報錯,由於x沒有聲明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行會報錯,由於 y 沒有聲明
  y + 2;
}).then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]

上面代碼中,catch方法拋出一個錯誤,由於後面沒有別的catch方法了,致使這個錯誤不會被捕獲,也不會傳遞到外層。若是改寫一下,結果就不同了。

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行會報錯,由於y沒有聲明
  y + 2;
}).catch(function(error) {
  console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

上面代碼中,第二個catch方法用來捕獲前一個catch方法拋出的錯誤。

五、Promise.prototype.finally()

finally方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。該方法是 ES2018 引入標準的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代碼中,無論promise最後的狀態,在執行完thencatch指定的回調函數之後,都會執行finally方法指定的回調函數。

下面是一個例子,服務器使用 Promise 處理請求,而後使用finally方法關掉服務器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回調函數不接受任何參數,這意味着沒有辦法知道,前面的 Promise 狀態究竟是fulfilled仍是rejected。這代表,finally方法裏面的操做,應該是與狀態無關的,不依賴於 Promise 的執行結果。

finally本質上是then方法的特例。

promise
.finally(() => {
  // 語句
});

// 等同於
promise
.then(
  result => {
    // 語句
    return result;
  },
  error => {
    // 語句
    throw error;
  }
);

上面代碼中,若是不使用finally方法,一樣的語句須要爲成功和失敗兩種狀況各寫一次。有了finally方法,則只須要寫一次。

它的實現也很簡單。

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

從上面的實現還能夠看到,finally方法老是會返回原來的值。

// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})

 六、Promise.all()

Promise.all方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.all([p1, p2, p3]);

上面代碼中,Promise.all方法接受一個數組做爲參數,p1p2p3都是 Promise 實例,若是不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。(Promise.all方法的參數能夠不是數組,但必須具備 Iterator 接口,且返回的每一個成員都是 Promise 實例。)

p的狀態由p1p2p3決定,分紅兩種狀況。

(1)只有p1p2p3的狀態都變成fulfilledp的狀態纔會變成fulfilled,此時p1p2p3的返回值組成一個數組,傳遞給p的回調函數。

(2)只要p1p2p3之中有一個被rejectedp的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

下面是一個具體的例子。

// 生成一個Promise對象的數組
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

上面代碼中,promises是包含 6 個 Promise 實例的數組,只有這 6 個實例的狀態都變成fulfilled,或者其中有一個變爲rejected,纔會調用Promise.all方法後面的回調函數。

下面是另外一個例子。

const databasePromise = connectDatabase();

const booksPromise = databasePromise
  .then(findAllBooks);

const userPromise = databasePromise
  .then(getCurrentUser);

Promise.all([
  booksPromise,
  userPromise
])
.then(([books, user]) => pickTopRecommentations(books, user));

上面代碼中,booksPromiseuserPromise是兩個異步操做,只有等到它們的結果都返回了,纔會觸發pickTopRecommentations這個回調函數。

注意,若是做爲參數的 Promise 實例,本身定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('報錯了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]

上面代碼中,p1resolvedp2首先會rejected,可是p2有本身的catch方法,該方法返回的是一個新的 Promise 實例,p2指向的其實是這個實例。該實例執行完catch方法後,也會變成resolved,致使Promise.all()方法參數裏面的兩個實例都會resolved,所以會調用then方法指定的回調函數,而不會調用catch方法指定的回調函數。

若是p2沒有本身的catch方法,就會調用Promise.all()catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('報錯了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了

 七、Promise.race()

Promise.race方法一樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.race([p1, p2, p3]);

上面代碼中,只要p1p2p3之中有一個實例率先改變狀態,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(console.log)
.catch(console.error);

上面代碼中,若是 5 秒以內fetch方法沒法返回結果,變量p的狀態就會變爲rejected,從而觸發catch方法指定的回調函數。

八、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方法。

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)參數不是具備then方法的對象,或根本就不是對象

若是參數是一個原始值,或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved

const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

上面代碼生成一個新的 Promise 對象的實例p。因爲字符串Hello不屬於異步操做(判斷方法是字符串對象不具備 then 方法),返回 Promise 實例的狀態從一輩子成就是resolved,因此回調函數會當即執行。Promise.resolve方法的參數,會同時傳給回調函數。

(4)不帶有任何參數

Promise.resolve方法容許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。

因此,若是但願獲得一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});

上面代碼的變量p就是一個 Promise 對象。

須要注意的是,當即resolve的 Promise 對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時。

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

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

九、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方法不一致。

const thenable = {
  then(resolve, reject) {
    reject('出錯了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true

上面代碼中,Promise.reject方法的參數是一個thenable對象,執行之後,後面catch方法的參數不是reject拋出的「出錯了」這個字符串,而是thenable對象。

十、應用

加載圖片

咱們能夠將圖片的加載寫成一個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;
  });
};

Generator 函數與 Promise 的結合

使用 Generator 函數管理流程,遇到異步操做的時候,一般返回一個Promise對象。

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);

上面代碼的 Generator 函數g之中,有一個異步操做getFoo,它返回的就是一個Promise對象。函數run用來處理這個Promise對象,並調用下一個next方法。

十一、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 庫BluebirdQwhen,早就提供了這個方法。

因爲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代碼塊。

11、Iterator 和 for...of 循環

一、Iterator(遍歷器)的概念

JavaScript 原有的表示「集合」的數據結構,主要是數組(Array)和對象(Object),ES6 又添加了MapSet。這樣就有了四種數據集合,用戶還能夠組合使用它們,定義本身的數據結構,好比數組的成員是MapMap的成員是對象。這樣就須要一種統一的接口機制,來處理全部不一樣的數據結構。

遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。

Iterator 的做用有三個:一是爲各類數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員可以按某種次序排列;三是 ES6 創造了一種新的遍歷命令for...of循環,Iterator 接口主要供for...of消費。

Iterator 的遍歷過程是這樣的。

(1)建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。

(2)第一次調用指針對象的next方法,能夠將指針指向數據結構的第一個成員。

(3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。

(4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。

每一次調用next方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含valuedone兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。

下面是一個模擬next方法返回值的例子。

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};
    }
  };
}

上面代碼定義了一個makeIterator函數,它是一個遍歷器生成函數,做用就是返回一個遍歷器對象。對數組['a', 'b']執行這個函數,就會返回該數組的遍歷器對象(即指針對象)it

指針對象的next方法,用來移動指針。開始時,指針指向數組的開始位置。而後,每次調用next方法,指針就會指向數組的下一個成員。第一次調用,指向a;第二次調用,指向b

next方法返回一個對象,表示當前數據成員的信息。這個對象具備valuedone兩個屬性,value屬性返回當前位置的成員,done屬性是一個布爾值,表示遍歷是否結束,便是否還有必要再一次調用next方法。

總之,調用指針對象的next方法,就能夠遍歷事先給定的數據結構。

對於遍歷器對象來講,done: falsevalue: undefined屬性都是能夠省略的,所以上面的makeIterator函數能夠簡寫成下面的形式。

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++]} :
        {done: true};
    }
  };
}

因爲 Iterator 只是把接口規格加到數據結構之上,因此,遍歷器與它所遍歷的那個數據結構,其實是分開的,徹底能夠寫出沒有對應數據結構的遍歷器對象,或者說用遍歷器對象模擬出數據結構。下面是一個無限運行的遍歷器對象的例子。

var it = idMaker();

it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...

function idMaker() {
  var index = 0;

  return {
    next: function() {
      return {value: index++, done: false};
    }
  };
}

上面的例子中,遍歷器生成函數idMaker,返回一個遍歷器對象(即指針對象)。可是並無對應的數據結構,或者說,遍歷器對象本身描述了一個數據結構出來。

若是使用 TypeScript 的寫法,遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規格能夠描述以下。

interface Iterable {
  [Symbol.iterator]() : Iterator,
}

interface Iterator {
  next(value?: any) : IterationResult,
}

interface IterationResult {
  value: any,
  done: boolean,
}

二、默認 Iterator 接口 § 

Iterator 接口的目的,就是爲全部數據結構,提供了一種統一的訪問機制,即for...of循環(詳見下文)。當使用for...of循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。

一種數據結構只要部署了 Iterator 接口,咱們就稱這種數據結構是「可遍歷的」(iterable)。

ES6 規定,默認的 Iterator 接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是「可遍歷的」(iterable)。Symbol.iterator屬性自己是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。至於屬性名Symbol.iterator,它是一個表達式,返回Symbol對象的iterator屬性,這是一個預約義好的、類型爲 Symbol 的特殊值,因此要放在方括號內(參見《Symbol》一章)。

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};

上面代碼中,對象obj是可遍歷的(iterable),由於具備Symbol.iterator屬性。執行這個屬性,會返回一個遍歷器對象。該對象的根本特徵就是具備next方法。每次調用next方法,都會返回一個表明當前成員的信息對象,具備valuedone兩個屬性。

ES6 的有些數據結構原生具有 Iterator 接口(好比數組),即不用任何處理,就能夠被for...of循環遍歷。緣由在於,這些數據結構原生部署了Symbol.iterator屬性(詳見下文),另一些數據結構沒有(好比對象)。凡是部署了Symbol.iterator屬性的數據結構,就稱爲部署了遍歷器接口。調用這個接口,就會返回一個遍歷器對象。

原生具有 Iterator 接口的數據結構以下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數的 arguments 對象
  • NodeList 對象

下面的例子是數組的Symbol.iterator屬性。

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

上面代碼中,變量arr是一個數組,原生就具備遍歷器接口,部署在arrSymbol.iterator屬性上面。因此,調用這個屬性,就獲得遍歷器對象。

對於原生部署 Iterator 接口的數據結構,不用本身寫遍歷器生成函數,for...of循環會自動遍歷它們。除此以外,其餘數據結構(主要是對象)的 Iterator 接口,都須要本身在Symbol.iterator屬性上面部署,這樣纔會被for...of循環遍歷。

對象(Object)之因此沒有默認部署 Iterator 接口,是由於對象的哪一個屬性先遍歷,哪一個屬性後遍歷是不肯定的,須要開發者手動指定。本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口並非很必要,由於這時對象實際上被看成 Map 結構使用,ES5 沒有 Map 結構,而 ES6 原生提供了。

一個對象若是要具有可被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屬性對應一個函數,執行後返回當前對象的遍歷器對象。

下面是經過遍歷器實現指針結構的例子。

function Obj(value) {
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
  var iterator = { next: next };

  var current = this;

  function next() {
    if (current) {
      var value = current.value;
      current = current.next;
      return { done: false, value: value };
    } else {
      return { done: true };
    }
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;

for (var i of one){
  console.log(i); // 1, 2, 3
}

上面代碼首先在構造函數的原型鏈上部署Symbol.iterator方法,調用該方法會返回遍歷器對象iterator,調用該對象的next方法,在返回一個值的同時,自動將內部指針移到下一個實例。

下面是另外一個爲對象添加 Iterator 接口的例子。

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

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

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

[...document.querySelectorAll('div')] // 能夠執行了

NodeList 對象是相似數組的對象,原本就具備遍歷接口,能夠直接遍歷。上面代碼中,咱們將它的遍歷接口改爲數組的Symbol.iterator屬性,能夠看到沒有任何影響。

下面是另外一個相似數組的對象調用數組的Symbol.iterator方法的例子。

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

注意,普通對象部署數組的Symbol.iterator方法,並沒有效果。

let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}

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

var obj = {};

obj[Symbol.iterator] = () => 1;

[...obj] // TypeError: [] is not a function

上面代碼中,變量objSymbol.iterator方法對應的不是遍歷器生成函數,所以報錯。

有了遍歷器接口,數據結構就能夠用for...of循環遍歷(詳見下文),也能夠使用while循環遍歷。

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  var x = $result.value;
  // ...
  $result = $iterator.next();
}

上面代碼中,ITERABLE表明某種可遍歷的數據結構,$iterator是它的遍歷器對象。遍歷器對象每次移動指針(next方法),都檢查一下返回值的done屬性,若是遍歷還沒結束,就移動遍歷器對象的指針到下一步(next方法),不斷循環。

三、調用 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()

 四、字符串的 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方法,達到修改遍歷器行爲的目的。

var str = new String("hi");

[...str] // ["h", "i"]

str[Symbol.iterator] = function() {
  return {
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

[...str] // ["bye"]
str // "hi"

上面代碼中,字符串 str 的Symbol.iterator方法被修改了,因此擴展運算符(...)返回的值變成了bye,而字符串自己仍是hi

五、Iterator 接口與 Generator 函數 § 

Symbol.iterator方法的最簡單實現,仍是使用下一章要介紹的 Generator 函數。

let myIterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
  }
}
[...myIterable] // [1, 2, 3]

// 或者採用下面的簡潔寫法

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// "hello"
// "world"

上面代碼中,Symbol.iterator方法幾乎不用部署任何代碼,只要用 yield 命令給出每一步的返回值便可。

六、遍歷器對象的 return(),throw()

遍歷器對象除了具備next方法,還能夠具備return方法和throw方法。若是你本身寫遍歷器對象生成函數,那麼next方法是必須部署的,return方法和throw方法是否部署是可選的。

return方法的使用場合是,若是for...of循環提早退出(一般是由於出錯,或者有break語句),就會調用return方法。若是一個對象在完成遍歷前,須要清理或釋放資源,就能夠部署return方法。

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      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);
  throw new Error();
}

上面代碼中,狀況一輸出文件的第一行之後,就會執行return方法,關閉這個文件;狀況二會在執行return方法關閉文件以後,再拋出錯誤。

注意,return方法必須返回一個對象,這是 Generator 規格決定的。

throw方法主要是配合 Generator 函數使用,通常的遍歷器對象用不到這個方法。請參閱《Generator 函數》一章。

七、for...of 循環

ES6 借鑑 C++、Java、C# 和 Python 語言,引入了for...of循環,做爲遍歷全部數據結構的統一的方法。

一個數據結構只要部署了Symbol.iterator屬性,就被視爲具備 iterator 接口,就能夠用for...of循環遍歷它的成員。也就是說,for...of循環內部調用的是數據結構的Symbol.iterator方法。

for...of循環能夠使用的範圍包括數組、Set 和 Map 結構、某些相似數組的對象(好比arguments對象、DOM NodeList 對象)、後文的 Generator 對象,以及字符串。

數組

數組原生具有iterator接口(即默認部署了Symbol.iterator屬性),for...of循環本質上就是調用這個接口產生的遍歷器,能夠用下面的代碼證實。

const arr = ['red', 'green', 'blue'];

for(let v of arr) {
  console.log(v); // red green blue
}

const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for(let v of obj) {
  console.log(v); // red green blue
}

上面代碼中,空對象obj部署了數組arrSymbol.iterator屬性,結果objfor...of循環,產生了與arr徹底同樣的結果。

for...of循環能夠代替數組實例的forEach方法。

const arr = ['red', 'green', 'blue'];

arr.forEach(function (element, index) {
  console.log(element); // red green blue
  console.log(index);   // 0 1 2
});

JavaScript 原有的for...in循環,只能得到對象的鍵名,不能直接獲取鍵值。ES6 提供for...of循環,容許遍歷得到鍵值。

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

上面代碼代表,for...in循環讀取鍵名,for...of循環讀取鍵值。若是要經過for...of循環,獲取數組的索引,能夠藉助數組實例的entries方法和keys方法(參見《數組的擴展》一章)。

for...of循環調用遍歷器接口,數組的遍歷器接口只返回具備數字索引的屬性。這一點跟for...in循環也不同。

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循環不會返回數組arrfoo屬性。

Set 和 Map 結構

Set 和 Map 結構也原生具備 Iterator 接口,能夠直接使用for...of循環。

var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit

var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

上面代碼演示瞭如何遍歷 Set 結構和 Map 結構。值得注意的地方有兩個,首先,遍歷的順序是按照各個成員被添加進數據結構的順序。其次,Set 結構遍歷時,返回的是一個值,而 Map 結構遍歷時,返回的是一個數組,該數組的兩個成員分別爲當前 Map 成員的鍵名和鍵值。

let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
  console.log(pair);
}
// ['a', 1]
// ['b', 2]

for (let [key, value] of map) {
  console.log(key + ' : ' + value);
}
// a : 1
// b : 2

計算生成的數據結構

有些數據結構是在現有數據結構的基礎上,計算生成的。好比,ES6 的數組、Set、Map 都部署瞭如下三個方法,調用後都返回遍歷器對象。

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

這三個方法調用後生成的遍歷器對象,所遍歷的都是計算生成的數據結構。

let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
  console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']

相似數組的對象

相似數組的對象包括好幾類。下面是for...of循環用於字符串、DOM NodeList 對象、arguments對象的例子。

// 字符串
let str = "hello";

for (let s of str) {
  console.log(s); // h e l l o
}

// DOM NodeList對象
let paras = document.querySelectorAll("p");

for (let p of paras) {
  p.classList.add("test");
}

// arguments對象
function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs('a', 'b');
// 'a'
// 'b'

對於字符串來講,for...of循環還有一個特色,就是會正確識別 32 位 UTF-16 字符。

for (let x of 'a\uD83D\uDC0A') {
  console.log(x);
}
// 'a'
// '\uD83D\uDC0A'

並非全部相似數組的對象都具備 Iterator 接口,一個簡便的解決方法,就是使用Array.from方法將其轉爲數組。

let arrayLike = { length: 2, 0: 'a', 1: 'b' };

// 報錯
for (let x of arrayLike) {
  console.log(x);
}

// 正確
for (let x of Array.from(arrayLike)) {
  console.log(x);
}

對象

對於普通的對象,for...of結構不能直接使用,會報錯,必須部署了 Iterator 接口後才能使用。可是,這樣狀況下,for...in循環依然能夠用來遍歷鍵名。

let es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (let e of es6) {
  console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function

上面代碼表示,對於普通的對象,for...in循環能夠遍歷鍵名,for...of循環會報錯。

一種解決方法是,使用Object.keys方法將對象的鍵名生成一個數組,而後遍歷這個數組。

for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

另外一個方法是使用 Generator 函數將對象從新包裝一下。

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

與其餘遍歷語法的比較

以數組爲例,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命令都不能奏效。

for...in循環能夠遍歷數組的鍵名。

for (var index in myArray) {
  console.log(myArray[index]);
}

for...in循環有幾個缺點。

  • 數組的鍵名是數字,可是for...in循環是以字符串做爲鍵名「0」、「1」、「2」等等。
  • for...in循環不只遍歷數字鍵名,還會遍歷手動添加的其餘鍵,甚至包括原型鏈上的鍵。
  • 某些狀況下,for...in循環會以任意順序遍歷鍵名。

總之,for...in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。

for...of循環相比上面幾種作法,有一些顯著的優勢。

for (let value of myArray) {
  console.log(value);
}
  • 有着同for...in同樣的簡潔語法,可是沒有for...in那些缺點。
  • 不一樣於forEach方法,它能夠與breakcontinuereturn配合使用。
  • 提供了遍歷全部數據結構的統一操做接口。

下面是一個使用 break 語句,跳出for...of循環的例子。

for (var n of fibonacci) {
  if (n > 1000)
    break;
  console.log(n);
}

上面的例子,會輸出斐波納契數列小於等於 1000 的項。若是當前項大於 1000,就會使用break語句跳出for...of循環。

12、Generator 函數的語法

一、

簡介

基本概念

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數徹底不一樣。本章詳細介紹 Generator 函數的語法和 API,它的異步編程應用請看《Generator 函數的異步應用》一章。

Generator 函數有多種理解角度。語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。

執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。

形式上,Generator 函數是一個普通函數,可是有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用yield表達式,定義不一樣的內部狀態(yield在英語裏的意思就是「產出」)。

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

var hw = helloWorldGenerator();

上面代碼定義了一個 Generator 函數helloWorldGenerator,它內部有兩個yield表達式(helloworld),即該函數有三個狀態:hello,world 和 return 語句(結束執行)。

而後,Generator 函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。

下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法能夠恢復執行。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面代碼一共調用了四次next方法。

第一次調用,Generator 函數開始執行,直到遇到第一個yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hellodone屬性的值false,表示遍歷尚未結束。

第二次調用,Generator 函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值worlddone屬性的值false,表示遍歷尚未結束。

第三次調用,Generator 函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句後面的表達式的值(若是沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。

第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefineddone屬性爲true。之後再調用next方法,返回的都是這個值。

總結一下,調用 Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有着valuedone兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。

ES6 沒有規定,function關鍵字與函數名之間的星號,寫在哪一個位置。這致使下面的寫法都能經過。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

因爲 Generator 函數仍然是普通函數,因此通常的寫法是上面的第三種,即星號緊跟在function關鍵字後面。本書也採用這種寫法。

yield 表達式

因爲 Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield表達式就是暫停標誌。

遍歷器對象的next方法的運行邏輯以下。

(1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。

(3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。

(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined

須要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。

function* gen() {
  yield  123 + 456;
}

上面代碼中,yield後面的表達式123 + 456,不會當即求值,只會在next方法將指針移到這一句時,纔會求值。

yield表達式與return語句既有類似之處,也有區別。類似之處在於,都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。一個函數裏面,只能執行一次(或者說一個)return語句,可是能夠執行屢次(或者說多個)yield表達式。正常函數只能返回一個值,由於只能執行一次return;Generator 函數能夠返回一系列的值,由於能夠有任意多個yield。從另外一個角度看,也能夠說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是「生成器」的意思)。

Generator 函數能夠不用yield表達式,這時就變成了一個單純的暫緩執行函數。

function* f() {
  console.log('執行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

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

另外須要注意,yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

上面代碼在一個普通函數中使用yield表達式,結果產生一個句法錯誤。

下面是另一個例子。

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};

for (var f of flat(arr)){
  console.log(f);
}

上面代碼也會產生句法錯誤,由於forEach方法的參數是一個普通函數,可是在裏面使用了yield表達式(這個函數裏面還使用了yield*表達式,詳細介紹見後文)。一種修改方法是改用for循環。

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== 'number') {
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)) {
  console.log(f);
}
// 1, 2, 3, 4, 5, 6

另外,yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

yield表達式用做函數參數或放在賦值表達式的右邊,能夠不加括號。

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

與 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屬性,也是一個遍歷器對象生成函數,執行後返回它本身。

next 方法的參數

yield表達式自己沒有返回值,或者說老是返回undefinednext方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值。

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開始遞增。

這個功能有很重要的語法意義。Generator 函數從暫停狀態到恢復運行,它的上下文狀態(context)是不變的。經過next方法的參數,就有辦法在 Generator 函數開始運行以後,繼續向函數體內部注入值。也就是說,能夠在 Generator 函數運行的不一樣階段,從外部向內部注入不一樣的值,從而調整函數行爲。

再看一個例子。

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方法提供參數,返回結果就徹底不同了。上面代碼第一次調用bnext方法時,返回x+1的值6;第二次調用next方法,將上一次yield表達式的值設爲12,所以y等於24,返回y / 3的值8;第三次調用next方法,將上一次yield表達式的值設爲13,所以z等於13,這時x等於5y等於24,因此return語句的值等於42

注意,因爲next方法的參數表示上一個yield表達式的返回值,因此在第一次使用next方法時,傳遞參數是無效的。V8 引擎直接忽略第一次使用next方法時的參數,只有從第二次使用next方法開始,參數纔是有效的。從語義上講,第一個next方法用來啓動遍歷器對象,因此不用帶有參數。

再看一個經過next方法的參數,向 Generator 函數內部輸入值的例子。

function* dataConsumer() {
  console.log('Started');
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b

上面代碼是一個很直觀的例子,每次經過next方法向 Generator 函數輸入值,而後打印出來。

若是想要第一次調用next方法時,就可以輸入值,能夠在 Generator 函數外面再包一層。

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return 'DONE';
});

wrapped().next('hello!')
// First input: hello!

上面代碼中,Generator 函數若是不用wrapper先包一層,是沒法第一次調用next方法,就輸入參數的。

三、for...of 循環 § 

for...of循環能夠自動遍歷 Generator 函數時生成的Iterator對象,且此時再也不須要調用next方法。

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

上面代碼使用for...of循環,依次顯示 5 個yield表達式的值。這裏須要注意,一旦next方法的返回對象的done屬性爲truefor...of循環就會停止,且不包含該返回對象,因此上面代碼的return語句返回的6,不包括在for...of循環之中。

下面是一個利用 Generator 函數和for...of循環,實現斐波那契數列的例子。

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

從上面代碼可見,使用for...of語句時不須要使用next方法。

利用for...of循環,能夠寫出遍歷任意對象(object)的方法。原生的 JavaScript 對象沒有遍歷接口,沒法使用for...of循環,經過 Generator 函數爲它加上這個接口,就能夠用了。

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

上面代碼中,對象jane原生不具有 Iterator 接口,沒法用for...of遍歷。這時,咱們經過 Generator 函數objectEntries爲它加上遍歷器接口,就能夠用for...of遍歷了。加上遍歷器接口的另外一種寫法是,將 Generator 函數加到對象的Symbol.iterator屬性上面。

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for...of循環之外,擴展運算符(...)、解構賦值和Array.from方法內部調用的,都是遍歷器接口。這意味着,它們均可以將 Generator 函數返回的 Iterator 對象,做爲參數。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 擴展運算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解構賦值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循環
for (let n of numbers()) {
  console.log(n)
}
// 1

四、Generator.prototype.throw() § 

Generator 函數返回的遍歷器對象,都有一個throw方法,能夠在函數體外拋出錯誤,而後在 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對象的實例。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
i.next();
i.throw(new Error('出錯了!'));
// Error: 出錯了!(…)

注意,不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。後者只能被函數體外的catch語句捕獲。

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('內部捕獲', e);
    }
  }
};

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

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]

上面代碼之因此只捕獲了a,是由於函數體外的catch語句塊,捕獲了拋出的a錯誤之後,就不會再繼續try代碼塊裏面剩餘的語句了。

若是 Generator 函數內部沒有部署try...catch代碼塊,那麼throw方法拋出的錯誤,將被外部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

上面代碼中,Generator 函數g內部沒有部署try...catch代碼塊,因此拋出的錯誤直接被外部catch代碼塊捕獲。

若是 Generator 函數內部和外部,都沒有部署try...catch代碼塊,那麼程序將報錯,直接中斷執行。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

上面代碼中,g.throw拋出錯誤之後,沒有任何try...catch代碼塊能夠捕獲這個錯誤,致使程序報錯,中斷執行。

throw方法拋出的錯誤要被內部捕獲,前提是必須至少執行過一次next方法。

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log('內部捕獲');
  }
}

var g = gen();
g.throw(1);
// Uncaught 1

上面代碼中,g.throw(1)執行時,next方法一次都沒有執行過。這時,拋出的錯誤不會被內部捕獲,而是直接在外部拋出,致使程序出錯。這種行爲其實很好理解,由於第一次執行next方法,等同於啓動執行 Generator 函數的內部代碼,不然 Generator 函數尚未開始執行,這時throw方法拋錯只可能拋出在函數外部。

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

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

上面代碼中,g.throw方法被捕獲之後,自動執行了一次next方法,因此會打印b。另外,也能夠看到,只要 Generator 函數內部部署了try...catch代碼塊,那麼遍歷器的throw方法拋出的錯誤,不影響下一次遍歷。

另外,throw命令與g.throw方法是無關的,二者互不影響。

var gen = function* gen(){
  yield console.log('hello');
  yield console.log('world');
}

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

try {
  throw new Error();
} catch (e) {
  g.next();
}
// hello
// world

上面代碼中,throw命令拋出的錯誤不會影響到遍歷器的狀態,因此兩次執行next方法,都進行了正確的操做。

這種函數體內捕獲錯誤的機制,大大方便了對錯誤的處理。多個yield表達式,能夠只用一個try...catch代碼塊來捕獲錯誤。若是使用回調函數的寫法,想要捕獲多個錯誤,就不得不爲每一個函數內部寫一個錯誤處理語句,如今只在 Generator 函數內部寫一次catch語句就能夠了。

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

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42);
} catch (err) {
  console.log(err);
}

上面代碼中,第二個next方法向函數體內傳入一個參數 42,數值是沒有toUpperCase方法的,因此會拋出一個 TypeError 錯誤,被函數體外的catch捕獲。

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

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done

上面代碼一共三次運行next方法,第二次運行的時候會拋出錯誤,而後第三次運行的時候,Generator 函數就已經結束了,再也不執行下去了。

 五、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方法後,返回值的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() // { value: undefined, done: true }

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

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

上面代碼中,調用return方法後,就開始執行finally代碼塊,而後等到finally代碼塊執行完,再執行return方法。

六、next()、throw()、return() 的共同點 § 

next()throw()return()這三個方法本質上是同一件事,能夠放在一塊兒理解。它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield表達式。

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

throw()是將yield表達式替換成一個throw語句。

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 至關於將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));

return()是將yield表達式替換成一個return語句。

gen.return(2); // Object {value: 2, done: true}
// 至關於將 let result = yield x + y
// 替換成 let result = return 2;

七、yield* 表達式

若是在 Generator 函數內部,調用另外一個 Generator 函數,默認狀況下是沒有效果的。

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

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

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

上面代碼中,foobar都是 Generator 函數,在bar裏面調用foo,是不會有效果的。

這個就須要用到yield*表達式,用來在一個 Generator 函數裏面執行另外一個 Generator 函數。

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"

再來看一個對比的例子。

function* inner() {
  yield 'hello!';
}

function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}

var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"

function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

上面例子中,outer2使用了yield*outer1沒使用。結果就是,outer1返回一個遍歷器對象,outer2返回該遍歷器對象的內部值。

從語法角度看,若是yield表達式後面跟的是一個遍歷器對象,須要在yield表達式後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield*表達式。

let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

上面代碼中,delegatingIterator是代理者,delegatedIterator是被代理者。因爲yield* delegatedIterator語句獲得的值,是一個遍歷器,因此要用星號表示。運行結果就是使用一個遍歷器,遍歷了多個 Generator 函數,有遞歸的效果。

yield*後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for...of循環。

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

// 等同於

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

上面代碼說明,yield*後面的 Generator 函數(沒有return語句時),不過是for...of的一種簡寫形式,徹底能夠用後者替代前者。反之,在有return語句時,則須要用var value = yield* iterator的形式獲取return語句的值。

若是yield*後面跟着一個數組,因爲數組原生支持遍歷器,所以就會遍歷數組成員。

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

上面代碼中,yield命令後面若是不加星號,返回的是整個數組,加了星號就表示返回的是數組的遍歷器對象。

實際上,任何數據結構只要有 Iterator 接口,就能夠被yield*遍歷。

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

上面代碼中,yield表達式返回整個字符串,yield*語句返回單個字符。由於字符串具備 Iterator 接口,因此被yield*遍歷。

若是被代理的 Generator 函數有return語句,那麼就能夠向代理它的 Generator 函數返回數據。

function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

上面代碼在第四次調用next方法的時候,屏幕上會有輸出,這是由於函數fooreturn語句,向函數bar提供了返回值。

再看一個例子。

function* genFuncWithReturn() {
  yield 'a';
  yield 'b';
  return 'The result';
}
function* logReturned(genObj) {
  let result = yield* genObj;
  console.log(result);
}

[...logReturned(genFuncWithReturn())]
// The result
// 值爲 [ 'a', 'b' ]

上面代碼中,存在兩次遍歷。第一次是擴展運算符遍歷函數logReturned返回的遍歷器對象,第二次是yield*語句遍歷函數genFuncWithReturn返回的遍歷器對象。這兩次遍歷的效果是疊加的,最終表現爲擴展運算符遍歷函數genFuncWithReturn返回的遍歷器對象。因此,最後的數據表達式獲得的值等於[ 'a', 'b' ]。可是,函數genFuncWithReturnreturn語句的返回值The result,會返回給函數logReturned內部的result變量,所以會有終端輸出。

yield*命令能夠很方便地取出嵌套數組的全部成員。

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

下面是一個稍微複雜的例子,使用yield*語句遍歷徹底二叉樹。

// 下面是二叉樹的構造函數,
// 三個參數分別是左樹、當前節點和右樹
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// 下面是中序(inorder)遍歷函數。
// 因爲返回的是一個遍歷器,因此要用generator函數。
// 函數體內採用遞歸算法,因此左樹和右樹要用yield*遍歷
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// 下面生成二叉樹
function make(array) {
  // 判斷是否爲葉節點
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);

// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

 八、做爲對象屬性的 Generator 函數

若是一個對象的屬性是 Generator 函數,能夠簡寫成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

上面代碼中,myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個 Generator 函數。

它的完整形式以下,與上面的寫法是等價的。

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

 九、Generator 函數的this

Generator 函數老是返回一個遍歷器,ES6 規定這個遍歷器是 Generator 函數的實例,也繼承了 Generator 函數的prototype對象上的方法。

function* g() {}

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

let obj = g();

obj instanceof g // true
obj.hello() // 'hi!'

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

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

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

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

Generator 函數也不能跟new命令一塊兒用,會報錯。

function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

上面代碼中,new命令跟構造函數F一塊兒使用,結果報錯,由於F不是構造函數。

那麼,有沒有辦法讓 Generator 函數返回一個正常的對象實例,既能夠用next方法,又能夠得到正常的this

下面是一個變通方法。首先,生成一個空對象,使用call方法綁定 Generator 函數內部的this。這樣,構造函數調用之後,這個空對象就是 Generator 函數的實例對象了。

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代碼中,首先是F內部的this對象綁定obj對象,而後調用它,返回一個 Iterator 對象。這個對象執行三次next方法(由於F內部有兩個yield表達式),完成 F 內部全部代碼的運行。這時,全部內部屬性都綁定在obj對象上了,所以obj對象也就成了F的實例。

上面代碼中,執行的是遍歷器對象f,可是生成的對象實例是obj,有沒有辦法將這兩個對象統一呢?

一個辦法就是將obj換成F.prototype

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

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改爲構造函數,就能夠對它執行new命令了。

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

function F() {
  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

十、含義 

Generator 與狀態機

Generator 是實現狀態機的最佳結構。好比,下面的clock函數就是一個狀態機。

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

上面代碼的clock函數一共有兩種狀態(TickTock),每運行一次,就改變一次狀態。這個函數若是用 Generator 實現,就是下面這樣。

var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

上面的 Generator 實現與 ES5 實現對比,能夠看到少了用來保存狀態的外部變量ticking,這樣就更簡潔,更安全(狀態不會被非法篡改)、更符合函數式編程的思想,在寫法上也更優雅。Generator 之因此能夠不用外部變量保存狀態,是由於它自己就包含了一個狀態信息,即目前是否處於暫停態。

Generator 與協程

協程(coroutine)是一種程序運行的方式,能夠理解成「協做的線程」或「協做的函數」。協程既能夠用單線程實現,也能夠用多線程實現。前者是一種特殊的子例程,後者是一種特殊的線程。

(1)協程與子例程的差別

傳統的「子例程」(subroutine)採用堆棧式「後進先出」的執行方式,只有當調用的子函數徹底執行完畢,纔會結束執行父函數。協程與其不一樣,多個線程(單線程狀況下,即多個函數)能夠並行執行,可是隻有一個線程(或函數)處於正在運行的狀態,其餘線程(或函數)都處於暫停態(suspended),線程(或函數)之間能夠交換執行權。也就是說,一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程。

從實現上看,在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代價,實現多任務的並行。

(2)協程與普通線程的差別

不難看出,協程適合用於多任務運行的環境。在這個意義上,它與普通的線程很類似,都有本身的執行上下文、能夠分享全局變量。它們的不一樣之處在於,同一時間能夠有多個線程處於運行狀態,可是運行的協程只能有一個,其餘協程都處於暫停狀態。此外,普通的線程是搶先式的,到底哪一個線程優先獲得資源,必須由運行環境決定,可是協程是合做式的,執行權由協程本身分配。

因爲 JavaScript 是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。

Generator 函數是 ES6 對協程的實現,但屬於不徹底實現。Generator 函數被稱爲「半協程」(semi-coroutine),意思是隻有 Generator 函數的調用者,才能將程序的執行權還給 Generator 函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。

若是將 Generator 函數看成協程,徹底能夠將多個須要互相協做的任務寫成 Generator 函數,它們之間使用yield表達式交換控制權。

Generator 與上下文 § 

JavaScript 代碼運行時,會產生一個全局的上下文環境(context,又稱運行環境),包含了當前全部的變量和對象。而後,執行函數(或塊級代碼)的時候,又會在當前上下文環境的上層,產生一個函數運行的上下文,變成當前(active)的上下文,由此造成一個上下文環境的堆棧(context stack)。

這個堆棧是「後進先出」的數據結構,最後產生的上下文環境首先執行完成,退出堆棧,而後再執行完成它下層的上下文,直至全部代碼執行完成,堆棧清空。

Generator 函數不是這樣,它執行產生的上下文環境,一旦遇到yield命令,就會暫時退出堆棧,可是並不消失,裏面的全部變量和對象會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會從新加入調用棧,凍結的變量和對象恢復執行。

function* gen() {
  yield 1;
  return 2;
}

let g = gen();

console.log(
  g.next().value,
  g.next().value,
);

上面代碼中,第一次執行g.next()時,Generator 函數gen的上下文會加入堆棧,即開始運行gen內部的代碼。等遇到yield 1時,gen上下文退出堆棧,內部狀態凍結。第二次執行g.next()時,gen上下文從新加入堆棧,變成當前的上下文,從新恢復執行。

十一、應用

Generator 能夠暫停函數執行,返回任意表達式的值。這種特色使得 Generator 有多種應用場景。

(1)異步操做的同步化表達

Generator 函數的暫停執行的效果,意味着能夠把異步操做寫在yield表達式裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了,由於異步操做的後續操做能夠放在yield表達式下面,反正要等到調用next方法時再執行。因此,Generator 函數的一個重要實際意義就是用來處理異步操做,改寫回調函數。

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// 加載UI
loader.next()

// 卸載UI
loader.next()

上面代碼中,第一次調用loadUI函數時,該函數不會執行,僅返回一個遍歷器。下一次對該遍歷器調用next方法,則會顯示Loading界面(showLoadingScreen),而且異步加載數據(loadUIDataAsynchronously)。等到數據加載完成,再一次使用next方法,則會隱藏Loading界面。能夠看到,這種寫法的好處是全部Loading界面的邏輯,都被封裝在一個函數,循序漸進很是清晰。

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

下面是另外一個例子,經過 Generator 函數逐行讀取文本文件。

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

上面代碼打開文本文件,使用yield表達式能夠手動逐行讀取文件。

(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
  }
}

而後,使用一個函數,按次序自動執行全部步驟。

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 若是Generator函數未結束,就繼續調用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

注意,上面這種作法,只適合同步操做,即全部的task都必須是同步的,不能有異步操做。由於這裏的代碼一獲得返回值,就繼續往下執行,沒有判斷異步操做什麼時候完成。若是要控制異步的操做流程,詳見後面的《異步操做》一章。

下面,利用for...of循環會自動依次執行yield命令的特性,提供一種更通常的控制流管理的方法。

let steps = [step1Func, step2Func, step3Func];

function* iterateSteps(steps){
  for (var i=0; i< steps.length; i++){
    var step = steps[i];
    yield step();
  }
}

上面代碼中,數組steps封裝了一個任務的多個步驟,Generator 函數iterateSteps則是依次爲這些步驟加上yield命令。

將任務分解成步驟以後,還能夠將項目分解成多個依次執行的任務。

let jobs = [job1, job2, job3];

function* iterateJobs(jobs){
  for (var i=0; i< jobs.length; i++){
    var job = jobs[i];
    yield* iterateSteps(job.steps);
  }
}

上面代碼中,數組jobs封裝了一個項目的多個任務,Generator 函數iterateJobs則是依次爲這些任務加上yield*命令。

最後,就能夠用for...of循環一次性依次執行全部任務的全部步驟。

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

再次提醒,上面的作法只能用於全部步驟都是同步操做的狀況,不能有異步操做的步驟。若是想要依次執行異步的步驟,必須使用後面的《異步操做》一章介紹的方法。

for...of的本質是一個while循環,因此上面的代碼實質上執行的是下面的邏輯。

var it = iterateJobs(jobs);
var res = it.next();

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

(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方法。

下面是一個對數組部署 Iterator 接口的例子,儘管數組原生具備這個接口。

function* makeSimpleGenerator(array){
  var nextIndex = 0;

  while(nextIndex < array.length){
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(['yo', 'ya']);

gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done  // true

 

(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 函數,致使能夠像處理數組那樣,處理這三個返回的函數。

for (task of doStuff()) {
  // task是一個函數,能夠像回調函數那樣使用它
}

實際上,若是用 ES5 表達,徹底能夠用數組模擬 Generator 的這種用法。

function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

上面的函數,能夠用如出一轍的for...of循環處理!兩相一比較,就不難看出 Generator 使得數據或者操做,具有了相似數組的接口。

12、Generator函數的異步應用

異步編程對 JavaScript 語言過重要。Javascript 語言的執行環境是「單線程」的,若是沒有異步編程,根本無法用,非卡死不可。本章主要介紹 Generator 函數如何完成異步操做。

一、傳統方法

ES6 誕生之前,異步編程的方法,大概有下面四種。

  • 回調函數
  • 事件監聽
  • 發佈/訂閱
  • Promise 對象

Generator 函數將 JavaScript 異步編程帶入了一個全新的階段。

二、基本概念

異步

所謂"異步",簡單說就是一個任務不是連續完成的,能夠理解成該任務被人爲分紅兩段,先執行第一段,而後轉而執行其餘任務,等作好了準備,再回過頭執行第二段。

好比,有一個任務是讀取文件進行處理,任務的第一段是向操做系統發出請求,要求讀取文件。而後,程序執行其餘任務,等到操做系統返回文件,再接着執行任務的第二段(處理文件)。這種不連續的執行,就叫作異步。

相應地,連續的執行就叫作同步。因爲是連續執行,不能插入其餘任務,因此操做系統從硬盤讀取文件的這段時間,程序只能乾等着。

回調函數

JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直接調用這個函數。回調函數的英語名字callback,直譯過來就是"從新調用"。

讀取文件進行處理,是這樣寫的。

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});

上面代碼中,readFile函數的第三個參數,就是回調函數,也就是任務的第二段。等到操做系統返回了/etc/passwd這個文件之後,回調函數纔會執行。

一個有趣的問題是,爲何 Node 約定,回調函數的第一個參數,必須是錯誤對象err(若是沒有錯誤,該參數就是null)?

緣由是執行分紅兩段,第一段執行完之後,任務所在的上下文環境就已經結束了。在這之後拋出的錯誤,原來的上下文環境已經沒法捕捉,只能看成參數,傳入第二段。

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方法捕捉執行過程當中拋出的錯誤。

能夠看到,Promise 的寫法只是回調函數的改進,使用then方法之後,異步任務的兩段執行看得更清楚了,除此之外,並沒有新意。

Promise 的最大問題是代碼冗餘,原來的任務被 Promise 包裝了一下,無論什麼操做,一眼看去都是一堆then,原來的語義變得很不清楚。

那麼,有沒有更好的寫法呢?

三、Generator 函數

協程

傳統的編程語言,早有異步編程的解決方案(實際上是多任務的解決方案)。其中有一種叫作"協程"(coroutine),意思是多個線程互相協做,完成異步任務。

協程有點像函數,又有點像線程。它的運行流程大體以下。

  • 第一步,協程A開始執行。
  • 第二步,協程A執行到一半,進入暫停,執行權轉移到協程B
  • 第三步,(一段時間後)協程B交還執行權。
  • 第四步,協程A恢復執行。

上面流程的協程A,就是異步任務,由於它分紅兩段(或多段)執行。

舉例來講,讀取文件的協程寫法以下。

function* asyncJob() {
  // ...其餘代碼
  var f = yield readFile(fileA);
  // ...其餘代碼
}

上面代碼的函數asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執行到此處,執行權將交給其餘協程。也就是說,yield命令是異步兩個階段的分界線。

協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行。它的最大優勢,就是代碼的寫法很是像同步操做,若是去除yield命令,簡直如出一轍。

協程的 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 函數不一樣於普通函數的另外一個地方,即執行它不會返回結果,返回的是指針對象。調用指針gnext方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的yield語句,上例是執行到x + 2爲止。

換言之,next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示 Generator 函數是否執行完畢,便是否還有下一個階段。

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

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

異步任務的封裝

下面看看如何使用 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 函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。

四、Thunk 函數

Thunk 函數是自動執行 Generator 函數的一種方法。

參數的求值策略

Thunk 函數早在上個世紀 60 年代就誕生了。

那時,編程語言剛剛起步,計算機學家還在研究,編譯器怎麼寫比較好。一個爭論的焦點是"求值策略",即函數的參數到底應該什麼時候求值。

var x = 1;

function f(m) {
  return m * 2;
}

f(x + 5)

上面代碼先定義函數f,而後向它傳入表達式x + 5。請問,這個表達式應該什麼時候求值?

一種意見是"傳值調用"(call by value),即在進入函數體以前,就計算x + 5的值(等於 6),再將這個值傳入函數f。C 語言就採用這種策略。

f(x + 5)
// 傳值調用時,等同於
f(6)

另外一種意見是「傳名調用」(call by name),即直接將表達式x + 5傳入函數體,只在用到它的時候求值。Haskell 語言採用這種策略。

f(x + 5)
// 傳名調用時,等同於
(x + 5) * 2

傳值調用和傳名調用,哪種比較好?

回答是各有利弊。傳值調用比較簡單,可是對參數求值的時候,實際上還沒用到這個參數,有可能形成性能損失。

function f(a, b){
  return b;
}

f(3 * x * x - 2 * x - 1, x);

上面代碼中,函數f的第一個參數是一個複雜的表達式,可是函數體內根本沒用到。對這個參數求值,其實是沒必要要的。所以,有一些計算機學家傾向於"傳名調用",即只在執行時求值。

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 函數的定義,它是「傳名調用」的一種實現策略,用來替換某個表達式。

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

Thunkify 模塊

生產環境的轉換器,建議使用 Thunkify 模塊。

首先是安裝。

$ npm install thunkify

使用方式以下。

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

Thunkify 的源碼與上一節那個簡單的轉換器很是像。

function thunkify(fn) {
  return function() {
    var args = new Array(arguments.length);
    var ctx = this;

    for (var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function (done) {
      var called;

      args.push(function () {
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};

它的源碼主要多了一個檢查機制,變量called確保回調函數只運行一次。這樣的設計與下文的 Generator 函數相關。請看下面的例子。

function f(a, b, callback){
  var sum = a + b;
  callback(sum);
  callback(sum);
}

var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3

上面代碼中,因爲thunkify只容許回調函數執行一次,因此只輸出一行結果。

Generator 函數的流程管理

你可能會問, Thunk 函數有什麼用?回答是之前確實沒什麼用,可是 ES6 有了 Generator 函數,Thunk 函數如今能夠用於 Generator 函數的自動流程管理。

Generator 函數能夠自動執行。

function* gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

上面代碼中,Generator 函數gen會自動執行完全部步驟。

可是,這不適合異步操做。若是必須保證前一步執行完,才能執行後一步,上面的自動執行就不可行。這時,Thunk 函數就能派上用處。以讀取文件爲例。下面的 Generator 函數封裝了兩個異步操做。

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

上面代碼中,yield命令用於將程序的執行權移出 Generator 函數,那麼就須要一種方法,將執行權再交還給 Generator 函數。

這種方法就是 Thunk 函數,由於它能夠在回調函數裏,將執行權交還給 Generator 函數。爲了便於理解,咱們先看如何手動執行上面這個 Generator 函數。

var g = gen();

var r1 = g.next();
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

上面代碼中,變量g是 Generator 函數的內部指針,表示目前執行到哪一步。next方法負責將指針移動到下一步,並返回該步的信息(value屬性和done屬性)。

仔細查看上面的代碼,能夠發現 Generator 函數的執行過程,實際上是將同一個回調函數,反覆傳入next方法的value屬性。這使得咱們能夠用遞歸來自動完成這個過程。

Thunk 函數的自動流程管理

Thunk 函數真正的威力,在於能夠自動執行 Generator 函數。下面就是一個基於 Thunk 函數的 Generator 執行器。

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
  }

  next();
}

function* g() {
  // ...
}

run(g);

上面代碼的run函數,就是一個 Generator 函數的自動執行器。內部的next函數就是 Thunk 的回調函數。next函數先將指針移到 Generator 函數的下一步(gen.next方法),而後判斷 Generator 函數是否結束(result.done屬性),若是沒結束,就將next函數再傳入 Thunk 函數(result.value屬性),不然就直接退出。

有了這個執行器,執行 Generator 函數方便多了。無論內部有多少個異步操做,直接把 Generator 函數傳入run函數便可。固然,前提是每個異步操做,都要是 Thunk 函數,也就是說,跟在yield命令後面的必須是 Thunk 函數。

var g = function* (){
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
};

run(g);

上面代碼中,函數g封裝了n個異步的讀取文件操做,只要執行run函數,這些操做就會自動完成。這樣一來,異步操做不只能夠寫得像同步操做,並且一行代碼就能夠執行。

Thunk 函數並非 Generator 函數自動執行的惟一方案。由於自動執行的關鍵是,必須有一種機制,自動控制 Generator 函數的流程,接收和交還程序的執行權。回調函數能夠作到這一點,Promise 對象也能夠作到這一點。

五、co 模塊

基本用法

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 函數執行結束,就會輸出一行提示。

co 模塊的原理

爲何 co 能夠自動執行 Generator 函數?

前面說過,Generator 就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。

兩種方法能夠作到這一點。

(1)回調函數。將異步操做包裝成 Thunk 函數,在回調函數裏面交回執行權。

(2)Promise 對象。將異步操做包裝成 Promise 對象,用then方法交回執行權。

co 模塊其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個模塊。使用 co 的前提條件是,Generator 函數的yield命令後面,只能是 Thunk 函數或 Promise 對象。若是數組或對象的成員,所有都是 Promise 對象,也能夠使用 co,詳見後文的例子。

上一節已經介紹了基於 Thunk 函數的自動執行器。下面來看,基於 Promise 對象的自動執行器。這是理解 co 模塊必須的。

基於 Promise 對象的自動執行

仍是沿用上面的例子。首先,把fs模塊的readFile方法包裝成一個 Promise 對象。

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

而後,手動執行上面的 Generator 函數。

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});

手動執行其實就是用then方法,層層添加回調函數。理解了這一點,就能夠寫出一個自動執行器。

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);

上面代碼中,只要 Generator 函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。

co 模塊的源碼

co 就是上面那個自動執行器的擴展,它的源碼只有幾十行,很是簡單。

首先,co 函數接受 Generator 函數做爲參數,返回一個 Promise 對象。

function co(gen) {
  var ctx = this;

  return new Promise(function(resolve, reject) {
  });
}

在返回的 Promise 對象裏面,co 先檢查參數gen是否爲 Generator 函數。若是是,就執行該函數,獲得一個內部指針對象;若是不是就返回,並將 Promise 對象的狀態改成resolved

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);
  });
}

接着,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,從而終止執行。

處理併發的異步操做

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異步操做,等到它們所有完成,纔會進行下一步。

實例:處理 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方法,在dataenderror三個事件上添加一次性回調函數。變量res只有在data事件發生時纔有值,而後累加每一個數據塊之中valjean這個詞出現的次數。

十3、async函數

一、含義

ES2017 標準引入了 async 函數,使得異步操做變得更加方便。

async 函數是什麼?一句話,它就是 Generator 函數的語法糖。

前文有一個 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 () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比較就會發現,async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。

async函數對 Generator 函數的改進,體如今如下四點。

(1)內置執行器。

Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍,只要一行。

asyncReadFile();

上面的代碼調用了asyncReadFile函數,而後它就會自動執行,輸出最後結果。這徹底不像 Generator 函數,須要調用next方法,或者用co模塊,才能真正執行,獲得最後結果。

(2)更好的語義。

asyncawait,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

(3)更廣的適用性。

co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。

(4)返回值是 Promise。

async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then方法指定下一步的操做。

進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

二、基本用法 § 

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

下面是一個例子。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

上面代碼是一個獲取股票報價的函數,函數前面的async關鍵字,代表該函數內部有異步操做。調用該函數時,會當即返回一個Promise對象。

下面是另外一個例子,指定多少毫秒後輸出一個值。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

上面代碼指定 50 毫秒之後,輸出hello world

因爲async函數返回的是 Promise 對象,能夠做爲await命令的參數。因此,上面的例子也能夠寫成下面的形式。

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

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 對象。

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: 出錯了

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

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語句是不會執行的,由於第一個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

錯誤處理

若是await後面的異步操做出錯,那麼等同於async函數返回的 Promise 對象被reject

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方法的回調函數被調用,它的參數就是拋出的錯誤對象。具體的執行機制,能夠參考後文的「async 函數的實現原理」。

防止出錯的方法,也是將其放在try...catch代碼塊之中。

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出錯了');
    });
  } catch(e) {
  }
  return await('hello world');
}

若是有多個await命令,能夠統一放在try...catch結構中。

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

下面的例子使用try...catch結構,實現屢次重複嘗試。

const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error');
      break;
    } catch(err) {}
  }
  console.log(i); // 3
}

test();

上面代碼中,若是await操做成功,就會使用break語句退出循環;若是失敗,會被catch語句捕捉,而後進入下一輪循環。

使用注意點

第一點,前面已經說過,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 = await getFoo();
let bar = await getBar();

上面代碼中,getFoogetBar是兩個獨立的異步操做(即互不依賴),被寫成繼發關係。這樣比較耗時,由於只有getFoo完成之後,纔會執行getBar,徹底可讓它們同時觸發。

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

上面兩種寫法,getFoogetBar都是同時觸發,這樣就會縮短程序的執行時間。

第三點,await命令只能用在async函數之中,若是用在普通函數,就會報錯。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 報錯
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代碼會報錯,由於await用在普通函數之中了。可是,若是將forEach方法的參數改爲async函數,也有問題。

function dbFuc(db) { //這裏不須要 async
  let docs = [{}, {}, {}];

  // 可能獲得錯誤結果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

上面代碼可能不會正常工做,緣由是這時三個db.post操做將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for循環。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

若是確實但願多個請求併發執行,能夠使用Promise.all方法。當三個請求都會resolved時,下面兩種寫法效果相同。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的寫法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

目前,esm模塊加載器支持頂層await,即await命令能夠不放在 async 函數裏面,直接使用。

// async 函數的寫法
const start = async () => {
  const res = await fetch('google.com');
  return res.text();
};

start().then(console.log);

// 頂層 await 的寫法
const res = await fetch('google.com');
console.log(await res.text());

上面代碼中,第二種寫法的腳本必須使用esm加載器,纔會生效。

 四、async 函數的實現原理

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

async function fn(args) {
  // ...
}

// 等同於

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

全部的async函數均可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。

下面給出spawn函數的實現,基本就是前文自動執行器的翻版。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

五、與其餘異步處理方法的比較

咱們經過一個例子,來看 async 函數與 Promise、Generator 函數的比較。

假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。若是當中有一個動畫出錯,就再也不往下執行,返回上一個成功執行的動畫的返回值。

首先是 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(thencatch等等),操做自己的語義反而不容易看出來。

接着是 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。

最後是 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 寫法,自動執行器須要用戶本身提供。

 六、實例:按順序完成異步操做 § 

實際開發中,常常遇到一組異步操做,須要按照順序完成。好比,依次遠程讀取一組 URL,而後按照讀取的順序輸出結果。

Promise 的寫法以下。

function logInOrder(urls) {
  // 遠程讀取全部URL
  const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
  });

  // 按次序輸出
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text));
  }, Promise.resolve());
}

上面代碼使用fetch方法,同時遠程讀取一組 URL。每一個fetch操做都返回一個 Promise 對象,放入textPromises數組。而後,reduce方法依次處理每一個 Promise 對象,而後使用then,將全部 Promise 對象連起來,所以就能夠依次輸出結果。

這種寫法不太直觀,可讀性比較差。下面是 async 函數實現。

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

上面代碼確實大大簡化,問題是全部遠程操做都是繼發。只有前一個 URL 返回結果,纔會去讀取下一個 URL,這樣作效率不好,很是浪費時間。咱們須要的是併發發出遠程請求。

async function logInOrder(urls) {
  // 併發讀取遠程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序輸出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

上面代碼中,雖然map方法的參數是async函數,但它是併發執行的,由於只有async函數內部是繼發執行,外部不受影響。後面的for..of循環內部使用了await,所以實現了按順序輸出。

七、異步遍歷器

《遍歷器》一章說過,Iterator 接口是一種數據遍歷的協議,只要調用遍歷器對象的next方法,就會獲得一個對象,表示當前遍歷指針所在的那個位置的信息。next方法返回的對象的結構是{value, done},其中value表示當前的數據的值,done是一個布爾值,表示遍歷是否結束。

這裏隱含着一個規定,next方法必須是同步的,只要調用就必須馬上返回值。也就是說,一旦執行next方法,就必須同步地獲得valuedone這兩個屬性。若是遍歷指針正好指向同步操做,固然沒有問題,但對於異步操做,就不太合適了。目前的解決方法是,Generator 函數裏面的異步操做,返回一個 Thunk 函數或者 Promise 對象,即value屬性是一個 Thunk 函數或者 Promise 對象,等待之後返回真正的值,而done屬性則仍是同步產生的。

ES2018 引入了」異步遍歷器「(Async Iterator),爲異步操做提供原生的遍歷器接口,即valuedone這兩個屬性都是異步產生。

異步遍歷的接口

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

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

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

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

下面是一個異步遍歷器的例子。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

asyncIterator
.next()
.then(iterResult1 => {
  console.log(iterResult1); // { value: 'a', done: false }
  return asyncIterator.next();
})
.then(iterResult2 => {
  console.log(iterResult2); // { value: 'b', done: false }
  return asyncIterator.next();
})
.then(iterResult3 => {
  console.log(iterResult3); // { value: undefined, done: true }
});

上面代碼中,異步遍歷器其實返回了兩次值。第一次調用的時候,返回一個 Promise 對象;等到 Promise 對象resolve了,再返回一個表示當前數據成員信息的對象。這就是說,異步遍歷器與同步遍歷器最終行爲是一致的,只是會先返回 Promise 對象,做爲中介。

因爲異步遍歷器的next方法,返回的是一個 Promise 對象。所以,能夠把它放在await命令後面。

async function f() {
  const asyncIterable = createAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { value: 'a', done: false }
  console.log(await asyncIterator.next());
  // { value: 'b', done: false }
  console.log(await asyncIterator.next());
  // { value: undefined, done: true }
}

上面代碼中,next方法用await處理之後,就沒必要使用then方法了。整個流程已經很接近同步處理了。

注意,異步遍歷器的next方法是能夠連續調用的,沒必要等到上一步產生的 Promise 對象resolve之後再調用。這種狀況下,next方法會累積起來,自動按照每一步的順序運行下去。下面是一個例子,把全部的next方法放在Promise.all方法裏面。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
  asyncIterator.next(), asyncIterator.next()
]);

console.log(v1, v2); // a b

另外一種用法是一次性調用全部的next方法,而後await最後一步操做。

async function runner() {
  const writer = openFile('someFile.txt');
  writer.next('hello');
  writer.next('world');
  await writer.return();
}

runner();

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 對象被rejectfor 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

Node v10 支持異步遍歷器,Stream 就部署了這個接口。下面是讀取文件的傳統寫法與異步遍歷器寫法的差別。

// 傳統寫法
function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );
  readStream.on('data', (chunk) => {
    console.log('>>> '+chunk);
  });
  readStream.on('end', () => {
    console.log('### DONE ###');
  });
}

// 異步遍歷器寫法
async function main(inputFilePath) {
  const readStream = fs.createReadStream(
    inputFilePath,
    { encoding: 'utf8', highWaterMark: 1024 }
  );

  for await (const chunk of readStream) {
    console.log('>>> '+chunk);
  }
  console.log('### DONE ###');
}

異步 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);
  }
}

上面代碼中,map是一個 Generator 函數,第一個參數是可遍歷對象iterable,第二個參數是一個回調函數funcmap的做用是將iterable每一步返回的值,使用func進行處理。上面有兩個版本的map,前一個處理同步遍歷器,後一個處理異步遍歷器,能夠看到兩個版本的寫法基本上是一致的。

下面是另外一個異步 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屬性,這一點是與同步 Generator 函數一致的。

異步 Generator 函數內部,可以同時使用awaityield命令。能夠這樣理解,await命令用於將外部操做產生的值輸入函數內部,yield命令用於將函數內部的值輸出。

上面代碼定義的異步 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 對象。若是像上面那個例子那樣,yield命令後面是一個字符串,會被自動包裝成一個 Promise 對象。

function fetchRandom() {
  const url = 'https://www.random.org/decimal-fractions/'
    + '?num=1&dec=10&col=1&format=plain&rnd=new';
  return fetch(url);
}

async function* asyncGenerator() {
  console.log('Start');
  const result = await fetchRandom(); // (A)
  yield 'Result: ' + await result.text(); // (B)
  console.log('Done');
}

const ag = asyncGenerator();
ag.next().then(({value, done}) => {
  console.log(value);
})

上面代碼中,agasyncGenerator函數返回的異步遍歷器對象。調用ag.next()之後,上面代碼的執行順序以下。

  1. ag.next()馬上返回一個 Promise 對象。
  2. asyncGenerator函數開始執行,打印出Start
  3. await命令返回一個 Promise 對象,asyncGenerator函數停在這裏。
  4. A 處變成 fulfilled 狀態,產生的值放入result變量,asyncGenerator函數繼續往下執行。
  5. 函數在 B 處的yield暫停執行,一旦yield命令取到值,ag.next()返回的那個 Promise 對象變成 fulfilled 狀態。
  6. ag.next()後面的then方法指定的回調函數開始執行。該回調函數的參數是一個對象{value, done},其中value的值是yield命令後面的那個表達式的值,done的值是false

A 和 B 兩行的做用相似於下面的代碼。

return new Promise((resolve, reject) => {
  fetchRandom()
  .then(result => result.text())
  .then(result => {
     resolve({
       value: 'Result: ' + result,
       done: false,
     });
  });
});

若是異步 Generator 函數拋出錯誤,會致使 Promise 對象的狀態變爲reject,而後拋出的錯誤被catch方法捕獲。

async function* asyncGenerator() {
  throw new Error('Problem!');
}

asyncGenerator()
.next()
.catch(err => console.log(err)); // Error: Problem!

注意,普通的 async 函數返回的是一個 Promise 對象,而異步 Generator 函數返回的是一個異步 Iterator 對象。能夠這樣理解,async 函數和異步 Generator 函數,是封裝異步操做的兩種方法,都用來達到同一種目的。區別在於,前者自帶執行器,後者經過for await...of執行,或者本身編寫執行器。下面就是一個異步 Generator 函數的執行器。

async function takeAsync(asyncIterable, count = Infinity) {
  const result = [];
  const iterator = asyncIterable[Symbol.asyncIterator]();
  while (result.length < count) {
    const {value, done} = await iterator.next();
    if (done) break;
    result.push(value);
  }
  return result;
}

上面代碼中,異步 Generator 函數產生的異步遍歷器,會經過while循環自動執行,每當await iterator.next()完成,就會進入下一輪循環。一旦done屬性變爲true,就會跳出循環,異步遍歷器執行結束。

下面是這個自動執行器的一個使用實例。

async function f() {
  async function* gen() {
    yield 'a';
    yield 'b';
    yield 'c';
  }

  return await takeAsync(gen());
}

f().then(function (result) {
  console.log(result); // ['a', 'b', 'c']
})

異步 Generator 函數出現之後,JavaScript 就有了四種函數形式:普通函數、async 函數、Generator 函數和異步 Generator 函數。請注意區分每種函數的不一樣之處。基本上,若是是一系列按照順序執行的異步操做(好比讀取文件,而後寫入新內容,再存入硬盤),能夠使用 async 函數;若是是一系列產生相同數據結構的異步操做(好比一行一行讀取文件),能夠使用異步 Generator 函數。

異步 Generator 函數也能夠經過next方法的參數,接收外部傳入的數據。

const writer = openFile('someFile.txt');
writer.next('hello'); // 當即執行
writer.next('world'); // 當即執行
await writer.return(); // 等待寫入結束

上面代碼中,openFile是一個異步 Generator 函數。next方法的參數,向該函數內部的操做傳入數據。每次next方法都是同步執行的,最後的await命令用於等待整個寫入操做結束。

最後,同步的數據結構,也能夠使用異步 Generator 函數。

async function* createAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem;
  }
}

上面代碼中,因爲沒有異步操做,因此也就沒有使用await關鍵字。

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

十4、Class的基本語法

一、簡介

JavaScript 語言中,生成實例對象的傳統方法是經過構造函數。下面是一個例子。

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

上面這種寫法跟傳統的面嚮對象語言(好比 C++ 和 Java)差別很大,很容易讓新學習這門語言的程序員感到困惑。

ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,做爲對象的模板。經過class關鍵字,能夠定義類。

基本上,ES6 的class能夠看做只是一個語法糖,它的絕大部分功能,ES5 均可以作到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。上面的代碼用 ES6 的class改寫,就是下面這樣。

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

上面代碼定義了一個「類」,能夠看到裏面有一個constructor方法,這就是構造方法,而this關鍵字則表明實例對象。也就是說,ES5 的構造函數Point,對應 ES6 的Point類的構造方法。

Point類除了構造方法,還定義了一個toString方法。注意,定義「類」的方法的時候,前面不須要加上function這個關鍵字,直接把函數定義放進去了就能夠了。另外,方法之間不須要逗號分隔,加了會報錯。

ES6 的類,徹底能夠看做構造函數的另外一種寫法。

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

上面代碼代表,類的數據類型就是函數,類自己就指向構造函數。

使用的時候,也是直接對類使用new命令,跟構造函數的用法徹底一致。

class Bar {
  doStuff() {
    console.log('stuff');
  }
}

var b = new Bar();
b.doStuff() // "stuff"

構造函數的prototype屬性,在 ES6 的「類」上面繼續存在。事實上,類的全部方法都定義在類的prototype屬性上面。

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

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同於

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

在類的實例上面調用方法,其實就是調用原型上的方法。

class B {}
let b = new B();

b.constructor === B.prototype.constructor // true

上面代碼中,bB類的實例,它的constructor方法就是B類原型的constructor方法。

因爲類的方法都定義在prototype對象上面,因此類的新方法能夠添加在prototype對象上面。Object.assign方法能夠很方便地一次向類添加多個方法。

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

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

prototype對象的constructor屬性,直接指向「類」的自己,這與 ES5 的行爲是一致的。

Point.prototype.constructor === Point // true

另外,類的內部全部定義的方法,都是不可枚舉的(non-enumerable)。

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

上面代碼中,toString方法是Point類內部定義的方法,它是不可枚舉的。這一點與 ES5 的行爲不一致。

var Point = function (x, y) {
  // ...
};

Point.prototype.toString = function() {
  // ...
};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

上面代碼採用 ES5 的寫法,toString方法就是可枚舉的。

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

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

上面代碼中,Square類的方法名getArea,是從表達式獲得的。

二、嚴格模式 § 

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

考慮到將來全部的代碼,其實都是運行在模塊之中,因此 ES6 實際上把整個語言升級到了嚴格模式。

三、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也能夠執行。

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

Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'

 四、類的實例對象 § 

生成類的實例對象的寫法,與 ES5 徹底同樣,也是使用new命令。前面說過,若是忘記加上new,像函數那樣調用Class,將會報錯。

class Point {
  // ...
}

// 報錯
var point = Point(2, 3);

// 正確
var point = new Point(2, 3);

與 ES5 同樣,實例的屬性除非顯式定義在其自己(即定義在this對象上),不然都是定義在原型上(即定義在class上)。

//定義類
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

上面代碼中,xy都是實例對象point自身的屬性(由於定義在this變量上),因此hasOwnProperty方法返回true,而toString是原型對象的屬性(由於定義在Point類上),因此hasOwnProperty方法返回false。這些都與 ES5 的行爲保持一致。

與 ES5 同樣,類的全部實例共享一個原型對象。

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__
//true

上面代碼中,p1p2都是Point的實例,它們的原型都是Point.prototype,因此__proto__屬性是相等的。

這也意味着,能夠經過實例的__proto__屬性爲「類」添加方法。

__proto__ 並非語言自己的特性,這是各大廠商具體實現時添加的私有屬性,雖然目前不少現代瀏覽器的 JS 引擎中都提供了這個私有屬性,但依舊不建議在生產中使用該屬性,避免對環境產生依賴。生產環境中,咱們能夠使用 Object.getPrototypeOf 方法來獲取實例對象的原型,而後再來爲原型添加方法/屬性。

var p1 = new Point(2,3);
var p2 = new Point(3,2);

p1.__proto__.printName = function () { return 'Oops' };

p1.printName() // "Oops"
p2.printName() // "Oops"

var p3 = new Point(4,2);
p3.printName() // "Oops"

上面代碼在p1的原型上添加了一個printName方法,因爲p1的原型就是p2的原型,所以p2也能夠調用這個方法。並且,此後新建的實例p3也能夠調用這個方法。這意味着,使用實例的__proto__屬性改寫原型,必須至關謹慎,不推薦使用,由於這會改變「類」的原始定義,影響到全部實例。

五、Class 表達式

與函數同樣,類也能夠使用表達式的形式定義。

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

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

六、不存在變量提高

類不存在變量提高(hoist),這一點與 ES5 徹底不一樣。

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

上面代碼中,Foo類使用在前,定義在後,這樣會報錯,由於 ES6 不會把類的聲明提高到代碼頭部。這種規定的緣由與下文要提到的繼承有關,必須保證子類在父類以後定義。

{
  let Foo = class {};
  class Bar extends Foo {
  }
}

上面的代碼不會報錯,由於Bar繼承Foo的時候,Foo已經有定義了。可是,若是存在class的提高,上面代碼就會報錯,由於class會被提高到代碼頭部,而let命令是不提高的,因此致使Bar繼承Foo的時候,Foo尚未定義。

七、私有屬性和私有方法

現有的方法

私有方法是常見需求,但 ES6 不提供,只能經過變通方法模擬實現。

一種作法是在命名上加以區別。

class Widget {

  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}

上面代碼中,_bar方法前面的下劃線,表示這是一個只限於內部使用的私有方法。可是,這種命名是不保險的,在類的外部,仍是能夠調用到這個方法。

另外一種方法就是索性將私有方法移出模塊,由於模塊內部的全部方法都是對外可見的。

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

上面代碼中,foo是公有方法,內部調用了bar.call(this, baz)。這使得bar實際上成爲了當前模塊的私有方法。

還有一種方法是利用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;
  }

  // ...
};

上面代碼中,barsnaf都是Symbol值,致使第三方沒法獲取到它們,所以達到了私有方法和私有屬性的效果。

私有屬性的提案

與私有方法同樣,ES6 不支持私有屬性。目前,有一個提案,爲class加了私有屬性。方法是在屬性名以前,使用#表示。

class Point {
  #x;

  constructor(x = 0) {
    #x = +x; // 寫成 this.#x 亦可
  }

  get x() { return #x }
  set x(value) { #x = +value }
}

上面代碼中,#x就是私有屬性,在Point類以外是讀取不到這個屬性的。因爲井號#是屬性名的一部分,使用時必須帶有#一塊兒使用,因此#xx是兩個不一樣的屬性。

私有屬性能夠指定初始值,在構造函數執行時進行初始化。

class Point {
  #x = 0;
  constructor() {
    #x; // 0
  }
}

之因此要引入一個新的前綴#表示私有屬性,而沒有采用private關鍵字,是由於 JavaScript 是一門動態語言,使用獨立的符號彷佛是惟一的可靠方法,可以準確地區分一種屬性是否爲私有屬性。另外,Ruby 語言使用@表示私有屬性,ES6 沒有用這個符號而使用#,是由於@已經被留給了 Decorator。

這種寫法不只能夠寫私有屬性,還能夠用來寫私有方法。

class Foo {
  #a;
  #b;
  #sum() { return #a + #b; }
  printSum() { console.log(#sum()); }
  constructor(a, b) { #a = a; #b = b; }
}

上面代碼中,#sum()就是一個私有方法。

另外,私有屬性也能夠設置 getter 和 setter 方法。

class Counter {
  #xValue = 0;

  get #x() { return #xValue; }
  set #x(value) {
    this.#xValue = value;
  }

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

上面代碼中,#x是一個私有屬性,它的讀寫都經過get #x()set #x()來完成。

八、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,這樣就不會找不到print方法了。

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

  // ...
}

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

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }

  // ...
}

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

九、name 屬性

因爲本質上,ES6 的類只是 ES5 的構造函數的一層包裝,因此函數的許多特性都被Class繼承,包括name屬性。

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

name屬性老是返回緊跟在class關鍵字後面的類名。

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

與 ES5 同樣,在「類」的內部能夠使用getset關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。

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

存值函數和取值函數是設置在屬性的 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 徹底一致。

十一、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循環會自動調用這個遍歷器。

十二、Class 的靜態方法

類至關於實例的原型,全部在類中定義的方法,都會被實例繼承。若是在一個方法前,加上static關鍵字,就表示該方法不會被實例繼承,而是直接經過類來調用,這就稱爲「靜態方法」。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

上面代碼中,Foo類的classMethod方法前有static關鍵字,代表該方法是一個靜態方法,能夠直接在Foo類上調用(Foo.classMethod()),而不是在Foo類的實例上調用。若是在實例上調用靜態方法,會拋出一個錯誤,表示不存在該方法。

注意,若是靜態方法包含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 Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

上面代碼中,父類Foo有一個靜態方法,子類Bar能夠調用這個方法。

靜態方法也是能夠從super對象上調用的。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"

1三、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),而不是賦值處理,語義更好。

 1四、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會報錯。

十5、Class 的繼承

一、簡介 § 

Class 能夠經過extends關鍵字實現繼承,這比 ES5 的經過修改原型鏈實現繼承,要清晰和方便不少。

class Point {
}

class ColorPoint extends Point {
}

上面代碼定義了一個ColorPoint類,該類經過extends關鍵字,繼承了Point類的全部屬性和方法。可是因爲沒有部署任何代碼,因此這兩個類徹底同樣,等於複製了一個Point類。下面,咱們在ColorPoint內部加上代碼。

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方法和toString方法之中,都出現了super關鍵字,它在這裏表示父類的構造函數,用來新建父類的this對象。

子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類本身的this對象,必須先經過父類的構造函數完成塑造,獲得與父類一樣的實例屬性和方法,而後再對其進行加工,加上子類本身的實例屬性和方法。若是不調用super方法,子類就得不到this對象。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

上面代碼中,ColorPoint繼承了父類Point,可是它的構造函數沒有調用super方法,致使新建實例時報錯。

ES5 的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制徹底不一樣,實質是先將父類實例對象的屬性和方法,加到this上面(因此必須先調用super方法),而後再用子類的構造函數修改this

若是子類沒有定義constructor方法,這個方法會被默認添加,代碼以下。也就是說,無論有沒有顯式定義,任何一個子類都有constructor方法。

class ColorPoint extends Point {
}

// 等同於
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

另外一個須要注意的地方是,在子類的構造函數中,只有調用super以後,才能夠使用this關鍵字,不然會報錯。這是由於子類實例的構建,基於父類實例,只有super方法才能調用父類實例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正確
  }
}

上面代碼中,子類的constructor方法沒有調用super以前,就使用this關鍵字,結果報錯,而放在super方法以後就是正確的。

下面是生成子類實例的代碼。

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true
cp instanceof Point // true

上面代碼中,實例對象cp同時是ColorPointPoint兩個類的實例,這與 ES5 的行爲徹底一致。

最後,父類的靜態方法,也會被子類繼承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world

上面代碼中,hello()A類的靜態方法,B繼承A,也繼承了A的靜態方法。

二、Object.getPrototypeOf() § 

Object.getPrototypeOf方法能夠用來從子類上獲取父類。

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

所以,能夠使用這個方法判斷,一個類是否繼承了另外一個類。

三、super 關鍵字

super這個關鍵字,既能夠看成函數使用,也能夠看成對象使用。在這兩種狀況下,它的用法徹底不一樣。

第一種狀況,super做爲函數調用時,表明父類的構造函數。ES6 要求,子類的構造函數必須執行一次super函數。

class A {}

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

上面代碼中,子類B的構造函數之中的super(),表明調用父類的構造函數。這是必須的,不然 JavaScript 引擎會報錯。

注意,super雖然表明了父類A的構造函數,可是返回的是子類B的實例,即super內部的this指的是B,所以super()在這裏至關於A.prototype.constructor.call(this)

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A() // A
new B() // B

上面代碼中,new.target指向當前正在執行的函數。能夠看到,在super()執行時,它指向的是子類B的構造函數,而不是父類A的構造函數。也就是說,super()內部的this指向的是B

做爲函數時,super()只能用在子類的構造函數之中,用在其餘地方就會報錯。

class A {}

class B extends A {
  m() {
    super(); // 報錯
  }
}

上面代碼中,super()用在B類的m方法之中,就會形成句法錯誤。

第二種狀況,super做爲對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。

class A {
  p() {
    return 2;
  }
}

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

let b = new B();

上面代碼中,子類B當中的super.p(),就是將super看成一個對象使用。這時,super在普通方法之中,指向A.prototype,因此super.p()就至關於A.prototype.p()

這裏須要注意,因爲super指向父類的原型對象,因此定義在父類實例上的方法或屬性,是沒法經過super調用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

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

上面代碼中,p是父類A實例的屬性,super.p就引用不到它。

若是屬性定義在父類的原型對象上,super就能夠取到。

class A {}
A.prototype.x = 2;

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

let b = new B();

上面代碼中,屬性x是定義在A.prototype上面的,因此super.x能夠取到它的值。

ES6 規定,在子類普通方法中經過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()內部的this指向子類B的實例,致使輸出的是2,而不是1。也就是說,實際上執行的是super.print.call(this)

因爲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做爲對象,用在靜態方法之中,這時super將指向父類,而不是父類的原型對象。

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

上面代碼中,super在靜態方法之中指向父類,在普通方法之中指向父類的原型對象。

另外,在子類的靜態方法中經過super調用父類的方法時,方法內部的this指向當前的子類,而不是子類的實例。

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

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

B.x = 3;
B.m() // 3

上面代碼中,靜態方法B.m裏面,super.print指向父類的靜態方法。這個方法裏面的this指向的是B,而不是B的實例。

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

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super); // 報錯
  }
}

上面代碼中,console.log(super)當中的super,沒法看出是做爲函數使用,仍是做爲對象使用,因此 JavaScript 引擎解析代碼的時候就會報錯。這時,若是能清晰地代表super的數據類型,就不會報錯。

class A {}

class B extends A {
  constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
  }
}

let b = new B();

上面代碼中,super.valueOf()代表super是一個對象,所以就不會報錯。同時,因爲super使得this指向B的實例,因此super.valueOf()返回的是一個B的實例。

最後,因爲對象老是繼承其餘對象的,因此能夠在任意一個對象中,使用super關鍵字。

var obj = {
  toString() {
    return "MyObject: " + super.toString();
  }
};

obj.toString(); // MyObject: [object Object]

四、類的 prototype 屬性和__proto__屬性 § 

大多數瀏覽器的 ES5 實現之中,每個對象都有__proto__屬性,指向對應的構造函數的prototype屬性。Class 做爲構造函數的語法糖,同時有prototype屬性和__proto__屬性,所以同時存在兩條繼承鏈。

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

(2)子類prototype屬性的__proto__屬性,表示方法的繼承,老是指向父類的prototype屬性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代碼中,子類B__proto__屬性指向父類A,子類Bprototype屬性的__proto__屬性指向父類Aprototype屬性。

這樣的結果是由於,類的繼承是按照下面的模式實現的。

class A {
}

class B {
}

// B 的實例繼承 A 的實例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 繼承 A 的靜態屬性
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;

這兩條繼承鏈,能夠這樣理解:做爲一個對象,子類(B)的原型(__proto__屬性)是父類(A);做爲一個構造函數,子類(B)的原型對象(prototype屬性)是父類的原型對象(prototype屬性)的實例。

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

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

class B extends A {
}

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

下面,討論兩種狀況。第一種,子類繼承Object類。

class A extends Object {
}

A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

這種狀況下,A其實就是構造函數Object的複製,A的實例就是Object的實例。

第二種狀況,不存在任何繼承。

class A {
}

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

這種狀況下,A做爲一個基類(即不存在任何繼承),就是一個普通函數,因此直接繼承Function.prototype。可是,A調用後返回一個空對象(即Object實例),因此A.prototype.__proto__指向構造函數(Object)的prototype屬性。

實例的 __proto__ 屬性

子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

上面代碼中,ColorPoint繼承了Point,致使前者原型的原型是後者的原型。

所以,經過子類實例的__proto__.__proto__屬性,能夠修改父類實例的行爲。

p2.__proto__.__proto__.printName = function () {
  console.log('Ha');
};

p1.printName() // "Ha"

上面代碼在ColorPoint的實例p2上向Point類添加方法,結果影響到了Point的實例p1

 五、原生構造函數的繼承 § 

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

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

之前,這些原生構造函數是沒法繼承的,好比,不能本身定義一個Array的子類。

function MyArray() {
  Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true
  }
});

上面代碼定義了一個繼承 Array 的MyArray類。可是,這個類的行爲與Array徹底不一致。

var colors = new MyArray();
colors[0] = "red";
colors.length  // 0

colors.length = 0;
colors[0]  // "red"

之因此會發生這種狀況,是由於子類沒法得到原生構造函數的內部屬性,經過Array.apply()或者分配給原型對象都不行。原生構造函數會忽略apply方法傳入的this,也就是說,原生構造函數的this沒法綁定,致使拿不到內部屬性。

ES5 是先新建子類的實例對象this,再將父類的屬性添加到子類上,因爲父類的內部屬性沒法獲取,致使沒法繼承原生的構造函數。好比,Array構造函數有一個內部屬性[[DefineOwnProperty]],用來定義新屬性時,更新length屬性,這個內部屬性沒法在子類獲取,致使子類的length屬性行爲不正常。

下面的例子中,咱們想讓一個普通對象繼承Error對象。

var e = {};

Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]

Object.getOwnPropertyNames(e)
// []

上面代碼中,咱們想經過Error.call(e)這種寫法,讓普通對象e具備Error對象的實例屬性。可是,Error.call()徹底忽略傳入的第一個參數,而是返回一個新對象,e自己沒有任何變化。這證實了Error.call(e)這種寫法,沒法繼承原生構造函數。

ES6 容許繼承原生構造函數定義子類,由於 ES6 是先新建父類的實例對象this,而後再用子類的構造函數修飾this,使得父類的全部行爲均可以繼承。下面是一個繼承Array的例子。

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

上面代碼定義了一個MyArray類,繼承了Array構造函數,所以就能夠從MyArray生成數組的實例。這意味着,ES6 能夠自定義原生數據結構(好比ArrayString等)的子類,這是 ES5 沒法作到的。

上面這個例子也說明,extends關鍵字不只能夠用來繼承類,還能夠用來繼承原生的構造函數。所以能夠在原生數據結構的基礎上,定義本身的數據結構。下面就是定義了一個帶版本功能的數組。

class VersionedArray extends Array {
  constructor() {
    super();
    this.history = [[]];
  }
  commit() {
    this.history.push(this.slice());
  }
  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]);
  }
}

var x = new VersionedArray();

x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]

x.commit();
x.history // [[], [1, 2]]

x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]

x.revert();
x // [1, 2]

上面代碼中,VersionedArray會經過commit方法,將本身的當前狀態生成一個版本快照,存入history屬性。revert方法用來將數組重置爲最新一次保存的版本。除此以外,VersionedArray依然是一個普通數組,全部原生的數組方法均可以在它上面調用。

下面是一個自定義Error子類的例子,能夠用來定製報錯時的行爲。

class ExtendableError extends Error {
  constructor(message) {
    super();
    this.message = message;
    this.stack = (new Error()).stack;
    this.name = this.constructor.name;
  }
}

class MyError extends ExtendableError {
  constructor(m) {
    super(m);
  }
}

var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
// Error
//     at MyError.ExtendableError
//     ...

注意,繼承Object的子類,有一個行爲差別

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

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

六、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.prototype, mixin); // 拷貝實例屬性
    copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷貝原型屬性
  }

  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) {
  // .

十6、修飾器

 一、類的修飾

許多面向對象的語言都有修飾器(Decorator)函數,用來修改類的行爲。目前,有一個提案將這項功能,引入了 ECMAScript。

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

上面代碼中,@testable就是一個修飾器。它修改了MyTestableClass這個類的行爲,爲它加上了靜態屬性isTestabletestable函數的參數targetMyTestableClass類自己。

基本上,修飾器的行爲就是下面這樣。

@decorator
class A {}

// 等同於

class A {}
A = decorator(A) || A;

也就是說,修飾器是一個對類進行處理的函數。修飾器函數的第一個參數,就是所要修飾的目標類。

function testable(target) {
  // ...
}

上面代碼中,testable函數的參數target,就是會被修飾的類。

若是以爲一個參數不夠用,能夠在修飾器外面再封裝一層函數。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上面代碼中,修飾器testable能夠接受參數,這就等於能夠修改修飾器的行爲。

注意,修飾器對類的行爲的改變,是代碼編譯時發生的,而不是在運行時。這意味着,修飾器能在編譯階段運行代碼。也就是說,修飾器本質就是編譯時執行的函數。

前面的例子是爲類添加一個靜態屬性,若是想添加實例屬性,能夠經過目標類的prototype對象操做。

function testable(target) {
  target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

上面代碼中,修飾器函數testable是在目標類的prototype對象上添加屬性,所以就能夠在實例上調用。

下面是另一個例子。

// mixins.js
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list)
  }
}

// main.js
import { mixins } from './mixins'

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'

上面代碼經過修飾器mixins,把Foo對象的方法添加到了MyClass的實例上面。能夠用Object.assign()模擬這個功能。

const Foo = {
  foo() { console.log('foo') }
};

class MyClass {}

Object.assign(MyClass.prototype, Foo);

let obj = new MyClass();
obj.foo() // 'foo'

實際開發中,React 與 Redux 庫結合使用時,經常須要寫成下面這樣。

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了裝飾器,就能夠改寫上面的代碼。

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

相對來講,後一種寫法看上去更容易理解。

二、方法的修飾

修飾器不只能夠修飾類,還能夠修飾類的屬性。

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

上面代碼中,修飾器readonly用來修飾「類」的name方法。

修飾器函數readonly一共能夠接受三個參數。

function readonly(target, name, descriptor){
  // descriptor對象原來的值以下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 相似於
Object.defineProperty(Person.prototype, 'name', descriptor);

修飾器第一個參數是類的原型對象,上例是Person.prototype,修飾器的本意是要「修飾」類的實例,可是這個時候實例還沒生成,因此只能去修飾原型(這不一樣於類的修飾,那種狀況時target參數指的是類自己);第二個參數是所要修飾的屬性名,第三個參數是該屬性的描述對象。

另外,上面代碼說明,修飾器(readonly)會修改屬性的描述對象(descriptor),而後被修改的描述對象再用來定義屬性。

下面是另外一個例子,修改屬性描述對象的enumerable屬性,使得該屬性不可遍歷。

class Person {
  @nonenumerable
  get kidCount() { return this.children.length; }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

下面的@log修飾器,能夠起到輸出日誌的做用。

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

const math = new Math();

// passed parameters should get logged now
math.add(2, 4);

上面代碼中,@log修飾器的做用就是在執行原始的操做以前,執行一次console.log,從而達到輸出日誌的目的。

修飾器有註釋的做用。

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}

從上面代碼中,咱們一眼就能看出,Person類是可測試的,而name方法是隻讀和不可枚舉的。

下面是使用 Decorator 寫法的組件,看上去一目瞭然。

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.scss'
})
export class MyComponent {
  @Prop() first: string;
  @Prop() last: string;
  @State() isVisible: boolean = true;

  render() {
    return (
      <p>Hello, my name is {this.first} {this.last}</p>
    );
  }
}

若是同一個方法有多個修飾器,會像剝洋蔥同樣,先從外到內進入,而後由內向外執行。

function dec(id){
  console.log('evaluated', id);
  return (target, property, descriptor) => console.log('executed', id);
}

class Example {
    @dec(1)
    @dec(2)
    method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1

上面代碼中,外層修飾器@dec(1)先進入,可是內層修飾器@dec(2)先執行。

除了註釋,修飾器還能用來類型檢查。因此,對於類來講,這項功能至關有用。從長期來看,它將是 JavaScript 代碼靜態分析的重要工具。

三、爲何修飾器不能用於函數?

修飾器只能用於類和類的方法,不能用於函數,由於存在函數提高。

var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {
}

上面的代碼,意圖是執行後counter等於 1,可是實際上結果是counter等於 0。由於函數提高,使得實際執行的代碼是下面這樣。

@add
function foo() {
}

var counter;
var add;

counter = 0;

add = function () {
  counter++;
};

下面是另外一個例子。

var readOnly = require("some-decorator");

@readOnly
function foo() {
}

上面代碼也有問題,由於實際執行是下面這樣。

var readOnly;

@readOnly
function foo() {
}

readOnly = require("some-decorator");

總之,因爲存在函數提高,使得修飾器不能用於函數。類是不會提高的,因此就沒有這方面的問題。

另外一方面,若是必定要修飾函數,能夠採用高階函數的形式直接執行。

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

 四、core-decorators.js

core-decorators.js是一個第三方模塊,提供了幾個常見的修飾器,經過它能夠更好地理解修飾器。

(1)@autobind

autobind修飾器使得方法中的this對象,綁定原始對象。

import { autobind } from 'core-decorators';

class Person {
  @autobind
  getPerson() {
    return this;
  }
}

let person = new Person();
let getPerson = person.getPerson;

getPerson() === person;
// true

(2)@readonly

readonly修飾器使得屬性或方法不可寫。

import { readonly } from 'core-decorators';

class Meal {
  @readonly
  entree = 'steak';
}

var dinner = new Meal();
dinner.entree = 'salmon';
// Cannot assign to read only property 'entree' of [object Object]

(3)@override

override修飾器檢查子類的方法,是否正確覆蓋了父類的同名方法,若是不正確會報錯。

import { override } from 'core-decorators';

class Parent {
  speak(first, second) {}
}

class Child extends Parent {
  @override
  speak() {}
  // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
  @override
  speaks() {}
  // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  //
  //   Did you mean "speak"?
}

(4)@deprecate (別名@deprecated)

deprecatedeprecated修飾器在控制檯顯示一條警告,表示該方法將廢除。

import { deprecate } from 'core-decorators';

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//     See http://knowyourmeme.com/memes/facepalm for more details.
//

(5)@suppressWarnings

suppressWarnings修飾器抑制deprecated修飾器致使的console.warn()調用。可是,異步代碼發出的調用除外。

import { suppressWarnings } from 'core-decorators';

class Person {
  @deprecated
  facepalm() {}

  @suppressWarnings
  facepalmWithoutWarning() {
    this.facepalm();
  }
}

let person = new Person();

person.facepalmWithoutWarning();
// no warning is logged

 五、使用修飾器實現自動發佈事件

咱們能夠使用修飾器,使得對象的方法被調用時,自動發出一個事件。

const postal = require("postal/lib/postal.lodash");

export default function publish(topic, channel) {
  const channelName = channel || '/';
  const msgChannel = postal.channel(channelName);
  msgChannel.subscribe(topic, v => {
    console.log('頻道: ', channelName);
    console.log('事件: ', topic);
    console.log('數據: ', v);
  });

  return function(target, name, descriptor) {
    const fn = descriptor.value;

    descriptor.value = function() {
      let value = fn.apply(this, arguments);
      msgChannel.publish(topic, value);
    };
  };
}

上面代碼定義了一個名爲publish的修飾器,它經過改寫descriptor.value,使得原方法被調用時,會自動發出一個事件。它使用的事件「發佈/訂閱」庫是Postal.js

它的用法以下。

// index.js
import publish from './publish';

class FooComponent {
  @publish('foo.some.message', 'component')
  someMethod() {
    return { my: 'data' };
  }
  @publish('foo.some.other')
  anotherMethod() {
    // ...
  }
}

let foo = new FooComponent();

foo.someMethod();
foo.anotherMethod();

之後,只要調用someMethod或者anotherMethod,就會自動發出一個事件。

$ bash-node index.js
頻道:  component
事件:  foo.some.message
數據:  { my: 'data' }

頻道:  /
事件:  foo.some.other
數據:  undefined

六、Mixin

在修飾器的基礎上,能夠實現Mixin模式。所謂Mixin模式,就是對象繼承的一種替代方案,中文譯爲「混入」(mix in),意爲在一個對象之中混入另一個對象的方法。

請看下面的例子。

const Foo = {
  foo() { console.log('foo') }
};

class MyClass {}

Object.assign(MyClass.prototype, Foo);

let obj = new MyClass();
obj.foo() // 'foo'

上面代碼之中,對象Foo有一個foo方法,經過Object.assign方法,能夠將foo方法「混入」MyClass類,致使MyClass的實例obj對象都具備foo方法。這就是「混入」模式的一個簡單實現。

下面,咱們部署一個通用腳本mixins.js,將 Mixin 寫成一個修飾器。

export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list);
  };
}

而後,就能夠使用上面這個修飾器,爲類「混入」各類方法。

import { mixins } from './mixins';

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // "foo"

經過mixins這個修飾器,實現了在MyClass類上面「混入」Foo對象的foo方法。

不過,上面的方法會改寫MyClass類的prototype對象,若是不喜歡這一點,也能夠經過類的繼承實現 Mixin。

class MyClass extends MyBaseClass {
  /* ... */
}

上面代碼中,MyClass繼承了MyBaseClass。若是咱們想在MyClass裏面「混入」一個foo方法,一個辦法是在MyClassMyBaseClass之間插入一個混入類,這個類具備foo方法,而且繼承了MyBaseClass的全部方法,而後MyClass再繼承這個類。

let MyMixin = (superclass) => class extends superclass {
  foo() {
    console.log('foo from MyMixin');
  }
};

上面代碼中,MyMixin是一個混入類生成器,接受superclass做爲參數,而後返回一個繼承superclass的子類,該子類包含一個foo方法。

接着,目標類再去繼承這個混入類,就達到了「混入」foo方法的目的。

class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}

let c = new MyClass();
c.foo(); // "foo from MyMixin"

若是須要「混入」多個方法,就生成多個混入類。

class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  /* ... */
}

這種寫法的一個好處,是能夠調用super,所以能夠避免在「混入」過程當中覆蓋父類的同名方法。

let Mixin1 = (superclass) => class extends superclass {
  foo() {
    console.log('foo from Mixin1');
    if (super.foo) super.foo();
  }
};

let Mixin2 = (superclass) => class extends superclass {
  foo() {
    console.log('foo from Mixin2');
    if (super.foo) super.foo();
  }
};

class S {
  foo() {
    console.log('foo from S');
  }
}

class C extends Mixin1(Mixin2(S)) {
  foo() {
    console.log('foo from C');
    super.foo();
  }
}

上面代碼中,每一次混入發生時,都調用了父類的super.foo方法,致使父類的同名方法沒有被覆蓋,行爲被保留了下來。

new C().foo()
// foo from C
// foo from Mixin1
// foo from Mixin2
// foo from

 七、Trait

Trait 也是一種修飾器,效果與 Mixin 相似,可是提供更多功能,好比防止同名方法的衝突、排除混入某些方法、爲混入的方法起別名等等。

下面採用traits-decorator這個第三方模塊做爲例子。這個模塊提供的traits修飾器,不只能夠接受對象,還能夠接受 ES6 類做爲參數。

import { traits } from 'traits-decorator';

class TFoo {
  foo() { console.log('foo') }
}

const TBar = {
  bar() { console.log('bar') }
};

@traits(TFoo, TBar)
class MyClass { }

let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar

上面代碼中,經過traits修飾器,在MyClass類上面「混入」了TFoo類的foo方法和TBar對象的bar方法。

Trait 不容許「混入」同名方法。

import { traits } from 'traits-decorator';

class TFoo {
  foo() { console.log('foo') }
}

const TBar = {
  bar() { console.log('bar') },
  foo() { console.log('foo') }
};

@traits(TFoo, TBar)
class MyClass { }
// 報錯
// throw new Error('Method named: ' + methodName + ' is defined twice.');
//        ^
// Error: Method named: foo is defined twice.

上面代碼中,TFooTBar都有foo方法,結果traits修飾器報錯。

一種解決方法是排除TBarfoo方法。

import { traits, excludes } from 'traits-decorator';

class TFoo {
  foo() { console.log('foo') }
}

const TBar = {
  bar() { console.log('bar') },
  foo() { console.log('foo') }
};

@traits(TFoo, TBar::excludes('foo'))
class MyClass { }

let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar

上面代碼使用綁定運算符(::)在TBar上排除foo方法,混入時就不會報錯了。

另外一種方法是爲TBarfoo方法起一個別名。

import { traits, alias } from 'traits-decorator';

class TFoo {
  foo() { console.log('foo') }
}

const TBar = {
  bar() { console.log('bar') },
  foo() { console.log('foo') }
};

@traits(TFoo, TBar::alias({foo: 'aliasFoo'}))
class MyClass { }

let obj = new MyClass();
obj.foo() // foo
obj.aliasFoo() // foo
obj.bar() // bar

上面代碼爲TBarfoo方法起了別名aliasFoo,因而MyClass也能夠混入TBarfoo方法了。

aliasexcludes方法,能夠結合起來使用。

@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}

上面代碼排除了TExamplefoo方法和bar方法,爲baz方法起了別名exampleBaz

as方法則爲上面的代碼提供了另外一種寫法。

@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}

八、Babel 轉碼器的支持 § 

目前,Babel 轉碼器已經支持 Decorator。

首先,安裝babel-corebabel-plugin-transform-decorators。因爲後者包括在babel-preset-stage-0之中,因此改成安裝babel-preset-stage-0亦可。

$ npm install babel-core babel-plugin-transform-decorators

而後,設置配置文件.babelrc

{
  "plugins": ["transform-decorators"]
}

這時,Babel 就能夠對 Decorator 轉碼了。

腳本中打開的命令以下。

babel.transform("code", {plugins: ["transform-decorators"]})

Babel 的官方網站提供一個在線轉碼器,只要勾選 Experimental,就能支持 Decorator 的在線轉碼。

 十7、Module的語法

一、概述 § 

歷史上,JavaScript 一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,好比 Ruby 的require、Python 的import,甚至就連 CSS 都有@import,可是 JavaScript 任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙。

在 ES6 以前,社區制定了一些模塊加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用於服務器,後者用於瀏覽器。ES6 在語言標準的層面上,實現了模塊功能,並且實現得至關簡單,徹底能夠取代 CommonJS 和 AMD 規範,成爲瀏覽器和服務器通用的模塊解決方案。

ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,CommonJS 模塊就是對象,輸入時必須查找對象屬性。

// CommonJS模塊
let { stat, exists, readFile } = require('fs');

// 等同於
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代碼的實質是總體加載fs模塊(即加載fs的全部方法),生成一個對象(_fs),而後再從這個對象上面讀取 3 個方法。這種加載稱爲「運行時加載」,由於只有運行時才能獲得這個對象,致使徹底沒辦法在編譯時作「靜態優化」。

ES6 模塊不是對象,而是經過export命令顯式指定輸出的代碼,再經過import命令輸入。

// ES6模塊
import { stat, exists, readFile } from 'fs';

上面代碼的實質是從fs模塊加載 3 個方法,其餘方法不加載。這種加載稱爲「編譯時加載」或者靜態加載,即 ES6 能夠在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。固然,這也致使了無法引用 ES6 模塊自己,由於它不是對象。

因爲 ES6 模塊是編譯時加載,使得靜態分析成爲可能。有了它,就能進一步拓寬 JavaScript 的語法,好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態加載帶來的各類好處,ES6 模塊還有如下好處。

  • 再也不須要UMD模塊格式了,未來服務器和瀏覽器都會支持 ES6 模塊格式。目前,經過各類工具庫,其實已經作到了這一點。
  • 未來瀏覽器的新 API 就能用模塊格式提供,再也不必須作成全局變量或者navigator對象的屬性。
  • 再也不須要對象做爲命名空間(好比Math對象),將來這些功能能夠經過模塊提供。

本章介紹 ES6 模塊的語法,下一章介紹如何在瀏覽器和 Node 之中,加載 ES6 模塊。

二、嚴格模式

ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";

嚴格模式主要有如下限制。

  • 變量必須聲明後再使用
  • 函數的參數不能有同名屬性,不然報錯
  • 不能使用with語句
  • 不能對只讀屬性賦值,不然報錯
  • 不能使用前綴 0 表示八進制數,不然報錯
  • 不能刪除不可刪除的屬性,不然報錯
  • 不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]
  • eval不會在它的外層做用域引入變量
  • evalarguments不能被從新賦值
  • arguments不會自動反映函數參數的變化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局對象
  • 不能使用fn.callerfn.arguments獲取函數調用的堆棧
  • 增長了保留字(好比protectedstaticinterface

上面這些限制,模塊都必須遵照。因爲嚴格模式是 ES5 引入的,不屬於 ES6,因此請參閱相關 ES5 書籍,本書再也不詳細介紹了。

其中,尤爲須要注意this的限制。ES6 模塊之中,頂層的this指向undefined,即不該該在頂層代碼使用this

三、export 命令 § 

模塊功能主要由兩個命令構成:exportimportexport命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。

一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸出該變量。下面是一個 JS 文件,裏面使用export命令輸出變量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

上面代碼是profile.js文件,保存了用戶信息。ES6 將其視爲一個模塊,裏面用export命令對外部輸出了三個變量。

export的寫法,除了像上面這樣,還有另一種。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

上面代碼在export命令後面,使用大括號指定所要輸出的一組變量。它與前一種寫法(直接放置在var語句前)是等價的,可是應該優先考慮使用這種寫法。由於這樣就能夠在腳本尾部,一眼看清楚輸出了哪些變量。

export命令除了輸出變量,還能夠輸出函數或類(class)。

export function multiply(x, y) {
  return x * y;
};

上面代碼對外輸出一個函數multiply

一般狀況下,export輸出的變量就是原本的名字,可是能夠使用as關鍵字重命名。

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

上面代碼使用as關鍵字,重命名了函數v1v2的對外接口。重命名後,v2能夠用不一樣的名字輸出兩次。

須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。

// 報錯
export 1;

// 報錯
var m = 1;
export m;

上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出 1,第二種寫法經過變量m,仍是直接輸出 1。1只是一個值,不是接口。正確的寫法是下面這樣。

// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};

上面三種寫法都是正確的,規定了對外的接口m。其餘腳本能夠經過這個接口,取到值1。它們的實質是,在接口名與模塊內部變量之間,創建了一一對應的關係。

一樣的,functionclass的輸出,也必須遵照這樣的寫法。

// 報錯
function f() {}
export f;

// 正確
export function f() {};

// 正確
function f() {}
export {f};

另外,export語句輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代碼輸出變量foo,值爲bar,500 毫秒以後變成baz

這一點與 CommonJS 規範徹底不一樣。CommonJS 模塊輸出的是值的緩存,不存在動態更新,詳見下文《Module 的加載實現》一節。

最後,export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,下一節的import命令也是如此。這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了 ES6 模塊的設計初衷。

function foo() {
  export default 'bar' // SyntaxError
}
foo()

上面代碼中,export語句放在函數之中,結果報錯。

四、import 命令 § 

使用export命令定義了模塊的對外接口之後,其餘 JS 文件就能夠經過import命令加載這個模塊。

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

上面代碼的import命令,用於加載profile.js文件,並從中輸入變量。import命令接受一對大括號,裏面指定要從其餘模塊導入的變量名。大括號裏面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。

若是想爲輸入的變量從新取一個名字,import命令要使用as關鍵字,將輸入的變量重命名。

import { lastName as surname } from './profile.js';

import命令輸入的變量都是隻讀的,由於它的本質是輸入接口。也就是說,不容許在加載模塊的腳本里面,改寫接口。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;

上面代碼中,腳本加載了變量a,對其從新賦值就會報錯,由於a是一個只讀的接口。可是,若是a是一個對象,改寫a的屬性是容許的。

import {a} from './xxx.js'

a.foo = 'hello'; // 合法操做

上面代碼中,a的屬性能夠成功改寫,而且其餘模塊也能夠讀到改寫後的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都看成徹底只讀,輕易不要改變它的屬性。

import後面的from指定模塊文件的位置,能夠是相對路徑,也能夠是絕對路徑,.js後綴能夠省略。若是隻是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置。

import {myMethod} from 'util';

上面代碼中,util是模塊文件名,因爲不帶有路徑,必須經過配置,告訴引擎怎麼取到這個模塊。

注意,import命令具備提高效果,會提高到整個模塊的頭部,首先執行。

foo();

import { foo } from 'my_module';

上面的代碼不會報錯,由於import的執行早於foo的調用。這種行爲的本質是,import命令是編譯階段執行的,在代碼運行以前。

因爲import是靜態執行,因此不能使用表達式和變量,這些只有在運行時才能獲得結果的語法結構。

// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

上面三種寫法都會報錯,由於它們用到了表達式、變量和if結構。在靜態分析階段,這些語法都是無法獲得值的。

最後,import語句會執行所加載的模塊,所以能夠有下面的寫法。

import 'lodash';

上面代碼僅僅執行lodash模塊,可是不輸入任何值。

若是屢次重複執行同一句import語句,那麼只會執行一次,而不會執行屢次。

import 'lodash';
import 'lodash';

上面代碼加載了兩次lodash,可是隻會執行一次。

import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';

上面代碼中,雖然foobar在兩個語句中加載,可是它們對應的是同一個my_module實例。也就是說,import語句是 Singleton 模式。

目前階段,經過 Babel 轉碼,CommonJS 模塊的require命令和 ES6 模塊的import命令,能夠寫在同一個模塊裏面,可是最好不要這樣作。由於import在靜態解析階段執行,因此它是一個模塊之中最先執行的。下面的代碼可能不會獲得預期結果。

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';

 五、模塊的總體加載 § 

除了指定加載某個輸出值,還能夠使用總體加載,即用星號(*)指定一個對象,全部輸出值都加載在這個對象上面。

下面是一個circle.js文件,它輸出兩個方法areacircumference

// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

如今,加載這個模塊。

// main.js

import { area, circumference } from './circle';

console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));

上面寫法是逐一指定要加載的方法,總體加載的寫法以下。

import * as circle from './circle';

console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));

注意,模塊總體加載所在的那個對象(上例是circle),應該是能夠靜態分析的,因此不容許運行時改變。下面的寫法都是不容許的。

import * as circle from './circle';

// 下面兩行都是不容許的
circle.foo = 'hello';
circle.area = function () {};

 七、export 與 import 的複合寫法 § 

若是在一個模塊之中,先輸入後輸出同一個模塊,import語句能夠與export語句寫在一塊兒。

export { foo, bar } from 'my_module';

// 能夠簡單理解爲
import { foo, bar } from 'my_module';
export { foo, bar };

上面代碼中,exportimport語句能夠結合在一塊兒,寫成一行。但須要注意的是,寫成一行之後,foobar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foobar

模塊的接口更名和總體輸出,也能夠採用這種寫法。

// 接口更名
export { foo as myFoo } from 'my_module';

// 總體輸出
export * from 'my_module';

默認接口的寫法以下。

export { default } from 'foo';

具名接口改成默認接口的寫法以下。

export { es6 as default } from './someModule';

// 等同於
import { es6 } from './someModule';
export default es6;

一樣地,默認接口也能夠更名爲具名接口。

export { default as es6 } from './someModule';

下面三種import語句,沒有對應的複合寫法。

import * as someIdentifier from "someModule";
import someIdentifier from "someModule";
import someIdentifier, { namedIdentifier } from "someModule";

爲了作到形式的對稱,如今有提案,提出補上這三種複合寫法。

export * as someIdentifier from "someModule";
export someIdentifier from "someModule";
export someIdentifier, { namedIdentifier } from "someModule";

 八、模塊的繼承

模塊之間也能夠繼承。

假設有一個circleplus模塊,繼承了circle模塊。

// circleplus.js

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

上面代碼中的export *,表示再輸出circle模塊的全部屬性和方法。注意,export *命令會忽略circle模塊的default方法。而後,上面代碼又輸出了自定義的e變量和默認方法。

這時,也能夠將circle的屬性或方法,更名後再輸出。

// circleplus.js

export { area as circleArea } from 'circle';

上面代碼表示,只輸出circle模塊的area方法,且將其更名爲circleArea

加載上面模塊的寫法以下。

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));

上面代碼中的import exp表示,將circleplus模塊的默認方法加載爲exp方法。

九、跨模塊常量 § 

本書介紹const命令的時候說過,const聲明的常量只在當前代碼塊有效。若是想設置跨模塊的常量(即跨多個文件),或者說一個值要被多個模塊共享,能夠採用下面的寫法。

// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

若是要使用的常量很是多,能夠建一個專門的constants目錄,將各類常量寫在不一樣的文件裏面,保存在該目錄下。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

而後,將這些文件輸出的常量,合併在index.js裏面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的時候,直接加載index.js就能夠了。

// script.js
import {db, users} from './constants/index';

 十、import()

簡介

前面介紹過,import命令會被 JavaScript 引擎靜態分析,先於模塊內的其餘語句執行(import命令叫作「鏈接」 binding 其實更合適)。因此,下面的代碼會報錯。

// 報錯
if (x === 2) {
  import MyModual from './myModual';
}

上面代碼中,引擎處理import語句是在編譯時,這時不會去分析或執行if語句,因此import語句放在if代碼塊之中毫無心義,所以會報句法錯誤,而不是執行時錯誤。也就是說,importexport命令只能在模塊的頂層,不能在代碼塊之中(好比,在if代碼塊之中,或在函數之中)。

這樣的設計,當然有利於編譯器提升效率,但也致使沒法在運行時加載模塊。在語法上,條件加載就不可能實現。若是import命令要取代 Node 的require方法,這就造成了一個障礙。由於require是運行時加載模塊,import命令沒法取代require的動態加載功能。

const path = './' + fileName;
const myModual = require(path);

上面的語句就是動態加載,require到底加載哪個模塊,只有運行時才知道。import命令作不到這一點。

所以,有一個提案,建議引入import()函數,完成動態加載。

import(specifier)

上面代碼中,import函數的參數specifier,指定所要加載的模塊的位置。import命令可以接受什麼參數,import()函數就能接受什麼參數,二者區別主要是後者爲動態加載。

import()返回一個 Promise 對象。下面是一個例子。

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
  });

import()函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也能夠使用。它是運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊。另外,import()函數與所加載的模塊沒有靜態鏈接關係,這點也是與import語句不相同。import()相似於 Node 的require方法,區別主要是前者是異步加載,後者是同步加載。

適用場合

下面是import()的一些適用場合。

(1)按需加載。

import()能夠在須要的時候,再加載某個模塊。

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

上面代碼中,import()方法放在click事件的監聽函數之中,只有用戶點擊了按鈕,纔會加載這個模塊。

(2)條件加載

import()能夠放在if代碼塊,根據不一樣的狀況,加載不一樣的模塊。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

上面代碼中,若是知足條件,就加載模塊 A,不然加載模塊 B。

(3)動態的模塊路徑

import()容許模塊路徑動態生成。

import(f())
.then(...);

上面代碼中,根據函數f的返回結果,加載不一樣的模塊。

注意點

import()加載模塊成功之後,這個模塊會做爲一個對象,看成then方法的參數。所以,能夠使用對象解構賦值的語法,獲取輸出接口。

import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});

上面代碼中,export1export2都是myModule.js的輸出接口,能夠解構得到。

若是模塊有default輸出接口,能夠用參數直接得到。

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});

上面的代碼也能夠使用具名輸入的形式。

import('./myModule.js')
.then(({default: theDefault}) => {
  console.log(theDefault);
});

若是想同時加載多個模塊,能夠採用下面的寫法。

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import()也能夠用在 async 函數之中。

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();

 十7、Module 的加載實現

一、瀏覽器加載 § 

傳統方法

HTML 網頁中,瀏覽器經過<script>標籤加載 JavaScript 腳本。

<!-- 頁面內嵌的腳本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

上面代碼中,因爲瀏覽器腳本的默認語言是 JavaScript,所以type="application/javascript"能夠省略。

默認狀況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標籤就會停下來,等到執行完腳本,再繼續向下渲染。若是是外部腳本,還必須加入腳本下載的時間。

若是腳本體積很大,下載和執行的時間就會很長,所以形成瀏覽器堵塞,用戶會感受到瀏覽器「卡死」了,沒有任何響應。這顯然是很很差的體驗,因此瀏覽器容許腳本異步加載,下面就是兩種異步加載的語法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代碼中,<script>標籤打開deferasync屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。

deferasync的區別是:defer要等到整個頁面在內存中正常渲染結束(DOM 結構徹底生成,以及其餘腳本執行完成),纔會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本之後,再繼續渲染。一句話,defer是「渲染完再執行」,async是「下載完就執行」。另外,若是有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的。

加載規則

瀏覽器加載 ES6 模塊,也使用<script>標籤,可是要加入type="module"屬性。

<script type="module" src="./foo.js"></script>

上面代碼在網頁中插入一個模塊foo.js,因爲type屬性設爲module,因此瀏覽器知道這是一個 ES6 模塊。

瀏覽器對於帶有type="module"<script>,都是異步加載,不會形成堵塞瀏覽器,即等到整個頁面渲染完,再執行模塊腳本,等同於打開了<script>標籤的defer屬性。

<script type="module" src="./foo.js"></script>
<!-- 等同於 -->
<script type="module" src="./foo.js" defer></script>

若是網頁有多個<script type="module">,它們會按照在頁面出現的順序依次執行。

<script>標籤的async屬性也能夠打開,這時只要加載完成,渲染引擎就會中斷渲染當即執行。執行完成後,再恢復渲染。

<script type="module" src="./foo.js" async></script>

一旦使用了async屬性,<script type="module">就不會按照在頁面出現的順序執行,而是隻要該模塊加載完成,就執行該模塊。

ES6 模塊也容許內嵌在網頁中,語法行爲與加載外部腳本徹底一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

對於外部的模塊腳本(上例是foo.js),有幾點須要注意。

  • 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
  • 模塊腳本自動採用嚴格模式,無論有沒有聲明use strict
  • 模塊之中,能夠使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也能夠使用export命令輸出對外接口。
  • 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
  • 同一個模塊若是加載屢次,將只執行一次。

下面是一個示例模塊。

import utils from 'https://example.com/js/utils.js';

const x = 1;

console.log(x === window.x); //false
console.log(this === undefined); // true

利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。

const isNotModuleScript = this !== undefined;

二、ES6 模塊與 CommonJS 模塊的差別

討論 Node 加載 ES6 模塊以前,必須瞭解 ES6 模塊與 CommonJS 模塊徹底不一樣。

它們有兩個重大差別。

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第二個差別是由於 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

下面重點解釋第一個差別。

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上面代碼說明,lib.js模塊加載之後,它的內部變化就影響不到輸出的mod.counter了。這是由於mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。

$ node main.js
3
4

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的import有點像 Unix 系統的「符號鏈接」,原始值變了,import加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。

仍是舉上面的例子。

// lib.js
export let counter = 3;
export function incCounter() {
  counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

上面代碼說明,ES6 模塊輸入的變量counter是活的,徹底反應其所在模塊lib.js內部的變化。

再舉一個出如今export一節中的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代碼中,m1.js的變量foo,在剛加載時等於bar,過了 500 毫秒,又變爲等於baz

讓咱們看看,m2.js可否正確讀取這個變化。

$ babel-node m2.js

bar
baz

上面代碼代表,ES6 模塊不會緩存運行結果,而是動態地去被加載的模塊取值,而且變量老是綁定其所在的模塊。

因爲 ES6 輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

上面代碼中,main.jslib.js輸入變量obj,能夠對obj添加屬性,可是從新賦值就會報錯。由於變量obj指向的地址是隻讀的,不能從新賦值,這就比如main.js創造了一個名爲objconst變量。

最後,export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();

上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';

如今執行main.js,輸出的是1

$ babel-node main.js
1

這就證實了x.jsy.js加載的都是C的同一個實例。

三、Node 加載

概述

Node 對 ES6 模塊的處理比較麻煩,由於它有本身的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將二者分開,ES6 模塊和 CommonJS 採用各自的加載方案。

Node 要求 ES6 模塊採用.mjs後綴文件名。也就是說,只要腳本文件裏面使用import或者export命令,那麼就必須採用.mjs後綴名。require命令不能加載.mjs文件,會報錯,只有import命令才能夠加載.mjs文件。反過來,.mjs文件裏面也不能使用require命令,必須使用import

目前,這項功能還在試驗階段。安裝 Node v8.5.0 或以上版本,要用--experimental-modules參數才能打開該功能。

$ node --experimental-modules my-app.mjs

爲了與瀏覽器的import加載規則相同,Node 的.mjs文件支持 URL 路徑。

import './foo?query=1'; // 加載 ./foo 傳入參數 ?query=1

上面代碼中,腳本路徑帶有參數?query=1,Node 會按 URL 規則解讀。同一個腳本只要參數不一樣,就會被加載屢次,而且保存成不一樣的緩存。因爲這個緣由,只要文件名中含有:%#?等特殊字符,最好對這些字符進行轉義。

目前,Node 的import命令只支持加載本地模塊(file:協議),不支持加載遠程模塊。

若是模塊名不含路徑,那麼import命令會去node_modules目錄尋找這個模塊。

import 'baz';
import 'abc/123';

若是模塊名包含路徑,那麼import命令會按照路徑去尋找這個名字的腳本文件。

import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';

若是腳本文件省略了後綴名,好比import './foo',Node 會依次嘗試四個後綴名:./foo.mjs./foo.js./foo.json./foo.node。若是這些腳本文件都不存在,Node 就會去加載./foo/package.jsonmain字段指定的腳本。若是./foo/package.json不存在或者沒有main字段,那麼就會依次加載./foo/index.mjs./foo/index.js./foo/index.json./foo/index.node。若是以上四個文件仍是都不存在,就會拋出錯誤。

最後,Node 的import命令是異步加載,這一點與瀏覽器的處理方法相同。

內部變量

ES6 模塊應該是通用的,同一個模塊不用修改,就能夠用在瀏覽器環境和服務器環境。爲了達到這個目標,Node 規定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內部變量。

首先,就是this關鍵字。ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊,這是二者的一個重大差別。

其次,如下這些頂層變量在 ES6 模塊之中都是不存在的。

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

若是你必定要使用這些變量,有一個變通方法,就是寫一個 CommonJS 模塊輸出這些變量,而後再用 ES6 模塊加載這個 CommonJS 模塊。可是這樣一來,該 ES6 模塊就不能直接用於瀏覽器環境了,因此不推薦這樣作。

// expose.js
module.exports = {__dirname};

// use.mjs
import expose from './expose.js';
const {__dirname} = expose;

上面代碼中,expose.js是一個 CommonJS 模塊,輸出變量__dirname,該變量在 ES6 模塊之中不存在。ES6 模塊加載expose.js,就能夠獲得__dirname

ES6 模塊加載 CommonJS 模塊

CommonJS 模塊的輸出都定義在module.exports這個屬性上面。Node 的import命令加載 CommonJS 模塊,Node 會自動將module.exports屬性,看成模塊的默認輸出,即等同於export default xxx

下面是一個 CommonJS 模塊。

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// 等同於
export default {
  foo: 'hello',
  bar: 'world'
};

import命令加載上面的模塊,module.exports會被視爲默認輸出,即import命令實際上輸入的是這樣一個對象{ default: module.exports }

因此,一共有三種寫法,能夠拿到 CommonJS 模塊的module.exports

// 寫法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};

// 寫法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};

// 寫法三
import * as baz from './a';
// baz = {
//   get default() {return module.exports;},
//   get foo() {return this.default.foo}.bind(baz),
//   get bar() {return this.default.bar}.bind(baz)
// }

上面代碼的第三種寫法,能夠經過baz.default拿到module.exportsfoo屬性和bar屬性就是能夠經過這種方法拿到了module.exports

下面是一些例子。

// b.js
module.exports = null;

// es.js
import foo from './b';
// foo = null;

import * as bar from './b';
// bar = { default:null };

上面代碼中,es.js採用第二種寫法時,要經過bar.default這樣的寫法,才能拿到module.exports

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function

上面代碼中,bar自己是一個對象,不能看成函數調用,只能經過bar.default調用。

CommonJS 模塊的輸出緩存機制,在 ES6 加載方式下依然有效。

// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);

上面代碼中,對於加載foo.js的腳本,module.exports將一直是123,而不會變成null

因爲 ES6 模塊是編譯時肯定輸出接口,CommonJS 模塊是運行時肯定輸出接口,因此採用import命令加載 CommonJS 模塊時,不容許採用下面的寫法。

// 不正確
import { readFile } from 'fs';

上面的寫法不正確,由於fs是 CommonJS 格式,只有在運行時才能肯定readFile接口,而import命令要求編譯時就肯定這個接口。解決方法就是改成總體輸入。

// 正確的寫法一
import * as express from 'express';
const app = express.default();

// 正確的寫法二
import express from 'express';
const app = express();

CommonJS 模塊加載 ES6 模塊

CommonJS 模塊加載 ES6 模塊,不能使用require命令,而要使用import()函數。ES6 模塊的全部輸出接口,會成爲輸入對象的屬性。

// es.mjs
let foo = { bar: 'my-default' };
export default foo;

// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
//   get default() {
//     ...
//   }
// }
console.log(es_namespace.default);
// { bar:'my-default' }

上面代碼中,default接口變成了es_namespace.default屬性。

下面是另外一個例子。4

// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};

// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
//   get foo() {return foo;}
//   get bar() {return foo;}
//   get f() {return f;}
//   get c() {return c;}
// }

四、循環加載

「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。

// a.js
var b = require('b');

// b.js
var a = require('a');

一般,「循環加載」表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。

可是實際上,這是很難避免的,尤爲是依賴關係複雜的大項目,很容易出現a依賴bb依賴cc又依賴a這樣的狀況。這意味着,模塊加載機制必須考慮「循環加載」的狀況。

對於 JavaScript 語言來講,目前最多見的兩種模塊格式 CommonJS 和 ES6,處理「循環加載」的方法是不同的,返回的結果也不同。

CommonJS 模塊的加載原理

介紹 ES6 如何處理「循環加載」以前,先介紹目前最流行的 CommonJS 模塊格式的加載原理。

CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

上面代碼就是 Node 內部加載模塊後生成的一個對象。該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略了。

之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。

CommonJS 模塊的循環加載

CommonJS 模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。

讓咱們來看,Node 官方文檔裏面的例子。腳本文件a.js代碼以下。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');

上面代碼之中,a.js腳本先輸出一個done變量,而後加載另外一個腳本文件b.js。注意,此時a.js代碼就停在這裏,等待b.js執行完畢,再往下執行。

再看b.js的代碼。

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');

上面代碼之中,b.js執行到第二行,就會去加載a.js,這時,就發生了「循環加載」。系統會去a.js模塊對應對象的exports屬性取值,但是由於a.js尚未執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。

a.js已經執行的部分,只有一行。

exports.done = false;

所以,對於b.js來講,它從a.js只輸入一個變量done,值爲false

而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.js接着往下執行,直到執行完畢。咱們寫一個腳本main.js,驗證這個過程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

執行main.js,運行結果以下。

$ node main.js

在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true

上面的代碼證實了兩件事。一是,在b.js之中,a.js沒有執行完畢,只執行了第一行。二是,main.js執行到第二行時,不會再次執行b.js,而是輸出緩存的b.js的執行結果,即它的第四行。

exports.done = true;

總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。

另外,因爲 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量的時候,必須很是當心。

var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一個部分加載時的值
};

上面代碼中,若是發生循環加載,require('a').foo的值極可能後面會被改寫,改用require('a')會更保險一點。

ES6 模塊的循環加載

ES6 處理「循環加載」與 CommonJS 有本質的不一樣。ES6 模塊是動態引用,若是使用import從一個模塊加載變量(即import foo from 'foo'),那些變量不會被緩存,而是成爲一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。

請看下面這個例子。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

上面代碼中,a.mjs加載b.mjsb.mjs又加載a.mjs,構成循環加載。執行a.mjs,結果以下。

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined

上面代碼中,執行a.mjs之後會報錯,foo變量未定義,這是爲何?

讓咱們一行行來看,ES6 循環加載是怎麼處理的。首先,執行a.mjs之後,引擎發現它加載了b.mjs,所以會優先執行b.mjs,而後再執行a.mjs。接着,執行b.mjs的時候,已知它從a.mjs輸入了foo接口,這時不會去執行a.mjs,而是認爲這個接口已經存在了,繼續往下執行。執行到第三行console.log(foo)的時候,才發現這個接口根本沒定義,所以報錯。

解決這個問題的方法,就是讓b.mjs運行的時候,foo已經有定義了。這能夠經過將foo寫成函數來解決。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};

// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

這時再執行a.mjs就能夠獲得預期結果。

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

這是由於函數具備提高做用,在執行import {bar} from './b'時,函數foo就已經有定義了,因此b.mjs加載的時候不會報錯。這也意味着,若是把函數foo改寫成函數表達式,也會報錯。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};

上面代碼的第四行,改爲了函數表達式,就不具備提高做用,執行就會報錯。

咱們再來看 ES6 模塊加載器SystemJS給出的一個例子。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n === 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
  return n !== 0 && even(n - 1);
}

上面代碼中,even.js裏面的函數even有一個參數n,只要不等於 0,就會減去 1,傳入加載的odd()odd.js也會作相似操做。

運行上面這段代碼,結果以下。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

上面代碼中,參數n從 10 變爲 0 的過程當中,even()一共會執行 6 次,因此變量counter等於 6。第二次調用even()時,參數n從 20 變爲 0,even()一共會執行 11 次,加上前面的 6 次,因此變量counter等於 17。

這個例子要是改寫成 CommonJS,就根本沒法執行,會報錯。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
var even = require('./even').even;
module.exports = function (n) {
  return n != 0 && even(n - 1);
}

上面代碼中,even.js加載odd.js,而odd.js又去加載even.js,造成「循環加載」。這時,執行引擎就會輸出even.js已經執行的部分(不存在任何結果),因此在odd.js之中,變量even等於undefined,等到後面調用even(n - 1)就會報錯。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function

 五、ES6 模塊的轉碼

瀏覽器目前還不支持 ES6 模塊,爲了如今就能使用,能夠將轉爲 ES5 的寫法。除了 Babel 能夠用來轉碼以外,還有如下兩個方法,也能夠用來轉碼。

ES6 module transpiler

ES6 module transpiler是 square 公司開源的一個轉碼器,能夠將 ES6 模塊轉爲 CommonJS 模塊或 AMD 模塊的寫法,從而在瀏覽器中使用。

首先,安裝這個轉碼器。

$ npm install -g es6-module-transpiler

而後,使用compile-modules convert命令,將 ES6 模塊文件轉碼。

$ compile-modules convert file1.js file2.js

-o參數能夠指定轉碼後的文件名。

$ compile-modules convert -o out.js file1.js

SystemJS

另外一種解決方法是使用 SystemJS。它是一個墊片庫(polyfill),能夠在瀏覽器內加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉爲 ES5 格式。它在後臺調用的是 Google 的 Traceur 轉碼器。

使用時,先在網頁內載入system.js文件。

<script src="system.js"></script>

而後,使用System.import方法加載模塊文件。

<script>
  System.import('./app.js');
</script>

上面代碼中的./app,指的是當前目錄下的 app.js 文件。它能夠是 ES6 模塊文件,System.import會自動將其轉碼。

須要注意的是,System.import使用異步加載,返回一個 Promise 對象,能夠針對這個對象編程。下面是一個模塊文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}

而後,在網頁內加載這個模塊文件。

<script>

System.import('app/es6-file').then(function(m) {
  console.log(new m.q().es6); // hello
});

</script>

上面代碼中,System.import方法返回的是一個 Promise 對象,因此能夠用then方法指定回調函數。

相關文章
相關標籤/搜索