本文翻译自 Nicolas Bevacqua 的书籍 ,这是该书的第三章。翻译采用意译并进行一定的删减和拓展,部分内容与原书有所不同。
类(classes
)可能是ES6提供的,我们使用最广的新功能之一了,它以原型链为基础,为我们提供了一种基于类编程的模式。Symbol
是一种新的基本类型(JS中的第七种基本类型,另外六种为undefined
、null
、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)),它可以用来定义不可变值。本章,我们将首先讨论类和符号,之后我们还将对ES6对对象的拓展及处于stage2
阶段的装饰器进行简单的讲解。
类
我们知道,JavaScript是一门基于原型链的语言,ES6中的类和其它面向对象语言中的类在本质上有很大的不同,JavaScript中,类实际上是一种基于原型链的语法糖。
虽然如此,JavaScript中的类还是给我们的很多操作带来了方便,比如说可以轻易拓展其它类,通过简单的语法我们就可以拓展内置的Array
了,在下文中我们将详细说明如何使用。
类基础
基于已有的知识学习新知识是一种非常好的学习方法,对比学习可以让我们对新知识有更深的印象。由于JS中类实际上是一种基于原型链的语法糖,我们先简单复习基于原型链的JavaScript构造器要怎么使用,然后我们用ES6中类语法实现相同的功能作为对比。
下面代码中,我们新建了构造函数Fruit
用以表示某种水果。该构造函数接收两个参数,水果的名称 -- name
,水果的卡路里含量 -- calaries
。在Fruit
构造函数中我们设置了默认的块数 pieces=1
,通过原型链,我们还为该构造函数添加了两种方法:
chop
方法(切水果,每次调用会使得块数加一);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)// <- 105console.log(apple.pieces)// <- 1console.log(apple.calories)// <- 35
作为对比,接下来我们使用类语法来实现上述代码一样的过程。在类中,我们显式使用constructor
方法做为构造方法(其中this
指向类的实例),在类中定义方法类似在对象字面量中定义方法,见下述代码中chop
,bite
的定义。类所有的方法都声明在class
的块中,不需要再使用Fruit.prototype
这类样本代码,从这个角度看与基于原型的语法比起来,类语法语义清晰,使用起来也显得简洁。
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
是一样的:
// 用类声明Fruitclass Fruit {}// 使用构造函数声明Fruitfunction 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)// 2console.log(counter.next)// 3console.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.75console.log(banana.pieces)// <- 11console.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
对于之后我们理解迭代至关重要。
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()
这里有一点特别值得我们注意,在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))// <- trueconsole.log(Symbol() === Symbol())// <- falseconsole.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.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)
获取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 = documentconst frame = d.body.appendChild(d.createElement('iframe'))const framed = frame.contentWindowconst 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
,number
或default
,用以指明默认值的期待类型。
const morphling = { [Symbol.toPrimitive](hint) { if (hint === 'number') { return Infinity } if (hint === 'string') { return 'a lot' } return '[object Morphling]' }}console.log(+morphling) // + 号 // <- Infinityconsole.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] = falseconsole.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.iterator
的key
值,返回值是undefined
就说明了这个问题。
console.log(Symbol.keyFor(Symbol.iterator))// <- undefined
另外一个常用的符号是Symbol.iterator
,它为每一个对象定义了默认的迭代器。我们将在下一章中详细讲述Symbol.iterator
的细节内容。
内置对象的改进
我们在一章,已经讲述过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 }) => unknowngetUnknownProperties({ 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// <- falseObject.is(NaN, NaN)// <- true
使用严格相等运算符比较0
和-0
会得到true
,而使用Object.is
则会返回false
.
-0 === +0// <- trueObject.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
具有严重的性能问题,因此在如果你很在乎这个,使用前应好好考虑。
对性能问题的说明
使用
由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.__proto__ = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。Object.setPrototypeOf
来改变一个对象的原型是一个昂贵的操作,MDN是这样解释的:
装饰器(Decorators)
对于大多数编程语言而言,装饰器不是一个新概念。在现代编程语言中,装饰器模式相当常见,c# 中 有attributes
,Java中有annotations
,Python中有decorators
等等。目前也存在一个处于Stage2 的JavaScript的装饰器提案。
JavaScript中的装饰器语法和Python的非常类似。JavaScript的装饰器可以应用于任何对象或者静态声明的属性前。诸如对象字面量声明或class
声明前,或get
,set
,static
前。
@inanimateclass Car {}@expensive@speed('fast')class Lamborghini extends Car {}class View { @throttle(200) // reconcile once every 200ms at most reconcile() {}}
关于装饰器凹凸实验室的一篇文章解释的比较清楚,大家可以参考。
当装饰器作用于类本身的时候,我们操作的对象也是这个类本身,而当装饰器作用于类的某个具体的属性的时候,我们操作的对象既不是类本身,也不是类的属性,而是它的描述符(descriptor),而描述符里记录着我们对这个属性的全部信息,所以,我们可以对它自由的进行扩展和封装,最后达到的目的呢,就和之前说过的装饰器的作用是一样的。可以看如下两段代码加深理解
作用于类时
function isAnimal(target) { target.isAnimal = true; return target;}@isAnimalclass 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);