本文翻译自 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表达式的执行上下文,它可以是你的应用当前运行的页面、页面中的<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 = 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 是其默认的用法,用户不需要提供其它参数就可以简单使用这个库。不过,该库还支持多个高级的配置,只是默认是关闭的,比如说通过配置可以添加<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.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);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。