JavaScript类式继承的完整实现与原型链优化
本文深入解析JavaScript类式继承的实现机制。通过原型链绑定与构造函数调用,解决子类对父类成员的访问问题。重点讨论了直接赋值原型导致的耦合风险,并对比了实例化与空函数中转两种优化方案,最终提供可复用的extend工具函数,确保继承关系的正确性与安全性。
继承的目的与JavaScript的灵活性
在编写代码前,需明确继承的目的与收益。设计类时,通常旨在减少重复代码并降低类间耦合。然而,同时兼顾这两点颇具挑战,需根据具体场景选择策略。传统面向对象语言中,继承往往导致强耦合;而JavaScript凭借灵活性,既可设计强耦合也可设计弱耦合,既可实现高效也可实现低效代码,具体方案视情况而定。
本文介绍JavaScript实现继承的三种方式:类式继承、原型继承和掺元类。此处先简述类式继承,其余两种将在后续文章中介绍,欢迎持续关注与指导。
类式继承概述。
原型链机制与类式继承基础
JavaScript类式继承基于原型链实现。原型链是什么?JavaScript对象拥有一个prototype属性,该属性返回对象类型的引用,用于提供该类的一组基本功能。
对prototype属性或许已有印象,常见的用法如下。
代码如下:
var Person = function(){
this.name = "liyatang";
};
Person.prototype = {
//可以在这里提供Person的基本功能
getName : function(){
return this.name;
}
}
我们将类的基本功能置于prototype属性中,表明Person对象引用具备XXX功能。
理解原型后,需进一步理解原型链。访问对象的成员(属性或方法)时,若当前对象中未找到该成员,JavaScript会在prototype属性指向的对象中查找;若仍未找到,则继续向下一级prototype指向的对象查找,直至找到。若全程未找到,则返回undefined。
原型链提供了什么启示?不难联想到,让一个类继承另一个类,只需将子类的prototype设置为指向父类的一个实例。这将父类成员绑定到子类上,因为在子类中查找不到某成员时,会向父类中查找。(注:上述描述用词可能不够严谨,旨在通俗易懂地解释原理)。
类式继承的实现与潜在问题
接下来,我们需要一个Chinese类,继承Person类的name和getName成员。
代码如下:
var Chinese = function(name, nation){
//继承,需要调用父类的构造函数,可以用call调用,this指向Chinese
//使Person在此作用域上,才可以调用Person的成员
Person.call(this,name);
this.nation = nation;
};
Chinese.prototype = Person.prototype;
//这里不可和以前一样,因为覆盖掉了prototype属性
//Chinese.prototype = {
// getNation : function(){
// return this.nation;
// }
//};
//以后的方法都需要这样加
Chinese.prototype.getNation = function(){
return this.nation;
};
继承关系建立后,我们可以这样调用它。
代码如下:
var c = new Chinese("liyatang","China");
alert(c.getName());// liyatang
于是类式继承看似已完成。但真的完成了吗?在alert处设置断点使用Firebug调试,会发现原Person.prototype被修改,添加了getNation方法。
这是因为上述代码中Chinese.prototype = Person.prototype;属于引用类型,修改Chinese的同时也修改了Person。这种强耦合性是不可容忍的,也非我们期望的效果。
弱化耦合的两种优化方案
我们可以另起一个对象或实例化一个实例来弱化耦合性。
代码如下:
//第一种
//Chinese.prototype = new Person();
//第二种
//var F = function(){};
//F.prototype = Person.prototype;
//Chinese.prototype = F.prototype;
这两种方法有何区别?第二种方法中增加了一个空函数F,这样做避免了创建父类的一个实例。因为父类可能较为庞大,且父类构造函数可能存在副作用或执行大量计算任务。因此,力荐第二种方法。
构造函数指向的修正
到此就结束了吗?并未!在对象属性prototype下还有一个constructor属性,它保存了对构造特定对象实例的函数的引用。据此说法,Chinese.prototype.constructor应等于Chinese,但实际上并非如此。
回顾之前设置Chinese原型链时,我们覆盖了Chinese.prototype为Person.prototype。因此,此时Chinese.prototype.constructor指向Person。我们需要添加以下代码进行修正。
代码如下:
//对这里的if条件不需要细究,知道Chinese.prototype.constructor = Chinese就行
if(Chinese.prototype.constructor == Object.prototype.constructor){
Chinese.prototype.constructor = Chinese;
}
整理全部代码如下。
代码如下:
var Person = function(name){
this.name = name;
};
Person.prototype = {
getName : function(){
return this.name;
}
};
var Chinese = function(name, nation){
Person.call(this,name);
this.nation = nation;
};
var F = function(){};
F.prototype = Person.prototype;
Chinese.prototype = F.prototype;
if(Chinese.prototype.constructor == Object.prototype.constructor){
Chinese.prototype.constructor = Chinese;
}
Chinese.prototype.getNation = function(){
return this.nation;
};
var c = new Chinese("liyatang","China");
alert(c.getName());
封装可复用的继承工具函数
若可将继承代码封装在函数中以方便复用,最终整理代码如下。
代码如下:
function extend(subClass,superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype; //加多了个属性指向父类本身以便调用父类函数
if(superClass.prototype.constructor == Object.prototype.constructor){
superClass.prototype.constructor = superClass;
}
}
var Person = function(name){
this.name = name;
};
Person.prototype = {
getName : function(){
return this.name;
}
};
var Chinese = function(name, nation){
Person.call(this,name);
this.nation = nation;
};
extend(Chinese, Person);
Chinese.prototype.getNation = function(){
return this.nation;
};
var c = new Chinese("liyatang","China");
alert(c.getName());
关于继承方案的进一步思考
发表后修改说明:
在一楼评论的启发下,我对extend函数有了新的看法。此前在讨论如何设置原型链时提出了两种方法。
代码如下:
//第一种
//Chinese.prototype = new Person();
//第二种
//var F = function(){};
//F.prototype = Person.prototype;
//Chinese.prototype = F.prototype;
虽然第二种方法减少了调用父类构造函数的路径,但在设计Chinese类时使用了Person.call(this,name);这里同样相当于调用了父类的构造函数。
然而,若使用第一种方法,可以减少在Chinese中再写Person.call(this,name);这部分代码在子类中往往容易被遗忘。不妨将这种功能代码放入extend中,只需写Chinese.prototype = new Person();也能达到同样的目的:耦合不强。
但忽略的一点是,Chinese.prototype = new Person();这样写是否正确?答案是否定的!很明显new Person()需要传递一个name参数。我们不可能在extend函数中处理这部分工作,只好在Chinese类中调用父类的构造函数了。这也符合面向对象的设计思路。
然而常被忽略的一点在于,Chinese.prototype = new Person(); 这种写法是否正确?答案是否定的!因为显然 new Person() 需要接收一个 name 参数。我们不可能在 extend 函数内部处理这部分工作,因此只能在 Chinese 类中调用父类的构造函数。这也符合面向对象的设计思想。
这是我第一次撰写此类技术文章,基本按个人思路展开,难免存在考虑不周或解释不清之处,欢迎留言反馈,谢谢。
复制代码
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
