JavaScript 经典类继承框架实现详解

作者:异步漫步时间:2026-10-29 11:52:34浏览:378

本文介绍一个基于原型链的轻量级 JavaScript 类继承框架。通过区分初始化与实例化阶段,实现简单的类定义与单一继承。核心机制包括利用 initializing 标志控制构造函数调用,以及通过闭包动态生成 _super 方法,使子类能安全调用父类被覆写的方法。该方案简洁、无依赖,适合学习 JS 面向对象编程。

JavaScript 类继承框架概述

在编写关于 JavaScript 的未完成书籍期间,我深入研究了 JS 的继承体系,并考察了多种模拟经典类继承的技术方案。在这些方案中,base2 库与 Prototype 库的实现令我印象最为深刻。

基于上述方案的核心思想,我们应当提炼出一个具备简单、可重用、易理解且无外部依赖特性的框架,其中简单性和可用性是设计的重点。以下是该框架的使用示例:


var Person = Class. extend ( {
 init: function (isDancing ) {
  this. dancing = isDancing;
 },
 dance: function ( ) {
  return this. dancing;
 }
} );
var Ninja = Person.extend({
 init: function(){
  this._super( false );
 },
 dance: function(){
  // Call the inherited version of dance()
  return this._super();
 },
 swingSword: function(){
  return true;
 }
});
var p = new Person(true);
p.dance(); // => true
var n = new Ninja();
n.dance(); // => false
n.swingSword(); // => true
// Should all be true
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class

 

在使用该框架时,有几点关键注意事项需要明确:

  • 构造函数必须保持简洁,具体逻辑应封装在 init 函数中实现。
  • 新定义的类必须继承自已有的类。
  • 所有定义的“类”均需继承自始祖类 Class。因此,若要创建一个全新的类,它必须是 Class 的子类。
  • 最具挑战性的一点在于:父类中被覆写的方法必须能够被访问到,这需要通过配置上下文环境来实现。
  • 在上面的示例中,你可以通过调用 this._super() 来访问 Person 父类的 init() 和 dance() 方法。

最终结果令人满意:它使类的定义更加结构化,保持了单一继承的原则,并允许调用超类方法。

框架实现与初始化机制

简单的类创建与继承

以下是其具体实现代码(为了方便阅读并添加了注释),代码量大约为 25 行。欢迎提出建议与反馈。


/* Simple JavaScript Inheritance
 * By John Resig http://ejohn.org/
 * MIT Licensed.
 */
// Inspired by base2 and Prototype
( function ( ) {
 var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
 // The base Class implementation (does nothing)
 this.Class = function(){};
  
 // Create a new Class that inherits from this class
 Class.extend = function(prop) {
  var _super = this.prototype;
   
  // Instantiate a base class (but only create the instance,
  // don't run the init constructor)
  initializing = true;
  var prototype = new this();
  initializing = false;
   
  // Copy the properties over onto the new prototype
  for (var name in prop) {
   // Check if we're overwriting an existing function
   prototype[name] = typeof prop[name] == "function" &&
    typeof _super[name] == "function" && fnTest.test(prop[name]) ?
    (function(name, fn){
     return function() {
      var tmp = this._super;
       
      // Add a new ._super() method that is the same method
      // but on the super-class
      this._super = _super[name];
       
      // The method only need to be bound temporarily, so we
      // remove it when we're done executing
      var ret = fn.apply(this, arguments);    
      this._super = tmp;
       
      return ret;
     };
    })(name, prop[name]) :
    prop[name];
  }
   
  // The dummy class constructor
  function Class() {
   // All construction is actually done in the init method
   if ( !initializing && this.init )
    this.init.apply(this, arguments);
  }
   
  // Populate our constructed prototype object
  Class.prototype = prototype;
   
  // Enforce the constructor to be what we expect
  Class.prototype.constructor = Class;
  // And make this class extendable
  Class.extend = arguments.callee;
   
  return Class;
 };
})();

其中,“初始化(initializing/don't call init)”的处理与“创建 _super 方法”是最为棘手的部分。接下来,我将对此做简要介绍,帮助大家更好地理解其实现机制。

初始化机制

为了说明基于函数原型的继承方式,首先回顾传统的实现过程:将子类的 prototype 属性指向父类的一个实例。如下所示:

 


function Person ( ) { }
function Ninja ( ) { }
Ninja. prototype = new Person ( );
// Allows for instanceof to work:
(new Ninja()) instanceof Person

然而,这里存在一个挑战:我们只希望获得 instanceof 的判断效果,而不希望实例化 Person 并调用其构造函数所带来的副作用。为防止这种情况,代码中设置了一个布尔参数 initializing。只有在实例化父类并将其配置到子类的 prototype 属性时,该值才为 true。这样做的目的是区分真正的实例化与设计继承时的构造函数调用,从而确保在真正实例化对象时调用 init 方法。


if ( !initializing )
 this.init.apply(this, arguments);

特别需要注意的是,由于 init 函数中可能运行相当消耗资源的代码(如连接服务器、创建 DOM 元素等,具体行为难以预测),因此做出这种区分是完全必要的。

超类方法调用原理

超类方法(Super Method)

在使用继承时,最常见的需求是子类能够访问超类中被覆写的方法。在该实现中,最终方案是提供一个临时方法(._super),该方法指向超类方法,且只能在子类方法中访问。


var Person = Class. extend ( {
 init: function (isDancing ) {
  this. dancing = isDancing;
 }
} );
var Ninja = Person.extend({
 init: function(){
  this._super( false );
 }
});
var p = new Person(true);
p.dancing; // => true
var n = new Ninja();
n.dancing; // => false

实现这一功能需要几个步骤。首先,我们使用 extend 方法来合并基本的 Person 实例(即类实例,前文提到过其构造过程)与字面对象(Person.extend() 的函数参数)。在合并过程中,进行了简单的检查:首先判断将被合并的属性是否为函数;如果是函数,则进一步检查将被覆写的超类属性是否也为函数。如果这两个检查均为 true,则需要为该属性准备 _super 方法。

注意,在这里创建了一个匿名闭包(返回的是函数对象)来封装增加的 super 方法。基于维护运行环境的需要,我们将旧的 this._super(无论其是否存在)保存起来,以便函数运行后重置。这有助于在出现同名方法(避免偶然丢失对象指针)的情况下防止不可预知的问题。

然后,创建新的 _super 方法,该方法对象仅指向超类中被覆写的方法。幸运的是,我们无需对 _super 做任何改动或变更作用域,因为函数的执行环境会随着函数调用对象自动变更(指针 this 会指向超类)。

最后,调用字面量对象的方法。方法执行中可能会使用 this._super(),方法执行完毕后,将属性 _super 重置回其原来状态,之后 return 退出函数。

以上可以通过多种方案达到相同的效果(我之前曾见过将 super 绑定到其自身,然后用 arguments.callee 访问),但我认为这种方法最能体现可用性与简洁性的特点。

在我已完成的多个基于 JavaScript 原型的工作中,只有这个类继承实现方案是我决定发表并与大家分享的。我认为,简洁的代码(易于学习、易于继承、下载体积更小)更值得提出来让大家探讨。因此,对于学习 JavaScript 类构造与继承的人们来说,这套实现方案是一个良好的起点。

作者最新文章