JavaScript中的对象继承(ES5)

453 阅读5分钟

原型链继承

在谈原型链继承之前,我们先回顾一下构造函数、原型和实例的关系。我们知道,每个构造函数都有指向原型对象的指针prototype,原型对象的内部也有一个指向构造函数的指针constructor,而实例也有一个指向原型对象的内部指针。当我们在实例上访问一个属性或方法时,JavaScript会优先在实例对象上找对应的属性和方法,如果有就返回,没有则会根据指向原型对象的指针去在原型对象上找,如果原型对象还有原型对象(套娃!),则继续向上找,直到找到最后原型对象Object

原型继承就是利用这种机制,通过重写子类的原型对象,使其原型对象等于父类的实例,这样我们就可以通过原型对象访问到这个实例以及其原型对象上的属性和方法,从而实现继承。

function Super() {
  this.name = 'LiHua'; 
  this.run = function () {
    console.log("开始跑步") 
  }
}

Super.prototype.stop = function () {
  console.log("停止跑步");
}

function Sub() {
}

Sub.prototype = new Super(); // 重写原型对象为Super的实例
Sub.prototype.constructor = Sub // 修正内部constructor指向

Sub.prototype.go = function () { // 添加原型方法
  console.log('继续')
}

var person = new Sub();

console.log(person.name)  // LiHua
person.run()  // 开始跑步
person.stop()  // 停止跑步
person.go()  // 继续

原型链继承存在的问题

  • 继承的父类实例属性被所有子类实例共享,当共享属性为引用类型时,容易导致意外的错误。
  • 在创建子类实例时,不能向父类的构造函数传参。 当通过原型链继承时,子类的原型对象被重写为父类实例,所以父类实例上的属性自然就成为子类的原型属性,则被所有子类实例共享自然是理所当然,然而,当这个属性为引用类型是自会出现如下问题:
function Super() {
  this.friend = ["熊大", "熊二"]; 
}

function Sub() {
}

Sub.prototype = new Super(); 

var person1 = new Sub();
var person2 = new Sub();

console.log(person1.friend);  // [ '熊大', '熊二' ]
person2.friend.push("光头强");
console.log(person1.friend);  // [ '熊大', '熊二', '光头强' ]

可以看到,当父类实例的属性为引用类型时,子类实例person2修改friend属性,导致了person1的属性也被改变。

借用构造函数继承(对象冒充继承)

顾名思义,借用构造函数继承通过在子类构造函数中调用父类的构造函数,使子类拥有父类构造函数的属性和方法,从而实现继承。

function Super(name) {
  this.name = name
  this.friend = ["熊大", "熊二"]; 
}

function Sub(name) {
  // 调用父类构造函数实现继承
  Super.call(this,name)
  /* 
   * 相当与这样
   * this.name = name
   * this.friend = ["熊大", "熊二"]; 
   */
}


var person1 = new Sub("李华");
var person2 = new Sub("小刚");

console.log(person1.name)  // 李华
console.log(person2.name)  // 小刚
console.log(person1.friend);  // [ '熊大', '熊二' ]
person2.friend.push("光头强");
console.log(person1.friend);  // [ '熊大', '熊二', '光头强' ]
console.log(person2.friend);  // [ '熊大', '熊二', '光头强' ]

可以看到,使用借用构造函数继承,并没有使父类实例的属性成为子类的原型属性,而是相把父类构造函数在子类构造函数运行一遍,从而使子类实例拥有父类实例的属性,而这一属性是不被所有子类实例共享的,解决原型链继承的问题。但是,借用构造函数继承的问题也是显而易见的。

借用构造函数继承存在的问题

  • 只能继承父类的构造函数的属性和方法,不能继承原型链中的函数和方法 在父类原型中定义的属性和方法对于子类来说是不可见的,子类所有方法和属性都在构造函数中定义,导致复用函数的意义不是很大。

组合继承

组合继承是将原型链继承和借用构造函数继承组合到一起,使用原型链继承实现对原型属性和方法的继承,使用借用构造函数继承实现对实例属性和方法的继承,从而解决了原型链继承和借用构造函数继承存在的问题。

function Super(name) {
  this.name = name
  this.friend = ["熊大", "熊二"]; 
}

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

function Sub(name, age) {
  // 调用父类构造函数继承实例属性和方法
  Super.call(this,name)
  // 子类自己的属性
  this.age = age
}

// 实现原型属性和方法的继承
Sub.prototype = new Super()
Sub.prototype.constructor = Sub  // 修正内部constructor指向

// 子类自己原型方法
Sub.prototype.sayAge = function () {
  console.log(this.age)
}


var person1 = new Sub("李华", 20);
person1.friend.push("光头强");
console.log(person1.friend);  // [ '熊大', '熊二', '光头强' ]
person1.sayName()  // 李华
person1.sayAge()  // 20

var person2 = new Sub("小刚", 18);
console.log(person2.friend);  // [ '熊大', '熊二' ]
person2.sayName()  // 小刚
person2.sayAge()  // 18

如上所示,子类继承了原型和实例上的方法和属性,实现的函数复用,而且每个子类实例都可以有自己的方法和属性,非常的灵活,这使得组合继承成为最常用的一种继承模式。

组合继承唯一的不足:调用两次父类构造函数,第一次是在重写原型对象,即Sub.prototype = new Super(),第二次是在实例化子类在子类构造函数中调用,即Super.call(this,name),第二次调用产生的实例属性和方法会覆盖第一次调用在原型上产生的属性和方法,导致原型对象上存在不必要的属性和方法(此例中为namefriend)。

寄生组合式继承

寄生组合式继承还是通过构造函数实现实例方法及属性的继承,但不再为了修改子类原型对象而调用父类构造函数,而是使父类原型对象的一个副本成为子类的原型对象来达到继承原型的属性和方法。

function Super(name) {
  this.name = name
  this.friend = ["熊大", "熊二"]; 
}

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

function Sub(name, age) {
  // 调用父类构造函数继承实例属性和方法
  Super.call(this,name)
  // 子类自己的属性
  this.age = age
}

// 实现原型属性和方法的继承

// 1.创建父类原型副本prototype
var Fn = function () {}
Fn.prototype = Super.prototype
var prototype = new Fn()
// 2.修正内部constructor以及prototype指向
prototype.constructor = Sub
Sub.prototype = prototype

// 子类自己原型方法
Sub.prototype.sayAge = function () {
  console.log(this.age)
}


var person1 = new Sub("李华", 20);
person1.friend.push("光头强");
console.log(person1.friend);  // [ '熊大', '熊二', '光头强' ]
person1.sayName()  // 李华
person1.sayAge()  // 20

var person2 = new Sub("小刚", 18);
console.log(person2.friend);  // [ '熊大', '熊二' ]
person2.sayName()  // 小刚
person2.sayAge()  // 18

最后

以上便是笔者对ES5中对象继承的自我理解总结,留下记录,日后温习,当然,如果能有幸帮到读者那便皆大欢喜!