最近重温了下John Resig经典的js单继承实现,觉得很有必要记录下来目前自己对大牛代码的理解,对自己来说也是一种阶段性的总结。
如下是作者的代码实现(注:方便快速阅读,已把英文注释翻译成了中文,里面有些注释是本人aboli加的):
// Simple JavaScript Inheritance
//简单的JavaScript继承
/*
I’ve been doing a lot of work, lately, with JavaScript inheritance – namely for my work-in-progress
JavaScript book – and in doing so have examined a number of different JavaScript classical-inheritance-simulating
techniques. Out of all the ones that I’ve looked at I think my favorites were the implementations
employed by base2 and Prototype.
I wanted to go about extracting the soul of these techniques into a simple, re-usable, form that could
be easily understood and didn’t have any dependencies. Additionally I wanted the result to be simple
and highly usable. Here’s an example of what you can do with it:
最近,我一直在做很多工作,使用JavaScript继承 - 即我正在进行的JavaScript书籍 - 并且这样做已经检查了许多不同
的JavaScript经典继承模拟技术。 在我看过的所有内容中,我认为我最喜欢的是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
A couple things to note about this implementation:
Creating a constructor had to be simple (in this case simply providing an init method does the trick).
In order to create a new ‘class’ you must extend (sub-class) an existing class.
All of the ‘classes’ inherit from a single ancestor: Class. Therefore if you want to create a brand
new class it must be a sub-class of Class.
And the most challenging one: Access to overridden methods had to be provided (with their context properly set).
You can see this with the use of this._super(), above, calling the original init() and dance()
methods of the Person super-class.
I’m pleased with the result: It helps to enforce the notion of ‘classes’ as a structure, maintains
simple inheritance, and allows for the super method calling.
Simple Class Creation and Inheritance
And here’s the implementation (reasonably sized and commented well) – clocking in at around 25 lines. Feedback is welcome and appreciated.
如下应该都是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 https://johnresig.com/
* 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)
// 实例化基类(但只创建实例,不要运行init构造函数)
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
// 检查我们是否覆盖现有功能
// reviewed by aboli:fnTest.test(prop[name])是检测属性值中是否有_super这个单词,即子类是否需要
// 调用父类的同名方法
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
// reviewed by aboli:这个闭包仅提供了让子类能够访问到其父类的手段,
// 赋值子类属性时this是无法确定的,仅当子类调用了该匿名函数时,才能将this确定为调用该匿名函数的子类
return function() {
var tmp = this._super; // reviewed by aboli:缓存子类中的_super属性值
// Add a new ._super() method that is the same method but on the super-class
// 添加一个新的._super()方法,该方法是父类上的同名方法
this._super = _super[name]; // reviewed by aboli:将属性值中的_super方法替换为父类的同名方法
// 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
// 所有构造实际上都是在init方法中完成的
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;
};
})();
/*
In my opinion the two trickiest parts are the “initializing/don’t call init” and “create _super
method” portions. I want to cover those briefly so that you will have a good understanding of
what’s being achieved in this method.
Initialization
In order to simulate inheritance with a function prototype we use the traditional technique of
creating an instance of the super-class function and assigning it to the prototype. Without using
the above it would look something like this:
function Person(){}
function Ninja(){}
Ninja.prototype = new Person();
// Allows for instanceof to work:
(new Ninja()) instanceof Person
What’s challenging about this, though, is that all we really want is the benefits of ‘instanceof’,
not the whole cost of instantiating a Person object and running its constructor. To counteract this
we have a variable in our code, initializing, that is set to true whenever we want to instantiate a
class with the sole purpose of using it for a prototype.
Thus when it comes time to actually construct the function we make sure that we’re not in an
initialization mode and run the init method accordingly:
if ( !initializing )
this.init.apply(this, arguments);
What’s especially important about this is that the init method could be running all sorts of costly
startup code (connecting to a server, creating DOM elements, who knows) so circumventing this ends
up working quite well.
Super Method
When you’re doing inheritance, creating a class that inherits functionality from a super-class, a
frequent desire is the ability to access a method that you’ve overridden. The final result, in this
particular implementation, is a new temporary method (._super) which is only accessible from within
a sub-classes’ method, referencing the super-classes’ associated method.
For example, if you wanted to call a super-classes’ constructor you could do that with this technique.
在我看来,两个最棘手的部分是“初始化/不调用init”和“创建_super方法”部分。我想简要介绍一下这些内容,以便您对
这种方法所取得的成果有一个很好的理解。
初始化
为了使用函数原型模拟继承,我们使用传统技术创建超类函数的实例并将其分配给原型。如果不使用上述内容,它将看起来
像这样:
function Person(){}
function Ninja(){}
Ninja.prototype = new Person();
//允许instanceof工作:
(new Ninja()) instanceof Person
但是,对此有什么挑战,我们真正想要的只是'instanceof'的好处,而不是实例化Person对象和运行其构造函数的全部成本。
为了抵消这种情况,我们在代码中有一个变量,初始化,只要我们想要实例化一个类,其唯一目的是将它用于原型,就设
置为true。
因此,当实际构建函数时,我们确保我们不处于初始化模式并相应地运行init方法:
if ( !initializing )
this.init.apply(this, arguments);
特别重要的是init方法可以运行各种昂贵的启动代码(连接到服务器,创建DOM元素,谁知道),因此规避这一点最终会很好
地运行。
超级方法
在进行继承时,创建一个从超类继承功能的类,经常需要能够访问您已覆盖的方法。在这个特定的实现中,最终结果是一个新
的临时方法(._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
/*
Implementing this functionality is a multi-step process. To start, note the object literal that
we’re using to extend an existing class (such as the one being passed in to Person.extend) needs
to be merged on to the base new Person instance (the construction of which was described
previously). During this merge we do a simple check: Is the property that we’re attempting merge
a function and is what we’re replacing also a function? If that’s the case then we need to go
about creating a way for our super method to work.
Note that we create an anonymous closure (which returns a function) that will encapsulate the new
super-enhanced method. To start we need to be a good citizen and save a reference to the old
this._super (disregarding if it actually exists) and restore it after we’re done. This will help
for the case where a variable with the same name already exists (don’t want to accidentally blow
it away).
Next we create the new _super method, which is just a reference to the method that exists on the
super-class’ prototype. Thankfully we don’t have to make any additional changes, or re-scoping,
here as the context of the function will be set automatically when it’s a property of our object
(this will refer to our instance as opposed to the super-class’).
Finally we call our original method, it does its work (possibly making use of _super as well)
after which we restore _super to its original state and return from the function.
Now there’s a number of ways in which a similar result, to the above, could be achieved (I’ve seen
implementations that have bound the super method to the method itself, accessible from
arguments.callee) but I feel that this technique provides the best mix of usability and
simplicity.
I’ll be covering a lot more of the nitty-gritty behind the JavaScript prototype system in my
completed work but I just wanted to get this Class implementation out there to get everyone
trying it out and playing with it. I think there’s a lot to be said for simplistic code
(easier to learn, easier to extend, less to download) so I think this implementation is a
good place to start and learn the fundamentals of JavaScript class construction and inheritance.
实现此功能是一个多步骤的过程。首先,请注意我们用于扩展现有类(例如传递给Person.extend的类)的对象文字需要
合并到基本的新Person实例(其结构如前所述) 。在这个合并过程中,我们做了一个简单的检查:我们正在尝试合并一
个函数的属性是否正在替换一个函数?如果是这种情况,那么我们需要为我们的超级方法创建一种方法。
请注意,我们创建了一个匿名闭包(返回一个函数),它将封装新的超级增强方法。首先,我们需要成为一个好公民并
保存对旧this._super的引用(忽略它是否确实存在)并在完成后恢复它。这将有助于已存在具有相同名称的变量的情
况(不想意外地将其吹掉)。
接下来,我们创建新的_super方法,它只是对超类原型上存在的方法的引用。值得庆幸的是,我们不必进行任何额外的
更改或重新确定范围,因为当它是我们对象的属性时,它将自动设置函数的上下文(这将引用我们的实例而不是超类') )。
最后我们调用我们的原始方法,它完成它的工作(也可能使用_super),之后我们将_super恢复到其原始状态并从函数
返回。
现在有很多方法可以实现上面的类似结果(我已经看到将超级方法绑定到方法本身的实现,可以从arguments.callee访问)
但我觉得这种技术提供了可用性和简单性的最佳组合。
在我完成的工作中,我将介绍JavaScript原型系统背后的更多细节,但我只是想让这个类实现在那里让每个人都尝试并使用
它。我认为简化代码有很多要说的(更容易学习,更容易扩展,更少下载)所以我认为这个实现是一个很好的起点和学习
JavaScript类构造和继承的基础知识的地方。
*/
逐行解读下代码:
从结构上看,整个继承机制的实现被包在一个自执行匿名函数IIFE(Immediately Invoked Function Expression)中,避免了全局污染,隐藏了实现细节。如下:
(function() {
...
})();
从结果上看,该匿名函数执行完后,会给全局对象window(此处的this指向全局对象)添加一个Class属性,该属性指向一个匿名函数,并为该匿名函数添加了一个extend方法,因为在js中,一切皆是对象,函数也是对象,extend就是该匿名函数的一个属性。如下:
(function() {
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
this.Class = function(){};
Class.extend = function(prop) {
...
};
})();
另外,对于函数内部定义的两个变量initializing、fnTest也很有必要讲解下。通读并理解完整个代码,或许你才会对这两个变量的作用有清晰的认知,从而对大牛在代码功能实现的整体把控和技术手段上由衷赞叹。首先说下initializing变量:
initializing被初始化为一个布尔值,它用来控制子类中init方法的执行。如果是通过父类调用extend构建的构造函数,则在获取父类实例时,此时的initializing值为true,不会执行父类的init方法,起作用的是如下这段:
Class.extend = function(prop) {
var _super = this.prototype;
initializing = true;
var prototype = new this();
initializing = false;
...
function Class() {
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
};
;如果是实例化构造函数,此时的initializing已经是false,则会执行对应构造函数的init方法,完成初始化工作。也许好多人会对为什么实例化构造函数(new的方式)的时候initializing的值是false产生疑问,这里可以解答下:在John Resig实现的这个单继承代码中,构造函数的产生,总是其父类调用自身的extend实现的,extend函数会返回内部的Class函数,这就是子类的构造函数,extend函数实际是一个闭包。由父类调用extend产生的每个子类都记录了父级作用域的initializing变量,当子类的构造函数在extend方法中返回时,initializing已被重新赋值为false,所以后续在new一个子类实例的时候,访问到的initializing总是false,这就保证了只有实例化构造函数才会执行构造函数的init方法。在js中new一个构造函数的内部过程如下:
function F() {
...
}
new F() -> function newInstance(F) {
return function() {
var p = {};
p.__proto__ = F.prototype;
F.call(p);
return p;
};
}
fnTest被赋值为一个正则表达式,作用是用来检测给子类原型对象赋值的属性中,对应的属性值中是否有调用_super方法,如果有_super这个单词,那么就将该属性值替换为一个IIFE,返回一个闭包,该闭包记录了构建子类构造函数的对象的原型对象,即父类构造函数的原型对象,这句话比较拗口,还是从代码中解释:
Class.extend = function(prop) {
var _super = this.prototype;
var prototype = new this();
for (var name in prop) {
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
...
this._super = _super[name];
...
};
})(name, prop[name]) :
prop[name];
}
...
};
如上图代码,父类通过extend方法构建子类构造函数时,此时的this指向父类构造函数,_super存储即是父类构造函数的原型对象,prototype保存的是父类实例。所以当new Ninja()调用时,会调用this.init,init方法通过原型链在Ninja构造函数的原型对象中找到,即IIFE,内部的_super指向Person.protoype。这样一来,一切都很清晰。
对于继承机制,子类构造函数的原型对象被设定为父类实例,这样子类既拥有了父类的属性,又能够访问父类原型对象(通过隐藏的__proto__),并且在父类构建子类构造函数时,还会对传入的对象参数给子类原型对象循环加上自己的属性跟方法。并且,返回的子类构造函数被添加了extend方法,保证了链式调用的能力。
读完整个实现代码,还是不由得对John Resig大牛的深厚功底由衷钦佩,闭包,this指向,原型,原型链,作用域...这种拿捏自如的手法,还是有好长的路要走~~~