本文翻译自 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);