原型&原型链&继承

355 阅读7分钟

要理解原型原型链,继承的问题,首先要知道js里的几个基本的游戏规则。

概念

  • 引用类型,都具有对象特性,即可自由扩展属性。
  • 引用类型,都有一个隐式原型(__proto__)属性,属性值是一个普通对象。
  • 引用类型,隐式原型 (__proto__)的属性值指向它的构造函数的显式原型(prototype)的属性值。
  • 当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型(__proto__)也就是它的构造函数的显式原型(prototype)中寻找。

这几个规则放到最上面,待会会用到。

原型链继承

原型链继承的重点是让子构造函数的原型等于父构造函数的实例。

function Father(){
  this.name = 'Father'
  this.height = 175
  this.sons = ['J','K','L'] 
  this.sayName = function(){
    console.log(this.name)
  }
}
Father.prototype.age = 22
function Son(){
  this.sex = 'man'
  this.name = 'Son'
}

// 重点:让子构造函数的原型等于父构造函数的实例。
let f1 = new Father()   // 创建父构造函数的实例
Son.prototype = f1      // 让子构造函数的原型等于父构造函数的实例

let s1 = new Son()
console.log(s1.name)   // Son
console.log(s1.age)   // 22
s1.sayName()           // Son
// 搜索轨迹
// s1 => f1 => Father.prototype…=> Object.prototype

// 缺点 :引用类型值会被所有实例共享
// 如果再创建一个实例,并且修改这个实例继承的引用类型属性
let s2 = new Son()
s2.sons.push('Q')
console.log(s1.sons)   //  ['J', 'K', 'L', 'Q']
console.log(s2.sons)   //  ['J', 'K', 'L', 'Q']

特点:

  • 可以继承父类构造函数属性,父类原型的属性。 缺点:
  • 新实例无法向父类构造函数传参。
  • 继承单一。
  • 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享。

于是,我们针对原型链继承的缺点进行修改,就有了借用构造函数继承:

借用构造函数继承

function Father(sons){
  this.name = 'Father'
  this.height = 175
  this.sons = ['J','K','L',...sons] 
  this.sayName = function(){
    console.log(this.name)
  }
}
Father.prototype.age = 22

function Son(){
  Father.call(this,['Q','W'])    // 这里借用构造函数  可以借用多个
  this.sex = 'man'
  this.name = 'Son'
}

let s1 = new Son()
s1.sons.push('E')
console.log('s1',s1)   // sons: (6) ['J', 'K', 'L', 'Q', 'W', 'E']

let s2 = new Son()
console.log('s2',s2)   // sons: (5) ['J', 'K', 'L', 'Q', 'W']

// 缺点:继承不了父构造函数原型上的属性
console.log(s2.age)    // undefined

特点:

  • 只继承了父类构造函数的属性,没有继承父类原型的属性。
  • 保证了原型链中引用类型值的独立,不再被所有实例共享。
  • 可以继承多个构造函数属性(call多个)。
  • 在子实例中可向父实例传参。 缺点:
  • 只能继承父类构造函数的属性。
  • 无法实现构造函数的复用。(每次用每次都要重新调用)。
  • 每个新实例都有父类构造函数的副本,臃肿。

借用构造函数继承一举解决了原型链继承的三个问题,但是,又出现了新问题,就是子类继承不到父类原型上的属性,从最后一行s2.age输出为undefined可以看出。

于是,我们针对借用构造函数的缺点1进行优化,就有了组合继承:

组合继承

组合继承是组合了前两个继承为一体,也就是组合了原型链继承和借用构造函数继承。
通过原型链对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

function Father(sons=[]){
  this.name = 'Father'
  this.height = 175
  this.sons = ['J','K','L',...sons] 
  this.sayName = function(){
    console.log(this.name)
  }
}
Father.prototype.age = 22

function Son(){
  Father.call(this,['Q','W'])    // 这里借用构造函数  可以借用多个
  this.sex = 'man'
  this.name = 'Son'
}
Son.prototype = new Father();    //继承父类方法,第二次调用Father()

console.log(Son.prototype.constructor)   // 子类的构造函数会被原型上的那个父类构造函数代替  Father(){...}  这里后面会讲
let s1 = new Son();
s1.sons.push("E");
console.log('s1',s1.sons);   // (6) ['J', 'K', 'L', 'Q', 'W', 'E']

let s2 = new Son();
console.log('s2',s2.sons);   // (5) ['J', 'K', 'L', 'Q', 'W']

console.log(s2.age)    // 22

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式, 而且, instanceofisPrototypeOf()也能用于识别基于组合继承创建的对象。

特点:

  • 可以继承父类原型上的属性,可以传参,可复用。
  • 每个新实例引入的构造函数属性是私有的。 缺点:
  • 调用了两次父类构造函数(耗内存),子类的构造函数会被原型上的那个父类构造函数代替。

原型式继承

该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型. 大意如下:

在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.

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

function Father(sons=[]){
  this.name = 'Father'
  this.height = 175
  this.sons = ['J','K','L',...sons]
  this.sayName = function(){
    console.log(this.name)
  }
}
Father.prototype.age = 22

let f1 = new Father()
let s1 = object(f1)
s1.sons.push('E')
console.log(s1.sons)     // (4) ['J', 'K', 'L', 'E']

let s2 = object(f1) 
console.log(s2.sons)     // (4) ['J', 'K', 'L', 'E']

console.log(s2.age)      // 22

用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。Object.create()就是这个原理。

特点:

  • 类似于复制一个对象,用函数来包装。 缺点:
  • 所有实例都会继承原型上的属性。
  • 无法实现复用。(新实例属性都是后面添加的)。

ECMAScript5 中,通过新增Object.create()方法规范化了上面的原型式继承。

寄生式继承

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function Father(sons=[]){
  this.name = 'Father'
  this.height = 175
  this.sons = ['J','K','L',...sons]
  this.sayName = function(){
    console.log(this.name)
  }
}
Father.prototype.age = 22

function createSon(original){
  let clone = object(original)     // 通过调用object函数创建一个新对象
  clone.sayHi = function(){        // 以某种方式来增强这个对象
    console.log("hi");
  };
  return clone;//返回这个对象
}
let f1 = new Father()

let s1 = createSon(f1)
s1.sons.push('E')
console.log('s1',s1)        // sons: (4) ['J', 'K', 'L', 'E']

let s2 = createSon(f1)
console.log('s2',s2)        // sons: (4) ['J', 'K', 'L', 'E']

console.log(s2.age)         // 22

仔细看,这不就是给原型式继承外面套了个壳子嘛

优点:

  • 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。 缺点:
  • 没用到原型,无法复用。

寄生组合式继承

// 寄生
function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
function Father(sons=[]){
  this.name = 'Father'
  this.height = 175
  this.sons = ['J','K','L',...sons]
  this.sayName = function(){
    console.log(this.name)
  }
}
Father.prototype.age = 22
let con = object(Father.prototype)      // 用原型式继承父类原型。 继承了 Father 的原型上的属性
// 组合
function Son(){
  Father.call(this)       // 用借用构造函数继承父类构造函数里的属性。 继承了父类构造函数里创建的属性
  this.sayHi = function(){        // 以某种方式来增强这个对象
    console.log("hi");
  };
}

Son.prototype = con    // 因为con 继承了 Father 原型上的属性,这样相当于 Son 也继承了原型上的属性
Son.prototype.constructor = Son          // 修复 Son 的构造函数指向
console.log(con.constructor)             // Son
console.log(Son.prototype.constructor)   // Son
let s1 = new Son()
console.log('s1',s1)
s1.sons.push('E')                   // sons: (4) ['J', 'K', 'L', 'E']
console.log('s1.__proto__',s1.__proto__)  // Father 

let s2 = new Son()
console.log('s2',s2)                // sons: (3) ['J', 'K', 'L']

寄生:

  • 在函数内返回对象然后调用。

组合:

  • 函数的原型等于另一个实例。
  • 在函数中用apply或者call引入另一个构造函数,可传参。

这是最成熟的方法,也是现在库实现的方法。在Vue源码在extend方法里就用到了这种继承方式。

继承这些知识点与其说是对象的继承,更像是函数的功能用法,如何用函数做到复用,组合,这些和使用继承的思考是一样的。上述几个继承的方法都可以手动修复他们的缺点,但就是多了这个手动修复就变成了另一种继承模式。

JS原型链与继承别再被问倒了 JS继承 JavaScript常用八种继承方案