Iterator接口的目的,就是爲全部數據結構,提供了一種統一的訪問機制,即for...of
循環git
遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。es6
Iterator的做用有三個:一是爲各類數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員可以按某種次序排列;三是ES6創造了一種新的遍歷命令for...of
循環,Iterator接口主要供for...of
消費。github
Iterator的遍歷過程是這樣的。編程
(1)建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。數組
(2)第一次調用指針對象的next
方法,能夠將指針指向數據結構的第一個成員。瀏覽器
(3)第二次調用指針對象的next
方法,指針就指向數據結構的第二個成員。數據結構
(4)不斷調用指針對象的next
方法,直到它指向數據結構的結束位置。app
每一次調用next
方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含value
和done
兩個屬性的對象。其中,value
屬性是當前成員的值,done
屬性是一個布爾值,表示遍歷是否結束。異步
在ES6中,有些數據結構原生具有Iterator接口(好比數組),即不用任何處理,就能夠被for...of
循環遍歷,有些就不行(好比對象)。緣由在於,這些數據結構原生部署了Symbol.iterator
屬性異步編程
(1)解構賦值
對數組和Set結構進行解構賦值時,會默認調用Symbol.iterator
方法。
let set = new Set().add('a').add('b').add('c'); let [x,y] = set; // x='a'; y='b' let [first, ...rest] = set; // first='a'; rest=['b','c'];
(2)擴展運算符
擴展運算符(...)也會調用默認的iterator接口。
// 例一 var str = 'hello'; [...str] // ['h','e','l','l','o'] // 例二 let arr = ['b', 'c']; ['a', ...arr, 'd'] // ['a', 'b', 'c', 'd']
(3)yield*
yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
for...of
循環,做爲遍歷全部數據結構的統一的方法。
for...of
循環可使用的範圍包括數組、Set 和 Map 結構、某些相似數組的對象(好比arguments
對象、DOM NodeList 對象)、後文的 Generator 對象,以及字符串。
for...of
循環能夠代替數組實例的forEach
方法。
JavaScript原有的for...in
循環,只能得到對象的鍵名,不能直接獲取鍵值。ES6提供for...of
循環,容許遍歷得到鍵值。
對於普通的對象,for...of
結構不能直接使用,會報錯,必須部署了iterator接口後才能使用。可是,這樣狀況下,for...in
循環依然能夠用來遍歷鍵名。
var es6 = { edition: 6, committee: "TC39", standard: "ECMA-262" }; for (let e in es6) { console.log(e); } // edition // committee // standard for (let e of es6) { console.log(e); } // TypeError: es6 is not iterable
for...in循環有幾個缺點。
總之,for...in
循環主要是爲遍歷對象而設計的,不適用於遍歷數組。
形式上,Generator 函數是一個普通函數,可是有兩個特徵。一是,function
關鍵字與函數名之間有一個星號;二是,函數體內部使用yield
語句,定義不一樣的內部狀態(yield
在英語裏的意思就是「產出」)。
Generator 函數是 ES6 提供的一種異步編程解決方案,從語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();
上面代碼定義了一個Generator函數helloWorldGenerator
,它內部有兩個yield
語句「hello」和「world」,即該函數有三個狀態:hello,world和return語句(結束執行)。
調用Generator函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next
方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield
語句(或return
語句)爲止。
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函數返回的遍歷器對象,只有調用next
方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield
語句就是暫停標誌。
yield
語句與return
語句既有類似之處,也有區別。
1. 類似之處在於,都能返回緊跟在語句後面的那個表達式的值。
2. 區別在於每次遇到yield
,函數暫停執行,下一次再從該位置繼續向後執行,而return
語句不具有位置記憶的功能。
3. 一個函數裏面,只能執行一次(或者說一個)return
語句,可是能夠執行屢次(或者說多個)yield
語句。正常函數只能返回一個值,由於只能執行一次return
;Generator函數能夠返回一系列的值,由於能夠有任意多個yield
。
Generator函數能夠不用yield
語句,這時就變成了一個單純的暫緩執行函數。
function* f() { console.log('執行了!') } var generator = f(); setTimeout(function () { generator.next() }, 2000);
上面代碼中,函數f
若是是普通函數,在爲變量generator
賦值時就會執行。可是,函數f
是一個Generator函數,就變成只有調用next
方法時,函數f
纔會執行。
另外須要注意,yield
語句不能用在普通函數中,不然會報錯。
另外,yield
語句若是用在一個表達式之中,必須放在圓括號裏面。
console.log('Hello' + yield); // SyntaxError console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK console.log('Hello' + (yield 123)); // OK
yield
語句用做函數參數或賦值表達式的右邊,能夠不加括號。
yield
句自己沒有返回值,或者說老是返回undefined
。next
方法能夠帶一個參數,該參數就會被看成上一個yield
語句的返回值。
function* f() { for(var i = 0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } var g = f(); g.next() // { value: 0, done: false } g.next() // { value: 1, done: false } g.next(true) // { value: 0, done: false }
上面代碼先定義了一個能夠無限運行的 Generator 函數f
,若是next
方法沒有參數,每次運行到yield
語句,變量reset
的值老是undefined
。當next
方法帶一個參數true
時,變量reset
就被重置爲這個參數(即true
),所以i
會等於-1
,下一輪循環就會從-1
開始遞增。
for...of
循環能夠自動遍歷Generator函數時生成的Iterator
對象,且此時再也不須要調用next
方法。
function *foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
上面代碼使用for...of
循環,依次顯示5個yield
語句的值。這裏須要注意,一旦next
方法的返回對象的done
屬性爲true
,for...of
循環就會停止,且不包含該返回對象,因此上面代碼的return
語句返回的6,不包括在for...of
循環之中。
JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直接調用這個函數。
基本上,ES6的class
能夠看做只是一個語法糖,它的絕大部分功能,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); //es6語法 //定義類 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } }
Point
類除了構造方法,還定義了一個toString
方法。注意,定義「類」的方法的時候,前面不須要加上function
這個關鍵字,直接把函數定義放進去了就能夠了。另外,方法之間不須要逗號分隔,加了會報錯。
ES6的類,徹底能夠看做構造函數的另外一種寫法。
class Point { // ... } typeof Point // "function" Point === Point.prototype.constructor // true //上面代碼代表,類的數據類型就是函數,類自己就指向構造函數。
使用的時候,也是直接對類使用new
命令,跟構造函數的用法徹底一致。
構造函數的prototype
屬性,在ES6的「類」上面繼續存在。事實上,類的全部方法都定義在類的prototype
屬性上面。
class Point { constructor(){ // ... } toString(){ // ... } toValue(){ // ... } } // 等同於 Point.prototype = { toString(){}, toValue(){} };
Object.assign
方法能夠很方便地一次向類添加多個方法。
class Point { constructor(){ // ... } } Object.assign(Point.prototype, { toString(){}, toValue(){} });
另外,類的內部全部定義的方法,都是不可枚舉的(non-enumerable)。
類的屬性名,能夠採用表達式。
let methodName = "getArea"; class Square{ constructor(length) { // ... } [methodName]() { // ... } } //上面代碼中,Square類的方法名getArea,是從表達式獲得的。
Class之間能夠經過extends
關鍵字實現繼承
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 調用父類的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 調用父類的toString() } }
子類必須在constructor
方法中調用super
方法,不然新建實例時會報錯。這是由於子類沒有本身的this
對象,而是繼承父類的this
對象,而後對其進行加工。若是不調用super
方法,子類就得不到this
對象。
Class做爲構造函數的語法糖,同時有prototype屬性和__proto__
屬性,所以同時存在兩條繼承鏈。
(1)子類的__proto__
屬性,表示構造函數的繼承,老是指向父類。
(2)子類prototype
屬性的__proto__
屬性,表示方法的繼承,老是指向父類的prototype
屬性。
class A { } class B extends A { } B.__proto__ === A // true B.prototype.__proto__ === A.prototype // true
類至關於實例的原型,全部在類中定義的方法,都會被實例繼承。若是在一個方法前,加上static
關鍵字,就表示該方法不會被實例繼承,而是直接經過類來調用,這就稱爲「靜態方法」。
class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
父類的靜態方法,能夠被子類繼承。
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod(); // 'hello'
靜態方法也是能夠從super
對象上調用的。
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod();
靜態屬性指的是Class自己的屬性,即Class.propname
,而不是定義在實例對象(this
)上的屬性。
class Foo { } Foo.prop = 1; Foo.prop // 1
目前,只有這種寫法可行,由於ES6明確規定,Class內部只有靜態方法,沒有靜態屬性。
目前,有一個提案,爲class
加了私有屬性。方法是在屬性名以前,使用#
表示。
修飾器(Decorator)是一個函數,用來修改類的行爲。修飾器對類的行爲的改變,是代碼編譯時發生的,而不是在運行時。這意味着,修飾器能在編譯階段運行代碼。
function testable(target) { target.isTestable = true; } @testable class MyTestableClass {} console.log(MyTestableClass.isTestable) // true
上面代碼中,@testable
就是一個修飾器。它修改了MyTestableClass
這個類的行爲,爲它加上了靜態屬性isTestable
。
基本上,修飾器的行爲就是下面這樣。
@decorator class A {} // 等同於 class A {} A = decorator(A) || A;
也就是說,修飾器本質就是編譯時執行的函數。
修飾器函數的第一個參數,就是所要修飾的目標類。
class Person { @readonly name() { return `${this.first} ${this.last}` } } //上面代碼中,修飾器readonly用來修飾「類」的name方法。
ES6 模塊不是對象,而是經過export
命令顯式指定輸出的代碼,再經過import
命令輸入。
// ES6模塊 import { stat, exists, readFile } from 'fs';
ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";
。
模塊功能主要由兩個命令構成:export
和import
。export
命令用於規定模塊的對外接口,import
命令用於輸入其餘模塊提供的功能。
// profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; // profile.js var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export {firstName, lastName, year};
一般狀況下,export
輸出的變量就是原本的名字,可是可使用as
關鍵字重命名。
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion };
須要特別注意的是,export
命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。
// 報錯 export 1; // 報錯 var m = 1; export m;
上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出1,第二種寫法經過變量m
,仍是直接輸出1。1
只是一個值,不是接口。正確的寫法是下面這樣。
// 寫法一 export var m = 1; // 寫法二 var m = 1; export {m}; // 寫法三 var n = 1; export {n as m};
上面三種寫法都是正確的,規定了對外的接口m
。其餘腳本能夠經過這個接口,取到值1
。它們的實質是,在接口名與模塊內部變量之間,創建了一一對應的關係。
export
命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯
使用export
命令定義了模塊的對外接口之後,其餘 JS 文件就能夠經過import
命令加載這個模塊。
// main.js import {firstName, lastName, year} from './profile'; function setName(element) { element.textContent = firstName + ' ' + lastName; }
import
語句會執行所加載的模塊,所以能夠有下面的寫法。
import 'lodash';
import * as circle from './circle'; console.log('圓面積:' + circle.area(4)); console.log('圓周長:' + circle.circumference(14));
爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default
命令,爲模塊指定默認輸出。
// export-default.js export default function () { console.log('foo'); }
上面代碼是一個模塊文件export-default.js
,它的默認輸出是一個函數。
其餘模塊加載該模塊時,import
命令能夠爲該匿名函數指定任意名字。
// import-default.js import customName from './export-default'; customName(); // 'foo'
// 第一組 export default function crc32() { // 輸出 // ... } import crc32 from 'crc32'; // 輸入 // 第二組 export function crc32() { // 輸出 // ... }; import {crc32} from 'crc32'; // 輸入
上面代碼的兩組寫法,第一組是使用export default
時,對應的import
語句不須要使用大括號;第二組是不使用export default
時,對應的import
語句須要使用大括號。
export default
命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export default
命令只能使用一次。因此,import
命令後面纔不用加大括號,由於只可能對應一個方法。
// modules.js function add(x, y) { return x * y; } export {add as default}; // 等同於 // export default add; // app.js import { default as xxx } from 'modules'; // 等同於 // import xxx from 'modules';
export { foo, bar } from 'my_module'; // 等同於 import { foo, bar } from 'my_module'; export { foo, bar };
ES6 模塊加載的機制,與 CommonJS 模塊徹底不一樣。CommonJS模塊輸出的是一個值的拷貝,而 ES6 模塊輸出的是值的引用。
瀏覽器使用 ES6 模塊的語法以下。
<script type="module" src="foo.js"></script>
上面代碼在網頁中插入一個模塊foo.js
,因爲type
屬性設爲module
,因此瀏覽器知道這是一個 ES6 模塊。
「循環加載」(circular dependency)指的是,a
腳本的執行依賴b
腳本,而b
腳本的執行又依賴a
腳本。
// a.js var b = require('b'); // b.js var a = require('a');
本書介紹const
命令的時候說過,const
聲明的常量只在當前代碼塊有效。若是想設置跨模塊的常量(即跨多個文件),能夠採用下面的寫法。
// constants.js 模塊 export const A = 1; export const B = 3; export const C = 4; // test1.js 模塊 import * as constants from './constants'; console.log(constants.A); // 1 console.log(constants.B); // 3