JavaScript面向对象编程:五种继承方式详解

作者:纸飞机

时间:2026-10-23

浏览:244

本文详细解析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程序设计有所帮助。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章