本文翻譯自 Nicolas Bevacqua 的書籍 《Practical Modern JavaScript》,這是該書的第三章。翻譯採用意譯並進行必定的刪減和拓展,部份內容與原書有所不一樣。javascript
類(classes
)多是ES6提供的,咱們使用最廣的新功能之一了,它以原型鏈爲基礎,爲咱們提供了一種基於類編程的模式。Symbol
是一種新的基本類型(JS中的第七種基本類型,另外六種爲undefined
、null
、布爾值(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的類中可能還會添加public
或private
等。
和普通函數聲明不一樣的是,類聲明並不會被提高到做用域的頂部,所以提早調用會報錯。
類聲明有兩種方法,一種是像函數聲明和函數表達式同樣,聲明爲表達式,以下代碼所示:
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()
的參數都將做爲Log
中constructor
的參數,這些參數將用以初始化類的實例:
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
語法綁定一個屬性,其後跟着一個函數,當爲該函數設置爲某個值時,其後的函數將被調用;
當結合使用getter
和setter
時,咱們能夠完成一些神奇的事情,下例中,咱們定義了類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
,傳入ls
的key
爲groceries
,當咱們設置ls.data
爲某個值時,該值將被轉換爲JSON對象字符串,並存儲在localStorage
中;當使用相應的key
進行讀取時,將提取出以前存儲在localStorage
中的內容,以JSON的格式進行解析後返回:
const ls = new LocalStorage('groceries') ls.data = ['apples', 'bananas', 'grapes'] console.log(ls.data) // <- ['apples', 'bananas', 'grapes']
除了使用getters
和setters
,咱們也能夠定義常規的實例方法,繼續以前定義過的Fruit
類,咱們再定義了一個能夠吃水果的Person
類,咱們實例化一個fruit
和一個person
,而後讓 person
吃 fruit
。這裏咱們讓person
吃完了全部的fruit
,結果是person
的satiety
(飽食度)上升到了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)
,此外還須要顯式的設置Banana
的prototype
。
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除了有肯定的name
和calories
,以及額外的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
類傳入name
和calories
,這樣就輕鬆的實現了對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
對於以後咱們理解迭代相當重要。
Symbol是ES6提供的一種新的JS基本類型。 它表明惟一值,和字符串,數值等基本類型的一個很大的不一樣點在於Symbol沒有字符表達形式。Symbol的主要目的是用以實現協議,好比說,使用Symbol定義的迭代協議規定了對象將如何被迭代,關於這個,咱們將在[Iterator Protocol and Iterable Protocol.]()這一章詳細闡述。
ES6提供的Symbol有以下三種不一樣類型:
local Symbol
;
global Symbol
;
語言內置Symbol
;
這三種類型的Symbol存在着必定的不一樣,咱們一種種來說解,首先看local Symbol
。
Local Symbol 經過 Symbol
包裝對象建立,以下:
const first = Symbol()
這裏有一點特別值得咱們注意,在Number
或String
等包裝對象前是可使用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 該如何使用,下面咱們再討論下其使用場景。
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.getOwnPropertySymbols
Symbol永遠不會暴露出來的,如下代碼中咱們用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
運行的腳本、任意類型的worker
(web worker
,service workers
或者shared workers
)等等。這些執行上下文每一種都有其全局對象,好比說頁面的全局對象window
,可是這種全局對象不能被其它代碼域好比說ServiceWorker
使用。相比而言,全局符號則更具全局性,它能夠被任何代碼域訪問。
ES6提供了兩個和全局符號相關的方法,Symbol.for
和Symbol.keyFor
。咱們看看它們分別該如何使用?
Symbol.for(key)
獲取symbolsSymbol.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爲JS語言行爲添加了鉤子,在必定程度上容許你拓展和自定義JS語言。
Symbol.toPrimitive
符號,是描述如何經過 Symbols 給語言添加額外的功能的最好的例子,這個Symbol的做用是,依據給定的類型返回默認值。該函數接收一個hint
參數,參數能夠是string
,number
或default
,用以指明默認值的期待類型。
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是跨代碼域共享的,以下所示:
const frame = document.createElement('iframe') document.body.appendChild(frame) Symbol.iterator === frame.contentWindow.Symbol.iterator // <- true
須要注意的是,雖然語言內置的這些Symbol是跨代碼塊共享的,可是他們並不在全局符號註冊表中,咱們在下述代碼中想要找到Symbol.iterator
的key
值,返回值是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.c
和b.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
以及,-0
和0
。
當NaN
與NaN
相比較時,嚴格相等運算符===
將返回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.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]]的新對象。
對於大多數編程語言而言,裝飾器不是一個新概念。在現代編程語言中,裝飾器模式至關常見,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);