弄清Classs,Symbols,Objects拓展 和 Decorators

本文翻譯自 Nicolas Bevacqua 的書籍 《Practical Modern JavaScript》,這是該書的第三章。翻譯採用意譯並進行必定的刪減和拓展,部份內容與原書有所不一樣。javascript

類(classes)多是ES6提供的,咱們使用最廣的新功能之一了,它以原型鏈爲基礎,爲咱們提供了一種基於類編程的模式。Symbol是一種新的基本類型(JS中的第七種基本類型,另外六種爲undefinednull、布爾值(Boolean)、字符串(String)、數值(Number)、對象(Object)),它能夠用來定義不可變值。本章,咱們將首先討論類和符號,以後咱們還將對ES6對對象的拓展及處於stage2階段的裝飾器進行簡單的講解。css

咱們知道,JavaScript是一門基於原型鏈的語言,ES6中的類和其它面嚮對象語言中的類在本質上有很大的不一樣,JavaScript中,類其實是一種基於原型鏈的語法糖。html

雖然如此,JavaScript中的類仍是給咱們的不少操做帶來了方便,好比說能夠輕易拓展其它類,經過簡單的語法咱們就能夠拓展內置的Array了,在下文中咱們將詳細說明如何使用。java

類基礎

基於已有的知識學習新知識是一種很是好的學習方法,對比學習可讓咱們對新知識有更深的印象。因爲JS中類其實是一種基於原型鏈的語法糖,咱們先簡單複習基於原型鏈的JavaScript構造器要怎麼使用,而後咱們用ES6中類語法實現相同的功能做爲對比。git

下面代碼中,咱們新建了構造函數Fruit用以表示某種水果。該構造函數接收兩個參數,水果的名稱 -- name,水果的卡路里含量 -- calaries。在Fruit構造函數中咱們設置了默認的塊數 pieces=1 ,經過原型鏈,咱們還爲該構造函數添加了兩種方法:github

  • chop 方法(切水果,每次調用會使得塊數加一);web

  • bite方法(接收一個名爲person的參數,它是一個對象,每次調用,該 person 將吃掉一塊水果,person 的飽腹感 person.satiety 將相應的增長,增長值爲一塊水果的calaries值,水果的總的卡路里值 this.calories將減小相應的值)。正則表達式

function Fruit(name, calories) {
  this.name = name
  this.calories = calories
  this.pieces = 1
}
Fruit.prototype.chop = function () {
  this.pieces++
}
Fruit.prototype.bite = function (person) {
  if (this.pieces < 1) {
    return
  }
  const calories = this.calories / this.pieces
  person.satiety += calories
  this.calories -= calories
  this.pieces--
}

接下來咱們建立一個Fruit構造函數的實例,調用三次 chop 方法將實例 apple 分爲四塊,新建person對象,傳入並調用三次bite方法,把apple 吃掉三塊。編程

const person = { satiety: 0 }
const apple = new Fruit('apple', 140)
apple.chop()
apple.chop()
apple.chop()
apple.bite(person)
apple.bite(person)
apple.bite(person)
console.log(person.satiety)
// <- 105
console.log(apple.pieces)
// <- 1
console.log(apple.calories)
// <- 35

做爲對比,接下來咱們使用類語法來實現上述代碼同樣的過程。在類中,咱們顯式使用constructor方法作爲構造方法(其中this指向類的實例),在類中定義方法相似在對象字面量中定義方法,見下述代碼中chop,bite的定義。類全部的方法都聲明在class的塊中,不須要再使用Fruit.prototype這類樣本代碼,從這個角度看與基於原型的語法比起來,類語法語義清晰,使用起來也顯得簡潔。json

class Fruit {
  constructor(name, calories) {
    this.name = name
    this.calories = calories
    this.pieces = 1
  }
  chop() {
    this.pieces++
  }
  bite(person) {
    if (this.pieces < 1) {
      return
    }
    const calories = this.calories / this.pieces
    person.satiety += calories
    this.calories -= calories
    this.pieces--
  }
}

雖然在類中定義方法和使用對象字面量相似,可是也有一個較大的不一樣點,那就是類中 方法之間不能使用逗號 ,這是類語法的要求。這種要求幫助咱們避免混用對象和類,類和對象原本也不同,這種要求的另一個好處在於爲將來類的改進作下了鋪墊,將來JS的類中可能還會添加publicprivate等。

和普通函數聲明不一樣的是,類聲明並不會被提高到做用域的頂部,所以提早調用會報錯。

類聲明有兩種方法,一種是像函數聲明和函數表達式同樣,聲明爲表達式,以下代碼所示:

const Person = class {
  constructor(name) {
    this.name = name
  }
}

類聲明的另一種語法以下:

const class Person{
  constructor(name) {
    this.name = name
  }
}

類還能夠做爲函數的返回值,這使得建立類工廠很是容易,以下代碼中,箭頭函數接收了一個名爲name的參數,super()方法把這個參數反饋給其父類Person.這樣就建立了一個基於Person的新類:

// 這裏實際用到的是類的第一種聲明方式
const createPersonClass = name => class extends Person {
  constructor() {
    super(name)
  }
}
const JakePerson = createPersonClass('Jake')
const jake = new JakePerson()

上面代碼中的extends關鍵字代表這裏使用到了類繼承,稍後咱們將詳細討論類繼承,在此以前咱們先仔細如何在類中定義屬性和方法。

類中的屬性和方法

類聲明中的constructor方法是可選的。若是省略,JS將爲咱們自動添加,下面用類聲明和用常規構造函數聲明的Fruit是同樣的:

// 用類聲明Fruit
class Fruit {
}

// 使用構造函數聲明Fruit
function Fruit() {
}

全部傳入類的參數,都將作爲類中constructor的參數,以下全部傳入Log()的參數都將做爲Logconstructor的參數,這些參數將用以初始化類的實例:

class Log {
  constructor(...args) {
    console.log(args)
  }
}
new Log('a', 'b', 'c')
// <- ['a' 'b' 'c']

下面的代碼中,咱們定義了類Counter,在constructor中定義的代碼會在實例化類時自動執行,這裏咱們在實例化時爲實例添加了一個count屬性,next屬性前面添加了get,則表示類Counter的全部實例都有一個next屬性,每次某實例訪問next屬性值時,其值都將+1:

class Counter {
  constructor(start) {
    this.count = start
  }
  get next() {
    return this.count++
  }
}

咱們新建了Counter類的實例counter,能夠發現每一次counter.next被調用的時,count值增長1。

const counter = new Counter(2)
console.log(counter.next)
//  2
console.log(counter.next)
//  3
console.log(counter.next)
//  4

getter 綁定一個屬性,其後爲一個函數,每次該屬性被訪問,其後的函數將被執行;

setter 語法綁定一個屬性,其後跟着一個函數,當爲該函數設置爲某個值時,其後的函數將被調用;

當結合使用gettersetter時,咱們能夠完成一些神奇的事情,下例中,咱們定義了類LocalStorage,這個類使用提供的存儲key,在讀取data值時,實現了同時在localStorage中存儲和取出相關數據。

class LocalStorage {
  constructor(key) {
    this.key = key
  }
  get data() {
    return JSON.parse(localStorage.getItem(this.key))
  }
  set data(data) {
    localStorage.setItem(this.key, JSON.stringify(data))
  }
}

咱們看看如何使用類LocalStorage

新建LocalStorage的實例ls,傳入lskeygroceries,當咱們設置ls.data爲某個值時,該值將被轉換爲JSON對象字符串,並存儲在localStorage中;當使用相應的key進行讀取時,將提取出以前存儲在localStorage中的內容,以JSON的格式進行解析後返回:

const ls = new LocalStorage('groceries')
ls.data = ['apples', 'bananas', 'grapes']
console.log(ls.data)
// <- ['apples', 'bananas', 'grapes']

除了使用getterssetters,咱們也能夠定義常規的實例方法,繼續以前定義過的Fruit類,咱們再定義了一個能夠吃水果的Person類,咱們實例化一個fruit和一個person,而後讓 personfruit 。這裏咱們讓person吃完了全部的fruit,結果是personsatiety(飽食度)上升到了40。

class Person {
  constructor() {
    this.satiety = 0
  }
  eat(fruit) {
    while (fruit.pieces > 0) {
      fruit.bite(this)
    }
  }
}
const plum = new Fruit('plum', 40)
const person = new Person()
person.eat(plum)
console.log(person.satiety)
// <- 40

有時候咱們可能會但願靜態方法直接定義在類上,若是使用ES6以前的語法,咱們須要將該方法直接添加於構造函數上,以下面的Person.isPerson:

function Person() {
  this.hunger = 100
}
Person.prototype.eat = function () {
  this.hunger--
}
Person.isPerson = function (person) {
  return person instanceof Person
}

類語法則容許經過添加前綴static來定義靜態方法Persion.isPerson

下屬代碼咱們給類MathHelper定義了一個靜態方法sum,這個方法將用以計算實例化時全部傳入參數的總和。

class MathHelper {
  static sum(...numbers) {
    return numbers.reduce((a, b) => a + b)
  }
}
console.log(MathHelper.sum(1, 2, 3, 4, 5))
// <- 15

類的繼承

ES6以前,你可使用原型鏈來模擬類的繼承,以下代碼所示,咱們新建了的構造函數Banana,用以拓展上文中定義的Fruit類,爲了Banana可以正確初始化,咱們須要在Banana中調用Fruit.call(this, 'banana', 105),此外還須要顯式的設置Bananaprototype

function Banana() {
  Fruit.call(this, 'banana', 105)
}
Banana.prototype = Object.create(Fruit.prototype)
Banana.prototype.slice = function () {
  this.pieces = 12
}

上述代碼一點也稱不上簡潔,通常JS開發者會使用庫來解決繼承問題。好比說Node.js就提供了util.inherits

const util = require('util')
function Banana() {
  Fruit.call(this, 'banana', 105)
}
util.inherits(Banana, Fruit)
Banana.prototype.slice = function () {
  this.pieces = 12
}

考慮到,banana除了有肯定的namecalories,以及額外的slice方法(用來把banana切爲12塊)外,Banana構造函數和Fruit構造函數其實沒有區別,咱們能夠在Banana中也執行bite

const person = { satiety: 0 }
const banana = new Banana()
banana.slice()
banana.bite(person)
console.log(person.satiety)
// <- 8.75
console.log(banana.pieces)
// <- 11
console.log(banana.calories)
// <- 96.25

下面咱們看看ES6爲繼承提供的解決方案,下述代碼中,這裏咱們建立了一個繼承自Fruit類的名爲Banana的類。能夠看出,這種語法很是清晰,咱們無須完全弄明白原型的機制就能夠得到咱們想要的結果,若是想給Fruit類傳遞參數,只須要使用super關鍵字便可。super關鍵字還能夠用以調用存在於父類中的方法,好比說super.chop,super`constructor`外面的方法中也可使用:

class Banana extends Fruit {
  constructor() {
    super('banana', 105)
  }
  slice() {
    this.pieces = 12
  }
}

基於JS函數的返回值能夠是任何表達式,下面咱們構建一個構造函數工廠,下面的代碼定義了一個名爲 createJuicyFruit 的函數,經過使用super咱們能夠給Fruit類傳入namecalories,這樣就輕鬆的實現了對createJuicyFruit類的拓展。

const createJuicyFruit = (...params) =>
  class JuicyFruit extends Fruit {
    constructor() {
      this.juice = 0
      super(...params)
    }
    squeeze() {
      if (this.calories <= 0) {
        return
      }
      this.calories -= 10
      this.juice += 3
    }
  }
  
class Plum extends createJuicyFruit('plum', 30) {
}

接下來咱們來說述Symbol,瞭解Symbol對於以後咱們理解迭代相當重要。

Symbols

Symbol是ES6提供的一種新的JS基本類型。 它表明惟一值,和字符串,數值等基本類型的一個很大的不一樣點在於Symbol沒有字符表達形式。Symbol的主要目的是用以實現協議,好比說,使用Symbol定義的迭代協議規定了對象將如何被迭代,關於這個,咱們將在[Iterator Protocol and Iterable Protocol.]()這一章詳細闡述。

ES6提供的Symbol有以下三種不一樣類型:

  • local Symbol

  • global Symbol

  • 語言內置Symbol

這三種類型的Symbol存在着必定的不一樣,咱們一種種來說解,首先看local Symbol

Local Symbol

Local Symbol 經過 Symbol 包裝對象建立,以下:

const first = Symbol()

這裏有一點特別值得咱們注意,在NumberString等包裝對象前是可使用new操做符的,在Symbol前則不能使用,使用了會拋出錯誤,以下:

const oops = new Symbol()
// <- TypeError, Symbol is not a constructor

爲了方便調試,咱們能夠給新建的Symbol添加描述:

const mystery = Symbol('my symbol')

和數值和字符串同樣,Symbol是不可變的,可是和他們不一樣的是,Symbol是惟一的。描述並不影響惟一性,由具備相同描述的Symbol依舊是不相等的,下面代碼說明了這個問題:

console.log(Number(3) === Number(3))
// <- true
console.log(Symbol() === Symbol())
// <- false
console.log(Symbol('my symbol') === Symbol('my symbol'))
// <- false

Symbols的類別爲symbol,使用 typeof 可返回其類型:

console.log(typeof Symbol())
// <- 'symbol'
console.log(typeof Symbol('my symbol'))
// <- 'symbol'

Symbols 能夠用做對象的屬性名,這裏咱們用計算屬性名來講明該如何使用,以下:

const weapon = Symbol('weapon')
const character = {
  name: 'Penguin',
  [weapon]: 'umbrella'
}
console.log(character[weapon])
// <- 'umbrella'

須要注意的是,許多傳統的從對象中提取鍵的方法中對Symbol無效,也就是說他們獲取不到Symbol。以下代碼中的for...in ,Object,keys,Object.getOwnPropertyNames都不能訪問到 Symbol 類型的屬性。

for (let key in character) {
  console.log(key)
  // <- 'name'
}
console.log(Object.keys(character))
// <- ['name']
console.log(Object.getOwnPropertyNames(character))
// <- ['name']

Symbol的這方面的特性使得ES6以前的沒有使用Symbol的代碼並不會因爲Symbol的出現而受影響。以下代碼中,咱們將對象解析爲JSON,結果中的符號屬性被丟棄了。

console.log(JSON.stringify(character))
// <- '{"name":"Penguin"}'

不過,Symbols毫不是一種用來隱藏屬性的安全機制。採用特定的方法,它是可見的,以下所示:

console.log(Object.getOwnPropertySymbols(character))
// <- [Symbol(weapon)]

這意味着,Symbols 並不是不可枚舉的,只是它對通常方法不可見而已,經過Object.getOwnPropertySymbols咱們能夠獲取任何對象中的全部Symbol

如今咱們已經知道了 Symbol 該如何使用,下面咱們再討論下其使用場景。

Symbols的使用實例

Symbol最重要的用途就是用以免命名衝突了,以下代碼中,咱們給DOM元素添加了自定義的屬性,使用Symbol不用擔憂屬性與其它屬性甚至以後JS語言會加入的屬性相沖突:

const cache = Symbol('calendar')
function createCalendar(el) {
  if (cache in el) { // does the symbol exist in the element?
    return el[cache] // use the cache to avoid re-instantiation
  }
  const api = el[cache] = {
    // the calendar API goes here
  }
  return api
}

ES6 還提供的一種名爲WeakMap的新數據類型,它用於惟一地將對象映射到其餘對象。和數組查找表比起來,WeakMap查找複雜度始終爲O(1),咱們將在 [Leveraging ECMAScript Collections]() 一章和其它ES6新增數據類型一塊兒討論這個。

使用符號定義協議

前文中,咱們說過 Symbol 能夠用以定義協議。協議是定義行爲的通訊契約或約定。

下述代碼中,咱們給character對象有一個toJSON方法,這個方法,指定了對該對象使用JSON.stringify時被序列化的對象。

const character = {
  name: 'Thor',
  toJSON: () => ({
    key: 'value'
  })
}
console.log(JSON.stringify(character))
// <- '"{"key":"value"}"'

若是toJSON不是函數,對character對象執行JSON.stringify則會有不一樣的結果,character對象總體將被序列化。有時候這不是咱們想要的結果:

const character = {
  name: 'Thor',
  toJSON: true
}
console.log(JSON.stringify(character))
// <- '"{"name":"Thor","toJSON":true}"'

若是toJSON修飾符是Symbol類型,它就不會影響其它的對象屬性了,不經過Object.getOwnPropertySymbolsSymbol永遠不會暴露出來的,如下代碼中咱們用Symbol自定義序列化函數stringify

const json = Symbol('alternative to toJSON')
const character = {
  name: 'Thor',
  [json]: () => ({
    key: 'value'
  })
}
function stringify(target) {
  if (json in target) {
    return JSON.stringify(target[json]())
  }
  return JSON.stringify(target)
}
stringify(character)

使用 Symbol 須要咱們使用計算屬性名在對象字面量中定義 json,這樣作咱們定義的變量就不會和其它的用戶定義的屬性或者之後JS語言可能會加入的屬性有衝突。

接下來咱們繼續講解下一類符號--global symbol,這類符號能夠跨代碼域訪問。

全局符號

代碼域指的是任何JavaScript表達式的執行上下文,它能夠是你的應用當前運行的頁面、頁面中的<iframe>、由eval運行的腳本、任意類型的workerweb worker,service workers或者shared workers)等等。這些執行上下文每一種都有其全局對象,好比說頁面的全局對象window,可是這種全局對象不能被其它代碼域好比說ServiceWorker使用。相比而言,全局符號則更具全局性,它能夠被任何代碼域訪問。

ES6提供了兩個和全局符號相關的方法,Symbol.forSymbol.keyFor。咱們看看它們分別該如何使用?

經過Symbol.for(key)獲取symbols

Symbol.for(key)方法將在運行時的符號註冊表中查找key,若是全局註冊表中存在key則返回其對於的Symbol,若是不存在該key對於的Symbol,該方法會在全局註冊表中建立一個新的key值爲該key值的Symbol。這意味着,Symbol.for(key)是冪等的(屢次執行,結果惟一),先進行查找,不存在則新建立,而後返回查找到的或新建立的Symbol。

咱們看看使用示例,下面的代碼中,

  • 第一次調用Symbol.for建立了一個key爲example的Symbol,添加到到註冊表,並返回了該Symbol;

  • 第二次調用Symbol.for因爲該key已經在註冊表中存在,所以返回了以前建立的全局符號。

const example = Symbol.for('example')
console.log(example === Symbol.for('example'))
// <- true

全局的符號註冊表經過key標記符號,key還將做爲新建立符號的描述信息。考慮到這些符號在運行時是全局的,在符號的key前添加前綴用以區分你的代碼能夠有效避免潛在的命名衝突。

使用Symbol.keyFor(symbol)來提取符號的key

好比說現存一個名爲爲symbol的全局符號,使用Symbol.keyFor(symbol)將返回全局註冊表中該symbol對應的key值。咱們看如下實例:

const example = Symbol.for('example')
console.log(Symbol.keyFor(example))
// <- 'example'

值得注意的是,若是符號非全局符號,該方法將返回undefined

console.log(Symbol.keyFor(Symbol()))
// <- undefined

在全局符號註冊表中,使用local Symbol是匹配不到值的,即便它們的描述相同也是如此,local Symbol 不是全局符號註冊表的一部分:

const example = Symbol.for('example')
console.log(Symbol.keyFor(Symbol('example')))
// <- undefined

全局符號相關的方法主要就是這兩個了,下面咱們看看該如何實際使用:

全局符號實踐

某符號爲全局符號意味着該符號能夠被任何代碼域獲取,且在任何代碼域中調用,它們都將返回相同的值。下面的例子,咱們使用Symbol.for分別在頁面中和<iframe>中查找key 爲example 的Symbol,實踐代表,它們是相同的。

const d = document
const frame = d.body.appendChild(d.createElement('iframe'))
const framed = frame.contentWindow
const s1 = window.Symbol.for('example')
const s2 = framed.Symbol.for('example')
console.log(s1 === s2)
// <- true

使用全局符號就像咱們使用全局變量同樣,合理使用在某些時候很是便利,可是不合理使用又會形成災難。全局符號在符號須要跨代碼域使用時很是有用,好比說跨ServiceWorker和瀏覽器頁面,可是濫用會致使Symbol難易管理,容易衝突。

下面咱們來看,最後一種Symbol,內置的經常使用Symbol。

內置的經常使用Symbol

內置的經常使用Symbol爲JS語言行爲添加了鉤子,在必定程度上容許你拓展和自定義JS語言。

Symbol.toPrimitive符號,是描述如何經過 Symbols 給語言添加額外的功能的最好的例子,這個Symbol的做用是,依據給定的類型返回默認值。該函數接收一個hint參數,參數能夠是string,numberdefault,用以指明默認值的期待類型。

const morphling = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') {
      return Infinity
    }
    if (hint === 'string') {
      return 'a lot'
    }
    return '[object Morphling]'
  }
}
console.log(+morphling) // + 號 
// <- Infinity
console.log(`That is ${ morphling }!`)
// <- 'That is a lot!'
console.log(morphling + ' is powerful')
// <- '[object Morphling] is powerful'

另外一個經常使用的內置Symbol是 Symbol.match ,該Symbol指定了匹配的是正則表達式而不是字符串,以.startWith,.endWith.includes,這三個ES6提供新字符串方法爲例。

"/bar/".startsWith(/bar/); 
// Throws TypeError, 由於 /bar/ 是一個正則表達式

const text = '/an example string/'
const regex = /an example string/
regex[Symbol.match] = false
console.log(text.startsWith(regex))
// <- true

若是正則表達式沒有經過Symbol修改,這裏將拋出錯誤,由於.startWith方法但願其參數是一個字符串而非正則表達式。

內置Symbol不在全局註冊表中可是跨域共享

這些內置的Symbol是跨代碼域共享的,以下所示:

const frame = document.createElement('iframe')
document.body.appendChild(frame)
Symbol.iterator === frame.contentWindow.Symbol.iterator
// <- true

須要注意的是,雖然語言內置的這些Symbol是跨代碼塊共享的,可是他們並不在全局符號註冊表中,咱們在下述代碼中想要找到Symbol.iteratorkey值,返回值是undefined就說明了這個問題。

console.log(Symbol.keyFor(Symbol.iterator))
// <- undefined

另一個經常使用的符號是Symbol.iterator,它爲每個對象定義了默認的迭代器。咱們將在下一章中詳細講述Symbol.iterator的細節內容。

內置對象的改進

咱們在ES6 概要一章,已經講述過ES6中對象字面量語法的改進,這裏咱們再補充一下內置對象新增的方法。

除了前面討論過的Object.getOwnPropertySymbols,新增的對象方法還有Object.assign,Object.is以及Object.setPrototypeOf

使用Object.assign來拓展對象

咱們在實際開發中經常使用各類庫,一些庫在容許咱們自定義某些行爲,不過爲了使用方便這些庫一般也給出了默認值,而咱們的自定義經常就是在默認值的基礎上進行的。

假如說如今有這麼一個Markdown庫。其接收一個 input 參數,依據input表明的Markdown內容,轉換其爲 Html 是其默認的用法,用戶不須要提供其它參數就能夠簡單使用這個庫。不過,該庫還支持多個高級的配置,只是默認是關閉的,好比說經過配置能夠添加<script><iframe>,能夠啓用 css 來高亮渲染代碼片斷。

好比說,該庫的默認選項以下:

const defaults = {
  scripts: false,
  iframes: false,
  highlightSyntax: true
}

咱們可使用解構將defaults對象設置爲options的默認值,在之前,若是用戶想要自定義,用戶必須提供每一個選項的值。

function md(input, options=defaults) {
}

Object.assign 就是爲這種場景而生,這個方法能夠很是方便的合併默認值和用戶提供的值,以下代碼所示,咱們傳入{}做爲Object.assign的第一個參數,以後這個參數將不斷與後面的參數對比合並,後面參數中的重複值將覆蓋前面之後的值,待全部的比較合併完成,咱們將得到最終的值。

function md(input, options) {
  const config = Object.assign({}, defaults, options)
}

理解Object.assign第一個參數的特殊意義

Object.assign的返回值是依據第一個參數而來的,第一個參數最終會修改成返回值,參數可看作(target, ...sources),全部的 sources 都會被應用到target中。

若是這裏咱們的第一個參數不是一個空對象,而是defaults,那麼Object.assign()執行結束以後,defaults對象的值也將被改變,雖然這裏咱們會獲得和前面那個例子中同樣的結果,可是因爲default值被改變,在別的地方可能也會致使一些意想不到的問題。

function md(input, options) {
  const config = Object.assign(defaults, options)
}

所以,最好把Object.assign的第一個參數始終設置爲{}

下面的代碼加深你對Object.assign的理解:

const defaults = {
  first: 'first',
  second: 'second'
}
function applyDefaults(options) {
  return Object.assign({}, defaults, options)
}
applyDefaults()
// <- { first: 'first', second: 'second' }
applyDefaults({ third: 3 })
// <- { first: 'first', second: 'second', third: 3 }
applyDefaults({ second: false })
// <- { first: 'first', second: false }

須要注意的是,Object.assign只會考慮可枚舉的屬性(包括字符串屬性和符號屬性)。

const defaults = {
  [Symbol('currency')]: 'USD'
}
const options = {
  price: '0.99'
}
Object.defineProperty(options, 'name', {
  value: 'Espresso Shot',
  enumerable: false
})
console.log(Object.assign({}, defaults, options))
// <- { [Symbol('currency')]: 'USD', price: '0.99' }

不過Object.assign也不是萬能的,好比說其複製並不是深複製,Object.assign不會對對象進行迴歸處理,值爲對象的屬性將會被target直接引用。

下例中,你可能但願f屬性能夠被添加到target.a,而保持b.c,b.d不變,可是實際上,當使用Object.assign時,b.cb.d屬性丟失了。

Object.assign({}, { a: { b: 'c', d: 'e' } }, { a: { f: 'g' } })
// <- { a: { f: 'g' } }

一樣的,數據也存在相似的問題,如下代碼中,若是你期待Object.assign進行遞歸處理,你將大失所望。

Object.assign({}, { a: ['b', 'c', 'd'] }, { a: ['e', 'f'] })
// <- { a: ['e', 'f'] }

在本書寫做過程當中,存在一個處於stage 3的ECMAScript提議,用以在對象中使用拓展符,其使用相似於數組等可迭代對象。對對象使用拓展和使用Object.assign的結果相似。

下述代碼展現了對象拓展符的使用方法:

const grocery = { ...details }
// Object.assign({}, details)
const grocery = { type: 'fruit', ...details }
// Object.assign({ type: 'fruit' }, details)
const grocery = { type: 'fruit', ...details, ...fruit }
// Object.assign({ type: 'fruit' }, details, fruit)
const grocery = { type: 'fruit', ...details, color: 'red' }
// Object.assign({ type: 'fruit' }, details, { color: 'red' })

該提案也包含對象剩餘值,使用和數組剩餘值相似。

下面是對象剩餘值的使用實例,就像數組剩餘值同樣,其須要位於結構的最後面:

const getUnknownProperties = ({ name, type, ...unknown }) =>  unknown
getUnknownProperties({
  name: 'Carrot',
  type: 'vegetable',
  color: 'orange'
})
// <- { color: 'orange' }

咱們能夠利用相似的方法在變量聲明時解構對象,下例中,每個未明確指明的屬性都將位於meta對象中:

const { name, type, ...meta } = {
  name: 'Carrot',
  type: 'vegetable',
  color: 'orange'
}
// <- name = 'Carrot'
// <- type = 'vegetable'
// <- meta = { color: 'orange' }

咱們將在[Practical Considerations.]()一章再詳細討論對象解構和剩餘值。

使用Object.is對比對象

Object.is方法和嚴格相等運算符===略有不一樣。主要表如今兩個地方,NaN以及,-00

NaNNaN相比較時,嚴格相等運算符===將返回false,由於NaN和自己也不相等,Object.is則在這種狀況下返回true.

NaN === NaN
// <- false
Object.is(NaN, NaN)
// <- true

使用嚴格相等運算符比較0-0會獲得true,而使用Object.is則會返回false.

-0 === +0
// <- true
Object.is(-0, +0)
// <- false

Object.setPrototpyeOf

Object.setPrototypeOf,名如其意,它用以設置某個對象的原型指向的對象。與遺留方法__proto__相比,它是被承認的設置對象原型的方法。

還記得嗎,咱們在ES5中引入了Object.create,這個方法容許咱們以任何傳遞給Object.create的參數做爲新建對象的原型鏈:

const baseCat = { type: 'cat', legs: 4 }
const cat = Object.create(baseCat)
cat.name = 'Milanesita'

Object.create方法只能在新建立的對象時指定原型,Object.setPrototypeOf則能夠用以改變任何已經存在的對象的原型鏈:

const baseCat = { type: 'cat', legs: 4 }
const cat = Object.setPrototypeOf(
  { name: 'Milanesita' },
  baseCat
)

Object.create比起來,Object.setPrototypeOf具備嚴重的性能問題,所以在若是你很在意這個,使用前應好好考慮。

對性能問題的說明

使用Object.setPrototypeOf來改變一個對象的原型是一個昂貴的操做,MDN是這樣解釋的:
因爲現代 JavaScript 引擎優化屬性訪問所帶來的特性的關係,更改對象的 [[Prototype]]在各個瀏覽器和 JavaScript 引擎上都是一個很慢的操做。其在更改繼承的性能上的影響是微妙而又普遍的,這不只僅限於 obj.__proto__ = ... 語句上的時間花費,並且可能會延伸到任何代碼,那些能夠訪問任何[[Prototype]]已被更改的對象的代碼。若是你關心性能,你應該避免設置一個對象的 [[Prototype]]。相反,你應該使用 Object.create()來建立帶有你想要的[[Prototype]]的新對象。

裝飾器(Decorators)

對於大多數編程語言而言,裝飾器不是一個新概念。在現代編程語言中,裝飾器模式至關常見,c# 中 有attributes,Java中有annotations,Python中有decorators等等。目前也存在一個處於Stage2 的JavaScript的裝飾器提案。

JavaScript中的裝飾器語法和Python的很是相似。JavaScript的裝飾器能夠應用於任何對象或者靜態聲明的屬性前。諸如對象字面量聲明或class聲明前,或get,set,static前。

@inanimate
class Car {}

@expensive
@speed('fast')
class Lamborghini extends Car {}

class View {
  @throttle(200) // reconcile once every 200ms at most
  reconcile() {}
}

關於裝飾器凹凸實驗室的一篇文章解釋的比較清楚,你們能夠參考Javascript 中的裝飾器

當裝飾器做用於類自己的時候,咱們操做的對象也是這個類自己,而當裝飾器做用於類的某個具體的屬性的時候,咱們操做的對象既不是類自己,也不是類的屬性,而是它的描述符(descriptor),而描述符裏記錄着咱們對這個屬性的所有信息,因此,咱們能夠對它自由的進行擴展和封裝,最後達到的目的呢,就和以前說過的裝飾器的做用是同樣的。能夠看以下兩段代碼加深理解

做用於類時

function isAnimal(target) {
    target.isAnimal = true;
      return target;
}
@isAnimal
class Cat {
    ...
}
console.log(Cat.isAnimal);    // true

// 至關於
    
Cat = isAnimal(function Cat() { ... });

做用於類屬性時

function readonly(target, name, descriptor) {
    discriptor.writable = false;
    return discriptor;
}
class Cat {
    @readonly
    say() {
        console.log("meow ~");
    }
}
var kitty = new Cat();
kitty.say = function() {
    console.log("woof !");
}
kitty.say()    // meow ~

// 至關於
let descriptor = {
    value: function() {
        console.log("meow ~");
    },
    enumerable: false,
    configurable: true,
    writable: true
};
descriptor = readonly(Cat.prototype, "say", descriptor) || descriptor;
Object.defineProperty(Cat.prototype, "say", descriptor);

有用的連接

相關文章
相關標籤/搜索