js函数(类)的继承机制的设计与实现

一、背景与问题
  • 根据项目需求,领导分给我一个任务,即设计一组基于Microsoft VML技术的通用的图表控件。简单来说,就是要我做一个类似于Highcharts的图表控件,当然图表的种类没有Highcharts那么丰富,目前主要包含了饼状图、柱状图、折线图和箱线图等。

  • 根据自己的开发经验和习惯,接到任务后,我首先对工作需求进行了分析,并进行了部分设计工作:1,提取各种图表的特征,按照特征分类;2,设计图表的逻辑组织结构;3,绘制图表控件类图。

这里写图片描述

  • 但是,这里有一个问题,我用面向对象的方法来设计图表控件,将来却要用javascript来实现,而javascript是基于原型的面向对象脚本语言,与传统的基于类的面向对象存在一定区别。也就是说,如果按照图1中的设计,用java、C++或者C#这类的平台是很容易实现的,但是换成更像结构化编程模式的javascript,则会存在一些问题,如继承问题

  • 接下来,本文将研究如何在js中实现更接近基于类的面向对象的继承技术。

二、研究与解决

1,通过调研,一种实现js函数继承的方式是依据js的prototype属性,测试代码如下:

            function test(){
                B.prototype = new A();
                B.prototype.construct = B;
                var b = new B();
                b.show();
            }
            function A(){
                this.name = "A";
                this.show = function(){
                    console.log("我是 "+ this.name);
                }   
            }
            function B(){
            }
  • 测试结果
    这里写图片描述

  • 这表明,通过prototype,是可以实现属性和共有函数的继承。

2,基于1中的代码,如果在子函数B中具备和父函数A相同的属性和方法时,会是什么情况呢?更改函数B的测试代码如下:

            function B(){
                this.name = "B";
            }
  • 测试结果
    这里写图片描述
            function B(){
                this.name = "B";
                this.show = function(){
                    console.log("调用的是我自己的show函数");
                }
            }
  • 测试结果
    这里写图片描述

  • 说明如果子函数中包含有与父函数同名的属性和方法时,子函数的属性和方法会覆盖父函数的属性和方法。这和C++是一致的。

3,将函数的继承写在子函数体外,感觉不是咱们专业程序员会干的事情,应该将其放入子函数体内部。这会有什么影响吗?测试代码如下:

            function test(){
                var b = new B();
                b.show_b();
                b.show_b_x();
                b.show_a();
            }
            function A(){
                this.name_a = "A";
                this.show_a = function(){
                    console.log("我是 "+ this.name_a);
                }   
            }
            function B(){
                B.prototype = new A();
                B.prototype.construct = B;
                this.name_b = "B";
                this.show_b = function(){
                    console.log("我是 "+ this.name_b);
                };
                this.show_b_x = function(){
                    var obj_this = new B();

                    obj_this.show_a();
                    console.log("obj_this是在内部声明的哦,可以调用父方法的函数");
                };
            }
  • 测试结果
    这里写图片描述

  • 当把继承部分的代码放入子函数体内后,其作用域只限于当前子函数体内,因此在test()函数中声明的子函数对象b无法调用父函数的show_a方法,而在子函数体内申明的obj_this可以调用父函数的show_a方法。这真是一个令人沮丧的结果。

4,其实三中的问题很好解决,只需要在子函数体内部申明一个初始化函数将obj_this返回就好。测试代码如下:

            function test(){
                var b = B().instantiate();
                b.show_b();
                b.show_a();
            }
            function A(){
                this.name_a = "A";
                this.show_a = function(){
                    console.log("我是 "+ this.name_a);
                }   
            }
            function B(){
                B.prototype = new A();
                B.prototype.construct = B;
                var obj_this = null;
                this.name_b = "B";
                this.instantiate = function(){//返回可访问父函数属性和方法的对象
                    obj_this = new B();
                    return obj_this;
                };
                this.show_b = function(){
                    console.log("我是 "+ this.name_b);
                };
                return this;//注意,这句话非常关键
            }
  • 测试结果
    这里写图片描述

  • 可见这样的改动是有效的,子函数创建的对象b能够访问子函数以及父函数的属性和方法。

5,到这里似乎已经能够解决继承问题了,但是,子函数内部的其它方法能否通过obj_this来访问父函数方法呢?测试代码如下:

            function test(){
                var b = B().instantiate();
                b.show_b();
                b.show_a();
                b.show_b_x();
            }
            function A(){
                this.name_a = "A";
                this.show_a = function(){
                    console.log("我是 "+ this.name_a);
                }   
            }
            function B(){
                B.prototype = new A();
                B.prototype.construct = B;
                var obj_this = null;
                this.name_b = "B";
                this.instantiate = function(){
                    obj_this = new B();
                    return obj_this;
                };
                this.show_b = function(){
                    console.log("我是 "+ this.name_b);
                };
                this.show_b_x = function(){
                    if(obj_this === null)
                        console.log("b中的obj_this为null,这是什么情况?");
                    console.log("我想访问父函数A的方法show_a"+obj_this.show_a());
                }
                return this;
            }
  • 测试结果
    这里写图片描述

  • 注意!这里非常关键。这部分极容易产生错觉,明明b在初始化的时候,B().instantiate()就已经将obj_this赋值了,而且b就是B().instantiate()返回的obj_this,b.show_a()没有问题,为何在b.show_b_x()中的obj_this会为null呢?

  • 是时候解开它的神秘面纱了。其实道理很简单,即B().instantiate()中的obj_this与b.show_b_x()中的obj_this不是同一个obj_this。设B().instantiate()中的obj_this为obj_this_1111,b.show_b_x()中的obj_this为obj_this_2222,那么它们的关系是obj_this_1111.obj_this == obj_this_2222。而obj_this_2222的初始化值为null,并没有调用instantiate(),所以obj_this_2222一直为空。好纠结,不过事实真相就是这样。

  • 解决5中的问题很好办,有三种方法:1,直接为obj_this222重新new一个对象;2,b在初始化完成后,立刻执行b.instantiate();3,设置一个私有函数,将obj_this_1111赋值给obj_this_2222。其中方法1和方法2虽然形式不同,但结果都一样,会增加内存消耗,多new一个对象,因此不是很好的选择。而方法3则不用再new对象,而是将obj_this_1111本身赋值给obj_this_2222,从性能来说,方法3优于方法1和方法2。方法3测试代码如下:

            function test(){
                var b = B().instantiate();
                b.show_b();
                b.show_a();
                b.show_b_x();
            }
            function A(){
                this.name_a = "A";
                this.show_a = function(){
                    console.log("我是 "+ this.name_a);
                }   
            }
            function B(){
                B.prototype = new A();
                B.prototype.construct = B;
                var obj_this = null;
                this.name_b = "B";
                this.instantiate = function(){
                    obj_this = new B();
                    obj_this.assign(obj_this);//将本身赋值给自己包       //含的obj_this,相当于obj_this.obj_this = obj_this
                    return obj_this;
                };
                this.assign = function(obj){///将本身赋值给自己包       //含的obj_this,相当于obj_this.obj_this = obj_this
                    obj_this = obj;
                };
                this.show_b = function(){
                    console.log("我是 "+ this.name_b);
                };
                this.show_b_x = function(){
                    if(obj_this === null)
                        console.log("b中的obj_this为null,这是什么情况?");
                    console.log("我成功了访问父函数A的方法show_a");
                    obj_this.show_a();
                }
                return this;
            }
  • 测试结果
    这里写图片描述

6,到此好像真的大功告成了,为了体现我们的专业性,现在可以在方法B的所有方法中用obj_this能够替代B方法的this指针。因为obj_this已经能够在子函数内部的所有方法中访问父函数和子函数的所有public属性和方法。
为了检查继承性,我们在创建一个C方法来继承B,看是否奏效。测试代码如下(注意!接下来的测试代码都采取了规范措施,在函数的内部方法中全用obj_this来代替this指针):

            unction test(){
                var c = C().instantiate();
                c.show_c();
                c.show_b();
                c.show_a();
            }
            function A(){
                this.name_a = "A";
                var obj_this = null;
                this.instantiate = function(){
                    obj_this = new A();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                this.show_a = function(){
                    console.log("我是 "+ obj_this.name_a);
                }
                return this;    
            }
            function B(){
                B.prototype = new A();
                B.prototype.construct = B;
                var obj_this = null;
                this.name_b = "B";
                this.instantiate = function(){
                    obj_this = new B();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                this.show_b = function(){
                    console.log("我是 "+ obj_this.name_b);
                };
                return this;
            }

            function C(){
                C.prototype = new B();
                C.prototype.construct = C;
                var obj_this = null;
                this.name_c = "C";
                this.instantiate = function(){
                    obj_this = new C();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                this.show_c = function(){
                    console.log("我是 "+ obj_this.name_c);
                };
                return this;
            }
  • 测试结果
    这里写图片描述

  • 又出错了,问题真多。其实细心的话,发现问题和5中的是一样的,问题出在C.prototype = new B();这一句上。不过这次解决起来很简,换成C.prototype = B().instantiate();即可,B方法同理。测试代码如下:

B.prototype = A().instantiate();

C.prototype = B().instantiate();
  • 测试结果
    这里写图片描述

7,此时功能已经齐全,即实现了js函数的继承机制。为了优化性能,可以将继承部分的代码放入instantiate()函数中,这样将减少对象的生成。测试代码如下:

            function test(){
                var c = C().instantiate();
                c.show_c();
                c.show_b();
                c.show_a();
                console.log("A:"+count_a+"   "+"B:"+count_b+"   "+"C:"+count_c);
            }
            var count_a = 0, count_b = 0, count_c = 0;
            function A(){
                count_a++;
                this.name_a = "A";
                var obj_this = null;
                this.instantiate = function(){
                    obj_this = new A();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                this.show_a = function(){
                    console.log("我是 "+ obj_this.name_a);
                }
                return this;    
            }
            function B(){
                count_b++;
                B.prototype = A().instantiate();
                B.prototype.construct = B;
                var obj_this = null;
                this.name_b = "B";
                this.instantiate = function(){
                    obj_this = new B();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                this.show_b = function(){
                    console.log("我是 "+ obj_this.name_b);
                };
                return this;
            }

            function C(){
                count_c++;
                C.prototype = B().instantiate();
                C.prototype.construct = C;
                var obj_this = null;
                this.name_c = "C";
                this.instantiate = function(){
                    obj_this = new C();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                this.show_c = function(){
                    console.log("我是 "+ obj_this.name_c);
                };
                return this;
            }
  • 测试结果
    这里写图片描述

  • 可见,A生成了8个对象(7次对象加载消耗),B生成了4个对象(3次对象加载消耗),C生成了2个对象(1次对象加载消耗)。如果不将承部分的代码放入instantiate(),对象的生成次数将以 2n 2 n 速度增加(*n是继承链的长度),这是不能容忍的。*将继承部分的代码放入instantiate()函数中后,修改函数B和C的代码,测试代码如下:

            function B(){
                count_b++;
                var obj_this = null;
                this.name_b = "B";
                this.instantiate = function(){
                    B.prototype = A().instantiate();
                    B.prototype.construct = B;
                    obj_this = new B();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                ...
                return this;
            }

            function C(){
                count_c++;
                var obj_this = null;
                this.name_c = "C";
                this.instantiate = function(){
                    C.prototype = B().instantiate();
                    C.prototype.construct = C;
                    obj_this = new C();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                ...
                return this;
            }
  • 测试结果
    这里写图片描述

  • 经过修改后,A,B和C都只生加载了2次对象(均只有一次对象加载消耗),且不会随着继承路径的增加而增加!即运用该方法实现js函数的继承,对象加载的上限为2次,其中只有1次对象加载消耗。在性能上是可以接受的。

三、结论
  • 本文设计了一种javascript方法的继承实现机制。定义了javascript继承函数的模板规范,描述如下:
//js函数继承机制实现的模板规范。
            //没有继承任何函数的函数规范定义
            function parent_function(){
                var obj_this = null;
                this.instantiate = function(){
                    obj_this = new parent_function();
                    obj_this.assign(obj_this);
                    return obj_this;
                };
                this.assign = function(obj){
                    obj_this = obj;
                };
                /...
                your code are written here
                parent_function函数的所有内部方法中用obj_this替代this指针,用obj_this来访问父函数和本函数的所有public方法和属性
                .../
                return this;
            }

            //继承了父函数的子函数的函数规范定义
            function your_function(){
                var obj_this = null;
                this.instantiate = function(){
                    your_function.prototype = parent_function().instantiate();//继承父函数
                    your_function.prototype.construct = your_function;
                    obj_this = new your_function();
                    obj_this.assign(obj_this);//将本身赋值给自己包       //含的obj_this,相当于obj_this.obj_this = obj_this

                    return obj_this;
                };
                this.assign = function(obj){//将本身赋值给自己包       //含的obj_this,相当于obj_this.obj_this = obj_this

                    obj_this = obj;
                };

                /...
                your code are written here
                your_function函数的所有内部方法中用obj_this替代this指针,用obj_this来访问父函数和本函数的所有public方法和属性
                .../

                return this;
            }
  • 通过实验证明(以及在实际工程项目中的应用证明),该方法能够实现js函数的单继承,且单个继承函数在实例化过程中只会产生一次冗余对象,不会随着继承路径的增加而增加,因此性能能够满足平常设计要求。
四、补充
  • 在不用本文提出的模板时,直接通过your_function.prototype = new parent_function();your_function.prototype.construct = your_function;这种方式继承,只能实现一次继承,即B继承A后,C再继承B,则C无法继承A。如果要C也要继承A,则需要在函数C中这样写:B.prototype = new A();B.construct = B;C.prototype = new B();C.construct = C。也就是说在C中要写清楚父函数的所有继承关系,这种设计不友好。因此,解决了这个问题也是本模板的一个亮点。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值