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 您的支持是我最大的动力!