JavaScript面向对象继承详解:类继承、原型继承与混合模式
本文深入解析JavaScript面向对象编程中的继承机制,详细阐述类继承(构造函数继承)、原型继承、实例继承、复制继承、克隆继承及混合继承等多种实现方式。通过对比各模式的优缺点,重点讲解如何利用call/apply、原型链及混合模式解决内存占用、参数传递及多重继承等技术难点,帮助开发者选择最适合的继承方案。
JavaScript继承概述
本文通过具体实例详细解析JavaScript面向对象编程中的三大核心特征之一:继承。内容旨在为读者提供清晰的参考与学习思路。
继承
在JavaScript中,继承的本质在于子类能够获取父类公开的属性与方法。在编程实践中,通常将共性属性与方法封装至父类,而子类则专注于定义独特属性。这种设计能有效减少代码冗余。作为面向对象编程的基石,继承是实现代码重用的关键机制。
本文内容整理自《jQuery 开发从入门到精通》一书,该书讲解详尽,值得推荐购买阅读。
继承的作用
实现继承主要具备以下作用:
① 子类实例能够共享超类的属性与方法。 ② 子类具备覆盖和扩展超类属性与方法的能力。
继承的分类
JavaScript语言本身不直接支持传统的“类”概念,而是通过构造器机制来模拟类的特性。JS中的继承方式多样,主要涵盖:类继承(即构造函数继承)、原型继承、实例继承、复制继承、克隆继承、混合继承以及多重继承等。
类继承(构造函数继承)
类继承
类继承亦称构造函数继承,其核心表现是在子类中执行父类的构造函数。其实现本质例如:将构造函数A的方法赋值给构造函数B,随后调用该方法,使得构造函数A在构造函数B内部执行,从而让构造函数B拥有构造函数A中定义的属性与方法。这便是B类继承A类的过程。具体示例如下:
function A(x){
this.x = x;
this.say = function() {
console.log(this.x + ' say');
}
}
function B(x,y) {
this.m = A; // 把构造函数A作为一个普通函数引给临时方法m()
this.m(x); // 把当前参数作为值传给构造函数A,并且执行
delete this.m; // 清除临时方法
this.y = y;
this.call = function(){
console.log(this.y);
}
}
// 测试类继承
var a = new A(1);
var b = new B(2,3);
a.say(); // 1 say
b.say(); // 2 say
b.call(); // 3
上述实现方式虽巧妙,但设计较为随意,缺乏严谨性。严谨的设计模式需考量各种潜在情况及类继承关系中的耦合性。因此,通常建议将子类自身的方法定义在原型中,此种创建方式更为优化:
function A(x){
this.x = x;
this.say = function() {
console.log(this.x + ' say');
}
}
function B(x,y){
this.y = y;
A.call(this,x); // 在构造函数B内调用超类A,实现绑定,用call来实现借用
}
B.prototype = new A(); // 设置原型链,建立继承关系
B.prototype.constructor = B; // 恢复B的原型对象的构造函数为B,如果不操作就会指向A
B.prototype.gety = function(){ // 给类B添加新的方法
return this.y;
}
// 测试 类继承
var a = new A('A');
var b = new B('Bx','By');
a.say(); // A say
b.say(); // Bx say
console.log(b.gety()); // By
console.log(B.prototype.__proto__ === A.prototype); // true;
console.log(b.constructor === B); // true 这里也可以把B中的 B.prototype.constructor = B; 去除,结果为false
在JavaScript中实现类继承,需重点处理以下三点:
① 在子类构造函数体内,利用函数call()调用父类构造函数,并将子类参数传递进去(如示例中的A.call(this,x)),从而使子类继承父类的所有属性与方法。 ② 在子类和父类之间建立原型链(如示例:B.prototype = new A())。为实现类的继承,必须确保原型链上的上下级关系,即设置子类的prototype属性指向父类的一个实例。 ③ 恢复子类原型对象的构造函数(如示例:B.prototype.constructor = B)。
在类继承中,call()和apply()方法被频繁使用。两者功能与用法基本相同,唯一区别在于第二个参数的类型不同。如需深入了解,可参考前文:https://www.jb51.net/article/166093.htm
此处需说明,类构造函数中的成员通常称为本地成员,而类的原型成员则位于原型中。本文主要探讨原型中成员的继承。本地成员的继承可通过call和apply实现。接下来我们查看类继承中原型成员继承的封装函数:
在函数体内,首先定义一个空函数F(),用于功能中转。将其原型指向父类的原型,并将空函数的实例传递给子类的原型。此举避免了直接实例化父类可能引发的内存问题,因为在实际开发中,父类可能体积庞大,直接实例化会占用大量内存。
// 定义一个继承函数
function extend(Sub,Sup){ // 有两个入口参数,Sub是子类,Sup是父类
var F = function(){}; // 建立一个临时的构造函数
F.prototype = Sup.prototype; // 把临时构造函数的原型指向父类的原型
Sub.prototype = new F(); // 实例化临时类,此处相当于把子类的原型指向父类的实例
Sub.prototype.constructor = Sub; // 恢复子类的构造函数
Sub.sup = Sup.prototype; // 在子类中定义一个本地属性存储超类原型,可避免子类和超类耦合
if(Sup.prototype.constructor === Object.prototype.constructor) { // 检测超类构造器是否为Object构造器
Sup.prototype.constructor = Sup;
}
}
// 下面定义两个类用来测试上面的继承函数
function A(x,y) {
this.x = x;
this.y = y;
}
A.prototype.add = function() {
return (this.x-0) + (this.y-0); // 此处-0 的目的是确保字符串类型可转成数值型
}
A.prototype.minus = function() {
return this.x - this.y;
}
function B(x,y) {
A.apply(this,[x,y]);
}
// 开始实现类继承中的原型成员继承
extend(B,A);
// 为了不与A类中的代码耦合可以单独为B定义一个同名的add
B.prototype.add = function() {
return B.sup.add.call(this); // 避免代码耦合
}
// 测试继承
var b = new B(1,2);
console.log(b.minus()); // -1
console.log(b.add()); // 3
原型继承
原型继承
原型继承是JavaScript中最通用的继承方式。它无需实例化对象,而是通过直接定义对象并被其他对象引用,从而形成继承关系。其中,被引用的对象被称为原型对象。
function A(){
this.color = 'red';
}
function B(){}
function C(){}
B.prototype = new A();
C.prototype = new B();
// 测试原型继承
var c = new C();
console.log(c.color); // red
原型继承显得简单,无需每次构造都调用父类的构造函数,也无需通过复制属性即可快速实现继承。但它也存在一些缺点:
① 每个类型仅有一个原型,因此不支持多重继承。 ② 对多参数或动态参数的父类支持不佳,灵活性不足。 ③ 占用内存较多,每次继承均需实例化一个父类,可能导致内存占用过高。
实例继承
实例继承
实例化类可创建新的实例对象,该实例对象将继承类的所有特征。
function Arr() {
var a = new Array();
return a;
}
var arr = new Arr();
arr[0] = 1;
arr[1] = 2;
console.log(arr); // [1,2]
console.log(Array.isArray(arr)); // true
console.log(arr instanceof Array); // true
console.log(arr instanceof Arr); // false
通过在构造函数中完成对类的实例化操作,然后返回实例对象,这便是实例继承的由来。实例继承可实现对所有对象的继承,包括自定义类、核心对象和DOM对象。但也存在一些缺点:
① 实例继承无法传递动态参数,它在函数体内封闭执行,不能通过call和apply来实现动态传参。 ② 实例继承仅返回一个对象,不支持多重继承。 ③ 实例继承对象仍保持与原对象的实例关系,无法实现继承对象是封装类的实例。例如:console.log(arr instanceof Arr); // false
复制继承
复制继承
复制继承利用for in循环遍历对象成员,逐一复制给另一个对象,以此方式实现继承。
function A(){
this.color = 'red';
}
A.prototype.say = function() {
console.log(this.color);
}
var a = new A();
var b = {};
// 开始拷贝
for(var item in a) {
b[item] = a[item];
}
// 开始测试
console.log(b.color); // red
b.say(); // red.
我们对其进行封装:
Function.prototype.extend = function(obj){
for(item in obj){
this.constructor.prototype[item] = obj[item];
}
}
function A(){
this.color = 'green';
}
A.prototype.say = function(){
console.log(this.color);
}
// 测试
var b = function(){};
b.extend(new A());
b.say(); // green
复制继承实际上是通过反射机制复制类对象中的可枚举属性和方法来模拟继承。这种方式可以实现多继承。但也有缺点:
① 由于依赖反射机制,无法继承非枚举类型的属性和方法。对于系统核心对象的只读方法和属性也无法继承。 ② 执行效率较差,结构越庞大,低效问题越明显。 ③ 若当前类型包含同名成员,这些成员会被父类的动态复制所覆盖。 ④ 在多重继承中,复制继承难以清晰描述父类和子类的相关性。 ⑤ 需在实例化后才能遍历成员,灵活性不足,且不支持动态参数。 ⑥ 复制继承仅是简单的引用赋值,若父类成员包含引用类型,会带来诸多副作用,如不安全、易受污染等。
克隆继承
克隆继承
通过对象克隆方式继承,可避免赋值对象成员带来的低效问题。 为Function对象扩展一个clone方法。该方法可将参数对象赋值给一个空的构造函数的原型对象,然后返回实例化后的对象,从而使该对象拥有构造函数包含的所有成员。
Function.prototype.clone = function(obj){
function Temp(){};
Temp.prototype = obj;
return new Temp();
}
function A(){
this.color = 'purple';
}
var o = Function.clone(new A());
console.log(o.color); // purple
混合继承与多重继承
混合继承
混合继承是将多种继承方式结合使用,发挥各自优势,以实现各种复杂应用。最常见的是将类继承和原型继承结合使用。
function A(x,y){
this.x = x;
this.y = y;
}
A.prototype.add = function(){
return (this.x-0) + (this.y-0);
}
function B(x,y){
A.call(this,x,y);
}
B.prototype = new A();
// 测试
var b = new B(2,1);
console.log(b.x); // 2
console.log(b.add()); // 3
多重继承
继承通常包括单向继承和多向继承。单向继承模式较为简单,每个子类有且仅有一个超类;而多重继承是一个较为复杂的继承模式,一个子类可拥有多个超类。JavaScript原型继承本身不支持多重继承,但可通过混合模式来实现。下面展示让类C来继承类A和类B:
function A(x){
this.x = x;
}
A.prototype.hi = function(){
console.log('hi');
}
function B(y){
this.y = y;
}
B.prototype.hello = function(){
console.log('hello');
}
// 给Function增加extend方法
Function.prototype.extend = function(obj) {
for(var item in obj) {
this.constructor.prototype[item] = obj[item];
}
}
// 在类C内部实现继承
function C(x,y){
A.call(this,x);
B.call(this,y);
};
C.extend(new A(1));
C.extend(new B(2));
// 通过复制继承后,C变成了一个对象,不再是构造函数了,可以直接调用
C.hi(); // hi
C.hello(); // hello
console.log(C.x); // 1
console.log(C.y); // 2
一个类继承另一个类会导致两者之间产生耦合。在JavaScript中,提供多种途径来避免耦合的发生,例如掺元类。
掺元类是一种比较特殊的类,通常不会被实例化或调用。定义掺元类的目的仅是向其他类提供通用的方法。
// 定义一个掺元类
function F(x,y){
this.x = x;
this.y = y;
}
F.prototype = {
getx:function(){
return this.x;
},
gety:function(){
return this.y;
}
}
// 原型拷贝函数
function extend(Sub,Sup){ // Sub 子类 , Sup 掺元类
for(var item in Sup.prototype){
if(!Sub.prototype[item]){ // 如果子类没有存在同名成员
Sub.prototype[item] = Sup.prototype[item]; // 那么复制掺元类成员到子类原型对象中
}
}
}
// 定义两个子类 A,B
function A(x,y){
F.call(this,x,y);
}
function B(x,y){
F.call(this,x,y);
}
// 调用extend函数实现原型属性,方法的拷贝
extend(A,F);
extend(B,F);
console.log(A.prototype);
// 测试继承结果
var a = new A(2,3);
console.log(a.x); // 2
console.log(a.getx()); // 2
console.log(a.gety()); // 3
var b = new B(1,2);
console.log(b.x); // 1
console.log(b.getx()); // 1
console.log(b.gety()); // 2
通过此种方式将多个子类合并到一个子类中,便实现了多重继承。
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
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
