【JavaScript基础】JavaScript的对象创建和继承

目录

  1. 创建对象
    1. 工厂模式
    2. 构造函数模式
    3. 原型模式
    4. 构造 + 原型模式
    5. 动态原型模式
    6. 寄生构造函数模式
    7. 稳妥构造函数模式
  2. 继承
    1. 简单实例
    2. 借用构造函数
    3. 组合继承
    4. 原型式继承
    5. 寄生式继承
    6. 寄生组合式继承
  3. 总结
    1. 对象创建
    2. 继承

创建对象

工厂模式

工厂模式使用 new Object 来创建一个基对象,然后再这个基对象上追加基本的属性,然后将该对象返回。

function createPerson(name, age, job) {
  const o = new Object()
  o.name = name
  o.age = age
  o.job = job
  o.sayName = () => console.log(this.name)
  return o
}

const Tom = createPerson('Tom', 23, 'Engineer')
const John = createPerson('John', 33, 'Doctor')

console.log(
  typeof Tom,
  typeof John,
  Tom instanceof Object,
  John instanceof Object
)

object object true true
undefined

使用工厂模式的缺陷在于,我们并不能识别所创建的对象,因为最终都是通过 new Object 创建而来,没法区分对象
具体是哪个类型,从结果输出看最后的类型都是被识别为 Obejct 了。

构造函数模式

通过构造函数我们可以创建特定类型的对象

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = () => console.log(this.name)
}

const tom = new Person('Tom', 23, 'Engineer')
const john = new Person('John', 33, 'Doctor')

console.log(
  typeof tom,
  typeof john,
  tom instanceof Person,
  john instanceof Person
)
console.log(
  'tom',
  tom.constructor === Person.prototype.constructor,
  tom.__proto__ === Person.prototype
)
console.log(
  'john',
  john.constructor === tom.constructor,
  john.__proto__ === Person.prototype
)

console.log(
  'john',
  john.constructor === tom.constructor,
  Object.getPrototypeOf(john) === Person.prototype
)

object object true true
tom true true
john true true
john true true
undefined

PS: __proto__ 指针不是每个浏览器都支持该属性,如果在没有该属性的情况下我们可以通过 Object.getPrototypeOf
来获取一个对象的原型对象。

使用构造函数来创建对象,使用 new Person 来调用构造函数之后会经历下面几个步骤:

  1. 创建一个新对象(如果构造函数有 return 一个对象,那么创建之后的对象即该对象)
  2. 将构造函数的作用域赋给新对象(同时 this 值就指向了这个心的对象,如 Person 之后的 this -> tom/john )
  3. 执行构造函数中的代码(为新对象添加新属性)
  4. 返回这个新对象

下图描述了构造函数,原型,以及实例之间的关系

构造函数模式的问题:

  1. 实例间的属性和方法共享问题
  2. 同功能下重复创建函数问题(比如:上例中的 sayName() 函数,里面实现和其作用都一样,但是却被创建了两份)

一种兼容方式:(通过创建外部函数,然后构造函数内引用实现)

function sayName() {
  console.log(this.name)
}

function Person(name) {
  this.name = name
  this.sayName = sayName
}

const p1 = new Person('Tom')
p1.sayName()

Tom
undefined

这种方式使用的是对象的引用特性,在创建实例的步骤中其中有一个步骤就是执行构造函数内代码,此时不同实例间最终都会引用到全局环境下的 sayName 这个函数。

而这种方式始终不好维护,设想每个构造函数如果有多个甚至更多的方法,那岂不是要声明 n 个全局函数,这肯定会让人发疯,抓狂。

正好原型模式可以解决整个问题。

原型模式

原型模式的实现是基于构造函数的原型对象,在创建实例的时候,这个原型对象是不会发生变化的,并且不同实例的原型指针 __proto__
都指向了同一个对象(并且也是唯一的原型对象)

构造函数那里的图依旧有效,并且很好的解释了原型对象是实现不同实例间的共享属性和方法的一种方式。

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
}

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

const p1 = new Person('Tom', 22, 'Engineer')
p1.sayName()
const p2 = new Person('John', 33, 'Doctor')
p2.sayName()

Tom
John
undefined

通过原型的方式,可以很好的将实例特有的属性或方法声明到构造函数内部,而共享的方法(比如: sayName )可以声明到原型之上。

对于对象原型,在使用 new 调用构造函数之后修改原型引用的值,并不能达到想要的结果,这是因为在 new 创建实例的时候,原型指针就已经指向了当前原型指针 Person.prototype 指向的内存处,然后给 Person.prototype 赋值之后其指向的是等号右边的对象在内存中的位置。

function Person() {

}

const p1 = new Person()

Person.prototype = {
  constructor: Person,
  sayName() {
    console.log('say name.')
  }
}

try {
  p1.sayName()
} catch (error){
  console.log(error.toString())
}

console.log(
  p1.__proto__ === Person.prototype,
  p1.__proto__ === Object.getPrototypeOf(p1),
  Object.getPrototypeOf(p1) === Person.prototype,
)

TypeError: p1.sayName is not a function
false true false
undefined

原型模式的问题:引用类型的属性如果是声明在原型之上,那所有的实例都将贡献这个引用,所有的实例改变引用值的
内容时,其他实例都能感知到,这并不是其他实例所希望看到的

function Person() { }

Person.prototype = {
  constructor: Person,
  names: ['John', 'Lily']
}

const p1 = new Person()
const p2 = new Person()

p1.names.push('Tom')
console.log(p1.names === p2.names, p1.names, 'p1 insert a person')
p2.names.push('Bob')
console.log(p1.names === p2.names, p2.names, 'p2 insert a person')

true [ 'John', 'Lily', 'Tom' ] 'p1 insert a person'
true [ 'John', 'Lily', 'Tom', 'Bob' ] 'p2 insert a person'
undefined

要解决这问题我们可以考虑采用构造函数 + 原型模式的结合。

构造 + 原型模式

构造函数结合原型的模式,其实就是将实例特有的属性或方法放到构造函数中声明,共有的属性或方法放到原型中声明

function Person(name, age) {
  // 每个人都有不同的名字和年龄
  this.name = name
  this.age = age
  this.friends = ['John', 'Lily']
}

Person.prototype = {
  constructor: Person,
  // 每个人都会有的方法并且功能也类似
  sayName() {
    console.log('my name is ' + this.name)
  }
}

const p1 = new Person('Dom', 22)
p1.friends.push('Bob')
const p2 = new Person('Tom', 11)
p2.friends.push('Cuci')

console.log(p1.friends === p2.friends, p1.friends, 'p1 known a new friend.')
console.log(p1.friends === p2.friends, p2.friends, 'p2 known a new friend.')

false [ 'John', 'Lily', 'Bob' ] 'p1 known a new friend.'
false [ 'John', 'Lily', 'Cuci' ] 'p2 known a new friend.'
undefined

动态原型模式

动态原型模式的原理是在构造函数中就去检查是否存在某个属性或方法,如果不存在那就立刻在原型上声明它,这样的好处是在第一次创建实例甚至后面创建实例的时候如果需要新增共享的属性或方法。

坏处:不能使用字面量方式重写原型,因为这会切断现有实例和新原型之间的联系。

function Person(name, age, sayName) {
  this.name = name
  this.age = age

  if (typeof this.sayName !== 'function') {
    // 甚至可以使用构造函数传入的参数类初始化这个方法
    Person.prototype.sayName = sayName || function() {
      console.log('inner share fn', this.name)
    }
  }
}

// 输出:inner share fn Tom
// const p1 = new Person('Tom', 22)
// 输出:outer share fn Tom
const p1 = new Person('Tom', 22, function() { console.log('outer share fn', this.name) })
p1.sayName()

outer share fn Tom
undefined

这里使用参数传入外部函数的方式不推荐使用,只是示例,否则就延续了构造函数模式中使用引用到外部函数的弊端了。

寄生构造函数模式

寄生构造函数的模式除了能使用 new 去创建对象之后,其他和工厂模式一模一样,它利用了使用 new 创建对象时步骤三,返回一个已经创建好的对象。

function Person(name, age) {
  const p = new Object()
  p.name = name
  p.age = age
  p.sayName = function() {
    console.log(p.name, 'inner')
  }
  return p
}

const p1 = new Person('Tom', 22)
p1.sayName()

console.log(
  'typeof: ' + typeof(p1),
  'instanceof: ' + (p1 instanceof Person)
)

Tom inner
typeof: object instanceof: false
undefined

结果会发现 p1 不是 Person 类型,因为 p1 实际上是 Person 构造函数中通过 const p = new Object()创建并 return p 返回的 p 指向的对象。

不推荐使用这种方式来创建对象, 除了一些特殊用处之外,比如:要基于原生对象或已有的对象要扩展出一个新的对象来

function Person() {
  this.name = 'Tom'
  this.age = 22
}

function NewPerson() {
  const np = new Person()

  // ... do some extend

  return np
}

// 或者基于原生对象扩展出新的对象,比如:
function SpecialArray() {
  const sa = new Array()

  // 将参数保存成数组
  sa.push.apply(sa, arguments)

  sa.toPipedString = function() {
    return this.join('|')
  }

  return sa
}

const sa = new SpecialArray('Tom', 'Lily', 'John')
console.log(sa.toPipedString())

稳妥构造函数模式

实现类似寄生构造函数,唯一不同点在于:

  1. 不使用 this

    这一点的目的是为了保护传入到构造函数内部的属性或方法,不被外部直接访问或修改,但是可以通过其提供的方法
    去访问或修改。

  2. 不使用 new

    不使用 new 来创建的目的是为了不要让 this 绑定到调用构造函数返回的对象上

示例:

function Parent() {

}

function Person(name, age) {
  const p = new Parent()
  p.sayName = function() {
    console.log(name, 'only me can visit the `name`.')
    console.log((this instanceof Person), 'inner, this is Person')
    console.log((this instanceof p.constructor), 'inner, this is p')
    console.log((this === global), 'inner, this is global')
  }

  console.log((this instanceof Person), 'outer, this is Person')
  console.log((this instanceof p.constructor), 'outer, this is p')
  console.log((this === global), 'outer, this is global')

  return p
}

const p1 = Person('Tom', 22)
p1.sayName()

console.log('p2 ======================== p2')
const p2 = new Person('Bom', 11)
p2.sayName()

false 'outer, this is Person'
false 'outer, this is p'
true 'outer, this is global'
Tom only me can visit the `name`.
false 'inner, this is Person'
true 'inner, this is p'
false 'inner, this is global'
p2 ======================== p2
true 'outer, this is Person'
false 'outer, this is p'
false 'outer, this is global'
Bom only me can visit the `name`.
false 'inner, this is Person'
true 'inner, this is p'
false 'inner, this is global'
undefined

继承

简单实例

function Parent() {

}

Parent.prototype.sayName = function(name) {
    console.log('parent say: ' + name)
}

function Child() {

}

Child.prototype = new Parent()
Child.prototype.sayName('prototype of child')
// : Parent {}, 这个好理解,Child 原型被重置为 Parent
console.log('=>', Child.prototype)

const c1 = new Child()
Child.prototype.sayName('instance of child')
console.log('=>', c1) // Parent {}
Child.prototype.sayName('c1 instanceof Child')
console.log('=>', c1 instanceof Child) // true
Child.prototype.sayName('c1 instanceof Parent')
console.log('=>', c1 instanceof Parent) // true
// 这里是因为 Child.prototype = new Parent() 了
// 原型指针被覆盖了,成为了 Parent 的一个实例,此时调用 c1.constructor
// 根据追溯方向(c1 -> c1.__proto__(Child.prototype) -> Parent)
console.log('child instance constructor', c1.constructor) // Parent

MDN 关于 instanceof 的定义:

The instanceof operator tests whether the prototype property of a constructor appears anywhere in the prototype chain of an object.

意思大概就是(比如: a instanceof B), B.prototype 这个属性是否存在于 a 对象的原型链上,如果能找到返回 true , 不能 false

借用构造函数

function Super() {
    this.colors = [ 'red' ]
}

function Sub() {
    // 借用父元素的构造函数,通过 this 调用它
    // 最后 Super 构造函数内部的代码,只要是通过 this. 来调用的
    // 都会被追加到传入的参数: this 上,即当前 Sub 的实例之上,
    // 从而使每个实例都有自己单独的属性
    Super.call(this)
}

const s1 = new Sub()
s1.colors.push('blue')
console.log(s1.colors.toString(), 's1 colors.')

const s2 = new Sub()
s2.colors.push('black')
console.log(s2.colors.toString(), 's2 colors.')

red,blue s1 colors.
red,black s2 colors.
undefined

通过借用父元素构造函数来达到实例的特有属性,并且还可以通过传参形式来初始化实例的特有属性。

function Super(name) {
    this.colors = [ 'red' ]
    this.name = name
}

function Sub(name) {
    // 借用父元素的构造函数,通过 this 调用它
    // 最后 Super 构造函数内部的代码,只要是通过 this. 来调用的
    // 都会被追加到传入的参数: this 上,即当前 Sub 的实例之上,
    // 从而使每个实例都有自己单独的属性
    Super.call(this, name)
}

const s1 = new Sub('Tom')
console.log(s1.name, 's1 name.')

const s2 = new Sub('John')
console.log(s2.name, 's2 name.')

Tom s1 name.
John s2 name.
undefined

借用构造函数缺点:方法都在构造函数中定义,这样就无法实现共享的方法,而在父元素中定义的方法,虽然可以共用但是对于子元素来说这些方法都是不可见的。

组合继承

组合继承实际上是对借用构造函数的一种进化或扩展,即达到了保持特有属性特性又能拥有共用的方法,前者在构造函数上定义,后者则在父元素的原型上声明。

示例:

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

Super.prototype.sayName = function() {
    console.log(this.name, ' say.')
}

function Sub(name) {
    Super.call(this, name)
}

Sub.prototype = new Super()
// console.log(new Sub().constructor, 'constructor changed')
// 这一步作用是还原构造函数指针,否则 Sub 示例的构造函数指针结果将是指向 Super
// 如上输出: [Function: Super]
Sub.prototype.constructor = Sub

const s1 = new Sub('Tom')
s1.sayName()
const s2 = new Sub('Jonh')
s2.sayName()
console.log(s1 instanceof Sub, 's1 instanceof Sub')
console.log(s2 instanceof Sub, 's2 instanceof Sub')
console.log(Sub.prototype.isPrototypeOf(s1), 'Sub isPrototypeof s1')
console.log(Sub.prototype.isPrototypeOf(s2), 'Sub isPrototypeof s2')

Tom  say.
Jonh  say.
true 's1 instanceof Sub'
true 's2 instanceof Sub'
true 'Sub isPrototypeof s1'
true 'Sub isPrototypeof s2'
undefined

原型式继承

function createObj(parent) {
    function F() { }
    F.prototype = parent
    return new F()
}

const parent = {
    name: 'parent',
    // 引用属性的值始终会被共享
    colors: ['red']
}

const obj1 = createObj(parent)
const obj2 = createObj(parent)
console.log(obj1.prototype === obj2.prototype, 'obj1.prototype === obj2.prototype')
obj1.name = 'obj1'
// 普通类型互不影响
// obj1 name: obj1
// obj2 name: parent
console.log(obj1.name, 'obj1 name')
console.log(obj2.name, 'obj2 name')
// 引用类型共享
obj1.colors.push('black')
console.log(obj1.colors, 'obj1.colors')
console.log(obj2.colors, 'obj2.colors')

ECMAScript5 新增类似功能的方法: Object.create(p, props) 方法接受两个参数,第一个 p 作为创建后对象的原型对象,即创建后的对象继承该对象的所有属性和方法,第二个 props 该参数是个对象,格式与 Object.defineProperties 的第二个参数相同,包含属性描述符内容的属性对象。

const parent = {
    name: 'parent',
    colors: ['red']
}

const child1 = Object.create(parent, {
    age: {
        value: 30
    }
})

const child2 = Object.create(parent, {
    age: {
        value: 25
    }
})

child1.colors.push('black')
console.log(
    'child1 age: ' + child1.age,
    ', child1 name: ' + child1.name,
    ', child1 colors: ' + child1.colors
)
console.log(
    'child2 age: ' + child2.age,
    ', child2 name: ' + child2.name,
    ', child2 colors: ' + child2.colors
)

child1 age: 30 , child1 name: parent , child1 colors: red,black
child2 age: 25 , child2 name: parent , child2 colors: red,black
undefined

因此使用 Object.create(p, props) 创建的对象对 p 对象属于浅复制,因此引用类型属于共享属性。

寄生式继承

基于基础对象,创建新的对象,然后进行扩展将这个新对象返回,那么新对象将继承基础对象。

function createObj(parent) {
    function F() {}
    F.prototype = parent
    return new F()
}

function createAnother(parent) {
    // 基于 parent 创建新对象 o,继承自 parent
    const o = createObj(parent)
    // 对新对象 o 进行扩展
    o.sayName = function() {
        console.log('say name')
    }

    return o
}

寄生式继承模式,为新对象添加扩展函数是针对该新函数本身的,因此对于其他调用 createAnother 生成的新对象扩展的函数是无法被共享的,这个与使用 构造函数模式 创建对象存在同样方法无法共享问题。

寄生组合式继承

回顾下组合继承:

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

function Sub(name) {
    // 第二次调用 Super()
    Super.call(this, name)
}

// 第一次调用 Super()
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
// 添加共享方法
Sub.prototype.sayName = function() {
    console.log(this.name + ' say')
}

// 使用组合继承会发现父元素的构造函数被调用了两次
// 第一次调用时,原型对象上新增了个 name 属性
// 第二次调用时,子对象上新增了个 name 属性,并且会覆盖父元素上的 name
// 其实此时父元素上的 name 属性就基本不会被用到了,那就又何必去创建它,因此
// 我们采用寄生组合继承方式来规避这个问题

组合继承是让子类型的原型去继承父对象的一个实例来达到继承,此时子对象同时会有用父对象的属性和方法,但是属性应该是特用的,方法被共享,且组合继承导致父对象构造函数被调用两次且属性被覆盖而无用。

寄生组合继承是让子类型的原型去继承父对象的原型来达到继承,此时子对象调用父对象的构造函数去创建实例特有的属性,方法被添加到父对象的原型之上而避免父对象的构造函数多被调用一次而创建了一些附属在父对象构造函数之上的多余属性。

// 继承原型
function inheritPrototype(sub, sup) {
    const proto = createObj(sup.prototype)
    proto.constructor = sub
    sub.prototype = proto
}

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

function Sub(name) {
    // 这里只调用一次即可,给子对象添加属性
    Super.call(this, name)
}

inheritPrototype(Sub, Super)

// 此时给子对象原型添加方法也就是给父对象的原型添加了个新方法
// 然后改方法会被其他子对象共享
Sub.prototype.sayName = function() {
    console.log(this.name + ' say')
}

上例的实例原型分析图

总结

对象创建

方式 优点 缺点
工厂模式   1. 无类型
    2. 无共享
构造函数模式 1. 有类型 1. 无共享
原型模式 1. 有类型 1. 无特有
  2. 有共享  
组合模式(构造函数+原型) 1. 有类型  
  2. 有共享  
  3. 有特有  
动态原型模式   不能使用字面量覆盖原型
寄生构造模式(类似工厂模式)   破坏原有原型链
稳固构造函数模式 保护属性 不能使用 new 和 this

继承

方式 优点 缺点
原型链模式   1. 封闭性
    2. 引用类型问题
借用构造函数模式 1. 有特有属性 1. 共有属性问题
组合继承(原型+构造函数) 1. 有特有属性 1. 两次调用父类构造函数造成属性覆盖无用问题
  2. 有共有属性  
寄生继承模式(Object.create)   1. 引用类型问题
寄生组合式继承(寄生+构造+原型) 1. 有特有属性  
  2. 有共有属性  
  3. 无两次调用问题  

你可能感兴趣的:(Javascript)