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的继承。