Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

JavaScript 对象与继承

作者:oasis


对象的创建

工厂模式

js
function createPerson({name, age, job}) {
  const o = new Object()
  o.name = name
  o.age = age
  o.job = job
  return o
}

构造函数模式

js
function Person({name, age, job}) {
  this.name = name
  this.age = age
  this.job = job
}
new Person('mario', 51, 'lawer')

(1)在内存中创建一个新对象。

(2)这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的prototype 属性。

(3)构造函数内部的 this 被赋值为这个新对象(即this指向新对象)。

(4)执行构造函数内部的代码(给新对象添加属性)。

(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

构造函数存在的问题:

  • 定义的方法会在每个实例上创建一遍

可以通过原型模式解决上面的问题,将方法定义在原型上

原型模式

js
function Person() {}
Person.prototype.name = 'mario'
Person.prototype.age = 18
Person.prototype.job = 'Eginner'
Person.prototype.sayName = function () {
  console.log(this.name)
}
const p1 = new Person()
const p2 = new Person()
console.log(p1.name)
console.log(p2.name)
console.log(p1.sayName === p2.sayName)
// 如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性
p1.name = 'test'
console.log(p2.name)

只要创建一个函数,函数上就有 prototype 属性(指向原型对象)。默认情况下,所有原型独享自动获得一个名为 constructor 的属性,指向与之关联的构造函数。例如: Person.prototype.constructor 指向 Person

每次调用构造函数创建一个新实例,这个实例内部 [[Prototype]] 指针就会被赋值为构造函数的原型对象。有些浏览器暴露了 __proto__ 用于访问对象的原型。

js
function Person(){}
// Person.prototype 的 constructor 属性不指向 Person 了
Person.prototype = {
  name: 'mario',
  age: 18,
}
js
function Person(){}
// 修复 Person.prototype 的 constructor 属性不指向 Person
// 以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性。而原生constructor属性默认是不可枚举的
Person.prototype = {
  constructor: Person,
  name: 'mario',
  age: 18,
}
js
function Person(){}
Person.prototype = {
  name: 'mario',
  age: 18,
}
Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
})

继承

原型继承是通过原型继承多个引用类型的属性和方法。

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。

原型链继承的问题:

  • 原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因
  • 子类型在实例化时不能给父类型的构造函数传参

解决引用值导致的问题可通过“盗用”构造函数来处理。

js

function SuperType() {
  this.colors = [1,2,3]
}

function SubType() {
  SuperType.call(this)
}

盗用构造函数的问题:方法需要在构造函数内部定义,不能共享。

为了解决上面的问题,可以采用组合继承,也就是原型继承+盗用构造函数

js
function SuperType() {
  this.colors = [1,2,3]
}

SuperType.prototype.say = function () {
  console.log(this.colors);
}

function SubType() {
  SuperType.call(this)
}

原型式继承

js

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

寄生式继承

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

类的定义

js
class Person {}
const A = class {}

new 的处理逻辑和构造函数是一致的。类构造函数与构造函数的主要区别是,调用类构造函数必须使用new操作符。

调用类构造函数时如果忘了使用new则会抛出错误。

ECMAScript类就是一种特殊函数。声明一个类之后,通过typeof操作符检测类标识符,表明它是一个函数。