本文翻译自 Nicolas Bevacqua 的书籍 《Practical Modern JavaScript》,这是该书的第三章。翻译采用意译并进行一定的删减和拓展,部分内容与原书有所不同。
类(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)
// <- 105
console.log(apple.pieces)
// <- 1
console.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
是一样的:
// 用类声明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
对于之后我们理解迭代至关重要。
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))
// <- 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.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表达式的执行上下文,它可以是你的应用当前运行的页面、页面中的、由
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
分别在页面中和中查找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
,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不在全局注册表中但是跨域共享
这些内置的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 是其默认的用法,用户不需要提供其它参数就可以简单使用这个库。不过,该库还支持多个高级的配置,只是默认是关闭的,比如说通过配置可以添加或
,可以启用 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.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);
有用的链接
第一章 ECMAScript简史 和 JavaScript的未来
第二章 ES6 概要
第三章 Classs,Symbols,Objects拓展 和 Decorators
本章原文链接
本书Github地址,期待交流