JavaScript面向对象编程:五种继承方式详解
本文详细解析JavaScript中实现对象继承的五种核心方法。涵盖构造函数绑定、prototype模式、直接继承prototype、利用空对象中介以及拷贝继承(浅拷贝与深拷贝)。通过动物与猫、中国人与医生的实例,对比各方法的优缺点,重点讲解constructor属性修正及浅拷贝导致的引用篡改问题,帮助开发者掌握不同场景下的最佳实践。
本文通过具体实例解析JavaScript面向对象编程中的继承操作,供读者参考学习。
构造函数的继承
在前一篇关于JS面向对象编程封装的文章中,主要探讨了如何封装数据与方法,以及如何基于原型对象生成实例。
本文将重点介绍实现对象之间“继承”的五种具体方法。
假设现在存在一个名为“动物”的构造函数。
function Animal(){
this.species = "动物";
this.action="吃"
}
同时还有一个名为“猫”的构造函数。
function Cat(name,color){
this.name = name;
this.color = color;
}
接下来探讨如何使“猫”继承“动物”的特性。
第一种方法:构造函数绑定
这是最简单且常用的一种方法,通过调用call或apply方法,将父对象的构造函数绑定到子对象上。具体操作是在子对象的构造函数内部添加一行代码:
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黄色");
console.log(cat1.species); // 动物
console.log(cat1.action); // 吃
第二种方法:Prototype模式
这是更为常见的一种实现方式,利用prototype属性。
如果将“猫”的prototype对象指向一个Animal的实例,那么所有由Cat生成的“猫”实例都能继承Animal的属性。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
console.log(cat1.species); // 动物
console.log(cat1.action); // 吃
代码的第一行作用是将Cat的prototype对象指向一个Animal的实例。
Cat.prototype = new Animal();
这行代码实际上完全删除了prototype对象原有的值,并赋予其新值。那么第二行的含义是什么呢?
Cat.prototype.constructor = Cat;
任何prototype对象都包含一个constructor属性,指向其对应的构造函数。若没有“Cat.prototype = new Animal();”这一行,Cat.prototype.constructor原本指向Cat;但执行该行后,Cat.prototype.constructor将指向Animal。
console.log(Cat.prototype.constructor == Animal); //true
此外,每个实例也拥有一个constructor属性,默认调用prototype对象的constructor属性。
console.log(cat1.constructor == Cat.prototype.constructor); // true
因此,在执行“Cat.prototype = new Animal();”后,cat1.constructor也会指向Animal!
console.log(cat1.constructor == Animal); // true
这会导致继承链混乱(因为cat1明明是由构造函数Cat生成的),因此必须手动纠正,将Cat.prototype对象的constructor值重新设为Cat。这就是第二行代码的作用。
这是一个非常重要的编程规范,后续所有示例均遵循此原则:一旦替换了prototype对象。
o.prototype = {};
下一步必然是为新的prototype对象添加constructor属性,并将其指回原来的构造函数。
o.prototype.constructor = o;
第三种方法:直接继承prototype(存在浅拷贝缺陷)
这是对第二种方法的改进。由于Animal对象中不变的属性可以直接写入Animal.prototype,因此可以让Cat()跳过调用Animal(),直接继承Animal.prototype。
首先,我们将Animal对象进行如下改写:
function Animal(){ }
Animal.prototype.species = "动物";
接着,将Cat的prototype对象指向Animal的prototype对象,从而完成继承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黄色");
console.log(cat1.species); // 动物
与前一种方法相比,此方法效率更高(无需执行和建立Animal的实例),更节省内存。但其缺点是Cat.prototype和Animal.prototype现在指向同一个对象,对Cat.prototype的任何修改都会反映到Animal.prototype上(即浅拷贝问题)。
因此,上述代码存在潜在问题,请看第二行:
Cat.prototype.constructor = Cat;
这一句实际上也修改了Animal.prototype对象的constructor属性!
console.log(Animal.prototype.constructor); // Cat
第四种方法:利用空对象作为中介(解决浅拷贝问题)
鉴于“直接继承prototype”存在上述缺陷,第四种方法利用一个空对象作为中介。
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F是空对象,几乎不占用内存。此时修改Cat的prototype对象,便不会影响到Animal的prototype对象。
console.log(Animal.prototype.constructor); // Animal
我们将上述逻辑封装为一个函数,以便复用。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
使用时,方法如下:
extend(Cat,Animal);
var cat1 = new Cat("大毛","黄色");
console.log(cat1.species); // 动物
这个extend函数正是YUI库实现继承的方式。
另外,说明函数体最后一行:
Child.uber = Parent.prototype;
其含义是为子对象设置一个uber属性,该属性直接指向父对象的prototype属性(“uber”为德语,意为“向上”或“上一层”)。这在子对象上打开了一条通道,可直接调用父对象的方法。此行代码旨在实现继承的完备性,属于备用性质。
第五种方法:拷贝继承(解决浅拷贝问题)
上述方法均基于prototype对象实现继承。我们也可以换一种思路,纯粹采用“拷贝”方法实现继承。简言之,将父对象的所有属性和方法拷贝到子对象中,同样可以实现继承。这便是第五种方法。
首先,将Animal的所有不变属性放置在其prototype对象上。
function Animal(){}
Animal.prototype.species = "动物";
接着,编写一个函数来实现属性拷贝的目的。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
该函数的作用是将父对象prototype对象中的属性,逐一拷贝给Child对象的prototype对象。
使用时,写法如下:
extend2(Cat, Animal);
var cat1 = new Cat("大毛","黄色");
console.log(cat1.species); // 动物
非构造函数继承
一、什么是“非构造函数”的继承?
假设现在有一个对象,名为“中国人”。
var Chinese = {
nation:'中国'
};
还有另一个对象,名为“医生”。
var Doctor ={
career:'医生'
}
请问如何使“医生”继承“中国人”,即如何生成一个“中国医生”的对象?
需注意,这两个对象均为普通对象而非构造函数,无法使用构造函数方法实现“继承”。
二、object()方法
JSON格式的发明者Douglas Crockford提出了一种object()函数,可实现此功能。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
该object()函数仅做一件事:将子对象的prototype属性指向父对象,从而使子对象与父对象关联起来。
使用时,第一步先在父对象基础上生成子对象:
var Doctor = object(Chinese);
然后,再添加子对象本身的属性:
Doctor.career = '医生';
此时,子对象已继承父对象的属性。
console.log(Doctor.nation); //中国
三、浅拷贝
除了使用“prototype链”外,还有一种思路:将父对象的属性全部拷贝给子对象,也能实现继承。
以下函数用于执行拷贝操作:
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
使用时,写法如下:
var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
console.log(Doctor.nation); // 中国
然而,这种拷贝存在一个问题。若父对象的属性值为数组或另一个对象,子对象获得的仅是内存地址而非真正的拷贝,这可能导致父对象被意外篡改。
例如,现在给Chinese添加一个“出生地”属性,其值为一个数组。
Chinese.birthPlaces = ['北京','上海','香港'];
通过extendCopy()函数,Doctor继承了Chinese。
var Doctor = extendCopy(Chinese);
随后,我们为Doctor的“出生地”添加一个城市:
Doctor.birthPlaces.push('厦门');
结果如何?Chinese的“出生地”也被修改了!
console.log(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
console.log(Chinese.birthPlaces); //北京, 上海, 香港, 厦门
因此,extendCopy()仅拷贝基本类型数据,这种拷贝被称为“浅拷贝”。这也是早期jQuery实现继承的方式。
四、深拷贝
所谓“深拷贝”,即能实现真正意义上的数组和对象拷贝。其实现并不复杂,只需递归调用“浅拷贝”即可。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
使用时,写法如下:
var Doctor = deepCopy(Chinese);
现在,给父对象添加一个属性,值为数组。随后,在子对象上修改该属性:
Chinese.birthPlaces = ['北京','上海','香港'];
Doctor.birthPlaces.push('厦门');
此时,父对象将不再受到影响。
console.log(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
console.log(Chinese.birthPlaces); //北京, 上海, 香港
目前,jQuery库使用的正是这种继承方法。
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码的运行效果。
更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述内容能对大家的JavaScript程序设计有所帮助。
JS动态构建复杂嵌套JSON对象的方法
2026-11-05 15:41:52
JS负数取余为什么是负数?详解余数与模运算区别
2026-11-05 13:34:41
JS 解压出现乱码或报错怎么解决?
2026-11-05 12:32:38
JS 中 startsWith、indexOf 与正则判断字符串前缀的对比
2026-11-04 22:19:43
JS解析Excel后怎么将数据转换为JSON格式
2026-11-04 20:58:28
