再谈继承
在这里记录一下 JavaScript 实现继承的各种方法,直接参考自《JavaScript高级程序设计》这本书,红宝书写的实在是很好,涵盖了几乎基础和高级知识点。因此本文是算一篇笔记。
关于原型链实现继承我就不再赘述了,在前面的文章已经写过。
盗用构造函数
因为当原型中的属性为引用值时,其引用值会在所有实例间共享。如下:
//定义父类
function Base(){
this.colors = ["red", "green", "blue"];
}
//定义子类
function Sub(){}
//原型链继承
Sub.prototype = new Base();
//创建子类实例1
let sub1 = new Sub();
sub1.colors.push("white"); // 修改colors
console.log(sub1.colors); // => "red", "green", "blue", "white"
//创建子类实例2
let sub2 = new Sub();
console.log(sub2.colors);// => "red", "green", "blue", "white" colors在各个实例中共享
盗用构造函数技术就是解决引用值导致的问题:在子类构造函数中调用父类构造函数。
//定义父类
function Base(){
this.colors = ["red", "green", "blue"];
}
//定义子类 并在子类中调用父类构造函数
function Sub(){
// 继承并传参
Base.call(this,"args"); //绑定调用Base构造函数中的this为Sub的实例对象
}
//定义实例
let sub1 = new Sub();
sub1.colors.push("black");
console.log(sub1.colors); // "red","green","blue","black"
let sub2 = new Sub();
console.log(sub2.colors); // => "red","green","blue"
缺点:
必须在构造函数中定义方法,因此函数不能复用
子类不能访问父类原型上的方法
组合继承
综合了原型链和盗用构造函数,将两者的优点集中了起来。思路是盗用继承和原型链结合:
// 定义父类
function Base() {
this.colors = ["red", "green", "blue"];
}
Base.prototype.sayHi = function() {console.log("Hi!")}; // 父类原型上的方法
// 定义子类
function Sub(name) {
// 继承父类属性
Base.call(this, "args"); // 第二次调用父类构造函数
this.name = name;
}
// 原型链继承方法
Sub.prototype = new Base(); // 第一次调用父类构造函数
// 定义实例
let sub1 = new Sub("sub1");
sub1.sayHi(); // => "Hi!"
sub1.colors.pop();
console.log(sub1.colors); // => "red","green"
let sub2 = new Sub("sub2");
sub2.sayHi(); // => "Hi!"
console.log(sub2.colors); // => "red","green","blue"
组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof
操作符和 isPrototypeOf()
方法识别合成对象的能力。
缺点:
- 父类构造函数必须调用2次
可以看到colors
变量被存储了两次,分别在Sub的实例和Sub的原型对象上。
原型式继承
如下:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
let person = {
name: "",
friends: ["李四", "王五", "老六"]
};
let anotherPerson = object(person);
anotherPerson.name = "老大";
anotherPerson.friends.push("田七");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "老二";
yetAnotherPerson.friends.push("老八");
console.log(person.friends); // "李四,王五,老六,田七,老八"
ECMAScript5中Object.create()
将原型式继承的概念规范化了。
缺点:
- 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original) {
let clone = Object.create(original); // 生成继承original对象的clone对象
clone.sayHi = function() {console.log("Hi!")}; // 增强对象
return clone; // 返回该对象
}
缺点:
- 寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
为了解决组合式继承的效率问题:父类构造函数始终会被调用两次。将寄生式和组合式结合起来。
实现继承如下:
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建(父类)对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值(父类)对象(原型链继承)
}
图解:
完整示例:
function SuperType(name) {
this.name = name;
this.colors = ["red", "green", "blue"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name); // 其实就是ES6中调用的super()
//SubType.prototype.__proto__.constructor(name); 相当于这个
// this.name = "李四";
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
let sub1 = new SubType("张三", 18);
sub1.sayName(); // => "张三"
sub1.sayAge(); // => 18
sub1.colors.push("black");
console.log(sub1.colors); // => 'red', 'green', 'blue', 'black'
console.log("------------------------------")
let sub2 = new SubType("李四", 21);
sub2.sayName(); // => "李四"
sub2.sayAge(); // => 21
console.log(sub2.colors); // => 'red', 'green', 'blue'
Reference参考:
- JavaScript高级程序设计