一. 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. 原則:解構賦值的規則是,只要等號右邊的值不是對象或數組,就先將其轉爲對象。因爲undefined
和null
沒法轉爲對象,因此對它們進行解構賦值,都會報錯
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 參數:
// 正確的 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
,就是函數foo
的this
,因此t1
、t2
、t3
都輸出一樣的結果。由於全部的內層函數都是箭頭函數,都沒有本身的this
,它們的this
其實都是最外層foo
函數的this
。
除了this
,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:arguments
、super
、new.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); }
上面代碼中,函數m
和n
都屬於尾調用,由於它們都是函數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)
它接受三個參數。
這三個參數都應該是數值,若是不是,會自動轉爲數值。
[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
區分。
has
方法,是用來查找鍵名的,好比Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
。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
,而undefined
和null
會被處理成空字符串。// 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
屬性不是在該方法上面,而是該方法的屬性的描述對象的get
和set
屬性上面,返回值是方法名前加上get
和set
。
const obj = { get foo() {}, set foo(x) {} }; obj.foo.name // TypeError: Cannot read property 'name' of undefined const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo'); descriptor.get.name // "get foo" descriptor.set.name // "set foo"
若是對象的方法是一個 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 種方法遍歷對象的鍵名,都遵照一樣的屬性遍歷的次序規則。
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)、但還沒有被讀取的屬性,分配到指定的對象上面。全部的鍵和它們的值,都會拷貝到新對象上面。
因爲解構賦值要求等號右邊是一個對象,因此若是等號右邊是undefined
或null
,就會報錯,由於它們沒法轉爲對象。
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)。
因爲undefined
和null
沒法轉成對象,因此若是它們做爲首參數,就會報錯。
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
![](http://static.javashuo.com/static/loading.gif)
七. Symbol:新的原始數據類型Symbol
,表示獨一無二的值。
1. Symbol 函數前不能使用 new 命令,不然會報錯。這是由於生成的 Symbol 是一個原始類型的值,不是對象。也就是說,因爲 Symbol 值不是對象,因此不能添加屬性。基本上,它是一種相似於字符串的數據類型。
2. 屬性名的遍歷:
Symbol 做爲屬性名,該屬性不會出如今for...in
、for...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)]
上面代碼中,對象x
的size
屬性是一個 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(…)
一些方法:
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()
。
// 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 obj
和delete 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
之中,會自動執行全部觀察者。
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 是在本輪事件循環的末尾執行,老是晚於本輪循環的同步任務。
通常來講,調用resolve
或reject
之後,Promise 的使命就完成了,後繼操做應該放到then
方法裏面,而不該該直接寫在resolve
或reject
的後面。因此,最好在它們前面加上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
方法接受一個數組做爲參數,p1
、p2
、p3
都是 Promise 實例,若是不是,就會先調用下面講到的Promise.resolve
方法,將參數轉爲 Promise 實例,再進一步處理。(Promise.all
方法的參數能夠不是數組,但必須具備 Iterator 接口,且返回的每一個成員都是 Promise 實例。)
p
的狀態由p1
、p2
、p3
決定,分紅兩種狀況(且):
(1)只有p1
、p2
、p3
的狀態都變成fulfilled
,p
的狀態纔會變成fulfilled
,此時p1
、p2
、p3
的返回值組成一個數組,傳遞給p
的回調函數。
(2)只要p1
、p2
、p3
之中有一個被rejected
,p
的狀態就變成rejected
,此時第一個被reject
的實例的返回值,會傳遞給p
的回調函數。
注:若是做爲參數的 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: 報錯了]
上面代碼中,p1
會resolved
,p2
首先會rejected
,可是p2
有本身的catch
方法,該方法返回的是一個新的 Promise 實例,p2
指向的其實是這個實例。該實例執行完catch
方法後,也會變成resolved
,致使Promise.all()
方法參數裏面的兩個實例都會resolved
,所以會調用then
方法指定的回調函數,而不會調用catch
方法指定的回調函數。若是p2
沒有本身的catch
方法,就會調用Promise.all()
的catch
方法。
8. Promise.race():Promise.race
方法一樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。區別在於:只要p1
、p2
、p3
之中有一個實例率先改變狀態,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 接口的數據結構以下。
3. 遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。
Iterator 的遍歷過程是這樣的。
(1)建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next
方法,能夠將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next
方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next
方法,直到它指向數據結構的結束位置。
每一次調用next
方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含value
和done
兩個屬性的對象。其中,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
方法,就會返回一個有着value
和done
兩個屬性的對象。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
屬性爲true
,for...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 函數不一樣於普通函數的另外一個地方,即執行它不會返回結果,返回的是指針對象。調用指針g
的next
方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的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,爲異步操做提供原生的遍歷器接口,即value
和done
這兩個屬性都是異步產生。
(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 模塊格式。目前,經過各類工具庫,其實已經作到了這一點。navigator
對象的屬性。Math
對象),將來這些功能能夠經過模塊提供。3. 模塊功能主要由兩個命令構成:export
和import
。export
命令用於規定模塊的對外接口,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 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。
第二個差別是由於 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.json
的main
字段指定的腳本。若是./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. 全局常量和線程安全:
在let
和const
之間,建議優先使用const
,尤爲是在全局環境,不該該設置變量,只應設置常量。
const
優於let
有幾個緣由。一個是const
能夠提醒閱讀程序的人,這個變量不該該改變;另外一個是const
比較符合函數式編程思想,運算不改變值,只是新建值,並且這樣也有利於未來的分佈式運算;最後一個緣由是 JavaScript 編譯器會對const
進行優化,因此多使用const
,有利於提升程序的運行效率,let
和const
的本質區別,實際上是編譯器內部的處理不一樣。
全部的函數都應該設置爲常量。
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
這個類的行爲,爲它加上了靜態屬性isTestable
。testable
函數的參數target
是MyTestableClass
類自己。
十九. 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 數組的差別主要在如下方面。
new Array(10)
返回一個普通數組,裏面沒有任何成員,只是 10 個空位;new Uint8Array(10)
返回一個 TypedArray 數組,裏面 10 個成員都是 0。ArrayBuffer
對象之中,要獲取底層對象必須使用buffer
屬性。