JavaScript中五种主流继承模式的原理与对比

作者:青柠

时间:2026-10-29

浏览:461

本文深入解析JavaScript中五种主流继承模式,包括原型链、构造函数、组合、寄生组合及直接实例化。通过对比各方式在属性共享、方法复用及内存占用上的差异,重点阐述寄生组合继承为何成为最经典的实现方案,帮助开发者根据实际需求选择最优继承策略。

文章目录

引言

本文通过具体实例,对JavaScript编程中几种常见的继承方式进行深入分析与比较,旨在为读者提供实用的参考与借鉴。

引言

从严格的语言定义来看,JavaScript并非一门纯粹的面向对象语言。这种观点通常源于其弱类型特性与Java或C#等强类型语言在继承机制上的显著差异,从而被误认为是非主流的面向对象实现,甚至有书籍将其定义为“非完全面向对象”语言。然而,继承的具体形式并非核心,关键在于是否具备面向对象的思想。许多认为JavaScript不具备面向对象特性的观点,往往是因为未深入探究其继承机制所致。因此,本文旨在通过详细探讨JavaScript的继承方式,促进技术交流与理解。

JavaScript中实现继承的必要性

为何要在JavaScript中实现继承

早期PC性能有限,服务器承担所有压力,客户端浏览器仅具展示功能。受限于Table布局和拨号上网,网页加载缓慢。如今,硬件性能大幅提升,浏览器处理能力强悍,Web开发模式发生转变:服务端压力减轻,浏览器承担更多任务,分摊计算负载,既降低成本又提升前端开发的趣味性,催生了大量前端框架及MVC架构。在此背景下,JavaScript的角色已超越简单的表单验证、请求发送或DOM操作,更多地承担前端路由和业务逻辑处理。为了实现前台数据的抽离(即Model层),必须运用面向对象思维,而继承机制在此过程中显得尤为关键。

继承场景示例

从简单需求出发

假设我们需要在前端抽离出一个名为Person的Model,它包含name和age基本属性。由于每个人都具备说话能力,我们将say方法定义在原型对象上,供所有实例共享。现在,我们需要定义Man类,它不仅要继承Person的基本属性,还需添加自身特有的属性。


function Person (name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function(){
  console.log('hello, my name is ' + this.name);
};
function Man() {
  //my own properties
}

主流继承方式详解

接下来,我们将介绍几种主流的JavaScript继承实现方式。

原型链继承

1. 原型链继承


function Person (name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function(){
  console.log('hello, my name is ' + this.name);
};
function Man() {
}
Man.prototype = new Person('pursue');
var man1 = new Man();
man1.say(); //hello, my name is pursue
var man2 = new Man();
console.log(man1.say === man2.say);//true
console.log(man1.name === man2.name);//true

这种继承方式直观直接:为了获取Person的所有属性和方法(包括实例属性和原型属性),直接将父类的实例new Person('pursue')赋值给子类的原型。此时,子类的实例man1和man2本身是空对象,所有属性和方法均需通过原型链查找,导致找到的属性和方法均为同一份引用。因此,单纯利用原型链继承在实际应用中并不可行。

构造函数继承

2. 利用构造函数继承


function Person (name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function(){
  console.log('hello, my name is ' + this.name);
};
function Man(name, age) {
  Person.apply(this, arguments);
}
//Man.prototype = new Person('pursue');
var man1 = new Man('joe');
var man2 = new Man('david');
console.log(man1.name === man2.name);//false
man1.say(); //say is not a function

在此方式中,子类在构造函数内部通过apply调用父类构造函数,从而继承父类的属性。相比原型链继承,这种方式更为优越,因为每个实例都拥有独立的资源副本。然而,该方法仅能继承父类的实例属性,无法访问原型上的say方法。为了同时继承父类的所有属性和方法,必须修改原型链,由此引出了组合继承方式。

组合继承

3. 组合继承


function Person (name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function(){
  console.log('hello, my name is ' + this.name);
};
function Man(name, age) {
  Person.apply(this, arguments);
}
Man.prototype = new Person();
var man1 = new Man('joe');
var man2 = new Man('david');
console.log(man1.name === man2.name);//false
console.log(man1.say === man2.say);//true
man1.say(); //hello, my name is joe

需要注意的是,man1和man2的实例属性实际上覆盖了原型上的同名属性,但并未覆盖原型上的say方法(因为原型上原本就没有)。因此,man1.say === man2.say依然返回true。这提示我们在开发中需格外小心那些未被覆盖的原型属性,因为它们是所有实例共享的。

寄生组合继承

4. 寄生组合继承

虽然我不确定这种形式是否正式被称为“寄生组合继承”,但它确实是目前最流行、最经典的JavaScript继承方式。理解其关键在于掌握原型对象的结构:


function Person (name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function(){
  console.log('hello, my name is ' + this.name);
};
function Man(name, age) {
  Person.apply(this, arguments);
}
Man.prototype = Object.create(Person.prototype);//a.
Man.prototype.constructor = Man;//b.
var man1 = new Man('pursue');
var man2 = new Man('joe');
console.log(man1.say == man2.say);
console.log(man1.name == man2.name);

寄生组合继承与组合继承的区别仅在于构造子类原型对象的方式(a和b)。这里使用了Object.create(obj)方法,该方法对传入的obj对象进行浅拷贝,其效果类似于:


function create(obj){
  function T(){};
  T.prototype = obj;
  return new T();
}

因此,方式a能够将子类的原型对象与父类的原型对象进行有效连接,而不会像普通组合继承那样直接对子类原型进行复制(如Man.prototype = new Person();),后者只是粗暴地覆盖属性。寄生组合继承分别处理了实例属性和原型属性的继承,在实现逻辑上更为合理。

注意:代码b虽然不会改变instanceof的运算结果,但在需要使用constructor属性的场景中,这种处理方式更为严谨。

希望本文的阐述能对大家的JavaScript程序设计有所助益。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章