本文系统梳理了 JavaScript 中七种常见的继承实现方式,涵盖原型链、构造函数(Call)、冒充对象、混合继承、中间件、寄生组合式以及 ES6 Class 继承。重点解析了各模式的实现原理、优缺点及适用场景,特别指出寄生组合式继承为引用类型最理想的继承方案,并对比了 ES6 Class 语法的简洁性与底层机制。
原型链机制与关系
原型链机制:
在 ECMAScript 规范中,原型链被视为实现继承的核心机制。其基本逻辑是利用原型对象,使一个引用类型能够继承另一个引用类型的属性和方法。
构造函数、原型与实例之间的关系:
每个构造函数都拥有一个原型对象(prototype),该原型对象包含一个指向构造函数的指针(constructor)。同时,实例对象内部包含一个指向原型对象的指针()。具体关系如下图所示:

每一个 Function 实例都是 Object 基类的实例,因此每个 Function 对象的 都指向 Object.prototype。
当访问实例属性时,引擎会先在实例自身的自定义属性中查找;若未找到,则通过 沿原型链向上查找至所属类的原型对象;若仍不存在,则继续通过原型的 查找至 Object 的原型对象。若最终未找到,则返回 undefined。
因此,引用类型间的继承正是通过这种原型链机制实现的。
原型继承
一. 原型继承
原型继承的核心在于将父类的私有属性和公有方法全部转化为子类的公有属性。
其本质并非克隆一份一模一样的属性给子类,而是通过 建立子类与父类实例之间的原型链。当子类实例需要访问父类的属性或方法时,可通过 逐级向上查找并使用。
function Parent(){
this.x = 199;
this.y = 299;
}
Parent.prototype.say = function(){
console.log('say')
}
function Child(){
this.g = 90;
}
Child.prototype = new Parent();
var p = new Parent();
var c = new Child();
console.dir(c)
实现的关键在于重写原型对象:将子类的原型指向父类的实例。此时,子类的实例可通过 访问到 Child.prototype(即 Parent 的实例),从而获取父类的私有方法;再通过 指向父类的 prototype,即可获取父类原型上的方法。
这种方式实现了将父类的私有和公有方法/属性均作为子类的公有属性,从而通过原型链完成继承。
需要注意的是默认原型链,因为所有引用类型均继承自 Object,所以子类实例也能访问到 Object 上的方法,如 toString() 和 valueOf() 等。
结果如下图所示:

若在子类中添加新方法或重写父类方法,务必在替换原型的语句之后执行。
function Parent(){
this.x = 199;
this.y = 299;
}
Parent.prototype.say = function(){
console.log('say')
}
function Child(){
this.g = 90;
}
/*Child.prototype.Bs = function(){
console.log('Bs')
}*/在这里写子类的原型方法和属性是没用的因为会改变原型的指向,所以应该放到重新指定之后
Child.prototype = new Parent();
Child.prototype.constructor=Child//由于重新修改了Child的原型导致默认原型上的constructor丢失,我们需要自己添加上,其实没啥用,加不加都一样
Child.prototype.Bs = function(){
console.log('Bs')
}
Child.prototype.say = function(){
console.log('之后改的')
}
var p = new Parent();
var c = new Child();
console.dir(c)
c.Bs() //Bs
c.say() // 之后改的
p.say() //say 不影响父类实例访问父类的方法
存在的问题:
1. 子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们要清楚一件事情就是我们操作基本数据类型的时候操作的是值,在操作应用数据类型的时候操作的是地址,如果说父类的私有属性中引用类型的属性,那他被子类继承的时候会作为公有属性,这样子类一操作这个属性的时候,会影响到子类二。
2. 在创建子类实例时,无法向父类构造函数传递参数。换言之,无法在不影响所有对象实例的前提下,向父类构造函数传递特定参数。
因此,实际开发中很少单独使用原型继承。
Call 继承
二. Call 继承
该方法通过改变方法的 this 指向并执行方法,在子类构造函数中调用父类.call(this),可将父类的私有属性转化为子类的私有属性。
function Parent() {
this.x = 100;
this.y = 199;
}
Parent.prototype.fn = function() {}
function Child() {
this.d = 100;
Parent.call(this); //构造函数中的this就是当前实例
}
var p = new Parent();
var c = new Child();
console.log(p) //Parent {x: 100, y: 199}
console.log(c) //Child {d: 100, x: 100, y: 199}
在子类构造函数中,将父类的 this 指向改为子类实例,并执行父类方法。此时,父类中的 this.x 变为子类实例的 .x,从而继承父类的私有属性。此方式仅能继承父类的私有属性和方法。
冒充对象继承
三. 冒充对象继承
冒充对象继承的原理是循环遍历父类实例,将父类实例的私有方法全部提取并添加到子类实例中。
function Parent(){
this.x = 100;
}
Parent.prototype.getX = function(){
console.log('getX')
}
function Child(){
var p = new Parent();
for(var attr in p){//for in 可以遍历到原型上的公有自定义属性
this[attr] = p[attr]
}
//以下代码是只获得到私有方法和属性,如果不加这个的话就可以遍历到所有方法和属性
/*if(e.hasOwnProperty(attr)){
this[attr] = e[attr]
}
e.propertyIsEnumerable()*///可枚举属性==> 可以拿出来一一列举的属性
}
var p = new Parent();
var c = new Child();
console.dir(c)
for in 循环可以遍历到原型上的公有自定义属性,因此能获取私有和公有的属性与方法。若需仅获取私有部分,需添加限制条件;若不进行 hasOwnProperty 判断,则会将父类的公有和私有属性均作为子类的私有属性。
混合继承与拷贝继承
四. 混合继承
混合继承结合了 Call 继承和原型继承,同时获取了私有和公有属性。虽然子类的原型上多了一套通过 Call 继承来的父类私有属性,但这不会产生问题,因为子类自身也通过 Call 继承拥有一套相同的属性。
function Parent(){
this.x=100;
}
Parent.prototype.getX = function(){}
function Child(){
Parent.call(this);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var p = new Parent();
var c = new Child();
console.log(c)//Child {x: 100}
存在的问题:
无论何种情况,构造函数都会被调用两次:一次在创建子类型原型时,另一次在子类型构造函数内部。这意味着子类最终包含父类型对象的全部实例属性,但必须在调用子类构造函数时重写这些属性。
另一种形式为 Call + 拷贝继承
//混合继承:call继承+拷贝继承
function extend(newEle,oldEle){
for(var attr in oldEle){
newEle[attr]=oldEle[attr];
}
}
function F(){
this.x=100;
this.showX=function(){}
}
F.prototype.getX=function(){};
F.prototype.getX1=function(){};
var f1=new F;
console.dir(f1)
function S(){
F.call(this)//call继承
}
extend(S.prototype, F.prototype);//拷贝继承
S.prototype.cc=function(){ }
var p1=new S;
console.dir(p1);
该方式使用 Call 继承获取父类的私有方法,使用 for in 拷贝获取父类的公有属性和方法,具有较高的实用性。
中间件继承
五. 中间件继承
中间件继承利用原型链机制,子类的 prototype.__proto__ 原本应直接指向 Object.prototype。
从父类的原型上的 也可到达 Object.prototype。在父类.prototype 上停留片刻,父类.prototype 即成为中间件,使得子类可将父类的公有方法作为自己的公有方法继承。
function Parent(){
this.x = 100;
}
Parent.prototype.getX = function(){}
function Child(){
}
Child.prototype.__proto__ = Parent.prototype;
var p = new Parent();
var c = new Child()
console.log(c)
寄生组合式继承
六. 寄生组合式继承
寄生式组合:Call 继承 + Object.create();
所谓寄生组合式继承,即通过借用构造函数继承属性,通过原型链混合形式继承方法。
基本思路是不必为了指定子类原型而调用父类构造函数,只需获取父类型原型的一个副本。
本质上,这是使用寄生式继承父类原型,然后将结果指定给子类原型。
function F(){
this.x=100;
}
F.prototype.showX=function(){};
function S(){
this.y = 200
F.call(this)//只继承了私有的;
}
function inheritPrototype(subType,superType){
var prototype = Object.create(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype;//指定对象
}
inheritPrototype(S,F)
var p1=new S;
console.dir(p1)
1、第一步是创建父类型原型的一个副本。
2、第二步是为创建的副本增加 constructor 属性,以弥补因重写原型而失去的默认 constructor 属性。
3、第三步是将创建的对象赋值给子类型的原型。
该方案的高效性体现在仅调用一次 SuperType 构造函数,避免了在 SubType.prototype 上创建不必要的多余属性。同时保持原型链不变,可正常使用 instanceof 和 isPrototypeOf(),因此寄生组合式继承是引用类型最理想的继承方法。
ES6 Class 继承
七. Class 继承
Class 可通过 extends 关键字实现继承,相比 ES5 通过修改原型链实现继承,这种方式更加清晰和方便。
class Father{
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
class Son extends Father{
constructor(x,y,color){
super(x,y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
console.log( super.toString()+this.color); // 调用父类的toString()
}
}
let son = new Son(3,4,'red');
son.toString();//结果为(3,4)red
上述代码定义了一个 Son 类,该类通过 extends 关键字,继承了 Father 类的所有属性和方法。
在 constructor 方法和 toString 方法中出现的 super 关键字,表示父类的构造函数,用于新建父类的 this 对象。
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类的 this 对象必须先通过父类构造函数完成塑造,获得与父类相同的实例属性和方法,然后再进行加工,添加子类自己的实例属性和方法。若不调用 super 方法,子类将无法获得 this 对象。
以上就是 JavaScript 常见继承方式汇总的详细内容,更多关于 JavaScript 继承方式的资料请关注本站其它相关文章!