ES6核心內容精講--快速實踐ES6(二)

Iterator和for...of

是什麼:

Iterator(迭代器)是專門用來控制如何遍歷的對象,具備特殊的接口。javascript

Iterator接口是一種數據遍歷的協議,只要調用迭代器對象對象的next方法,就會獲得一個對象,表示當前遍歷指針所在的那個位置的信息,這個包含done和value兩個屬性。html

迭代器對象建立後,能夠反覆調用 next()使用。前端

怎麼用:

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

ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是「可遍歷的」(iterable)。Symbol.iterator屬性自己是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。node

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this
    let index = 0
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          }
        } else {
          return { value: undefined, done: true }
        }
      }
    }
  }
}

for(let item of obj){
    console.log(item)
}
// hello
// world

如上,for-of循環首先調用obj對象的Symbol.iterator方法,緊接着返回一個新的迭代器對象。迭代器對象能夠是任意具備.next()方法的對象,for-of循環將重複調用這個方法,每次循環調用一次。return的對象中value表示當前的值,done表示是否完成迭代。git

Iterator的做用有三個:es6

  1. 爲各類數據結構,提供一個統一的、簡便的訪問接口;github

  2. 使得數據結構的成員可以按某種次序排列;後端

  3. ES6創造了一種新的遍歷命令for...of循環,Iterator接口主要供for...of消費。設計模式

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

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

Symbol

是什麼

ES6引入了一種第六種基本類型的數據:Symbol。Symbol是一種特殊的、不可變的數據類型,能夠做爲對象屬性的標識符使用。

怎麼用

調用Symbol()建立一個新的symbol,它的值與其它任何值皆不相等。

var sym = new Symbol() // TypeError,阻止建立一個顯式的Symbol包裝器對象而不是一個Symbol值
var s1 = Symbol('foo')
var s2 = Symbol('foo')
s1 === s2 // false

經常使用使用場景:

因爲每個Symbol值都是不相等的,所以常做爲對象的屬性名來防止某一個鍵被不當心改寫或覆蓋,這個以symbol爲鍵的屬性能夠保證不與任何其它屬性產生衝突。

做爲對象屬性名時的遍歷:參見對象的遍歷那節

內置的Symbol值:

除了定義本身使用的Symbol值之外,ES6還提供了11個內置的Symbol值,指向語言內部使用的方法。其中一個很重要的就是Iterator中提到的Symbol.iterator

Reflect(反射)

是什麼

Reflect是一個內置的對象,它提供可攔截JavaScript操做的方法。

爲何要增長Reflect對象

參考連接

1)更有用的返回值

好比,Object.defineProperty(obj, name, desc)在沒法定義屬性時,會拋出一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。

// 老寫法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

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

3)更加可靠的函數調用方式

在ES5中,當咱們想傳一個參數數組args來調用函數f,而且將this綁定爲this,能夠這樣寫:

f.apply(obj, args)

可是,f多是一個故意或者不當心定義了它本身的apply方法的對象。當你想確保你調用的是內置的apply方法時,一種典型的方法是這樣寫的:

Function.prototype.apply.call(f, obj, args)

可是這種方法不只冗長並且難以理解。經過使用Reflect,你能夠以一種更簡單、容易的方式來可靠地進行函數調用

Reflect.apply(f, obj, args)

4)可變參數的構造函數

假設你想調用一個參數是可變的構造函數。在ES6中,因爲新的擴展運算符,你可能能夠這樣寫:

var obj = new F(...args)

在ES5中,這更加難寫,由於只有經過F.apply或者F.call傳遞可變參數來調用函數,可是沒有F.contruct來傳遞可變參數實例化一個構造函數。經過Reflect,在ES5中能夠這樣寫(內容翻譯自參考連接,連接的項目是ES6 Reflect和Proxy的一個ES5 shim,因此會這麼說):

var obj = Reflect.construct(F, args)

5)爲Proxy(代理,見下一章)的traps提供默認行爲

當使用Proxy對象去包裹存在的對象時,攔截一個操做是很常見的。執行一些行爲,而後去「作默認的事情」,這是對包裹的對象進行攔截操做的典型形式。例如,我只是想在獲取對象obj的屬性時log出全部的屬性:

var loggedObj = new Proxy(obj, {
  get: function(target, name) {
    console.log("get", target, name);
    // now do the default thing
  }
});

Reflect和Proxy的API被設計爲互相聯繫、協同的,所以每一個Proxy trap都有一個對應的Reflect去「作默認的事情」。所以當你發現你想在Proxy的handler中「作默認的事情」是,正確的事情永遠都是去調用Reflect對象對應的方法:

var loggedObj = new Proxy(obj, {
  get: function(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
  }
});

Reflect方法的返回類型已經被確保了能和Proxy traps的返回類型兼容。

6)控制訪問或者讀取時的this

var name = ... // get property name as a string
Reflect.get(obj, name, wrapper) // if obj[name] is an accessor, it gets run with `this === wrapper`
Reflect.set(obj, name, value, wrapper)

靜態方法

Reflect對象一共有14個靜態方法(其中Reflect.enumerate被廢棄)

與大多數全局對象不一樣,Reflect沒有構造函數。不能將其與一個new運算符一塊兒使用,或者將Reflect對象做爲一個函數來調用。

Reflect對象提供如下靜態函數,它們與代理處理程序方法(Proxy的handler)有相同的名稱。這些方法中的一些與Object上的對應方法基本相同,有些遍歷操做稍有不一樣,見對象擴展遍歷那節。

Reflect.apply()

對一個函數進行調用操做,同時能夠傳入一個數組做爲調用參數。和Function.prototype.apply()功能相似。

Reflect.construct()

對構造函數進行new操做,至關於執行new target(...args)。

Reflect.defineProperty()

和Object.defineProperty()相似。

Reflect.deleteProperty()

刪除對象的某個屬性,至關於執行delete target[name]。

Reflect.enumerate()

該方法會返回一個包含有目標對象身上全部可枚舉的自身字符串屬性以及繼承字符串屬性的迭代器,for...in 操做遍歷到的正是這些屬性。

Reflect.get()

獲取對象身上某個屬性的值,相似於target[name]。

Reflect.getOwnPropertyDescriptor()

相似於Object.getOwnPropertyDescriptor()。

Reflect.getPrototypeOf()

相似於Object.getPrototypeOf()。

Reflect.has()

判斷一個對象是否存在某個屬性,和in運算符的功能徹底相同。

Reflect.isExtensible()

相似於Object.isExtensible().

Reflect.ownKeys()

返回一個包含全部自身屬性(不包含繼承屬性)的數組。

Reflect.preventExtensions()

相似於Object.preventExtensions()。

Reflect.set()

設置對象身上某個屬性的值,相似於target[name] = val。

Reflect.setPrototypeOf()

相似於Object.setPrototypeOf()。

Proxy(代理)

是什麼

Proxy對象用於定義基本操做的自定義行爲 (例如屬性查找,賦值,枚舉,函數調用等)。

一些術語:

  • handler:包含traps的對象。
  • traps:提供訪問屬性的方法,與操做系統中的traps定義類似。
  • target:被代理虛擬化的對象,這個對象經常用做代理的存儲後端。

用法

ES6原生提供Proxy構造函數,用來生成Proxy實例。

var proxy = new Proxy(target, handler);

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

下面代碼對一個空對象進行了代理,重定義了屬性的讀取(get)和設置(set)行爲。

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    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

handler對象的方法

handler是一個包含了Proxy的traps的佔位符對象。

全部的trap都是可選的,若是某個trap沒有定義,將會對target進行默認操做。這些trap和Reflect的靜態方法是對應的,可使用Reflect對應的靜態方法提供默認行爲。上面的例子中,handler定義了get和set兩個trap,每一個trap都是一個方法,接收一些參數。返回了對應的Reflect方法來執行默認方法。

handler的每一個方法能夠理解爲對相應的某個方法進行代理攔截。

handler.getPrototypeOf(target):Object.getPrototypeOf的一個trap

handler.setPrototypeOf(target, proto):Object.setPrototypeOf的一個trap

handler.isExtensible(target):Object.isExtensible的一個trap

handler.preventExtensions(target):Object.preventExtensions的一個trap

handler.getOwnPropertyDescriptor(target, propKey):Object.getOwnPropertyDescriptor的一個trap

handler.defineProperty(target, propKey, propDesc):Object.defineProperty的一個trap

handler.has(target, propKey):in操做的一個trap

handler.get(target, propKey, receiver):獲取屬性值的一個trap

handler.set(target, propKey, value, receiver):設置屬性值的一個trap

handler.deleteProperty(target, propKey):delete操做的一個trap

handler.ownKeys(target):Object.getOwnPropertyNames和Object.getOwnPropertySymbols的一個trap

handler.apply(target, object, args):函數調用的一個trap

handler.construct(target, args):new操做的一個trap

Proxy.revocable()

Proxy.revocable方法返回一個可取消的Proxy實例。

let target = {};
let handler = {};

let {proxy, revoke} = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

Proxy.revocable方法返回一個對象,該對象的proxy屬性是Proxy實例,revoke屬性是一個函數,能夠取消Proxy實例。上面代碼中,當執行revoke函數以後,再訪問Proxy實例,就會拋出一個錯誤。

Proxy.revocable的一個使用場景是,目標對象不容許直接訪問,必須經過代理訪問,一旦訪問結束,就收回代理權,不容許再次訪問。

使用場景

上面說的那些可能都比較虛,去看一下w3cplus上翻譯的實例解析ES6 Proxy使用場景,可能就會更清楚地明白該怎麼用。

如實例解析ES6 Proxy使用場景中所說,Proxy其功能很是相似於設計模式中的代理模式,該模式經常使用於三個方面:

  • 攔截和監視外部對對象的訪問
  • 下降函數或類的複雜度
  • 在複雜操做前對操做進行校驗或對所需資源進行管理

有如下5個常見使用場景:

  1. 抽離校驗模塊

  2. 私有屬性

  3. 訪問日誌

  4. 預警和攔截

  5. 過濾操做

類與繼承

類:

將原先JavaScript中傳統的經過構造函數生成新對象的方式變爲類的方式,contructor內是構造函數執行的代碼,外面的方法爲原型上的方法

// 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 Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  // 靜態方法,static關鍵字,就表示該方法不會被實例繼承(可是會被子類繼承),而是直接經過類來調用
  static classMethod() {
    return 'hello'
  }

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

繼承:

經過extends關鍵字來實現。super關鍵字則是用來調用父類

ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制徹底不一樣,實質是先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this。理解了這句話,下面1,2兩點也就順其天然了:

1)子類必須在constructor方法中調用super方法,不然新建實例時會報錯。這是由於子類沒有本身的this對象,而是繼承父類的this對象,而後對其進行加工。若是不調用super方法,子類就得不到this對象。

2)在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。

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

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

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

3)mixin: 繼承多個類

function mix(...mixins) {
  class Mix {}

  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) {
  // ...
}

4)new.target屬性:經過檢查new.target對象是不是undefined,能夠判斷函數是否經過new進行調用。

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, '張三')  // 報錯

Decorator(裝飾器)

是什麼

Decorator是用來修改類(包括類和類的屬性)的一個函數。

這是ES的一個提案,實際上是ES7的特性,目前Babel轉碼器已經支持。

怎麼用

1)修飾類:在類以前使用@加函數名,裝飾器函數的第一個參數,就是所要修飾的目標類

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

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

裝飾器函數也能夠是一個工廠方法

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

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

2)修飾類的屬性:修飾器函數一共能夠接受三個參數,第一個參數是所要修飾的目標對象,第二個參數是所要修飾的屬性名,第三個參數是該屬性的描述對象。裝飾器在做用於屬性的時候,其實是經過Object.defineProperty來進行擴展和封裝的。

下面是一個例子,修改屬性描述對象的enumerable屬性,使得該屬性不可遍歷。

class Person {
  @nonenumerable
  get kidCount() { return this.children.length; }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

實踐

core-decorators.js這個第三方模塊提供了幾個常見的修飾器。

在修飾器的基礎上,能夠實現Mixin模式等。

Module(模塊)

在ES6以前,前端和nodejs實踐中已經有一些模塊加載方案,如CommonJS、AMD、CMD等。ES6在語言標準的層面上,實現了模塊功能。

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

export

一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。必須使用export關鍵字輸出該變量。有如下兩種不一樣的導出方式:

命名導出

命名導出規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。

export { myFunction }; // 導出一個函數聲明
export const foo = Math.sqrt(2); // 導出一個常量

默認導出 (每一個腳本只能有一個),使用export default命令:

export default myFunctionOrClass

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

對於只導出一部分值來講,命名導出的方式頗有用。在導入時候,可使用相同的名稱來引用對應導出的值。

關於默認導出方式,每一個模塊只有一個默認導出。一個默認導出能夠是一個函數,一個類,一個對象等。當最簡單導入的時候,這個值是將被認爲是」入口」導出值。

import

使用export命令定義了模塊的對外接口之後,其餘JS文件就能夠經過import命令加載這個模塊。

import { foo, bar } from 'my_module' // 指定加載某個輸出值

import 'lodash'; // 僅執行

import { lastName as surname } from './profile'; // 爲輸入的模塊重命名

import * as circle from './circle'; // 總體加載

/*export和import複合寫法*/
export { foo, bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';
export { foo, bar };

ES6模塊與CommonJS模塊的差別

它們有兩個重大差別。

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

CommonJS是運行時加載,ES6是編譯時加載,使得靜態分析成爲可能

注意事項

  1. ES6的模塊自動採用嚴格模式。所以ES6模塊中,頂層的this指向undefined。

  2. export通常放在兩頭即開始或者結尾這樣更能清晰地明白暴露了什麼變量

  3. 注意,import命令具備提高效果,會提高到整個模塊的頭部,首先執行。由於不是運行時加載,不支持條件加載、按需加載等

相關文章
相關標籤/搜索