Iterator和for...of循環
1. Iterator(遍歷器)的概念
Iterator接口的目的,就是為所有數據結構,提供了一種統一的訪問機制,即for...of
循環
遍歷器(Iterator)就是這樣一種機制。它是一種接口,為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。
Iterator的作用有三個:一是為各種數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員能夠按某種次序排列;三是ES6創造了一種新的遍歷命令for...of
循環,Iterator接口主要供for...of
消費。
Iterator的遍歷過程是這樣的。
(1)創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
(2)第一次調用指針對象的next
方法,可以將指針指向數據結構的第一個成員。
(3)第二次調用指針對象的next
方法,指針就指向數據結構的第二個成員。
(4)不斷調用指針對象的next
方法,直到它指向數據結構的結束位置。
每一次調用next
方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含value
和done
兩個屬性的對象。其中,value
屬性是當前成員的值,done
屬性是一個布爾值,表示遍歷是否結束。
在ES6中,有些數據結構原生具備Iterator接口(比如數組),即不用任何處理,就可以被for...of
循環遍歷,有些就不行(比如對象)。原因在于,這些數據結構原生部署了Symbol.iterator
屬性
2. 調用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*后面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
3. 字符串的Iterator接口
4. for...of循環
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循環是以字符串作為鍵名“0”、“1”、“2”等等。
- for...in循環不僅遍歷數字鍵名,還會遍歷手動添加的其他鍵,甚至包括原型鏈上的鍵。
- 某些情況下,for...in循環會以任意順序遍歷鍵名。
總之,for...in
循環主要是為遍歷對象而設計的,不適用于遍歷數組。
- 有著同for...in一樣的簡潔語法,但是沒有for...in那些缺點。
- 不同用于forEach方法,它可以與break、continue和return配合使用。
- 提供了遍歷所有數據結構的統一操作接口。
Generator 函數的語法
形式上,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 }
1. yield語句
由于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
語句用作函數參數或賦值表達式的右邊,可以不加括號。
2. next方法的參數
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
開始遞增。
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
上面代碼使用for...of
循環,依次顯示5個yield
語句的值。這里需要注意,一旦next
方法的返回對象的done
屬性為true
,for...of
循環就會中止,且不包含該返回對象,所以上面代碼的return
語句返回的6,不包括在for...of
循環之中。
Generator 函數的異步應用
1. 基本概念
回調函數
JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數里面,等到重新執行這個任務的時候,就直接調用這個函數。
Class
1. Class基本語法
基本上,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,是從表達式得到的。
2. Class的繼承
基本用法
Class之間可以通過extends
關鍵字實現繼承
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 調用父類的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 調用父類的toString() } }
子類必須在constructor
方法中調用super
方法,否則新建實例時會報錯。這是因為子類沒有自己的this
對象,而是繼承父類的this
對象,然后對其進行加工。如果不調用super
方法,子類就得不到this
對象。
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
3. Class 的靜態方法
類相當于實例的原型,所有在類中定義的方法,都會被實例繼承。如果在一個方法前,加上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();
4. Class的靜態屬性和實例屬性
靜態屬性指的是Class本身的屬性,即Class.propname
,而不是定義在實例對象(this
)上的屬性。
class Foo { } Foo.prop = 1; Foo.prop // 1
目前,只有這種寫法可行,因為ES6明確規定,Class內部只有靜態方法,沒有靜態屬性。
5. 類的私有屬性
目前,有一個提案,為class
加了私有屬性。方法是在屬性名之前,使用#
表示。
修飾器
1. 類的修飾
修飾器(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;
也就是說,修飾器本質就是編譯時執行的函數。
修飾器函數的第一個參數,就是所要修飾的目標類。
2. 方法的修飾
class Person { @readonly name() { return `${this.first} ${this.last}` } } //上面代碼中,修飾器readonly用來修飾“類”的name方法。
Module
ES6 模塊不是對象,而是通過export
命令顯式指定輸出的代碼,再通過import
命令輸入。
// ES6模塊 import { stat, exists, readFile } from 'fs';
1. 嚴格模式
ES6 的模塊自動采用嚴格模式,不管你有沒有在模塊頭部加上"use strict";
。
2. export 命令
模塊功能主要由兩個命令構成: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
命令可以出現在模塊的任何位置,只要處于模塊頂層就可以。如果處于塊級作用域內,就會報錯
3. import 命令
使用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';
4. export 與 import 的復合寫法
export { foo, bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module'; export { foo, bar };
5. ES6模塊加載的實質
ES6 模塊加載的機制,與 CommonJS 模塊完全不同。CommonJS模塊輸出的是一個值的拷貝,而 ES6 模塊輸出的是值的引用。
6. 瀏覽器的模塊加載
瀏覽器使用 ES6 模塊的語法如下。
<script type="module" src="foo.js"></script>
上面代碼在網頁中插入一個模塊foo.js
,由于type
屬性設為module
,所以瀏覽器知道這是一個 ES6 模塊。
7. 循環加載
“循環加載”(circular dependency)指的是,a
腳本的執行依賴b
腳本,而b
腳本的執行又依賴a
腳本。
// a.js var b = require('b'); // b.js var a = require('a');
8. 跨模塊常量
本書介紹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
文章列表