简述一个javascript简单继承工具的实现原理
背景
由于本人非常希望能够开发自己的游戏,所以业余时间一直在想着能不能自己一些好玩又有趣的东西出来,最近随着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();
简述一个javascript简单继承工具的实现原理的更多相关文章
- JavaScript简单继承
很多C#或C++开发人员习惯使用继承来开发项目,所以当他们想学习JavaScript语言时,第一个问题一般是:“我怎么在JavaScript中使用继承?”. 实际上JavaScript使用了一种不同于 ...
- JSLint是一个JavaScript的代码质量工具
JSLint是一个JavaScript的代码质量工具 可能都或多或少的知道JSLint是一个JavaScript的代码质量工具,一个JavaScript语法检查器和校验器,它能分析JavaScript ...
- 一个分门别列介绍JavaScript各种常用工具的脑图
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:一个分门别列介绍JavaScript各种常用工具的脑图.
- 一个最简单 node.js 命令行工具
一个最简单 node.js 命令行工具 node.js cli $ node cli.js xyz # OR $ node cli xyz 接受参数 process.argv js "use ...
- 尝试做一个.NET简单、高效、避免OOM的Excel工具
Github : https://github.com/shps951023/MiniExcel 简介 我尝试做一个.NET简单.高效.避免OOM的Excel工具 目前主流框架大多将资料全载入到记忆体 ...
- 详解Javascript的继承实现(二)
上文<详解Javascript的继承实现>介绍了一个通用的继承库,基于该库,可以快速构建带继承关系和静态成员的javascript类,好使用也好理解,额外的好处是,如果所有类都用这种库来构 ...
- 创建你的第一个JavaScript库
是否曾对Mootools的魔力感到惊奇?是否有想知道Dojo如何做到那样的?是否对jQuery感到好奇?在这个教程中,我们将了解它们背后的东西并且动手创建一个超级简单的你最喜欢的库. 我们其乎每天都在 ...
- 加速编码的 JavaScript 库和工具
JavaScript库是 一个提前写好的JavaScript文件库,它可以很容易的开发基于JavaScript的应用,特别是AJAX和一些其它的以web为中心的技术.运用JavaScript最基本的方 ...
- Javascript开发之工具归纳
写在前面 由于JS开发对我来说是全新的技术栈,开发过程中遇到了各种各样的框架.工具,同时也感叹一下相对于.Net的框架(工具框架以及测试框架等)JS框架真的是太丰富了.社区的力量果然强大---也是由此 ...
随机推荐
- hpuoj 1706: 牛B【正向拓扑】【建图】
1706: 牛B 时间限制: 1 Sec 内存限制: 128 MB提交: 22 解决: 6[提交][状态][讨论版] 题目描述 一群来自日本恐怖分子带着AK47,火箭弹,开着坦克,带着飞机,强行洗 ...
- php优化技巧
PHP优化的目的是花最少的代价换来最快的运行速度与最容易维护的代码.本文给大家提供全面的优化技巧. 1.echo比print快. 2.使用echo的多重参数代替字符串连接. 3.在执行for循环之前确 ...
- [置顶] Android布局管理器 - 详细解析布局实现
布局管理器都是以ViewGroup为基类派生出来的; 使用布局管理器可以适配不同手机屏幕的分辨率,尺寸大小; 布局管理器之间的继承关系 : 在上面的UML图中可以看出, 绝对布局 帧布局 网格布局 相 ...
- [置顶] Array ArrayList LinkList的区别剖析
这是一个面试中我们经常被问到的问题 Array.ArrayList.LinkList之间的区别:Array.ArrayList.LinkList均属于泛型的范畴,都用来存放元素,主要区别是Array是 ...
- 【机房重构】SQL之视图
近期在重构机房收费系统,越往后就会越感觉到这里很多其它的是对之前学过知识(数据库,设计模式)的一种应用和回想.比方在登录功能中用到了抽象加反射,在学生下机中,我们能够用触发器来同一时候更新两个表.这里 ...
- MST最小生成树及Prim普鲁姆算法
MST在前面学习了Kruskal算法,还有一种算法叫做Prim的.这两者的区别是Prim算法适合稠密图,比如说鸟巢这种几乎所有点都有相连的图.其时间复杂度为O(n^2),其时间复杂度与边的数目无关:而 ...
- C. Tourist Problem
http://codeforces.com/problemset/problem/340/C 赛时没想出赛后却能较快想出深深的教育自己做题一定要静下心来,不要轻易放弃,认真思考,不要浮躁着急,不要太容 ...
- Service 如何知道caller
重写Binder的onTransact方法 1 you need to do that in Binder#onTransact method, this is a good place for ...
- [转] Node.js 服务端实践之 GraphQL 初探
https://medium.com/the-graphqlhub/your-first-graphql-server-3c766ab4f0a2#.n88wyan4e 0.问题来了 DT 时代,各种业 ...
- 使用SqlAlchemy时如何方便的取得dict数据、dumps成Json
使用Sqlalchemy可以方便的从数据库读取出python对象形式的数据(吐槽:说实话对象形式也没多方便,还不如我之前从关系型数据库直接读取出dict形式的数据用起来方便,具体参见我以前的文章htt ...