背景
由于本人非常希望能够开发自己的游戏,所以业余时间一直在想着能不能自己一些好玩又有趣的东西出来,最近随着steam上众多独立游戏的爆发,感觉自己又燃烧了起来,所以又拾起了很久以前的一个2d引擎,决定利用业余时间进行全力开发。
该引擎基于 html5 canvas 和WebGL, 主要支持WebGL, canvas 由于性能问题目前只打算提供有限支持,由于之前曾经做过一个很简单的引擎,所以这次准备做出一个结构比较好的引擎,会陆续参考一些其他的框架,比如 PIXI ,cocos2dx-js 和秋叶原引擎, 这次的目标是高性能以及易用性,以及良好的结构,和扩展性,要兼顾这几个方面去做出项目,是非常困难的,所以打算逐步去实现,同时尽量使代码短小也是非常重要的。
关于引擎以及图形图像相关的知识,c-dog我呢,也需要去补充很多知识,所以就不多说关于这方面的知识了,这次的重点在于 John Resig 写的 一篇博客 , 由于需要在我的框架中实现一种使用简单,没有副作用没有其他依赖的 继承机制,所以我在设计自己的类继承方法时偶然发现了大神 的这篇博客, 这篇博客中设计了一种简单的类继承方法, 无论是实现还是使用,都很简单,网上的很多人也都转载了这篇博客, 但是没有人站出来剖析 这段代码,所以今天我打算简单的剖析并且分析一下这段代码,关于 javascript的基础知识请参见 javascript高级程序设计 这本书 , 这篇博客是没有基础知识的哦,如果对于 javascript的高级知识还不够牢固, 可以参见 这篇博客 进行巩固.
Jon Resig的源码剖析
/* 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;
};
})();
这段代码确实是非常短小,充分利用了js的特点(原型, 闭包), 主要的原理就是利用了js中的原型,在生成新对象的构造方法的时候,用一个临时构造方法替换了我们要去创建 的方法,把我们提供给extend 方法的参数逐步分析之后加到了临时的构造方法里, 最后返回给我们的,是那个临时的构造方法,在原理背后还有很多小细节需要处理,下面我逐步说明。
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
// The base Class implementation (does nothing)
this.Class = function(){};
分析:
上面的3行代码主要是为了解决3个问题, 变量initializing是为了解决当存在继承关系时 ,父类的初始化时机问题,因为我们不希望在为目标构造方法构造原型的时候初始化父类,我们需要在该类型定义好以后,在外部初始化该类的时候,再初始化父类,这个临时变量很简单的就解决了这个问题,先把该变量设置为false,然后构造目标构造函数,构造完成之后,把该变量设置为true, 最后保存在闭包中,在我们为我们新建的类生成对象的时候,父类也会同时被初始化。
单独看这个变量是很抽象的,需要结合extend函数中的代码。
fnTest这个正则表达式,很多分析这段代码的人都刻意回避了这个问题,包括ibm里的那篇文章,虽然其他部分解释的很详细,但是唯独这个正则没有解释,这个正则的意义是测试我们将要生成的类的构造函数中的与父类中同名方法的,测试的原因是我们需要判断一个方法是否调用了他的父类的同名方法,假如子类中存在与父类同名的方法,并且函数定义字符串中存在 _super这个字符串,那么我们就认为该方法调用了父类的同名方法,关于父类与子类的同名方法如何构造,将在后面说明。 但是 为什么要去测试 xyz 这个正则呢? xyz 到底是什么意思呢? 其实这里xyz是没有任何意义的,你把xyz 换成 abc也一样, 仔细看xyz的代码会发现,
/xyz/.test(function(){xyz;});
/xyz/.test("function(){xyz}");
是不一样的,一个是直接测试的函数对象,一个是字符串, 当我们得到一个函数的时候,并不能在外部得到他的定义字符串,这样测试一下,是为了兼容性,因为有的浏览器的js引擎,是不可以用正则直接测试函数对象的,如果刻意测试成功,则表明该子类的方法确实调用了父类的同名方法(按照约定,其实不一定真的调用了),因为 _ super 是我们自己定义的关键字,如果测试失败,那么我们就会把每个同名方法都当成调用了父类方法的同名方法进行处理。
this.Class = function(){};
这个Class 空构造函数,就是我们需要组装的构造函数的原型,我们再extend方法的参数里提供的方法,最终都要组装到这个类里。这里的this指向的是 window 或者nodejs中的全局对象。所以这里写不写this关系不大,感觉作者有点想迷惑小白。
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;
下面,我们直接进入extend函数体内部,这个继承体系的精华就在这里,要理解这部分代码,首先要理解这段代码里的很多this指向的是哪个对象。
第一条语句 _super指向的是this的原型, 这里的this有2种情况,当不存在继承时,this指向的就是 Class本身, 也就是他自己,在定义Class的地方的 this, 指向的是window, 在nodejs里就是全局空间,当存在继承时, this指向的是父类自己,所以 我们用this.prototype , 总是可以得到正确的原型,这个原型,不是我们正在定义的对象的原型,而是我们需他里面的与子类同名的方法,这里把它当作一个缓存来用。
var prototype = new this();
这里才是我们需要的真正的原型,它生成了一个当前类的对象,我们等会需要把目标构造函数的原型 指向它。
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];
}
上面这段代码最核心的,也是最难理解的,因为涉及到了闭包的东西,这个地方是必须使用闭包的,因为只有使用闭包才可以把函数外部的变量包含进去一起打包带走。
for循环遍历我们提供的各种方法,如果存在与父类中同名,而且在子类中有调用父类方法的方法,那么作者就使用一个闭包做了些小动作,作者先把目前对象的_super放在临时变量里,接着把_super指向一个方法,这个方法就是父类的同名方法,然后替换当前this之后直接执行该方法,并且返回一个结果,再把这个闭包函数付给prototype ,也就是我们真正需要的原型, 这样就可以起到一种很魔术的效果,当我们调用与父类同名且在内部调用了this._super()函数的方法的时候,这个this._super 就是指向的父类同名的该方法, 比如我们在
init::function(){
this._super();
}
里this._super指向的就是父类的 init
read:function(){
this._super();
}
上面的情况,this._super ,指向的就是父类的read方法。
这个魔术方法的根源就在于上面的闭包,在各个原型方法里,this._super已经不是动态生成的了,而是闭包进去的,所以可以达到这种效果
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;
最后这几句代码,可以说是清理现场了,经过改造的Class对象还不完全是我们想要的,它的原型的constructor被我们改变掉了,所以直接改回去就好, 不然我们生成的类是类型错误的,使用 instanceof的时候得到的结果也是错误的,最后把该对象的extend绑定到我们的extend方法上。
总结
其实不用惊讶John Resig的方法,因为我在没有参考它的代码之前,就已写了一个比他的代码还要超前的一个方法,我使用了js中类似于python中修饰器的技术,把当前的子类中的构造方法全部放到prototype中,父类的同名方法,全部放到当前对象的__super__属性中, 对象的属性全部写成修饰器, 修饰器的作用是去判断该属性是父类属性还是子类属性,因为继承本来就是实现子类包含父类从而避免重复代码的问题的。
在我的方法里,this.__super不再指向固定的方法,你可以用这样的方法进行访问
init:function(){
this.super.init(arguments);
}
我的super不会再初始化阶段绑定任何值。而且你也可以再任何子类方法中调用父类的任何同名或非同名方法。
但是这并不是必须的,我的方法仅仅是为了说明问题,面向对象不需要这么复杂的实现,所以我还是决定使用基于John Resig的方法的改进版,下面是 我的方法,欢迎大家提出意见。
// author : youngershen
// email : younger.x.shen@gmail.com
//
var SUPERCLASS = function(){};
(function(SUPERCLASS){
SUPERCLASS = SUPERCLASS || {};
var CLASS = SUPERCLASS;
var set_builder_func = function(prop){
return function(value){
if(this.super[prop] == undefined){
this[prop + '_value'] = value;
}else if(this[prop] == undefined){
this.super[prop] = value;
}
};
};
var get_builder_func = function(that, prop){
return function(){
return (function(prop){
return function(){
if(that[prop + '_value'] != undefined){
return that[prop + '_value'];
}else{
return that.super[prop];
}
}();
})(prop);
}
};
CLASS.extend = function(prop){
var this_super = {};
var initializing = false;
var METACLASS = function(){
if(initializing && this.init && arguments.length != 0){
this.super = this_super;
this.init.apply(this, arguments);
for(var prop in this){
if((typeof this[prop]) != 'function'){
if(this.super[prop] == undefined && prop != 'super'){
this[prop + '_value'] = this[prop];
}
if(prop == 'super'){
for(var _prop in this[prop]){
if((typeof this[prop][_prop]) != 'function'){
Object.defineProperty(this, _prop,{
enumerable:true,
configurable:true,
set:set_builder_func(_prop),
get:get_builder_func(this, _prop)
});
}
}
}else{
Object.defineProperty(this,prop, {
enumerable:true,
configurable:false,
set:set_builder_func(prop),
get:get_builder_func(this, prop)
});
}
}
}
}
};
var supertype = this.prototype;
var prototype = new this();
initializing = true;
if((this instanceof Function)){
for(var property in prop){
if(typeof prop[property] == "function" && typeof supertype[property] == 'function'){
this_super[property] = supertype[property]
prototype[property] = prop[property];
}else if((typeof prop[property] == 'function')){
prototype[property] = prop[property];
}
}
METACLASS.extend = arguments.callee;
METACLASS.prototype = prototype;
METACLASS.constructor = METACLASS;
supertype = null;
prototype = null;
return METACLASS;
};
}
})(SUPERCLASS);
var Person = SUPERCLASS.extend({
init:function(name,age){
this.name = name;
this.age = age;
},
say:function(){
console.log(this.name);
console.log(this.age)
}
});
var Student = Person.extend({
init:function(name, age, id){
debugger;
this.super.init(name, age);
this.id = id;
},
say:function(){
this.super.say();
console.log(this.id);
},
get_id:function(){
console.log(this.id);
}
}
);
var GoodStudent = Student.extend({
init:function(name, age, id, score){
console.log(this.super.init);
debugger;
this.super.init();
this.score = score;
},
score:function(){
conosole.log(this.score);
}
});
var p = new Person('younger', '28');
console.log(p.name);
console.log(p.age);
p.say();
console.log(p);
/*
var s = new Student('lily', 15, '123456');
s.say();
s.get_id();
console.log(s)
*/
var gs = new GoodStudent('hehe', 11, '123', 100);
console.log(gs);
gs.score();