【手撕JS】面对对象设计和原型链-继承(三)

258 阅读6分钟

往期回顾:

  1. 【手撕JS】面对对象设计和原型链-属性(一)
  2. 【手撕JS】面对对象设计和原型链-设计模式(二)

JS 可是红透一方的大人物,可是大人物也有他的烦恼。他不知道他应该让谁继承它精妙的想法。它有好几个孩子,都叫继承。让我们看看最终谁才是最后的赢家。

继承的几种方式

这里应用《javascript高级程序设计》中的原话 P174

  • 组合继承
    • 这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
  • 原型式继承
    • 可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制的副本还可以得到进一步的改造。
  • 寄生式继承
    • 与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合模式一起使用。
  • 寄生组合式继承
    • 集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效的方式

组合模式之前的原型链模式和构造函数模式

在说组合模式之前,要说组合模式到底是组合的哪两个

原型链模式

不多BB,直接上代码。

function Father(){
  this.name = "父类"
}

Father.prototype.getFatherName = function(){
   return this.name
}

function Children(){
    this.age= "3"
}
// 子类继承父类
Children.prototype = new Father()
// 定义子类方法
Children.prototype.getChildrenAge= function(){
    return this.age
}

var kid = new Children();

console.log(kid.getFatherName()) // 父类
console.log(kid.getChildrenAge()) // 3

kid.name
// "父类"
kid.age
// "3"

这个例子的原型链分析

image.png

解释一下: 我们在创建Children构造函数的时候将他的原型指定为了Father(),而Father里面的又有Father的原型。当我们实例化kid的时候,kid就拥有了一条从kid到children到father的原型链。最终通过原型链获得了他们共享的getChildrenName、getFatherName方法,以及name和age属性。

拓展小知识:

  • 当我们访问一个实例的某一个属性和方法的时候,会沿着原型链向上搜索,直到找到该属性或者方法,或者都没有这个属性和方法停止搜索。
  • 可以通过 instanceof 和 isPrototypeOf() 确定他的类型 这个也是逐级的。

注意点: 有时候需要定义超类没有的方法的时候,一定要写在替换原型之后哟。

它的缺点:

  1. 如果属性中有引用类型值,那么这个属性将会在整个原型链中同步被修改

举个栗子

function Father(){
  this.name = "父类",
  this.cars = ["吉利"]
}

Father.prototype.getFatherName = function(){
   return this.name
}

Father.prototype.buyCar= function(carName){
   return this.cars.push(carName)
}

function Children(){
    this.age= "3"
}
// 子类继承父类
Children.prototype = new Father()
// 定义子类方法
Children.prototype.getChildrenAge= function(){
    return this.age
}

var kid1 = new Children();
var kid2 = new Children();
kid1.buyCar("宝马")
console.log(kid1.cars) //  ["吉利", "宝马"]
console.log(kid2.cars) //  ["吉利", "宝马"]
  1. 不能向超类的构造类型中传递参数。

看样子它似乎还不够好。让我们来看看它的兄弟 借用构造函数模式

借用构造函数模式

它的基本思想很简单,在子类内部调用超类的构造函数。我们简单看下实现代码。

function Father(house){
  this.name = "父类",
  this.cars = ["吉利"],
  this.house = house
}

Father.prototype.getFatherName = function(){
   return this.name
}

function Children(){
    this.age= "3"
    // 子类继承父类
    Father.call(this,"汤臣三口") // or Father.apply(this)
}

// 定义子类方法
Children.prototype.getChildrenAge= function(){
    return this.age
}

var kid1 = new Children();
var kid2 = new Children();
kid1.cars.push("宝马")
console.log(kid1.cars) //  ["吉利", "宝马"]
console.log(kid2.cars) //  ["吉利"]
console.log(kid1.house) // "汤臣三口"
console.log(kid1.getFatherName) // undefined

他的优点:

  • 解决了原型链模式不能传参的问题,解决引用类型的问题 缺点:
  • 观察代码我们不难发现,丢失了父类的的函数。

看来他还是难以担当大任啊!让我们有请下一位!

组合继承

组合继承有时候又叫伪经典继承,它吸收了原型链模式和借用构造函数模式的优点。通过构造函数实现实例属性的继承,通过原型链实现方法的继承。

吃个栗子

function Father(house){
  this.name = "父类",
  this.cars = ["吉利"],
  this.house = house
}

Father.prototype.getFatherName = function(){
   return this.name
}

function Children(){
    this.age= "3"
    // 借用构造函数
    Father.call(this,"汤臣三口") // or Father.apply(this) 第二次调用
}
// 原型链继承
Children.prototype = new Father() // 第一次调用
// 修改构造函数的引用 步修改的话 无法用constructor来判断类型
Children.prototype.constructor = Children

// 定义子类方法
Children.prototype.getChildrenAge= function(){
    return this.age
}

var kid1 = new Children();
var kid2 = new Children();
kid1.cars.push("宝马")
console.log(kid1.cars) //  ["吉利", "宝马"]
console.log(kid2.cars) //  ["吉利"]
console.log(kid1.house) // "汤臣三口"
console.log(kid1.getFatherName()) // 父类

通过组合继承模式让kid 继承Children 和 Father的属性以及方法。 2个kid实例之间都有自己的属性,互不干扰。

缺点:

  • 会调用2次超类

组合继承选入候选人,让我们再来看看下一位的表现。

原型式继承

以下引用 《javascript高级程序设计》中 p169 页的定义

道格拉斯·克罗克福德在2006年写的《JavaScript中的原型式继承》中提出的一个想法,基本思路是借助原型可以基于已经有的对象创建新的对象,同时不必因此创建新的类型。

顶个梨子

var persion = {
   "name": "关羽",
   "friends": ["刘备","黄忠"]
}

var anotherPerson = Object.create(persion,{
    "name": {value: "赵云"},
})

var anotherPerson2 = Object.create(persion);
anotherPerson2.name = "马超"
anotherPerson2.friends.push("张飞")
console.log(anotherPerson2.friends) // ["刘备", "黄忠", "张飞"]
console.log(anotherPerson.friends) // ["刘备", "黄忠", "张飞"]

这个方法,其实就是浅拷贝,对于引用类型的属性还是会存在问题。 抬走,下一个!

寄生式继承

还是由上面那位大佬提出的思路。基本思路和寄生构造函数还有工厂模式类型,创建一个仅仅用于封装继承过程的函数,在函数内部增强函数,然后对它说,你被加强了,赶紧出去送。

吃个栗子

function createPerson(origonal){
    var clone = Object.create(origonal)
    clone.sayName = function(){
        console.log(this.name)
    }
    return clone
}

var persion = {
   "name": "关羽",
   "friends": ["刘备","黄忠"]
}

var anotherPerson = createPerson(persion)
anotherPerson.sayName() //关羽

缺点:不能函数复用降低了效率。

可以,下一位选手已经迫不及待出来了,它觉得自己行了。

寄生组合式继承

难道你就是传说中的毒液蜘蛛侠?能力越大,责任越大。 寄生组合式继承解决了组合式继承的缺点。它的基本模式为

/**
* subType 子类
* superType 父类
*/
function inheritPrototype(subType,superType){
   var prototype = Object.create(superType.prototype) // 创建对象
   prototype.constructor = subType // 增强对象
   subType.prototype = prototype   // 指定对象
}

举个例子

function Person(name){
   this.name = name
   this.friends = ["刘备","黄忠"]
}
Person.prototype.sayName = function(){
   console.log(this.name)
}

function Chlidren(name, age){
   Person.call(this,name)
   this.age = age
}
// 继承
inheritPrototype(Chlidren, Person)

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

var zhaoyun = new Chlidren("赵云", "40")

他和组合模式不同的一步就是方法封装原型的那一步。那一步减少了一次父类的调用,从而提高了效率。

看来最后谁最好心里都有数了。

下一篇开新坑,作用域与闭包。