聊聊Js中的继承

ECMAScript支持的继承

许多OO语言都支持两种继承方式:接口继承实现继承
接口继承只继承函数签名,实现继承则继承实际的方法。由于ECMAScript中函数没有签名,所以它仅支持实现继承。

函数签名(拓展)

维基百科对函数签名的解释:


直译:函数签名定义了函数的输入输出,它包含了函数的参数个数、参数类型、参数顺序。函数签名通常在重载时使用,目的是在众多重载函数中找到正确的函数调用。

解释:JavaScript是一种类型松散或动态语言。这意味着不必提前声明变量的类型,类型将在程序处理时自动确定,函数的参数是由包含0或者多个值的数组来表示的。在其他语言中,命名参数这块必须要求事先创建函数签名,而将来的调用也必须与该签名一致。因此,ECMAScript中函数没有签名,并且也不支持重载。

ES5中的继承

ES5实现继承的6种方式

原型链

基本思想:将父类的实例作为子类的原型
示例代码:

// 父类(同时也是父类构造函数)
function Food (name) {
  // 属性
  this.name = name || 'Food';
  // 实例方法
  this.sayName = function(){
    console.log('该食物名称为:' + this.name);
  }
}
// 父类原型上定义方法
Food.prototype.setPrice = function(price) {
    this.price = price;
    console.log("该食物价格为:" + this.price)
};

// 子类(同时也是子类构造函数)
function Apple () {
    this.name = 'apple';
}

// 继承Food
Apple.prototype = new Food();

// 创建实例
var apple = new Apple();

console.log(apple instanceof Food); // true 
console.log(apple instanceof Apple); // true
console.log(apple.name); // apple
apple.setPrice(100); // 该食物价格为:100
apple.sayName(); // 该食物名称为:apple

优点:
1.父类新增原型方法、原型属性,子类都能访问到

缺点:
1.来自原型对象的引用属性是所有实例共享的,当一个实例改变了其从原型那里继承来的引用属性值时,其它继承自这个原型属性的值都将被改变。
2.创建子类实例时,无法向父类构造函数传参。(无法在不影响所有对象实例的情况下)

构造函数

基本思想:子类构造函数内部调用父类构造函数,相当于复制父类的实例属性给子类。
示例代码:

// 父类(同时也是父类构造函数)
function Food (name) {
  // 属性
  this.name = name || 'Food';
  // 实例方法
  this.sayName = function(){
    console.log('该食物名称为:' + this.name);
  }
}
// 父类原型上定义方法
Food.prototype.setPrice = function(price) {
    this.price = price;
    console.log("该食物价格为:" + this.price)
};

// 子类(同时也是子类构造函数)
function Apple (name) {
    Food.call(this, name); // 伪造继承效果
    this.color = 'red';
}

// 创建实例
var apple = new Apple('apple');

console.log(apple instanceof Food); // false 
console.log(apple instanceof Apple); // true
console.log(apple.name, apple.color); // apple red
apple.sayName(); // 该食物名称为:apple
apple.setPrice(100); // Uncaught TypeError: apple.setPrice is not a function

优点:
1.每个实例属性各自独立,解决了原型链继承中,子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类传递参数
3.可以实现多继承(call多个父类对象)

缺点:
1.只能继承父类的实例属性和方法,不能继承原型属性/方法
2.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。
3.实例并不是父类实例,而是子类实例。

组合继承

基本思想:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。(组合原型链继承和构造函数继承)

示例代码:

// 父类(同时也是父类构造函数)
function Food (name) {
  // 属性
  this.name = name || 'Food';
  // 实例方法
  this.sayName = function(){
    console.log('该食物名称为:' + this.name);
  }
}
// 父类原型上定义方法
Food.prototype.setPrice = function(price) {
    this.price = price;
    console.log("该食物价格为:" + this.price)
};

// 子类(同时也是子类构造函数)
function Apple (name) {
    // 继承父类属性
    Food.call(this, name);
    this.color = 'red';
}
// 继承父类原型方法
Apple.prototype = new Food();

// 创建实例
var apple = new Apple('apple');

console.log(apple instanceof Food); // true 
console.log(apple instanceof Apple); // true
console.log(apple.name, apple.color); // apple red
apple.sayName(); // 该食物名称为:apple
apple.setPrice(100); // 该食物价格为:100

优点:
1.弥补了构造函数继承的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2.既是子类的实例,也是父类的实例
3.可传参、不存在引用属性共享问题

缺点:
1.调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

原型式继承

基本思想:以一个对象做为另一个对象的基础(利用Object.create()方法)

示例代码:

// 被继承对象
var Food = {
  // 属性
  name: 'Food',
  // 实例方法
  sayName: function(){
    console.log('该食物名称为:' + this.name);
  }
}

// 创建实例
var apple = Object.create(Food, {
    color: {
        value: 'red'
    },
    name: {
        value: 'apple'
    }
});

console.log(apple.name, apple.color); // apple red
apple.sayName(); // 该食物名称为:apple


大家可能有些疑惑,Food.prototype为啥不存在,这是因为只有函数才有prototype属性,用于供它实例的对象继承。若大家对这个感兴趣可以参考我的一篇文章<<谈谈Js中的原型>>

优点:
1.适用于只想让一个对象与另一个对象保持类似的情况。

缺点:
1.存在引用属性共享问题。
2.不属于类式继承,缺少了类的概念。

寄生式继承

基本思想:创建一个用于封装继承过程的工厂函数,函数内部会将clone对象增强并返回。

示例代码:

// 寄生式继承
function createObject(originObj) {
    var clone = Object.create(originObj);
    clone.color = 'red';
    clone.name = 'apple';
    return clone;
}

// 被继承对象
var Food = {
  // 属性
  name: 'Food',
  // 实例方法
  sayName: function(){
    console.log('该食物名称为:' + this.name);
  }
}

// 创建实例
var apple = createObject(Food);

console.log(apple.name, apple.color); // apple red
apple.sayName(); // 该食物名称为:apple

优缺点同原型式。它的出现是为了接下来介绍的寄生组合式继承。

寄生组合式继承

基本思想:不必为了指定子类型的原型而调用父类的构造函数,所需的无非就是一个父类原型副本而已。使用寄生式继承来继承父类原型,然后再将结果给指定子类的原型。

示例代码:

// 寄生组合式
function inheritPrototype(child, parent) {
    var protoCopy = Object.create(parent.prototype); // 创建父类原型副本
    protoCopy.constructor = child; // 为副本添加constructor属性,弥补重写原型失去的constructor属性
    child.prototype = protoCopy; // 将副本赋给子类原型

}

// 父类(同时也是父类构造函数)
function Food (name) {
  // 属性
  this.name = name || 'Food';
  // 实例方法
  this.sayName = function(){
    console.log('该食物名称为:' + this.name);
  }
}   
// 父类原型上定义方法
Food.prototype.setPrice = function(price) {
    this.price = price;
    console.log("该食物价格为:" + this.price);
};

// 子类(同时也是子类构造函数)
function Apple (name) {
    Food.call(this, name);
    this.color = 'red';
}

inheritPrototype(Apple, Food);

Apple.prototype = new Food();    
var instance = new Apple('apple');


console.log(instance instanceof Apple); // true
console.log(instance instanceof Food); // true
instance.sayName(); // 该食物名称为:apple
instance.setPrice('100'); // 该食物价格为:100

优点:
1.最理想的继承方式。

缺点:
1.实现较为复杂。

ES6中的继承

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
大家可以参考阮一峰老师的ES6 Class的继承

相关参考

函数签名-MDN
Type signature-维基百科
为什么JS函数没有签名?-知乎