ECMAScript 6 筆記(五)

Iterator和for...of循環

1. Iterator(遍歷器)的概念

  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方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含valuedone兩個屬性的對象。其中,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句自己沒有返回值,或者說老是返回undefinednext方法能夠帶一個參數,該參數就會被看成上一個yield語句的返回值。

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

 

  上面代碼先定義了一個能夠無限運行的 Generator 函數f,若是next方法沒有參數,每次運行到yield語句,變量reset的值老是undefined。當next方法帶一個參數true時,變量reset就被重置爲這個參數(即true),所以i會等於-1,下一輪循環就會從-1開始遞增。

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屬性爲truefor...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 命令

  模塊功能主要由兩個命令構成:exportimportexport命令用於規定模塊的對外接口,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
相關文章
相關標籤/搜索