弄清Classs,Symbols,Objects拓展 和 Decorators

本文翻译自 Nicolas Bevacqua 的书籍 《Practical Modern JavaScript》,这是该书的第三章。翻译采用意译并进行必定的删减和拓展,部份内容与原书有所不一样。javascript

类(classes)多是ES6提供的,咱们使用最广的新功能之一了,它以原型链为基础,为咱们提供了一种基于类编程的模式。Symbol是一种新的基本类型(JS中的第七种基本类型,另外六种为undefinednull、布尔值(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的类中可能还会添加publicprivate等。

和普通函数声明不一样的是,类声明并不会被提高到做用域的顶部,所以提早调用会报错。

类声明有两种方法,一种是像函数声明和函数表达式同样,声明为表达式,以下代码所示:

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()的参数都将做为Logconstructor的参数,这些参数将用以初始化类的实例:

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 语法绑定一个属性,其后跟着一个函数,当为该函数设置为某个值时,其后的函数将被调用;

当结合使用gettersetter时,咱们能够完成一些神奇的事情,下例中,咱们定义了类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,传入lskeygroceries,当咱们设置ls.data为某个值时,该值将被转换为JSON对象字符串,并存储在localStorage中;当使用相应的key进行读取时,将提取出以前存储在localStorage中的内容,以JSON的格式进行解析后返回:

const ls = new LocalStorage('groceries')
ls.data = ['apples', 'bananas', 'grapes']
console.log(ls.data)
// <- ['apples', 'bananas', 'grapes']

除了使用getterssetters,咱们也能够定义常规的实例方法,继续以前定义过的Fruit类,咱们再定义了一个能够吃水果的Person类,咱们实例化一个fruit和一个person,而后让 personfruit 。这里咱们让person吃完了全部的fruit,结果是personsatiety(饱食度)上升到了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),此外还须要显式的设置Bananaprototype

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除了有肯定的namecalories,以及额外的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类传入namecalories,这样就轻松的实现了对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()

这里有一点特别值得咱们注意,在NumberString等包装对象前是可使用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.getOwnPropertySymbolsSymbol永远不会暴露出来的,如下代码中咱们用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运行的脚本、任意类型的workerweb worker,service workers或者shared workers)等等。这些执行上下文每一种都有其全局对象,好比说页面的全局对象window,可是这种全局对象不能被其它代码域好比说ServiceWorker使用。相比而言,全局符号则更具全局性,它能够被任何代码域访问。

ES6提供了两个和全局符号相关的方法,Symbol.forSymbol.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,numberdefault,用以指明默认值的期待类型。

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.iteratorkey值,返回值是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.cb.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以及,-00

NaNNaN相比较时,严格相等运算符===将返回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);

有用的连接

相关文章
相关标签/搜索