JavaScript框架继承写法

继承框架的功能分析

上篇文章记录了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 您的支持是我最大的动力!