《JavaScript 高级程序设计》第6章 对象、类和面向对象编程

6.对象、类和面向对象编程

理解对象

let book = {
  year_: 2017,
  edition: 1
}
Object.defineProperty(book, 'year', {
  get () {
    return this.year_
  },
  set (newValue) {
    if (newValue > 2017) {
      this.year_ = newValue
      this.edition += newValue - 2017
    }
  }
})
book.year = 2018
console.log(book.edition) // 2
let people = {
  name: 'Matt',
  age: 27
}
let {name, age} = people
console.log(name) // Matt
console.log(age) // 27

创建对象

工厂模式

function createPerson(name, age) {
  let o = new Object()
  o.name = name
  o.age = age
  o.sayName = function () {
    console.log(this.name)
  }
  return o
}
let person = createPerson('Tom', 29)

工厂模式可以解决创建多个相似对象的问题,但是没有解决对象标识的问题(对象是什么类型)

构造函数模式

function Person(name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}
let person = new Person('Tom', 29)

创建 Person 实例应使用 new 操作符,new 操作符会执行如下操作

  1. 在内存中创建一个对象
  2. 新对象内部[[prototype]]被赋值为构造函数的 prototype 属性
  3. this 指向新对象
  4. 执行构造函数中的代码
  5. 如果构造函数返回非空对象,则返回该对象;否则返回刚创建的新对象

原型模式

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。这个对象就是通过调用构造函数创建的对象实例的原型。使用原型对象的好处是,在其上面定义的属性和方法可以被对象实例共享。

function Person() {}
Person.prototype.name = 'Tom'
Person.prototype.age = 29
Person.prototype.sayName = function () {
  console.log(this.name)
}

let person = new Person()

原型对象(函数 prototype 指向的对象)会自动获得一个名为 constructor 的属性,指向与之关联的构造函数。每次调用构造函数创建的新实例,实例内部 [[Prototype]] 指针会指向构造函数的原型对象。因此,实例与构造函数没有直接联系,但是与构造函数的原型对象有。可以使用 isPrototypeOf 来确定两个对象之间的这种关系。 Object.getPrototypeof 方法可以返回参数内部特性 [[Prototype]] 的值,Object.setPrototypeOf 方法可以向实例私有特性 [[Protptype]] 写入一个新值(严重影响代码性能)。为避免性能下降,可以通过 Object.create() 创建一个新对象同时指定原型。 通过对象访问属性时,首先搜索对象实例本身,如果未发现给定的名称,搜索会沿着指针进入原型对象搜索。虽然可以通过实例读取原型对象的值,但是不能通过实例修改。在实例上添加同名属性会遮蔽原型对象上的同名属性,只有通过 delete 删除实例上的属性后才能访问到原型对象上的同名属性。 单独使用 in 操作符会在可以通过对象访问指定属性时返回 true,在 for-in 循环中时,会返回可以通过对象访问的所有可被枚举的属性(包括实例属性和原型属性)。

对象迭代

继承

原型链

原型链是 ECMAScript 的主要继承方式,其基本思想为通过原型继承多个引用类型的属性和方法

function SuperType() {
  this.property = true
}

SuperType.prototype.getSuperValue = function () {
  console.log(this.property)
}

function SubType() {
  this.subProperty = false
}

SubType.prototype = new SuperType()
let instance = new SubType()

instance.getSuperValue() // true

原型和实例的关系可以通过两种方式确定:

  1. instanceof 操作符
  2. isPrototypeOf 方法

原型链实现继承的问题:

  1. 原型中包含的引用值会在所有实例间共享
  2. 子类型在实例化时不能给夫类型的构造函数传参

盗用构造函数

盗用构造函数的一个优点是可以在子类构造函数中向父类构造函数传参

function SuperType (name) {
  this.name = name
}

function SubType () {
  superType.call(this, 'Tom')
  this.age = 29
}

let instance = new SubType

盗用构造函数的主要问题跟构造函数模式相同:必须在构造函数中定义方法,因此函数无法重用;子类也不能访问父类原型上定义的方法。

组合继承

组合继承综合了原型链和盗用构造函数,基本思路为使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  SuperType.call(this, name)
  this.age = age
}

SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
  console.log(this.age)
}

let instance = new SubType('Tom', 29)

原型式继承

function object (o) {
  function F () {}
  F.prototype = o
  return new F()
}

ES5 通过增加 Object.create 方法将上述原型式继承的概念规范化。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)。 原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。注意,属性中包含的引用值始终会在相关对象间共享。

寄生式继承

寄生式继承思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象并返回这个对象。

function createAnother (original) {
  let clone = object(original)
  clone.sayHi = function () {
    console.log('hi')
  }
  return clone
}

寄生式继承给对象添加的函数会导致函数难以重用,与构造函数模式类似。

寄生式组合继承

组合继承存在最主要的效率问题是父类构造函数会在创建子类型和子类构造函数中调用两次。为解决这个问题,寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链集成方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

function inheritPrototype(sunType, superType) {
  let prototype = object(superType.prototype) // 创建父类原型副本
  prototype.constructor = subType // 解决由于重写原型导致默认construcor丢失的问题
  subType.prototype = prototype // 新创建的对象赋值给子类型的原型
}

寄生式组合继承可以算是引用类型继承的最佳模式。

类定义

类定义有两种模式:类声明和类表达式 类可以包含:构造函数方法、实例方法、获取函数、设置函数和静态类方法。

类构造函数

constructor 关键字用于在类定义块内部创建类的构造函数。使用 new 操作符实例化 Person 的操作等于使用 new 调用其构造函数。类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。

实例、原型和类成员

class Person {
  constructor() {
    this.nicknames = ['Jack', 'Jake', 'J-Dog']
  }

  *[Symbol.iterator] () {
    yield *this.nicknames.entries()
  }
}

let p = new Person()
for (let [idx, nickname] of p) {
  console.log(nickname)
}

继承

ES6 类支持单继承。使用 extends 关键字就可以继承任何拥有 [[Constructor]] 和原型的对象。 派生类的方法可以通过 super 关键字引用它们的原型。 使用 super 时需要注意:

  1. super 只能在派生类构造函数和静态方法中使用
  2. 不能单独引用 super 关键字,要么用它调用构造函数,要么引用静态方法
  3. 调用 super() 会调用父类构造函数,并将返回的实例赋值给 this
  4. super() 的行为如同调用构造函数,如果需要给父类构造函数传参需要手动传入
  5. 如果没有定义类构造函数,在实例化派生类时会调用 super() 并传入所有传给派生类的参数
  6. 在类构造函数中,不能在调用 super() 之前引用 this
  7. 如果在派生类中显式定义了构造函数,要么调用 super() ,要么必须返回一个对象

定义抽象基类(只用于继承,本身不会实例化)可以通过 new.target 实现,在实例化时检测是否为抽象基类可以阻止对抽象基类的实例化。

class Vehicle {
  constructor() {
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated')
    }
  }
}

也可以通过在抽象基类构造函数中进行检查,要求派生类必须定义某个方法。

class Vehicle {
  constructor() {
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated')
    }
    if (!this.foo) {
      throw new Error('Inheriting class must define foo()')
    }
  }
}

继承内置引用类型可以用来扩展内置类型:

class SuperArray extends Array {
  // 洗牌算法
  shuffle() {
    for (let i = this.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i - 1))
      ;[this[i], this[j]] = [this[j], this[i]]
    }
  }
}

可以通过现有特性模拟实现多类继承,将不同类的行为集中到一个类:

class Vehicle {
}

let FooMixin = (Superclass) => class extends Superclass {
  foo() {
    console.log('foo')
  }
}
let BarMixin = (Superclass) => class extends Superclass {
  bar() {
    console.log('bar')
  }
}
let BazMixin = (Superclass) => class extends Superclass {
  baz() {
    console.log('baz')
  }
}

function mix(BaseClass, ...Mixins) {
  return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass)
}

class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {
}
相关文章