JavaScript 继承机制详解:从构造函数到寄生组合式继承

作者:异步漫步

时间:2026-10-29

浏览:203

本文系统梳理 JavaScript 中模拟继承的四种核心方式:构造函数借用、原型链继承、组合继承及寄生组合式继承。深入分析各方法的优缺点,如属性共享、参数传递及构造函数调用次数等问题,并探讨利用 call/apply 改变上下文及浅深拷贝实现属性复制的技术细节,帮助开发者根据实际需求选择最优继承方案。

文章目录

JavaScript 继承概述与核心用途

阅读了大量关于 JavaScript 继承的资料后,有必要进行系统性的总结。在此先阐述个人的理解,若有疏漏之处,恳请指正,以便纠正认知偏差。此外需说明,文中示例并非完全原创,部分仅修改了变量名,部分直接引用自其他来源。

JavaScript 继承的核心用途是什么?

首先明确,JavaScript 并不具备传统面向对象语言中严格的继承概念,其所谓的“继承”实质上是对继承行为的模拟。早期前端开发中,这类知识多用于应对面试(因面试官常问此题),实际开发中可能并未深入使用。但随着经验积累,即便概念模糊,实践中也难免涉及。继承的真正价值在于代码复用:当需要基于已有功能、对象或第三方库进行扩展时,直接继承即可,无需修改原有代码,这才是继承的本质意义。

JavaScript 实现继承的主要途径

JavaScript 如何实现继承? 暂不展示代码,先理清思路。继承的本质是将其他对象(JavaScript 中万物皆对象)的属性或方法转移到当前对象,使其可用,从而实现复用。明确目标后,具体实现手段便清晰了。基于 JavaScript 的特性,实现方式主要涵盖以下几种,可单独或组合使用。

1、构造函数法:JavaScript 虽无严格的构造函数定义,但可通过 new 关键字创建新对象。既然严格面向对象语言利用构造函数实现继承,JavaScript 自然可以模拟这一机制。因此,熟悉面向对象编程(OOP)的开发者通常会首先想到这种方法。

2、利用函数原型:通过原型链将两个对象链接起来。由于 JavaScript 的原型链机制较为独特,这种方法也易于理解。原型继承有多种形式,例如将什么作为继承对象的原型:被继承对象的原型、被继承对象的实例,或直接是被继承者。选择不同的原型作为继承基础,所得的继承效果各不相同。

3、复制属性和方法:将被继承对象的属性或方法全部克隆,转化为当前对象的属性和方法,从而顺畅使用。需注意,复制分为浅复制和深复制两种情况。

4、利用 call 和 apply 方法:这两个方法功能强大,可改变函数执行的上下文(this)。因此,借助它们也能实现对被继承对象方法的继承与复用。 综上所述,JavaScript 实现继承的途径大致如上。千变万化的实现方式均基于这几种方法组合、升级和完善。为何多数情况需组合使用?因为单一方法实现的效果往往不尽如人意。当然,具体项目中可根据实际需求选择合适方式,只要满足需求即可,并无强制规定。这就好比从北京去石家庄,飞机虽快,但若机场偏远,算上往返机场及市区的时间,高铁可能更优;若有车可开车,若想挑战还可骑自行车,一切视实际情况而定。

代码实现:下面结合具体代码阐述上述几种实现方法,部分示例源自他处,并添加了注释说明。

阅读了大量关于 JavaScript 继承的资料后,有必要进行系统性的总结。在此先阐述个人的理解,若有疏漏之处,恳请指正,以便纠正认知偏差。此外需说明,文中示例并非完全原创,部分仅修改了变量名,部分直接引用自其他来源。

JavaScript 继承的核心用途是什么?

首先明确,JavaScript 并不具备传统面向对象语言中严格的继承概念,其所谓的“继承”实质上是对继承行为的模拟。早期前端开发中,这类知识多用于应对面试(因面试官常问此题),实际开发中可能并未深入使用。但随着经验积累,即便概念模糊,实践中也难免涉及。继承的真正价值在于代码复用:当需要基于已有功能、对象或第三方库进行扩展时,直接继承即可,无需修改原有代码,这才是继承的本质意义。

JavaScript 如何实现继承? 暂不展示代码,先理清思路。继承的本质是将其他对象(JavaScript 中万物皆对象)的属性或方法转移到当前对象,使其可用,从而实现复用。明确目标后,具体实现手段便清晰了。基于 JavaScript 的特性,实现方式主要涵盖以下几种,可单独或组合使用。

1、构造函数法:JavaScript 虽无严格的构造函数定义,但可通过 new 关键字创建新对象。既然严格面向对象语言利用构造函数实现继承,JavaScript 自然可以模拟这一机制。因此,熟悉面向对象编程(OOP)的开发者通常会首先想到这种方法。

2、利用函数原型:通过原型链将两个对象链接起来。由于 JavaScript 的原型链机制较为独特,这种方法也易于理解。原型继承有多种形式,例如将什么作为继承对象的原型:被继承对象的原型、被继承对象的实例,或直接是被继承者。选择不同的原型作为继承基础,所得的继承效果各不相同。

3、复制属性和方法:将被继承对象的属性或方法全部克隆,转化为当前对象的属性和方法,从而顺畅使用。需注意,复制分为浅复制和深复制两种情况。

4、利用 call 和 apply 方法:这两个方法功能强大,可改变函数执行的上下文(this)。因此,借助它们也能实现对被继承对象方法的继承与复用。 综上所述,JavaScript 实现继承的途径大致如上。千变万化的实现方式均基于这几种方法组合、升级和完善。为何多数情况需组合使用?因为单一方法实现的效果往往不尽如人意。当然,具体项目中可根据实际需求选择合适方式,只要满足需求即可,并无强制规定。这就好比从北京去石家庄,飞机虽快,但若机场偏远,算上往返机场及市区的时间,高铁可能更优;若有车可开车,若想挑战还可骑自行车,一切视实际情况而定。

代码实现:下面结合具体代码阐述上述几种实现方法,部分示例源自他处,并添加了注释说明。

构造函数实现(借用构造函数)

一、构造函数实现(借用构造函数):


function Super(arg){
      this.arr1 = "I'm super "+arg;
      this.show = function(){
        alert(this.arr1);
      }
    }

    Super.prototype.say = function(){
      alert(this.arr1);
    }

    function suber(arg){
      Super.apply(this, arguments); //在suber的上下文中运行super
    }

    var sub =new suber("suber");
    var sub2 = new suber("suber1");

    console.log(sub.arr1); //I'm super suber
    console.log(sub.show); //function (){ alert(this.arr1);}
    console.log(sub.say); //undefined
    console.log(sub.show === sub2.show); //false

注意,发现 sub.say 为 undefined,说明该方法未被继承。此外,sub 和 sub2 的 show 属性不相等,表明两个函数指向不同的对象,即方法被复制了两份。

因此,仅使用此方法实现继承存在缺陷:原型对象上的属性和方法未被继承,而 Super 上的属性则为每个通过 new 创建的对象分别复制一份。可见,单独使用该方法并不妥当,因为原型上的方法未被继承。于是,开发者提出了原型继承的概念。

原型继承

二、原型继承:


function Super(arg){
      this.arr1 = "I'm super "+arg;
      this.show = function(){
        alert(this.arr1);
      }
    }

    Super.prototype.say = function(){
      alert(this.arr1);
    }

    function suber(arg){}

    suber.prototype = new Super();

    var sub = new suber("suber1");
    var sub2 = new suber("suber2");

    console.log(sub.arr1); //I'm super undefined
    console.log(sub.show); //function (){ alert(this.arr1);}
    console.log(sub.say); //function (){ alert(this.arr1);}
    console.log(sub.show === sub2.show);  //true;
    console.log(sub.say === sub2.say);  //true;


此次 arr1 成功继承,但参数未添加,结果为 undefined。因此,在子类声明时传入的参数,父类继承过来的属性无法接收。其他方面表现正常,show 和 say 均被继承。但需注意,say 是通过 Super 的原型对象继承而来,而 show 是创建 Super 对象实例时的实例属性。

那么,如何实现既能传递参数又能继承原型内容呢?将上述两种方法组合即可。于是,前辈们发明了以下方法:

组合继承(借用构造函数并设置原型)

三、组合继承(借用构造函数并设置原型):


function Super(arg){
      this.arr1 = "I'm super "+arg;
      this.show = function(){
        alert(this.arr1);
      }
    }

    Super.prototype.say = function(){
      alert(this.arr1);
    }

    function suber(arg){
      Super.apply(this, arguments);
    }

    suber.prototype = new Super();


    var sub = new suber("suber1");
    var sub2 = new suber("suber2");

    console.log(sub.arr1); //I'm super suber1
    console.log(sub.show); //function (){ alert(this.arr1);}
    console.log(sub.say); //function (){ alert(this.arr1);}
    console.log(sub.show === sub2.show);  //false;
    console.log(sub.say === sub2.say);  //true;

此次近乎完美,但 sub.show 与 sub2.show 并不相等。原因在于 apply 调用使 show 成为 suber 的自身属性,从而覆盖了 suber 原型中的 show(即 Super 作为 suber 原型对象实例对象的 show),导致每次复制一份。当然,此处无法避免。为减少此类多余开销,可将此类可共用函数放置于原型对象中。 由于 suber 构造函数内调用及为 suber 原型对象赋值时均调用了 Super,导致 Super 被调用两次。每次 new suber 对象时,Super 被执行两次,产生两个实例对象,消耗额外资源。

为解决此问题,前辈们再次创新,开发出以下方法:

寄生组合式继承

四、寄生组合继承: 该方法与方法三的主要区别在于,将父类原型赋给子类原型,而非父类实例。具体见示例:

 


function Super(arg){
  this.arr1 = "I'm super "+arg;
  
}

Super.prototype.show = function(){ //这个方法放到了原型对象上。
    alert(this.arr1);
  }
Super.prototype.say = function(){
  alert(this.arr1);
}

function suber(arg){
  Super.apply(this, arguments);
}

/*inherit函数的作用,使用一个新的空函数,来切断父类对象的原型对象与子类原型对象的直接联系,而是通过这个空构造的实例对象实现继承,这样可以避免更改子类原型的属性或者方法而影响了父类原型对象的属性或者方法。*/

function inherit(obj){ 
  function F(){}
  F.prototype = obj;
  return new F();  
}

suber.prototype = inherit(Super.prototype);


var sub = new suber("suber1");
var sub2 = new suber("suber2");

console.log(sub.arr1); //I'm super suber1
console.log(sub.show); //function (){ alert(this.arr1);}
console.log(sub.say); //function (){ alert(this.arr1);}
console.log(sub.show === sub2.show);  //true;
console.log(sub.say === sub2.say);  //true;

如此便消除了方法三的弊端,此法是否已完美适用?

复制属性实现与深复制

五、复制属性实现

我们可以编写一个拷贝函数来实现。


function extend(Super,suber){
  suber = suber || {};
  for(var i in Super){
    if(Super.hasOwnProperty(i)){
      suber[i] = Super[i];
    }
   }
  return suber;
}


var parent = {
  name:"dad",
  num:[1,2,3],
  say:function(){alert("dad");}
}

var child = {
  age:5,
  sex:"boy"
};

child = extend(parent, child);

//以下测试
console.log(child); /*{
             age:5,
             sex:"boy",
             name:"dad",
             num:[1,2,3],
             say:function(){alert("dad");}
            }*/
console.log(child.say === parent.say); //true
console.log(child.num === parent.num); //true

复制成功,child 成功继承了 parent 的部分属性。但后续两个测试结果显示它们相等,表明它们共用同一个数组和函数。函数共用尚可接受,但数组共用则存在问题:若一个 child 修改数组,其他被继承对象的数组也会随之改变,这显然不可取。 为何发生这种情况?JavaScript 中对象存储的是指针,指针指向值。我们复制的实际上是指向该对象的指针值,因此继承对象和被继承对象指向同一对象。接下来探讨如何使用深复制解决此问题。

深度复制对象属性:


function extenddeep(Super, suber){
      var tostr = Object.prototype.toString, astr = "[object Array]";
      suber = suber || {};

      for(var i in Super){
        if(Super.hasOwnProperty(i)){
          if(typeof Super[i] === "object"){
            suber[i] = (tostr.call(Super[i]) == astr) ? [] : {};
            extenddeep(Super[i],suber[i]);
          }else {
            suber[i] = Super[i];
          }
        }
      }

      return suber;
    }


    var parent = {
      name:"papa",
      num:[1,2,3],
      say:function(){alert("I'm father of my son!");}
    }

    var child = {
      name:"jone",
      sex:"boy",
    }

    var kid = extenddeep(parent, child);

    console.log(kid);  // {name: "papa"
                num: Array[3]
                say: ()
                sex: "boy"
              // }

    console.log(kid.say === parent.say); //true
    console.log(kid.num === parent.num);  //false
    console.log(kid.name); //papa

深复制完成后,是否发现问题?name 属于 parent,即若继承对象拥有与被继承对象同名的属性且未做处理,将被替换。因此,可先声明一个属性保存 parent 中的内容,其余为 child 自身内容,最后再将属性赋给 child 对象。

利用 call 和 apply 借用方法

六、利用 call 和 apply 这两个方法(借用方法)。 此法通过 call 和 apply 复用其他对象的方法,达到复用目的。


var one = {
      name:"object",
      say: function(greet){
        return greet + ', ' + this.name;
      }
    }

    var tow = {
      name:"two"
    }

    one.say.call(tow, "hi");  //hi, two

这就是借用方式,讲解完毕。

借用方法的上下文问题与 jQuery 源码分析

实际上,此处还有其他内容值得关注。借用并不意味着可以随意将某方法赋值给谁后继续正常使用。因此,平时使用借用时需关注上下文。下面查看易出错之处。


//赋值给一个变量时候上下文会变化
    var say = one.say;
    console.log(say('hoho')); // "hoho, undefined"

    //作为回调函数时也会发生变化
    var yetother = {
      name:"yetother obj",
      method:function(callback){
        return callback("Hola");
      }
    }

    console.log(yetother.method(one.say)); //"Hola, "

何意?即 this.name 为 undefined。当 one.say 赋值给 say 时,say 实际存储了指向函数对象的指针,而 say 变量显然是全局变量的一个属性,因此其运行时的实际上下文变为 window,此时 name 变为 undefined。回调同理,return 的是函数运行结果。若预先设置 window.name="windows",则结果将变为 "hoho, windows" 和 "Hola, windows"。


function bind(o, m){
      return function(){
        return m.apply(o, [].slice.call(arguments));
      }
    }

    var othersay = bind(yetother, one.say);

    othersay("Hola"); //"Hola, yetother obj"

通过 apply 可改变方法执行的上下文。因此,我们构建一个函数来实现此功能,通过方法调用实现上下文改变,从而避免上下文非期望值的情况。


//这段是直接复制过来的。
    // ECMAScript 5给Function.prototype添加了一个bind()方法,以便很容易使用apply()和call()。

      if (typeof Function.prototype.bind === 'undefined') {
        Function.prototype.bind = function (thisArg) {
          var fn = this,
      slice = Array.prototype.slice,
      args = slice.call(arguments, 1);
          return function () {
            return fn.apply(thisArg, args.concat(slice.call(arguments)));
          };
        };
      }

      var twosay2 = one.say.bind(two);
      console.log(twosay2('Bonjour')); // "Bonjour, another object"

      var twosay3 = one.say.bind(two, 'Enchanté');
      console.log(twosay3()); // "Enchanté, another object"

介绍完毕,现提出个人疑惑:当复制属性方法时,若被继承对象内部存在方法,如何单独复制出来?目前做法是直接共用,因方法通常不常修改。求解答?

以下为转载的 jQuery extend 方法源码,似乎也未对函数做特殊处理,继承后两个函数依然共用。

$.extend 源码


jQuery.extend = jQuery.fn.extend = function() {
          var options, name, src, copy, copyIsArray, clone,
             target = arguments[0] || {},
             i = 1,
             length = arguments.length,
             deep = false ;

          // Handle a deep copy situation
          //如果第一个参数是boolean类型
          //修正参数,将第二个参数作为target
          if ( typeof target === "boolean" ) {
             deep = target;

              // skip the boolean and the target
             target = arguments[ i ] || {};
              //i++是为了后续 i === length的判断
             i++;
          }

          // Handle case when target is a string or something (possible in deep copy)
          //如果目标既不是对象也不是方法(例如给基本类型扩展属性方法和属性不会报错但是是无用的),修正target为 js对象
          if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
             target = {};
          }

          // extend jQuery itself if only one argument is passed
          //如果只有一个参数,修正对象为JQuery函数或JQuery对象
          if ( i === length ) {
             target = this ;
              //修正target所在位置,后面的都是要添加给target的对象
             i--;
          }

          for ( ; i < length; i++ ) {
              // Only deal with non-null/undefined values
              if ( (options = arguments[ i ]) != null ) {
                 // Extend the base object
                 for ( name in options ) {
                    src = target[ name ];
                    copy = options[ name ];

                    // Prevent never-ending loop
                    //如果target和copy是同一个对象,略过,防止自己的属性引用了本身对象导致的循环引用,以致GC无法回收
                    if ( target === copy ) {
                        continue ;
                    }

                    // Recurse if we're merging plain objects or arrays
                    //如果是deep为true,并且要添加给target的copy为对象获数组
                    if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
                        if ( copyIsArray ) {
                           copyIsArray = false ;
                           clone = src && jQuery.isArray(src) ? src : [];

                       } else {
                           clone = src && jQuery.isPlainObject(src) ? src : {};
                       }
                 
                        // Never move original objects, clone them
                        //很巧妙 ,用一个递归,实现引用对象的深克隆,递归的返回条件是属性石基本类型,基本类型都是深克隆
                       target[ name ] = jQuery.extend( deep, clone, copy );

                    // Don't bring in undefined values
                    } else if ( copy !== undefined ) {
                        //浅克隆
                       target[ name ] = copy;
                    }
                 }
             }
          }

          // Return the modified object
          return target;
      };

以上关于 JavaScript 继承学习心得的总结即为小编分享的全部内容,希望能对大家有所参考,也希望大家多多支持本站。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章