JavaScript 经典类继承框架实现详解
本文介绍一个基于原型链的轻量级 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 类构造与继承的人们来说,这套实现方案是一个良好的起点。
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
