ES6筆記總結

一. let/const:javascript

1. 「暫時性死區」概念:在代碼塊內,使用let/const命令聲明變量以前,該變量都是不可用的。這在語法上,稱爲「暫時性死區」(temporal dead zone,簡稱 TDZ)。「暫時性死區」也意味着typeof再也不是一個百分之百安全的操做。html

2. 塊做用域與函數聲明:java

 
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重複聲明一次函數f
    function f() { console.log('I am inside!'); }
  }

  f();
}());  // I am inside!
 

上面代碼至關於如下代碼:node

 
// ES5 環境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());
 

在 ES5 中運行,會獲得「I am inside!」,由於在if內聲明的函數f會被提高到函數頭部。ES6 就徹底不同了,理論上會獲得「I am outside!」。由於塊級做用域內聲明的函數相似於let,對做用域以外沒有影響。可是,若是你真的在 ES6 瀏覽器中運行一下上面的代碼,是會報錯的。原來,若是改變了塊級做用域內聲明的函數的處理規則,顯然會對老代碼產生很大影響。爲了減輕所以產生的不兼容問題,ES6 在附錄 B裏面規定,瀏覽器的實現能夠不遵照上面的規定,有本身的行爲方式es6

  • 容許在塊級做用域內聲明函數。
  • 函數聲明相似於var,即會提高到全局做用域或函數做用域的頭部。
  • 同時,函數聲明還會提高到所在的塊級做用域的頭部。

根據這三條規則,在瀏覽器的 ES6 環境中,塊級做用域內聲明的函數,行爲相似於var聲明的變量。web

 
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重複聲明一次函數f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function
 

上面的代碼在符合 ES6 的瀏覽器中,都會報錯,由於實際運行的是下面的代碼:算法

 
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function
 

 3. const:const實際上保證的,並非變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。對於簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,所以等同於常量。但對於複合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,const只能保證這個指針是固定的(即老是指向另外一個固定的地址)。編程

4. 聲明變量:json

ES6 一共有 6 種聲明變量的方法:var, function, let, const, imort, class數組

5. var命令和function命令聲明的全局變量,依舊是頂層對象的屬性;另外一方面規定,let命令、const命令、class命令聲明的全局變量,不屬於頂層對象的屬性:

 
var a = 1;
// 若是在 Node 的 REPL 環境,能夠寫成 global.a
// 或者採用通用方法,寫成 this.a
window.a // 1

let b = 1;
window.b // undefined
 

二. 解構賦值:數組/對象/字符串/數組/布爾值

1. 只要某種數據結構具備 Iterator 接口,均可以採用數組形式的解構賦值(數組,Set,Generator等)。

2. 原則:解構賦值的規則是,只要等號右邊的值不是對象或數組,就先將其轉爲對象。因爲undefinednull沒法轉爲對象,因此對它們進行解構賦值,都會報錯

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]
 

 4. 解構賦值可使用圓括號的狀況:只有一種:賦值語句的非模式部分,可使用圓括號。

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

上面三行語句均可以正確執行,由於首先它們都是賦值語句,而不是聲明語句;其次它們的圓括號都不屬於模式的一部分。第一行語句中,模式是取數組的第一個成員,跟圓括號無關;第二行語句中,模式是p,而不是d;第三行語句與第一行語句的性質一致。

三. 字符串擴展:

1. 模板字符串:

(1)模板字符串之中還能調用函數。若是大括號中的值不是字符串,將按照通常的規則轉爲字符串。好比,大括號中是一個對象,將默認調用對象的toString方法。(模板字符串的大括號內部,就是執行 JavaScript 代碼)

function fn() {
  return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

(2)標籤模板:緊跟在一個函數名後面,該函數將被調用來處理這個模板字符串。這被稱爲「標籤模板」功能(tagged template)。

alert`123`
// 等同於
alert(123)
let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同於
tag(['Hello ', ' world ', ''], 15, 50);

 四. 函數的擴展:

1. 函數的 length 屬性:

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

上面代碼中,length屬性的返回值,等於函數的參數個數減去指定了默認值的參數個數。這是由於length屬性的含義是,該函數預期傳入的參數個數。某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,後文的 rest 參數也不會計入length屬性。

(function(...args) {}).length // 0

若是設置了默認值的參數不是尾參數,那麼length屬性也再也不計入後面的參數了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

 2. 做用域:

一旦設置了參數的默認值,函數進行聲明初始化時,參數會造成一個單獨的做用域(context)。等到初始化結束,這個做用域就會消失。這種語法行爲,在不設置參數默認值時,是不會出現的。

 
var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2
 
var x = 1;
function foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  console.log(x);
}

foo() // 2
x // 1
 

 3. rest 參數:

  • rest 參數以後不能再有其餘參數(即只能是最後一個參數),不然會報錯。
// 正確的
function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

// 報錯
function f(a, ...b, c) {
  // ...
}
  • 函數的length屬性,不包括 rest 參數。
(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

 4. 嚴格模式:只要函數參數使用了默認值、解構賦值、或者擴展運算符,那麼函數內部就不能顯式設定爲嚴格模式,不然會報錯。

函數執行的時候,先執行函數參數,而後再執行函數體。這樣就有一個不合理的地方,只有從函數體之中,才能知道參數是否應該以嚴格模式執行,可是參數卻應該先於函數體執行。

// 報錯
function doSomething(value = 070) {
  'use strict';
  return value;
}

 5. name 屬性:

  • Function構造函數返回的函數實例,name屬性的值爲anonymous
(new Function).name // "anonymous"
  • bind返回的函數,name屬性值會加上bound前綴。
function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

 6. 箭頭函數:

注意點:

(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。

(2)不能夠看成構造函數,也就是說,不可使用new命令,不然會拋出一個錯誤。

(3)不可使用arguments對象,該對象在函數體內不存在。若是要用,能夠用 rest 參數代替。

(4)不可使用yield命令,所以箭頭函數不能用做 Generator 函數。

上面四點中,第一點尤爲值得注意。this對象的指向是可變的,可是在箭頭函數中,它是固定的。

this指向的固定化,並非由於箭頭函數內部有綁定this的機制,實際緣由是箭頭函數根本沒有本身的this,致使內部的this就是外層代碼塊的this。正是由於它沒有this,因此也就不能用做構造函數。

 
function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數
  setInterval(() => this.s1++, 1000);
  // 普通函數
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
 
 
下面的代碼之中有幾個this?

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
 

上面代碼之中,只有一個this,就是函數foothis,因此t1t2t3都輸出一樣的結果。由於全部的內層函數都是箭頭函數,都沒有本身的this,它們的this其實都是最外層foo函數的this

除了this,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:argumentssupernew.target

 

因爲箭頭函數沒有本身的this,因此固然也就不能用call()apply()bind()這些方法去改變this的指向。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']

 

注:不適合使用箭頭函數的場合:

(1)定義函數的方法,且該方法內部包括this

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

上面代碼中,cat.jumps()方法是一個箭頭函數,這是錯誤的。調用cat.jumps()時,若是是普通函數,該方法內部的this指向cat;若是寫成上面那樣的箭頭函數,使得this指向全局對象,所以不會獲得預期結果。

(2)須要動態this的時候,也不該使用箭頭函數。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代碼運行時,點擊按鈕會報錯,由於button的監聽函數是一個箭頭函數,致使裏面的this就是全局對象。若是改爲普通函數,this就會動態指向被點擊的按鈕對象。

7. 嵌套的箭頭函數:

部署管道機制(pipeline)的例子,即前一個函數的輸出是後一個函數的輸入:

 
const pipeline = (...funcs) =>
  val => funcs.reduce((a, b) => b(a), val);

const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);

addThenMult(5)
// 12
 

若是以爲上面的寫法可讀性比較差,也能夠採用下面的寫法。

const plus1 = a => a + 1;
const mult2 = a => a * 2;

mult2(plus1(5))
// 12

 8. 尾調用優化:

尾調用:指某個函數的最後一步是調用另外一個函數。尾調用不必定出如今函數尾部,只要是最後一步操做便可。

 
// 如下都不屬於尾調用

// 狀況一
function f(x){
  let y = g(x);
  return y;
}

// 狀況二
function f(x){
  return g(x) + 1;
}

// 狀況三
function f(x){        
  g(x);
}

// 狀況三至關於如下
function f(x){ g(x); return undefined; }
 
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

上面代碼中,函數mn都屬於尾調用,由於它們都是函數f的最後一步操做。

9. 尾遞歸:

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

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代碼是一個階乘函數,計算n的階乘,最多須要保存n個調用記錄,複雜度 O(n) 。

若是改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1) 。

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

factorial(5, 1) // 120

  • 尾遞歸優化:
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

上面代碼中,參數 total 有默認值1,因此調用時不用提供這個值。

總結一下,遞歸本質上是一種循環操做。純粹的函數式編程語言沒有循環操做命令,全部的循環都用遞歸實現,這就是爲何尾遞歸對這些語言極其重要。對於其餘支持"尾調用優化"的語言(好比Lua,ES6),只須要知道循環能夠用遞歸代替,而一旦使用遞歸,就最好使用尾遞歸。

五. 數組的擴展:

1. 擴展運算符:

擴展運算符(spread)是三個點(...)。它比如 rest 參數的逆運算,將一個數組轉爲用逗號分隔的參數序列。

 
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
 

若是擴展運算符後面是一個空數組,則不產生任何效果。

擴展運算符若是放在括號中,JavaScript 引擎就會認爲這是函數調用。若是這時不是函數調用,就會報錯。

 
(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1 2
 

 擴展運算符內部調用的是數據結構的 Iterator 接口,所以只要具備 Iterator 接口的對象,均可以使用擴展運算符,好比 Map 結構。

 
let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]
 

 2. Array.from():

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

(2) Array.from()的另外一個應用是,將字符串轉爲數組,而後返回字符串的長度。由於它能正確處理各類 Unicode 字符,能夠避免 JavaScript 將大於\uFFFF的 Unicode 字符,算做兩個字符的 bug。

3. Array.of():Array.of方法用於將一組值,轉換爲數組。Array.of老是返回參數值組成的數組。若是沒有參數,就返回一個空數組。

4. copyWithin():

數組實例的copyWithin方法,在當前數組內部,將指定位置的成員複製到其餘位置(會覆蓋原有成員),而後返回當前數組。也就是說,使用這個方法,會修改當前數組。

 

Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三個參數。

  • target(必需):從該位置開始替換數據。若是爲負值,表示倒數。
  • start(可選):從該位置開始讀取數據,默認爲 0。若是爲負值,表示倒數。
  • end(可選):到該位置前中止讀取數據,默認等於數組長度。若是爲負值,表示倒數。

這三個參數都應該是數值,若是不是,會自動轉爲數值。

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

上面代碼表示將從 3 號位直到數組結束的成員(4 和 5),複製到從 0 號位開始的位置,結果覆蓋了原來的 1 和 2。

 5. find() / findIndex():

數組實例的find方法,用於找出第一個符合條件的數組成員。它的參數是一個回調函數,全部數組成員依次執行該回調函數,直到找出第一個返回值爲true的成員,而後返回該成員。若是沒有符合條件的成員,則返回undefined

數組實例的findIndex方法的用法與find方法很是相似,返回第一個符合條件的數組成員的位置,若是全部成員都不符合條件,則返回-1

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

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

上面代碼中,indexOf方法沒法識別數組的NaN成員,可是findIndex方法能夠藉助Object.is方法作到。

6. fill():給定值,填充一個數組。接受第二個和第三個參數,用於指定填充的起始位置和結束位置。

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

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

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

7. entries() / keys() / values() :

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

 
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
 

若是不使用for...of循環,能夠手動調用遍歷器對象的next方法,進行遍歷。

let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

8. includes():Array.prototype.includes方法返回一個布爾值,表示某個數組是否包含給定的值,與字符串的includes方法相似。第二個參數表示搜索的起始位置,默認爲0

 
[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true
 

indexOf方法有兩個缺點,一是不夠語義化,它的含義是找到參數值的第一個出現位置,因此要去比較是否不等於-1,表達起來不夠直觀。二是,它內部使用嚴格相等運算符(===)進行判斷,這會致使對NaN的誤判。

下面代碼用來檢查當前環境是否支持該方法,若是不支持,部署一個簡易的替代版本。

const contains = (() =>
  Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some(el => el === value)
)();
contains(['foo', 'bar'], 'baz'); // => false

另外,Map 和 Set 數據結構有一個has方法,須要注意與includes區分。

  • Map 結構的has方法,是用來查找鍵名的,好比Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set 結構的has方法,是用來查找值的,好比Set.prototype.has(value)WeakSet.prototype.has(value)

9. flat() / flatMap():

數組的成員有時仍是數組,Array.prototype.flat()用於將嵌套的數組「拉平」,變成一維的數組。該方法返回一個新數組,對原數據沒有影響。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

flat()默認只會「拉平」一層,若是想要「拉平」多層的嵌套數組,能夠將flat()方法的參數寫成一個整數,表示想要拉平的層數,默認爲1。若是無論有多少層嵌套,都要轉成一維數組,能夠用Infinity關鍵字做爲參數。

 
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
 

flatMap()方法對原數組的每一個成員執行一個函數(至關於執行Array.prototype.map()),而後對返回值組成的數組執行flat()方法。該方法返回一個新數組,不改變原數組。flatMap()只能展開一層數組。

// 至關於 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap()方法的參數是一個遍歷函數,該函數能夠接受三個參數,分別是當前數組成員、當前數組成員的位置(從零開始)、原數組。還能夠有第二個參數,用來綁定遍歷函數裏面的this

arr.flatMap(function callback(currentValue[, index[, array]]) {
  // ...
}[, thisArg])

10. 數組的空位:

數組的空位指,數組的某一個位置沒有任何值。好比,Array構造函數返回的數組都是空位。空位不是undefined,一個位置的值等於undefined,依然是有值的。空位是沒有任何值,in運算符能夠說明這一點。

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

ES5 對空位的處理,已經很不一致了,大多數狀況下會忽略空位。

  • forEach()filter()reduce()every() 和some()都會跳過空位。
  • map()會跳過空位,但會保留這個值
  • join()toString()會將空位視爲undefined,而undefinednull會被處理成空字符串。
 
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1

// filter方法
['a',,'b'].filter(x => true) // ['a','b']

// every方法
[,'a'].every(x => x==='a') // true

// reduce方法
[1,,2].reduce((x,y) => x+y) // 3

// some方法
[,'a'].some(x => x !== 'a') // false

// map方法
[,'a'].map(x => 1) // [,1]

// join方法
[,'a',undefined,null].join('#') // "#a##"

// toString方法
[,'a',undefined,null].toString() // ",a,,"
 

ES6 則是明確將空位轉爲undefined。好比 Array.from():

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

但不是全部 ES6 的方法對於空位的處理規則都是一致的,因此建議避免出現空位。

六. 對象的擴展:

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

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

obj.hello() // hi

注意,屬性名錶達式與簡潔表示法,不能同時使用,會報錯。

 
// 報錯
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正確
const foo = 'bar';
const baz = { [foo]: 'abc'};
 

注意,屬性名錶達式若是是一個對象,默認狀況下會自動將對象轉爲字符串[object Object],這一點要特別當心。

 
const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}
 

2. 若是對象的方法使用了取值函數(getter)和存值函數(setter),則name屬性不是在該方法上面,而是該方法的屬性的描述對象的getset屬性上面,返回值是方法名前加上getset

 
const obj = {
  get foo() {},
  set foo(x) {}
};

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

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

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
 

若是對象的方法是一個 Symbol 值,那麼name屬性返回的是這個 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
 

ES6 規定,全部 Class 的原型的方法都是不可枚舉的。總的來講,操做中引入繼承的屬性會讓問題複雜化,大多數時候,咱們只關心對象自身的屬性。因此,儘可能不要用for...in循環,而用Object.keys()代替。

3. 屬性的遍歷:for...in / Object.keys(obj) / Object.getOwnPropertyNames(obj) / Object.getOwnPropertySymblos(obj) / Reflect.ownKeys(obj):

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

  • 首先遍歷全部數值鍵,按照數值升序排列。
  • 其次遍歷全部字符串鍵,按照加入時間升序排列。
  • 最後遍歷全部 Symbol 鍵,按照加入時間升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

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

4. super 關鍵字:super指向當前對象的原型對象

 
const proto = {
  foo: 'hello'
};

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

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

注意:super必須用在對象的方法之中,只有對象方法的簡寫法可讓 JavaScript 引擎確認,定義的是對象的方法。

JavaScript 引擎內部,super.foo 等同於 Object.getPrototypeOf(this).foo(屬性)或 Object.getPrototypeOf(this).foo.call(this)(方法)。

5. 對象的解構賦值:

對象的解構賦值用於從一個對象取值,至關於將目標對象自身的全部可遍歷的(enumerable)、但還沒有被讀取的屬性,分配到指定的對象上面。全部的鍵和它們的值,都會拷貝到新對象上面。

因爲解構賦值要求等號右邊是一個對象,因此若是等號右邊是undefinednull,就會報錯,由於它們沒法轉爲對象。

let { x, y, ...z } = null; // 運行時錯誤
let { x, y, ...z } = undefined; // 運行時錯誤

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

與數組的擴展運算符同樣,對象的擴展運算符後面能夠跟表達式。

const obj = {
  ...(x > 1 ? {a: 1} : {}),
  b: 2,
};

 擴展運算符的參數對象之中,若是有取值函數get,這個函數是會執行的。

 
// 並不會拋出錯誤,由於 x 屬性只是被定義,但沒執行
let aWithXGetter = {
  ...a,
  get x() {
    throw new Error('not throw yet');
  }
};

// 會拋出錯誤,由於 x 屬性被執行了
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('throw now');
    }
  }
};
 

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

 
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
 
 
Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 針對+0 不等於 -0的狀況
      return x !== 0 || 1 / x === 1 / y;
    }
    // 針對NaN的狀況
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});
 

7. Object.assign():用於對象的合併,將源對象(source)的全部可枚舉屬性,複製到目標對象(target)。

因爲undefinednull沒法轉成對象,因此若是它們做爲首參數,就會報錯。

Object.assign(undefined) // 報錯
Object.assign(null) // 報錯

let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true

Object.assign拷貝的屬性是有限制的,只拷貝源對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性(enumerable: false)。

附:

Object.getPrototypeOf() 方法返回指定對象的原型(內部[[Prototype]]屬性的值)。

Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。 

8. Object.fromEntries():

Object.fromEntries()方法是 Object.entries()的逆操做,用於將一個鍵值對數組轉爲對象。該方法的主要目的,是將鍵值對的數據結構還原爲對象,所以特別適合將 Map 結構轉爲對象。

 
// 例一 const entries = new Map([ ['foo', 'bar'], ['baz', 42] ]); Object.fromEntries(entries) // { foo: "bar", baz: 42 }  // 例二 const map = new Map().set('foo', true).set('bar', false); Object.fromEntries(map) // { foo: true, bar: false }
 

該方法的一個用處是配合URLSearchParams對象,將查詢字符串轉爲對象。

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux')) // { foo: "bar", baz: "qux" }

附:URLSearchParams

 七. Symbol:新的原始數據類型Symbol,表示獨一無二的值。

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

2. 屬性名的遍歷:

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

 
const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols
// [Symbol(a), Symbol(b)]
 

 因爲以 Symbol 值做爲名稱的屬性,不會被常規方法遍歷獲得。咱們能夠利用這個特性,爲對象定義一些非私有的、但又但願只用於內部的方法。

 
let size = Symbol('size');

class Collection {
  constructor() {
    this[size] = 0;
  }

  add(item) {
    this[this[size]] = item;
    this[size]++;
  }

  static sizeOf(instance) {
    return instance[size];
  }
}

let x = new Collection();
Collection.sizeOf(x) // 0

x.add('foo');
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
 

上面代碼中,對象xsize屬性是一個 Symbol 值,因此Object.keys(x)Object.getOwnPropertyNames(x)都沒法獲取它。這就形成了一種非私有的內部方法的效果。

3. Symbol.for() / Symbol.keyFor():

Symbol.for():接受一個字符串做爲參數,而後搜索有沒有以該參數做爲名稱的 Symbol 值。若是有,就返回這個 Symbol 值,不然就新建並返回一個以該字符串爲名稱的 Symbol 值。

Symbol.keyFor():返回一個已登記的 Symbol 類型值的 key。

 
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

// 不一樣於 Symbol()
Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false
 
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

 4. 內置的 Symbol 值(11個):

Symbol.hasInstance / Symbol.isConcatSpreadable / Symbol.species / Symbol.match等

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

 
class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass() // true
 

八. Set 和 Map 數據結構:

1. Set:Set自己是一個構造函數,用來生成 Set 數據結構。

Set 函數能夠接受一個數組(或者具備 iterable 接口的其餘數據結構)做爲參數,用來初始化,且 Set 結構不會添加劇復的值。

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

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5

// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56

// 相似於
const set = new Set();
document
 .querySelectorAll('div')
 .forEach(div => set.add(div));
set.size // 56

[...new Set('ababbc')].join('')
// "abc"
 

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

2. Set 實例的屬性和方法:

屬性:size 至關於數組的 length;

方法:操做方法(用於操做數據)和遍歷方法(用於遍歷成員)。

  操做方法:

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

  遍歷方法:(Set的遍歷順序就是插入順序)

  • keys():返回鍵名的遍歷器
  • values():返回鍵值的遍歷器
  • entries():返回鍵值對的遍歷器
  • forEach():使用回調函數遍歷每一個成員
 
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 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),keys方法和values方法的行爲徹底一致。

 
// Set 結構的實例默承認遍歷,它的默認遍歷器生成函數就是它的values方法 =》這意味着,能夠省略values方法,直接用for...of循環遍歷 Set
Set.prototype[Symbol.iterator] === Set.prototype.values
// true

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

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

 3. WeakSet:結構與 Set 相似,也是不重複的值的集合,但成員只能是對象,而不能是其餘類型的值;WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,若是其餘對象都再也不引用該對象,那麼垃圾回收機制會自動回收該對象所佔用的內存,不考慮該對象還存在於 WeakSet 之中。

=》WeakSet 不能遍歷,是由於成員都是弱引用,隨時可能消失,遍歷機制沒法保證成員的存在

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

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

const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
 

  一些方法:

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

4. Map:鍵值對集合,區別於普通 Object 的一點就是,該鍵能夠是任意類型,好比對象;

(1) 不只僅是數組,任何具備 Iterator 接口、且每一個成員都是一個雙元素的數組的數據結構(詳見《Iterator》一章)均可以看成 Map 構造函數的參數。這就是說,Set 和 Map 均可以用來生成新的 Map。

(2) 若是對同一個鍵屢次賦值,後面的值將覆蓋前面的值。但只有對同一個對象的引用,Map 結構纔將其視爲同一個鍵。

// set和get方法,表面是針對同一個鍵,但實際上這是兩個值,內存地址是不同的,所以get方法沒法讀取該鍵,返回undefined
const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

(3) 一些屬性/方法:

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

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

get(key):讀取key對應的鍵值,若是找不到key,返回 undefined;

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

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

clear():清除全部成員,沒有返回值;

keys():返回鍵名的遍歷器;

values():返回鍵值的遍歷器;

entries():返回全部成員的遍歷器;

forEach():遍歷 Map 的全部成員;

注:Map 的遍歷順序就是插入順序; Map 結構的默認遍歷器接口(Symbol.iterator屬性),就是entries方法。

(4) Map 轉爲數組:使用擴展運算符(...),轉爲數組後纔可使用數組相關 map() / filter() 方法;

5. WeakMap:結構與 Map 結構相似,也是用於生成鍵值對的集合,2 點區別:WeakMap只接受對象做爲鍵名(null除外),不接受其餘類型的值做爲鍵名;WeakMap的鍵名所指向的對象,不計入垃圾回收機制。

基本上,若是你要往對象上添加數據,又不想幹擾垃圾回收機制,就可使用 WeakMap。一個典型應用場景是,在網頁的 DOM 元素上添加數據,就可使用WeakMap結構。當該 DOM 元素被清除,其所對應的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只有四個方法可用:get()set()has()delete()

  • WeakMap 的用途:WeakMap 應用的典型場合就是 DOM 節點做爲鍵名;另外一個用處是部署私有屬性。
 
// Countdown類的兩個內部屬性_counter和_action,是實例的弱引用,因此若是刪除實例,它們也就隨之消失,不會形成內存泄漏
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()
 

 九. Proxy:

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

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

var proxy new Proxy(target, handler);

 
var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    // target:目標對象;key:key-value中的key值;receiver:當前proxy代理對象;
    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);
  }
});
 
 
// 如下爲上述代碼運行後結果
obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2
 
 
var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },
 // 參數:目標對象、目標對象的上下文對象和目標對象的參數數組
  apply: function(target, thisBinding, args) {
    return args[0];
  },
  // 參數:target 目標對象;args:構造函數的參數對象;newTarget:創造實例對象時,new命令做用的構造函數
  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
 

2. Proxy 的一些方法:

具體參數及使用方法:http://es6.ruanyifeng.com/#docs/proxy

 proxy對象getReceiver屬性是由對象提供的,receiver指向proxy對象。

 若是一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,不然經過 Proxy 對象訪問(get)該屬性會報錯。

 
const target = Object.defineProperties({}, {
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed
 

若是目標對象自身的某個屬性,不可寫且不可配置,那麼set方法將不起做用,不是報錯。

嚴格模式下,set代理若是沒有返回true,就會報錯(也就是若是沒有return true就會報錯)。

3. this 問題:

Proxy 代理的狀況下,目標對象內部的this關鍵字會指向 Proxy 代理。this指向變化會致使 Proxy 沒法代理目標對象。

十. Reflect:

1. Reflect對象與Proxy對象同樣,也是 ES6 爲了操做對象而提供的新 API。

設計目的

(1) 將Object對象的一些明顯屬於語言內部的方法(好比Object.defineProperty),放到Reflect對象上;

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

(3) 讓Object操做都變成函數行爲。某些Object操做是命令式,好比name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)讓它們變成了函數行爲;

(4) Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法;

2. 靜態方法:

 3. 使用 Proxy 實現觀察者模式:

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

 
// 思路是函數返回一個原始對象的 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; }observable

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

 十一. Promise 對象:

1. Promise: Promise 對象是一個構造函數;

2. Promise 對象的特色:

(1)對象的狀態不受外界影響。Promise對象表明一個異步操做,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗);

(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected。只要這兩種狀況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱爲 resolved(已定型).

3. 使用:

 
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
 

執行順序: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最後輸出。

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

4. Promise.prototype.then():

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。

5. 「Promise 會吃掉錯誤」:Promise 內部的錯誤(任何報錯)不會影響到 Promise 外部的代碼;

6. Promise.prototype.finally():finally方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。

7. 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 實例,本身定義了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方法。

8. Promise.race():Promise.race方法一樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。區別在於:只要p1p2p3之中有一個實例率先改變狀態,p的狀態就跟着改變(或)。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

9. Promise.resolve():

當即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')則是當即執行,所以最早輸出。

10. Promise.reject():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對象。

11. Promise.try():Promise.try就是模擬try代碼塊,就像promise.catch模擬的是catch代碼塊。

十二. Iterator 和 for...of 循環:

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

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

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

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

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

Iterator 的遍歷過程是這樣的。

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

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

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

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

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

十三. Generator函數

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

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

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

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

var hw = helloWorldGenerator();
 

而後,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 }
 

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

2. 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)的語法功能。

 3. 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
 

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

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

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

5. 做爲對象屬性的 Generator 函數:

若是一個對象的屬性是 Generator 函數,能夠簡寫成下面的形式。

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

6. generator 的 this:

Generator 函數老是返回一個遍歷器,ES6 規定這個遍歷器是 Generator 函數的實例,也繼承了 Generator 函數的prototype對象上的方法。可是,若是把g看成普通的構造函數,並不會生效,由於g返回的老是遍歷器對象,而不是this對象。

Generator 函數也不能跟new命令一塊兒用,會報錯。

如何綁定 this:用call:

 
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
 

或者

 
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
 

7. 含義:

(1)狀態機:

 
var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

等同於

var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}
 

(2)協程:

一個線程(或函數)執行到一半,能夠暫停執行,將執行權交給另外一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種能夠並行執行、交換執行權的線程(或函數),就稱爲協程

在內存中,子例程只使用一個棧(stack),而協程是同時存在多個棧,但只有一個棧是在運行狀態,也就是說,協程是以多佔用內存爲代價,實現多任務的並行。

因爲 JavaScript 是單線程語言,只能保持一個調用棧。引入協程之後,每一個任務能夠保持本身的調用棧。這樣作的最大好處,就是拋出錯誤的時候,能夠找到原始的調用棧。不至於像異步操做的回調函數那樣,一旦出錯,原始的調用棧早就結束。

Generator 函數是 ES6 對協程的實現,但屬於不徹底實現。Generator 函數被稱爲「半協程」(semi-coroutine),意思是隻有 Generator 函數的調用者,才能將程序的執行權還給 Generator 函數。若是是徹底執行的協程,任何函數均可以讓暫停的協程繼續執行。

(3)上下文:

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上下文從新加入堆棧,變成當前的上下文,從新恢復執行。

8. 應用:

(1)異步操做的同步化表達:

Generator 函數的暫停執行的效果,意味着能夠把異步操做寫在yield表達式裏面,等到調用next方法時再日後執行。這實際上等同於不須要寫回調函數了,由於異步操做的後續操做能夠放在yield表達式下面,反正要等到調用next方法時再執行。因此,Generator 函數的一個重要實際意義就是用來處理異步操做,改寫回調函數。

(2)控制流管理:

 
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

或者

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();

都可寫爲:

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都必須是同步的,不能有異步操做。由於這裏的代碼一獲得返回值,就繼續往下執行,沒有判斷異步操做什麼時候完成。

(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
 

(4)做爲數據結構:

Generator 能夠看做是一個數組結構,由於 Generator 函數能夠返回一系列的值,這意味着它能夠對任意表達式,提供相似數組的接口。

好比:

 
function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

for (task of doStuff()) {
  // task是一個函數,能夠像回調函數那樣使用它
}
 

 9. 

調用 Generator 函數,會返回一個內部指針(即遍歷器)g。這是 Generator 函數不一樣於普通函數的另外一個地方,即執行它不會返回結果,返回的是指針對象。調用指針gnext方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的yield語句,上例是執行到x + 2爲止。

換言之,next方法的做用是分階段執行Generator函數。每次調用next方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句後面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示 Generator 函數是否執行完畢,便是否還有下一個階段。

10. Thunk 函數:

(1)參數的求值策略:

 
var x = 1;

function f(m) {
  return m * 2;
}

f(x + 5)
 

上面代碼先定義函數f,而後向它傳入表達式x + 5。請問,這個表達式應該什麼時候求值?

"傳值調用"(call by value),即在進入函數體以前,就計算x + 5的值(等於 6),再將這個值傳入函數f。C 語言就採用這種策略。

「傳名調用」(call by name),即直接將表達式x + 5傳入函數體,只在用到它的時候求值。Haskell 語言採用這種策略。

(2)編譯器的「傳名調用」實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作 Thunk 函數。

JavaScript 語言是傳值調用,它的 Thunk 函數含義有所不一樣。在 JavaScript 語言中,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);
    }
  };
};
 

 十四. Async 函數:

1. async 函數就是 Generator 函數的語法糖。async函數就是將 Generator 函數的星號(*)替換成 async,將yield替換成 await。

2. async函數的返回值是 Promise 對象。進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

3. 用法: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);
});
 

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

 
async function fn(args) {
  // ...
}

// 等同於

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

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

 5. 異步遍歷器:Async Iterator,爲異步操做提供原生的遍歷器接口,即valuedone這兩個屬性都是異步產生。

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

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

(2)for await ... of:for...of循環用於遍歷同步的 Iterator 接口。新引入的for await...of循環,則是用於遍歷異步的 Iterator 接口。

(3)異步 Generator 函數出現之後,JavaScript 就有了四種函數形式:普通函數、async 函數、Generator 函數和異步 Generator 函數。請注意區分每種函數的不一樣之處。基本上,若是是一系列按照順序執行的異步操做(好比讀取文件,而後寫入新內容,再存入硬盤),可使用 async 函數;若是是一系列產生相同數據結構的異步操做(好比一行一行讀取文件),可使用異步 Generator 函數。

十五. Class:

1. ES5 與 Class:

 
// ES5:

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

// Class 方式:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
 

2. 靜態方法:類至關於實例的原型,全部在類中定義的方法,都會被實例繼承。若是在一個方法前,加上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指的是類,而不是實例。

父類的靜態方法,能夠被子類繼承。靜態方法也是能夠從super對象上調用的。

 
class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', too';
  }
}

Bar.classMethod() // "hello, too"
 

3. 實例屬性的新寫法:

 
class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

// 也能夠

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}
 

4. 靜態屬性:

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

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

5. new target 屬性:

new是從構造函數生成實例對象的命令。ES6 爲new命令引入了一個new.target屬性,該屬性通常用在構造函數之中,返回new命令做用於的那個構造函數。若是構造函數不是經過new命令或Reflect.construct()調用的,new.target會返回undefined,所以這個屬性能夠用來肯定構造函數是怎麼調用的。

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

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

var person = new Person('張三'); // 正確
var notAPerson = Person.call(person, '張三');  // 報錯
 

上面代碼確保構造函數只能經過new命令調用。

Class 內部調用new.target,返回當前 Class。

須要注意的是,子類繼承父類時,new.target會返回子類。

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

 
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);  // 正確
 

 6. 繼承:

(1)Class 經過extends關鍵字實現繼承;

super關鍵字,表示父類的構造函數,用來新建父類的this對象(super 既能夠當函數使用(表明調用父類的構造函數),也能夠當對象使用(在普通方法中,指向父類的原型對象;在靜態方法中,指向父類))。

子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類本身的this對象,必須先經過父類的構造函數完成塑造,獲得與父類一樣的實例屬性和方法,而後再對其進行加工,加上子類本身的實例屬性和方法。若是不調用super方法,子類就得不到this對象。

 
class Point {
}

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

  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}
 

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

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

 (3)mixin:

多個類的接口「混入」(mix in)另外一個類:

 
function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷貝實例屬性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷貝靜態屬性
    copyProperties(Mix.prototype, mixin.prototype); // 拷貝原型屬性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

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

 十六. Module:

1. 編譯時加載:下面代碼的實質是從fs模塊加載 3 個方法,其餘方法不加載。這種加載稱爲「編譯時加載」或者靜態加載,即 ES6 能夠在編譯時就完成模塊加載。

// ES6模塊
import { stat, exists, readFile } from 'fs';

2. ES6 模塊的好處:

  • 再也不須要UMD模塊格式了,未來服務器和瀏覽器都會支持 ES6 模塊格式。目前,經過各類工具庫,其實已經作到了這一點。
  • 未來瀏覽器的新 API 就能用模塊格式提供,再也不必須作成全局變量或者navigator對象的屬性。
  • 再也不須要對象做爲命名空間(好比Math對象),將來這些功能能夠經過模塊提供。
  • 因爲 ES6 模塊是編譯時加載,使得靜態分析成爲可能。進一步拓寬 JavaScript 的語法,好比引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。

3. 模塊功能主要由兩個命令構成:exportimportexport命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。

4. export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,import命令也是如此。這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了 ES6 模塊的設計初衷。

function foo() {
  export default 'bar' // SyntaxError
}
foo()

5. import命令具備提高效果,會提高到整個模塊的頭部,首先執行。

6. export default 命令:

export default命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export default命令只能使用一次。因此,import命令後面纔不用加大括號,由於只可能惟一對應export default命令。

本質上,export default就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。因此,下面的寫法是有效的。

 
// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同於
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
 

7. import():返回一個 Promise 對象。

import()函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也可使用。它是運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊,因此能夠按需加載/條件加載。另外,import()函數與所加載的模塊沒有靜態鏈接關係,這點也是與import語句不相同。import()相似於 Node 的require方法,區別主要是前者是異步加載,後者是同步加載。

8. 加載規則:瀏覽器加載 ES6 模塊,也使用<script>標籤,可是要加入type="module"屬性。

<script type="module" src="./foo.js"></script>

上面代碼在網頁中插入一個模塊foo.js,因爲type屬性設爲module,因此瀏覽器知道這是一個 ES6 模塊。

瀏覽器對於帶有type="module"<script>,都是異步加載,不會形成堵塞瀏覽器,即等到整個頁面渲染完,再執行模塊腳本,等同於打開了<script>標籤的defer屬性。

若是網頁有多個<script type="module">,它們會按照在頁面出現的順序依次執行。

<script>標籤的async屬性也能夠打開,這時只要加載完成,渲染引擎就會中斷渲染當即執行。執行完成後,再恢復渲染。

一旦使用了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關鍵字,是無心義的。
  • 同一個模塊若是加載屢次,將只執行一次。

利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。

9. ES6 模塊與 CommonJS 模塊的差別:

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第一個,CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。

第二個差別是由於 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

10. Node 加載:

Node 對 ES6 模塊的處理比較麻煩,由於它有本身的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將二者分開,ES6 模塊和 CommonJS 採用各自的加載方案。

Node 要求 ES6 模塊採用.mjs後綴文件名。也就是說,只要腳本文件裏面使用import或者export命令,那麼就必須採用.mjs後綴名。require命令不能加載.mjs文件,會報錯,只有import命令才能夠加載.mjs文件。反過來,.mjs文件裏面也不能使用require命令,必須使用import

若是模塊名不含路徑,那麼import命令會去node_modules目錄尋找這個模塊。

若是腳本文件省略了後綴名,好比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 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊。

 11. SystemJS:

它是一個墊片庫(polyfill),能夠在瀏覽器內加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉爲 ES5 格式。它在後臺調用的是 Google 的 Traceur 轉碼器。

System.import使用異步加載,返回一個 Promise 對象。

十七. 編程風格:

1. 全局常量和線程安全:

letconst之間,建議優先使用const,尤爲是在全局環境,不該該設置變量,只應設置常量。

const優於let有幾個緣由。一個是const能夠提醒閱讀程序的人,這個變量不該該改變;另外一個是const比較符合函數式編程思想,運算不改變值,只是新建值,並且這樣也有利於未來的分佈式運算;最後一個緣由是 JavaScript 編譯器會對const進行優化,因此多使用const,有利於提升程序的運行效率,letconst的本質區別,實際上是編譯器內部的處理不一樣。

全部的函數都應該設置爲常量。

2. 不要在函數體內使用 arguments 變量,使用 rest 運算符(...)代替。由於 rest 運算符顯式代表你想要獲取參數,並且 arguments 是一個相似數組的對象,而 rest 運算符能夠提供一個真正的數組。

 
// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}

// good
function concatenateAll(...args) {
  return args.join('');
}
 

3. 使用默認值語法設置函數參數的默認值。

 
// bad
function handleThings(opts) {
  opts = opts || {};
}

// good
function handleThings(opts = {}) {
  // ...
}
 

4. 使用extends實現繼承,由於這樣更簡單,不會有破壞instanceof運算的危險。

 
// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
  Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function() {
  return this._queue[0];
}

// good
class PeekableQueue extends Queue {
  peek() {
    return this._queue[0];
  }
}
 

 十八. 修飾器:

修飾器(Decorator)函數,用來修改類的行爲。

修飾器是一個對類進行處理的函數。修飾器函數的第一個參數,就是所要修飾的目標類。

 
@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true
 

上面代碼中,@testable就是一個修飾器。它修改了MyTestableClass這個類的行爲,爲它加上了靜態屬性isTestabletestable函數的參數targetMyTestableClass類自己。

十九. ArrayBuffer:

1.

ArrayBuffer對象、TypedArray視圖和DataView視圖是 JavaScript 操做二進制數據的一個接口。它們都是以數組的語法處理二進制數據,因此統稱爲二進制數組。

這個接口的原始設計目的,與 WebGL 項目有關。所謂 WebGL,就是指瀏覽器與顯卡之間的通訊接口,爲了知足 JavaScript 與顯卡之間大量的、實時的數據交換,它們之間的數據通訊必須是二進制的,而不能是傳統的文本格式。文本格式傳遞一個 32 位整數,兩端的 JavaScript 腳本與顯卡都要進行格式轉化,將很是耗時。這時要是存在一種機制,能夠像 C 語言那樣,直接操做字節,將 4 個字節的 32 位整數,以二進制形式原封不動地送入顯卡,腳本的性能就會大幅提高。

二進制數組就是在這種背景下誕生的。它容許開發者以數組下標的形式,直接操做內存,大大加強了 JavaScript 處理二進制數據的能力,使得開發者有可能經過 JavaScript 與操做系統的原生接口進行二進制通訊。

二進制數組由三類對象組成:

(1)ArrayBuffer對象:表明內存之中的一段二進制數據,能夠經過「視圖」進行操做。「視圖」部署了數組接口,這意味着,能夠用數組的方法操做內存。

(2)TypedArray視圖:共包括 9 種類型的視圖,好比Uint8Array(無符號 8 位整數)數組視圖, Int16Array(16 位整數)數組視圖, Float32Array(32 位浮點數)數組視圖等等。

(3)DataView視圖:能夠自定義複合格式的視圖,好比第一個字節是 Uint8(無符號 8 位整數)、第2、三個字節是 Int16(16 位整數)、第四個字節開始是 Float32(32 位浮點數)等等,此外還能夠自定義字節序。

簡單說,ArrayBuffer對象表明原始的二進制數據,TypedArray視圖用來讀寫簡單類型的二進制數據,DataView視圖用來讀寫複雜類型的二進制數據。

注意,二進制數組並非真正的數組,而是相似數組的對象。

2. TypedArray 視圖:

ArrayBuffer對象做爲內存區域,能夠存放多種類型的數據。同一段內存,不一樣數據有不一樣的解讀方式,這就叫作「視圖」(view)。ArrayBuffer有兩種視圖,一種是TypedArray視圖,另外一種是DataView視圖。前者的數組成員都是同一個數據類型,後者的數組成員能夠是不一樣的數據類型。

目前,TypedArray視圖一共包括 9 種類型,每一種視圖都是一種構造函數。

  • Int8Array:8 位有符號整數,長度 1 個字節。
  • Uint8Array:8 位無符號整數,長度 1 個字節。
  • Uint8ClampedArray:8 位無符號整數,長度 1 個字節,溢出處理不一樣。
  • Int16Array:16 位有符號整數,長度 2 個字節。
  • Uint16Array:16 位無符號整數,長度 2 個字節。
  • Int32Array:32 位有符號整數,長度 4 個字節。
  • Uint32Array:32 位無符號整數,長度 4 個字節。
  • Float32Array:32 位浮點數,長度 4 個字節。
  • Float64Array:64 位浮點數,長度 8 個字節。

這 9 個構造函數生成的數組,統稱爲TypedArray視圖。它們很像普通數組,都有length屬性,都能用方括號運算符([])獲取單個元素,全部數組的方法,在它們上面都能使用。普通數組與 TypedArray 數組的差別主要在如下方面。

  • TypedArray 數組的全部成員,都是同一種類型。
  • TypedArray 數組的成員是連續的,不會有空位。
  • TypedArray 數組成員的默認值爲 0。好比,new Array(10)返回一個普通數組,裏面沒有任何成員,只是 10 個空位;new Uint8Array(10)返回一個 TypedArray 數組,裏面 10 個成員都是 0。
  • TypedArray 數組只是一層視圖,自己不儲存數據,它的數據都儲存在底層的ArrayBuffer對象之中,要獲取底層對象必須使用buffer屬性。

參考博客:http://www.javashuo.com/article/p-gbmhwmbf-hm.html

相關文章
相關標籤/搜索