继承框架的功能分析
上篇文章记录了JavaScript原型链的知识,这次带大家来写一个比较好用的继承框架。
首先分析一下我们的继承框架需要提供哪些功能:
1.语法简洁
最好是调用一个方法就可以实现继承,不需要来回操作prototype
2.可以在子类方法中调用父类同名方法
借鉴java中的super.funname()用法,用以在子类方法中选择是否调用父类当前方法
3.要有构造方法,并且构造方法也会继承
js类对象模型中也有构造函数的说法,即在生成对象时调用的方法,但之前文章中的继承方式不能处理构造方法的继承。
设定唯一的命名空间
首先设定一个唯一的命名空间,我们的所有的功能都是在此命名空间下,不会对window空间造成过多的污染。
var gaga = gaga || {};
定义最终的使用规则(假设我们已经写好了继承逻辑,最终要如何使用)
假定gaga.Class是最基础的父类
gaga.ObjA = gaga.Class.extend({
name:"objA",
init:function(){},
addlistener:function(){}
});
gaga.ObjB = gaga.ObjA.extend({
name:"objB",
init:function(){
this.super();
}
});
ObjB 是 ObjA的子类,拥有ObjA的全部特性,使用时:
var objA = new gaga.ObjA();
var objB = new gaga.ObjB();
可以得到objA 和 objB两个对象,这两个对象是父子关系,并且他们都是gaga.Class的子类的对象。但是可以看到在使用时没有对prototype的操作,也就是说,将prototype的操作封装起来,对外不可见,而这部分功能就是extend这个方法实现的。所以我们来看extend这个方法内部是如何操作的。
定义基础父类
代码如下:
gaga.Class = function(){
};
如上定义了一个Class类当然这个类是空的,内部没有任何内容,prototype也是纯净的。
定义extend方法
由最终使用规则得知,这个方法返回值是一个类,也就是说这个方法最后会返回一个function,所以此方法定义为这样的:
gaga.Class.extend = function(){
function rClass(){} //returnClass
return rClass;
}
extend的传入参数
extend方法的参数是一个对象,这个对象记录的是子类独特的prototype属性,除此之外,子类还应该拥有父类中原有的prototype属性,所以extend中最重要的功能是组装子类的prototype
在此之前先来做一些准备工作:
学习一个新的Object的方法defineProperty,调用方法:
Object.defineProperty(proto,name,desc);
proto:要更改的对象
name:要更改的对象中名为name的成员,如果没有就新增
desc:要更改对象成员的描述
其中desc的结构如下:
desc = {
set:function(){},
get:function(){},
writable:true,
enumerable:false,
configurable:true,
value:""
}
解释一下desc内部成员的含义 :
set 是一个function 在调用对象对应成员的赋值后会调用此函数
get 是一个function 在读取对象对应成员时会回调此函数
writable 是一个boolean 如果为false,则不用使用obj.key=""这种方式赋值
enumerable 是一个boolean 如果为false 则使用for key in obj 遍历obj成员时,不会枚举当前成员
configurable 是一个boolean 如果为false 则不能使用 delete obj.key 删除成员 也不能修改成员的值
value 就是当前对象对应成员的值,可以是js普通对象,也可以是function
之后我们会用Object.defineProperty方法对prototype进行改造,所以如果还不清楚此函数的具体用法,请事先百度相关知识。
我们先做简单逻辑的继承,即以传入的prop为主,之后加上原先父类中prop没有的属性,代码如下:
gaga.Class.extend=function(prop){
var _superProp = this.prototype; //拿到父类的prototype
var _prototype = Object.create(_superProp);
//构造一个以父类prototype为__proto__的对象作为子类的prototype
//当然还需要对此prototype做修饰
var desc={
writable:true,
enumerable:false,
configurable:true
};//设置默认的prototype成员的属性描述 定义所有子类成员都不可枚举,
//但都可以读写,当然你也可以设置为可以枚举,
//设为不可枚举的好处是对成员的一种保护。
function rClass(){}
rClass.prototype = _prototype;
//设置子类的constructor 其实不设置也可以,因为之后我们会重新定义构造函数
desc.value = Class;
Object.defineProperty(_prototype,"constructor",desc);
//遍历prop 并将对应的prototype修改掉
for(var name in prop){
desc.value=prop[name];
Object.defineProperty(_prototype,name,desc);
}
//将构造好的class返回 而此rClass就拥有父类和子类共同的属性
return rClass;
}
如上代码可以制造出一个拥有父类prototype和希望子类拥有独特prop组合起来的prototype的class,使用new关键字激活后,就可以得到一个拥有父类所有特性的子类对象。
但是,有个很严重的问题,比如如下代码:
var ObjA = gaga.Class.extend({
init:funtion(){
alert("objA init");
}
});
var objA = new ObjA();//这里还没有问题
var ObjB = ObjA.extend({
init:function(){
alert("objB init");
}
});//这里是错的 显示ObjA.extend没有定义
问题出在我们定义的extend方法只定义在gaga.Class上,却没有定义在它的子类上,所以它的子类显示extend没有定义,解决方法是在返回前 将extend方法赋值给rClass 代码如下:
rClass.extend=gaga.Class.extend
注意这里不是定义在prototype上而是定义在rClass本身上,这是因为extend是给Class本身使用的方法,而不是给Class生成的对象使用的。
以上完整代码如下:
gaga.Class.extend=function(prop){
var _superProp = this.prototype; //拿到父类的prototype
var _prototype = Object.create(_superProp);
//构造一个以父类prototype为__proto__的对象作为子类的prototype
//当然还需要对此prototype做修饰
var desc={
writable:true,
enumerable:false,
configurable:true
}; //设置默认的prototype成员的属性描述 定义所有子类成员都不可枚举,
//但都可以读写,当然你也可以设置为可以枚举,
//设为不可枚举的好处是对成员的一种保护。
function rClass(){}
rClass.prototype = _prototype;
//设置子类的constructor 其实不设置也可以,因为之后我们会重新定义构造函数
desc.value = Class;
Object.defineProperty(_prototype,"constructor",desc);
//遍历prop 并将对应的prototype修改掉
for(var name in prop){
desc.value=prop[name];
Object.defineProperty(_prototype,name,desc);
}
rClass.extend = gaga.Class.extend;
//将构造好的class返回 而此rClass就拥有父类和子类共同的属性
return rClass;
}
以上代码就大致实现了继承的功能,但是离我们的最终目标差距还比较大,我们还不能再子类方法中调用父类的方法,也没有构造方法的定义,接下来我们来研究如何在子类方法中调用同名的父类方法。
首先看如下代码:
gaga.ObjA = gaga.Class.extend({
name:"objA",
init:function(){},
addlistener:function(){}
});
gaga.ObjB = gaga.ObjA.extend({
name:"objB",
init:function(){
gaga.ObjA.prototype.init.call(this);
//或者 gaga.ObjA.prototype.init.apply(this,arguments);
}
});
可以清楚看到,这样的写法可以满足我们在子类中调用父类的同名方法的需求,而且,有些框架中继承也的确再用这种写法,不过这样有一个很明显的问题是,我要在子类的方法中访问父类的prototype,这是我们并不想做的。我们想要使用非显示的调用就可以调用父类同名方法,类似上文中写道的:
this.super();
鉴于此,我们需要对extend函数进行改造,起码要分析出来哪些属性是重写的,哪些重写的属性是function,如果重写的function中要调用父类的function需要做的事情,我们改造我们的代码如下:
gaga.Class.extend=function(prop){
var _superProp = this.prototype; //拿到父类的prototype
var _prototype = Object.create(_superProp);
//构造一个以父类prototype为__proto__的对象作为子类的prototype
//当然还需要对此prototype做修饰
var desc={
writable:true,
enumerable:false,
configurable:true
};
//设置默认的prototype成员的属性描述
//定义所有子类成员都不可枚举,但都可以读写,
//当然你也可以设置为可以枚举,设为不可枚举的好处是对成员的一种保护。
function rClass(){}
rClass.prototype = _prototype;
//设置子类的constructor 其实不设置也可以,因为之后我们会重新定义构造函数
desc.value = rClass;
Object.defineProperty(_prototype,"constructor",desc);
//遍历prop 并将对应的prototype修改掉
/*for(var name in prop){
desc.value=prop[name];
Object.defineProperty(_prototype,name,desc);
}*/
//新版本的遍历prop 将对应的prototype修改掉 start
var regSuper = /\b_super\b/;
//检测函数是否包含_super 如果包含则说明存在对父类同名方法的调用
for(var name in prop){
var isFun = (typeof prop[name] == "function");
//检测属性是否为函数
var isOverride = (typeof _prototype[name] == "function")
//检测父类中同名属性是否为函数
var hasSuperCall = regSuper.test(prop[name]);
//使用正则表达式检测子函数中有没有_super调用
if(isFun && isOverride && hasSuperCall){
//三种条件都满足的开启递归调用模式
desc.value=(function(name,propCall){
return function(){
var tmp = this._super;
//此处的this指向子类对象 暂存_super指向
this._super = _superProp[name];
//_super指向父类同名方法
var ret = propCall.apply(this,arguments);
//调用子类目标方法
//(目标方法中会调用this._super
//而此时_super已经指向父类同名方法);
this._super = tmp;
//将_super指针还原,这点很重要,
//因为父类中方法也可能会调用父类的父类方法,如果不还原,
//可能会造成指针混乱,这一点大家自己去思考
return ret;
}
//返回结果集
})(name,prop[name]);
//一定要使用闭包,这个地方的原因可以参考
//问题:每隔一秒钟输出i每次输出i+1 的闭包写法
//不在过多描述
Object.defineProperty(_prototype,name,desc);
//将生成的方法赋值给_prototype
//可以看出 这里其实使用了代理的设计模式,
//我在生成成员的时候,
//并非将传入的prop参数中的成员之间赋值,
//而是构造了一个代理方法,
//然后使用这个代理方法,请体会这里的技巧
}else{
//一般情况下,沿用之前的方法
desc.value=prop[name];
Object.defineProperty(_prototype,name,desc);
}
}
//新版本的遍历prop 将对应的prototype修改掉 end
rClass.extend = gaga.Class.extend;
//将构造好的class返回 而此rClass就拥有父类和子类共同的属性
return rClass;
}
此时我们便可以在子类中使用this._super()来调用父类中的同名方法了 请看实例:
gaga.ObjA = gaga.Class.extend({
name:"objA",
init:function(obj){
alert(obj+" in ObjA init")
},
addlistener:function(){}
});
gaga.ObjB = gaga.ObjA.extend({
name:"objB",
init:function(obj){
this._super(obj);
alert(obj+" in ObjB init");
}
});
var objB = new ObjB();
objB.init("objb");
我们还有最后一个问题,构造方法的问题,就是在生成对象时调用的方法,如果有了构造方法,我们在构造方法中调用init方法,就不用再外部调用init方法了(其实构造方法倒并不是必须的,因为就当前的结构来讲,已经可以处理我们大部分需求了,但是有了构造函数,能够让我们使用时更加像一般面向对象语言的用法)
假设我们已经有了构造方法,那么刚才的初始化过程可以简略为:
var objB = new ObjB("objb");
可以不显式调用init方法
如何实现这样的功能呢,我们还是要回头分析原始类中的rClass,因为ObjB其实是父类构造的一个rClass,再调用new ObjB时,程序当然会调用 rClass这个函数,我们想实现构造函数的功能,自然要从这个方法下手,只要在这个方法中调用一个我们约定好名字的方法,那么这个约定的方法可以称为构造方法,这个约定的名字我们定为"ctor" (constructor的简写),那么rClass应该这么写:
function rClass(){
if(this.ctor){
this.ctor.apply(this,arguments);
}
}
由此 在new一个对象时,如果当前类存在ctor方法(可以在子类定义,也可以在父类中定义,因为ctor也是prototype成员,也可以继承),则会直接调用此方法。至此完整的继承代码如下:
var gaga = gaga || {};
gaga.Class.extend=function(prop){
var _superProp = this.prototype; //拿到父类的prototype
var _prototype = Object.create(_superProp);
//构造一个以父类prototype为__proto__的对象作为子类的prototype
//当然还需要对此prototype做修饰
var desc={
writable:true,
enumerable:false,
configurable:true
};
//设置默认的prototype成员的属性描述
//定义所有子类成员都不可枚举,但都可以读写,
//当然你也可以设置为可以枚举,设为不可枚举的好处是对成员的一种保护。
//添加对构造函数的支持
function rClass(){
if(this.ctor){
this.ctor.apply(this,arguments);
}
}
rClass.prototype = _prototype;
//设置子类的constructor 其实不设置也可以,因为之后我们会重新定义构造函数
desc.value = rClass;
Object.defineProperty(_prototype,"constructor",desc);
//遍历prop 并将对应的prototype修改掉
/*for(var name in prop){
desc.value=prop[name];
Object.defineProperty(_prototype,name,desc);
}*/
//新版本的遍历prop 将对应的prototype修改掉 start
var regSuper = /\b_super\b/;
//检测函数是否包含_super 如果包含则说明存在对父类同名方法的调用
for(var name in prop){
var isFun = (typeof prop[name] == "function");
//检测属性是否为函数
var isOverride = (typeof _prototype[name] == "function")
//检测父类中同名属性是否为函数
var hasSuperCall = regSuper.test(prop[name]);
//使用正则表达式检测子函数中有没有_super调用
if(isFun && isOverride && hasSuperCall){
//三种条件都满足的开启递归调用模式
desc.value=(function(name,propCall){
return function(){
var tmp = this._super;
//此处的this指向子类对象 暂存_super指向
this._super = _superProp[name];
//_super指向父类同名方法
var ret = propCall.apply(this,arguments);
//调用子类目标方法
//(目标方法中会调用this._super
//而此时_super已经指向父类同名方法);
this._super = tmp;
//将_super指针还原,这点很重要,
//因为父类中方法也可能会调用父类的父类方法,如果不还原,
//可能会造成指针混乱,这一点大家自己去思考
return ret;
//返回结果集
}
})(name,prop[name]);
//一定要使用闭包,这个地方的原因可以参考
//问题:每隔一秒钟输出i每次输出i+1 的闭包写法
//不在过多描述
Object.defineProperty(_prototype,name,desc);
//将生成的方法赋值给_prototype
//可以看出 这里其实使用了代理的设计模式,
//我在生成成员的时候,
//并非将传入的prop参数中的成员之间赋值,
//而是构造了一个代理方法,
//然后使用这个代理方法,请体会这里的技巧
}else{
//一般情况下,沿用之前的方法
desc.value=prop[name];
Object.defineProperty(_prototype,name,desc);
}
}
//新版本的遍历prop 将对应的prototype修改掉 end
rClass.extend = gaga.Class.extend;
//将构造好的class返回 而此rClass就拥有父类和子类共同的属性
return rClass;
}
验证代码如下:
gaga.ObjA = gaga.Class.extend({
name:"objA",
ctor:function(obj){
alert("这是ObjA的ctor函数");
this.init(obj);
},
init:function(obj){
alert(obj+" in ObjA init")
},
addlistener:function(){}
});
gaga.ObjB = gaga.ObjA.extend({
name:"objB",
ctor:function(obj){
this._super(obj);
alert("这是ObjB的ctor函数");
},
init:function(obj){
this._super(obj);
alert(obj+" in ObjB init");
}
});
var objB = new ObjB("objb");
如上就是满足我们基本继承需求的JavaScript框架式继承写法。这里面主要用的的知识点包含(原型链,代理模式,正则表达式等),大家可以自己体会有了这种框架之后,在处理继承问题上的好处。当然由于JavaScript的特性,这个方案并不是没有漏洞的,但按照约定的套路开发,一般情况下不会有问题,之后我也会在“h5游戏引擎开发“这个课题中使用这个继承框架,敬请期待。
转载请注明出处:http://gagalulu.wang/blog/detail/9 您的支持是我最大的动力!