前言
最近在学习原型的相关内容时回顾到实现继承的方式,再次回温依旧很是陌生,在此记录以便深刻理解,以方便日后巩固。
继承的概念
继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。 前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签 名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。
原型链继承
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
首先是原型链继承,上方是一个简单的原型链继承的案例,
- 首先定义了SuperType构造函数(构造函数首字母大写)
- 然后在他的原型上添加了getSuperValue方法
- 之后定义了SubType构造函数,然后将SubType的原型重写为 SuperType的实例,以此来继承SuperType,这里通过对象字面量的形式重写SubType的原型,会导致构造函数变为SuperType,即 instance.constructor 为 SuberType
- 之后为SubType的原型上添加了一个getSubValue的方法
- 然后生成一个SubType的实例,这时打印instance的getSuperValue的方法,在SubType的原型上没有找到,继续向上查找,找到SuperType的原型上,找到之后方法内部返回了this.property,先在instance上查找,没有找到,向上从SubType的原型上查找,没有找到,继续向上从SuperType的实例上查找(SubType的原型,因为有重写SubType的原型,所以将后续重新和第一次通过new SuperType重写分开),之后从SuperType的原型上找到了(若找不到还会从Object的原型上查找,所有原型都继承自Object的原型,都以它为原型链的尽头)。

这张图描述了案例的原型链
问题:引用类型的值会在实例间共享
盗用构造函数
为了解决原型链继承引用类型实例间共享的问题,出现了盗用构造函数。
基本思路
在子类 构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()和 call()方法以新创建的对象为上下文执行构造函数
示例
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承 SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
示例中加粗的代码展示了盗用构造函数的调用。通过使用 call()(或 apply())方法,SuperType
构造函数在为 SubType 的实例创建的新对象的上下文中执行了。这相当于新的 SubType 对象上运行了 SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的 colors 属性。
传递参数
相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。来
看下面的例子:
function SuperType(name){
this.name = name;
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
在这个例子中,SuperType 构造函数接收
缺点
盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法
,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法。
组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基
本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。来看下面的例子
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
简单总结就是先盗用构造函数获得构造函数的内部属性,以此解决引用类型共享的问题,然后重写原型,继承父类原型上的发放,实现方法的共用。
原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
这个 object()函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返
回这个临时类型的一个实例。本质上,object()是对传入的对象执行了一次浅复制。来看下面的例子:
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。
你需要把这个对象先传给 object(),然后再对返回的对象进行适当修改。在这个例子中,person 对
象定义了另一个对象也应该共享的信息,把它传给 object()之后会返回一个新对象。这个新对象的原型 是 person,意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.friends 不仅是 person 的属性,也会跟 anotherPerson 和 yetAnotherPerson 共享。这里实际上克隆了两个 person。
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,
属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"
与原型式继承类似,在复制对象之后又以某种方式增强这个对象,这里是给这个对象增加了sayHi方法,然后返回这个增强后的对象。
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式
继承所必需的,任何返回新对象的函数都可以在这里使用。
通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调
用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。寄生式组合继承的基本模式如下所示
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,
因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和
isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。
本文详细介绍了JavaScript中的原型链继承、盗用构造函数(包括call/apply)、组合继承(结合原型链和构造函数)、原型式继承以及寄生式继承,探讨了它们的原理、优缺点和适用场景。
583

被折叠的 条评论
为什么被折叠?



