本文系统梳理 JavaScript 继承机制的演变。详细解析 ES5 时期的类式、构造函数、组合、原型式及寄生式组合继承等五种方式,剖析其优缺点与适用场景。重点介绍 ES6 Class 语法的继承实现,对比传统原型链修改方式,阐述其清晰性与便利性,帮助开发者深入理解对象继承原理。
JavaScript 继承机制概述
在 ES6 标准推出之前,JavaScript 语言本身并未内置类似其他面向对象语言的直接继承机制。
ES5 中的五种继承方式
在 ES5 规范中,开发者主要通过原型链和构造函数来模拟实现继承行为。
类式继承
类式继承是早期常见的一种实现方式。
//声明父类
function Father(){
this.fatherVal = 'father';
}
//为父类添加共有方法
Father.prototype.getFatherValue = function(){
return this.fatherVal;
}
//声明子类
function Child(){
this.childVal = 'child';
}
//继承父类
Child.prototype = new Father();
//为子类添加共有方法
Child.prototype.getChildValue = function(){
return this.childVal;
}
其核心逻辑是将子类的 prototype 属性指向父类的一个实例。这样,新创建的对象不仅复制了父类构造函数内部定义的属性和方法,还将自身的原型链指向父类的原型对象,从而同时拥有父类原型上的属性和父类构造函数中定义的属性与方法。
var instance = new Child();
console.log(instance.getFatherValue()); //father
console.log(instance.getChildValue()); //child
console.log(instance instanceof Child); //true
console.log(instance instanceof Father); //true
console.log(instance instanceof Object); //true
console.log(Child instanceof Father); //false
console.log(Child.prototype instanceof Father); //true
然而,这种方式存在明显的局限性。
首先,子类的所有实例会共享父类的公有引用类型属性。
其次,这种方式无法向父类的构造函数传递参数进行初始化。
function Father(){
this.companies =['bigo','yy','uc']
}
funtion Child(){}
Child.prototype = new Father();
var instanceA = new Child();
var instanceB = new Child();
console.log(instanceB.companies); //['bigo','yy','uc']
instanceA.companies.push('nemo');
console.log(instanceB.companies); //['bigo','yy','uc','nemo']
构造函数继承
构造函数继承(也称伪造对象或经典继承)是另一种实现路径。
//声明父类
function Father(val){
this.companies =['bigo','yy','uc']
this.val = val;
}
//声明父类原型方法
Father.prototype.getCom = function(){
console.log(this.companies);
}
//声明子类
function Child(val){
//继承
Father.call(this,val);
}
var instanceA = new Child('childA');
var instanceB = new Child('childB');
instanceA.companies.push('nemo');
console.log(instanceA.companies); //['bigo','yy','uc','nemo']
console.log(instanceA.val); //childA
console.log(instanceB.companies); //['bigo','yy','uc']
console.log(instanceB.val); //childB
其原理是在子类构造函数内部通过 call 方法调用父类构造函数,将子类中的变量在父类作用域中执行一遍,并将 this 绑定到子类实例上,从而继承父类的公有属性。
这种继承方式也有其不足之处。
由于它没有涉及原型链(prototype)的设计,父类定义在原型上的方法无法被子类继承。若希望子类能使用这些方法,必须将它们定义在构造函数内部,但这会导致每个实例都创建一份独立的方法副本,无法实现方法复用。
组合继承
组合继承结合了类式继承和构造函数继承的优点。
//声明父类
function Father(val){
this.companies =['bigo','yy','uc']
this.val = val;
}
//声明父类原型方法
Father.prototype.getValue = function(){
console.log(this.val);
}
//声明子类
function Child(val,newVal){
//构造函数式继承
Father.call(this,val);
this.newVal = newVal;
}
//类式继承
Child.prototype = new Father();
//声明子类原型方法
Child.prototype.getNewValue = function(){
console.log(this.newVal);
}
var instanceA = new Child("fatherA","childA");
instanceA.companies.push('nemo');
console.log(instanceA.companies); //['bigo','yy','uc','nemo']
instanceA.getValue(); //fatherA
instanceA.getNewValue(); //childA
var instanceB = new Child("fatherB","childB");
console.log(instanceA.companies); //['bigo','yy','uc']
instanceB.getValue(); //fatherB
instanceB.getNewValue(); //childB
但这种模式存在一个主要缺陷。
在执行构造函数继承时,父类构造函数被调用了一次;而在实现子类原型的类式继承时,通过 new 父类再次调用了一次父类构造函数。这导致父类构造函数被执行了两次,造成了一定的性能浪费。
原型式与寄生式继承
原型式继承可以看作是类式继承的一种封装形式。
function inheritObject(obj){
function F(){};
F.prototype = obj;
return new F();
}
var situation = {
companies:['bigo','yy','uc'];
area:'guangzhou';
}
var situationA = inheritObject(situation);
situationA.area = 'shenzhen';
situationA.companies.push('tencent');
var situationB = inheritObject(situation);
situationB.area = 'beijing';
situationB.companies.push('baidu');
console.log(situationA.area); //shenzhen
console.log(situationA.companies); //['bigo','yy','uc','tencent','baidu']
console.log(situationB.area); //beijing
console.log(situationB.companies); //['bigo','yy','uc','tencent','baidu']
console.log(situation.area); //guangzhou
console.log(situation.companies); //['bigo','yy','uc','tencent','baidu']
它利用一个过渡对象作为中间层,类似于类式继承中的子类角色,最终返回一个新的实例化对象。
其缺点与类式继承类似。
父类的公有引用属性依然会被所有实例共享,存在数据污染的风险。
寄生式继承是在原型式继承的基础上进行的增强。
function inheritObject(obj){
function F(){};
F.prototype = obj;
return new F();
}
var situation = {
companies:['bigo','yy','uc'];
area:'guangzhou';
}
function createSituation(obj){
//通过原型继承创建新对象
var newObj = new inheritObject(obj);
//定义新对象方法
newObj.getArea = function(){
console.log(newObj.area)
}
//返回对象
return obj;
}
它在返回对象前添加了一些新属性和方法,但本质上仍保留了原型式继承的缺点,即共享引用属性问题。
寄生式组合继承
寄生式组合继承被认为是解决继承问题的最佳模式。
function inheritObject(obj){
function F(){};
F.prototype = obj;
return new F();
}
//传递参数 child,parent 子类父类
function inheritPrototype(child,parent){
//复制一份父类的原型副本保存在变量中;
var fatherProto = inheritObject(father.prototype);
//修正因为重写子类原型导致子类的constructor属性被修改;
fatherProto.constructor = child;
//设置子类的原型
child.prototype = fatherProto;
}
//声明父类
function Father(val){
this.companies =['bigo','yy','uc']
this.val = val;
}
//声明父类原型方法
Father.prototype.getValue = function(){
console.log(this.val);
}
//声明子类
function Child(val,newVal){
//构造函数式继承
Father.call(this,val);
this.newVal = newVal;
}
//类式继承
Child.prototype = new Father();
inheritPrototype(Child,Father);
//声明子类原型方法
Child.prototype.getNewValue = function(){
console.log(this.newVal);
}
其实现思路首先通过构造函数继承调用父类构造函数,此时已继承了实例属性,但还缺少原型的副本。
接着通过原型式继承获取父类原型的副本,但此时该副本的 constructor 属性需要重新指向子类。
最后,将这个修正后的原型副本 fatherProto 赋值给子类的 prototype 属性。
总结来说,这种方式既保留了构造函数继承对实例属性的继承,又通过原型链实现了方法的共享,同时避免了组合继承中父类构造函数被调用两次的问题。其核心改进在于将子类的 prototype 指向了父类原型的一个引用副本,而非父类的实例。
var instanceA = new Child("fatherA","childA");
instanceA.companies.push('nemo');
console.log(instanceA.companies); //['bigo','yy','uc','nemo']
instanceA.getValue(); //fatherA
instanceA.getNewValue(); //childA
var instanceB = new Child("fatherB","childB");
console.log(instanceA.companies); //['bigo','yy','uc']
instanceB.getValue(); //fatherB
instanceB.getNewValue(); //childB
需要注意的是,如果子类需要添加新的原型方法,必须通过 prototype 点语法逐个添加,否则直接赋值会覆盖掉已继承的父类原型对象。
ES6 Class 继承语法
ES6 引入了 Class 语法,Class 可以通过 extends 关键字实现继承。相比 ES5 通过修改原型链来实现继承的方式,Class 语法更加清晰且易于使用。
在 Class 中实现继承的核心是使用 extends 关键字指定父类,并且在子类的构造函数中必须调用 super()。
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
这段代码逻辑可以理解为 Parent.call(this, value),即调用父类构造函数并绑定 this。
如果子类没有显式定义 constructor 方法,JavaScript 引擎会默认添加一个。
以上内容涵盖了 JavaScript 继承机制的主要实现方式,希望能为读者提供有益参考。