zoukankan      html  css  js  c++  java
  • js种4种继承法的优缺点

      一:构造法继承

      昨天扔了一堆JavaScript类'继承'的代码,这些代码其实并不是所有的都能正常的执行。不是我不原意写出都能好好执行的继承类代码,而是这些方法本身就各自有自己的优缺点。下面我分别说它们的原理和使用时注意事项。

        构造继承法的原理:

        构造继承法关键代码是function ArrayList01()中的:

      this.base = CollectionBase;
      
    this.base(); 


        这里的base不是C#派生类中的base那个概念,完全就是一个任意的JavaScript变量名。调用this.base();其实就是执行的CollectionBase();,不过不是new CollectionBase();哦!没有new基类那么怎么得到CollectionBase中的方法和属性呢?这里使用了this作用域的一个hack,'欺骗'了脚本引擎。当我们从类ArrayList01的构造函数中调用this.base();时,在基类CollectionBase中的this就是ArrayList01的一个实例,于是执行CollectionBase的构造函数,就动态的把基类的成员和方法attach到ArrayList01实例中了。构造法的问题也就是从这里产生了,注意哦。

        构造继承法的缺陷:

        第一个问题,关键是出在上面那两段代码上,因为始终没有new基类呀。这样带来的问题就是不能把基类CollectionBase中的原型属性和方法attach到ArrayList01的实例中,啊,这样还算是什么继承啊?! 所以在上篇文章中我为了不改动CollectionBase类,而单独为其实现了一个Add()方法(只是为了统一示例代码而已)。解决这个缺陷的方法其实也很简单,就是要求基类不能使用prototype来导入属性和方法,而要把所有的属性和方法都写到构造函数中去,分别是:this.Attribute = ...; 和 this.Method = function() {...};这种形式。
        第二个问题,是this.base = CollectionBase;和this.base();必须写在派生类构造函数的最开头(不是一定要是第一行和第二行,而是它们的前面不能有this.xxx这种定义为ArrayList01导入的任何属性或方法),因为调用this.base();时会向this(ArrayList01的一个实例)中注入基类的属性和方法,如果基类中有和this中已导入的属性和方法重名的,就自动覆盖掉子类中的方法。
        第三个问题,是子类也不能使用prototype来导入属性和方法,这和问题二中的重名覆盖道理一样,由于prototype导入的属性和方法在子类一new的时候就生成了,所以也存在和基类重名而被覆盖的潜在错误威胁。解决办法,和基类编写规则一样不要使用prototype,并且将子类的属性和方法定义代码放在导入继承的代码(this.base = CollectionBase;this.base();)之后。

        构造继承法的示例:   

    <script language="JavaScript">
    document.write('构造继承法:
    <br>');
    var arrayList11 = new ArrayList01();
    arrayList11.Add('a');
    arrayList11.Add('b');
    arrayList11.foo();
    var arrayList12 = new ArrayList01();
    arrayList12.Add('a');
    arrayList12.Add('b');
    arrayList12.Add('c');
    arrayList12.foo();
    </script>


        示例运行结果为:

    构造继承法:
    [class ArrayList01]: 
    2: a,b
    [class ArrayList01]: 
    3: a,b,c


        小结一下:JavaScript的构造继承法其实看起来还是比较直观的,因为子类构造函数中有对基类构造函数的调用,似乎在语法上还比较容易被接受。可是由于没有new基类,带来了不能获得prototype导入的属性和方法的缺陷。解决这个缺陷的方法虽然不难,可是由此而给基类编写限制一个凌驾于JavaScript语法规范的规则,可操作性不是很好。子类也不能利用prototype特性来导入属性和方法,会与基类之间存在潜在的重名覆盖问题。所以对于复杂的基类,不推荐这种继承方法,因为类复杂了,使用prototype可以规范类代码,使类的定义看起来比较的舒服

        应用场景:小规模类之间的继承,基类和子类的属性方法在5-8个。还有就是以构造函数中赋值方式导入类的属性和方法,而不用prototype导入的类编写习惯的时候。

      二:原型继承法

      

    原型(prototype)是JavaScript实现面向对象编程的一个基础,但它并不是唯一的构造类的方法,我们完全可以不使用 prototype而实现类的编写(把属性和方法的附加全都写在构造函数里面就行了)。不过原型除了可以为Object的子类添加新的属性和方法外,它还可以为脚本环境中的内部对象继续添加原型属性和方法,比如我们最常用的给内部对象String添加一个Trim方法来删除字符串两端的空格,代码为:

       String.prototype.Trim = function()
       {
            
    return this.replace(/(^\s*)|(\s*$)/g, '');
       }

        这样我们就可以在任何的String实例中使用Trim方法了,用惯了这种原型系统,有的时候反而还觉得传统OOP没有它爽,kaka。言归正传,继续讲我们的原型继承法。

        原型继承法的原理:

        原型继承法的关键代码是其构造函数function ArrayList02()下的第一句:

       ArrayList02.prototype = new CollectionBase();
       ArrayList02.prototype.constructor = ArrayList02;

        Ae 把prototype都覆盖成基类的一个实例了,子类还怎么使用prototype呢?这里不要着急,反正JavaScript的对象实例都是可以动态增删属性和方法的,基类实例作为prototype不就正好等于extends了CollectionBase吗?之后再使用 XXX.prototype.xxx = function(),可以继续获得新增了属性和方法的对象。注意ArrayList02.prototype是在ArrayList02的构造函数外被初始化为基类的实例的

        再来看第二句,为什么要把ArrayList02赋值给新prototype的constructor呢?如果不做这个赋值,当我们从 ArrayList02的实例中,通过???.prototype.constructor去却其构造函数,将会获得CollectionBase。这不是我们的本意,而且这是使用instanceof关键之比较对象实例和对象也会出错。

        原型继承法的缺陷:

        原型继承法有两个缺陷,第一个是由于类的原型(prototype)实际上是一个Object的实例,它不能再次被实例化(它的初始化在脚本装载时已经执行完毕)。这么意思呢?我们知道在新建对象实例时,我们使用语句new ArrayList02(),这时我们可以看做JavaScript脚本引擎把prototype的一个浅拷贝作为this返回给类的实例(这里其实没有发生拷贝,只是利用浅拷贝这个概念来帮助理解),如果类没有附加任何原型属性和原型方法,那么就等于返回了一个new Object()实例。问题就出在这里了,由于new对 prototype执行的是浅拷贝,如果prototype的原型属性里有对象类型的属性,那么就会造成共享对象实例的问题(类似于在传统OOP的类定义中使用了static修饰符来修饰了属性)。这个缺陷下面会有示例演示,避免的办法就是不要在基类中定义对象类型的属性,比如: Array、Object和Date等。
        第二个缺陷和上次讲的"构造继承法"的缺陷差不多,也是关于子类定义是语句顺序的。就是说代码:ArrayList02.prototype = new CollectionBase();必须在所有prototype定义之前执行,很简单,如果在原型属性和方法都导入完成后,再执行这个语句,就定于把之前的导入全都覆盖掉了:(。解决办法就是按我给文章(1)中的那个顺序来写,郁闷吧?kaka

        原型继承法的示例:

    <script language="JavaScript">
    document.write('原形继承法:
    <br>'); 
    var arrayList21 = new ArrayList02();
    arrayList21.Add('a');
    arrayList21.Add('b');
    arrayList21.foo();
    var arrayList22 = new ArrayList02();
    arrayList22.Add('a');
    arrayList22.Add('b');
    arrayList22.Add('c');
    arrayList22.foo();
    </script>


        示例运行结果为:

    原形继承法:
    [class ArrayList02]: 2: a,b
    [class ArrayList02]: 3: a,b,c,a,b


        发现问题了吧?实例arrayList22的foo()居然输出了a,b,c,a,b@_@... 这就是前面说的prototype对象浅拷贝带来的问题。不过为什么arrayList22中的集合计数器却仍然是3呢?这是因为this.m_Count是整数类型,这种类型又叫值类型(和C#里的值类型、对象类型概念一样的)。值类型不存在浅拷贝和深拷贝的问题,可以看成都是深拷贝。

        小结:JavaScript的原型继承法,虽然有prototype浅拷贝那么严重的bug,不过它却是使用比较多的继承方式。因为我们很少在基类里定义属性,更别说对象类型的属性了,所以引发这个错误的可能性不是很大,只是它是个潜在的隐患:(。至于第二个缺陷,也是一个潜在bug,只要自己定义类的时候能比较清醒就不会犯错误。'重载'也比较简单,如果在ArrayList02.prototype = new CollectionBase();后有重名的属性或方法导入,自动覆盖基类中的属性或方法就像当于重载了。

        应用场景:基类没有属性,至少是要没有对象类型的属性。这种继承的优点是保持了子类构造函数的完整,可以不在里面添加任何和继承有关系的代码,所有继承和重载操作都由对原型(prototype)的操作来完成。

      三:实例继承法

      实例继承法的原理:

        实例继承法的关键代码是其构造函数function ArrayList03()中的:

     var base = new CollectionBase();
     
    // ...
     return base;


        其实就是在子类构造时创建基类的一个实例,然后把基类实例作为返回值实返回。这时的所谓继承操作都是对Object实例(基类也就是一个Object的派生类的实例)的动态读写,为其添加属性和方法等,根本没有涉及任何与继承相关的范畴,不过最终的效果上还是让人觉的是实现了继承。

        实例继承法的缺陷:

        这种继承法看起来还是比较清楚的,特别是如果你已理解了JavaScript对象的动态特性。不过这种方法最大的缺陷也是来至于对类代码的书写的要求上,由于我们在子类构造函数中创建了基类实例,所以对基类的书写时没有任何的要求,只要是一个脚本引擎认为正确的类就可以了。不过子类就不能随便写了,由于子类的属性和方法通过对象的动态特性来实现,所以子类也不能使用原型属性(prototype)来实现属性和方法的导入,而必须用inline的方式写在子类的构造函数里,这个和构造法实现继承的限制很相似,不过前者是限制基类的书写不能使用prototype属性。

        实例继承法的示例:

     document.write('实例继承法:<br>'); 
     
    var arrayList31 = new ArrayList03();
     arrayList31.Add('a');
     arrayList31.Add('b');
     arrayList31.foo();
     
    var arrayList32 = new ArrayList03();
     arrayList32.Add('a');
     arrayList32.Add('b');
     arrayList32.Add('c');
     arrayList32.foo();


        示例运行结果:

     实例继承法:
     [class ArrayList03]: 
    2: a,b
     [class ArrayList03]: 
    3: a,b,c


        小结:实例继承法其实有些偷梁换柱的味道,因为这样得到的实例,针对instanceOf来说的话,完全是其基类的一个扩展。而子类的实例是被扔掉了的,因为new ArrayList03()返回的是基类实例(return base;)。这完全没有了任何继承的味道,叫做类扩展还贴切些。优点是对基类的编写没有任何特殊要求,不过同样需要规定子类的写法,子类不能使用prototype来导入原型方法,并且在子类构造函数中创建基类实例var base = new CollectionBase();需要在构造函数开头(即任何向base添加属性和方法之前)。

        应用场景:没有太经典的应用场景,不过对于基类比较复杂,而子类需要添加的属性方法很少的继承,实例法还是显得挺清晰的。特别是对于JScript对象动态扩展很熟悉的人,就更觉得明确了。

      四:附加继承法

      附加继承法,虽然是我自己杜撰出来的,而且还有一些前面三种继承法的影子,不过这个方法不可否认的,可以把前面说到继承的问题都cut掉。下面我们就来仔细说说到底它是为什么这么有武功和智慧的呢?

        附加继承法的原理:

        附加继承法的关键代码是其构找函数ArrayList04()中的:

      this.base = new CollectionBase();
      
      
    for ( var key in this.base )
      {
          
    if ( !this[key] )
          {
              
    this[key] = this.base[key];
          } 
      }


        这里其实给不给this附加一个base并不重要,也一点不会影响我们的这个继承方法。首先我们看到在构造函数的第一句话中,我们立马就new了一个基类实例出来,这就说明我们的继承对基类的书写是没有任何要求的,用前面实例继承法中的说法就是,只要脚本引擎认为正确的类就都可以。我们知道构造继承法为什么有问题呢?就是因为它始终没有创建基类的实例。而原型继承法虽然也创建了基类实例,不过它把积累实例直接赋给了子类的prototype属性,以至于搞的对子类书写有特殊的要求。

        然后接下来一个for( in )循环,把基类具有的所有属性和方法都附加到子类的实例this中了,这也是我把这个继承方法叫附加法的原因。这一步和构造继承法的原理相当的类似,只是构造继承法是用了this作用域置换的一个技巧,把这个附加的过程让基类构造函数来完成了,不过同时也给构造继承法带来基类书写的特别要求,不能使用其prototype特性。当然附加法仍然是没有这个要求的。

        附加继承法的Update:

     Object.prototype.Extends = function(BaseClass)
     {
         
    if ( arguments.length >= 6 )
         {
             
    throw new Error('Only can supprot at most 5 parameters.');
         }
        
    var base;
        
    if ( arguments.length > 1 )
         {
             
    var arg01 = arguments[1];
             
    var arg02 = arguments[2];
             
    var arg03 = arguments[3];
            
    var arg04 = arguments[4];
             base 
    = new BaseClass(arg01, arg02, arg03, arg04);
         }
        
    else
         {
             base 
    = new BaseClass();
         }
         
    for ( var key in base )
         {
             
    if ( !this[key] )
             {
                 
    this[key] = base[key];
                 
    if ( typeof(base[key]) != 'function' )
                 {
                     
    delete base[key];
                 }
             }
         }
         
    this.base = base;
         
    // base.Inherit = this;
     
    };

        
        这样我们就的继承就可以直接写成:

     function ArrayList04()
     {
         
    this.Extends(CollectionBase);
         
    // ...
     
    }


        同时还提供了对基类继承时,传递参数给基类的支持,比如:

     function ListItem()
     {
         
    this.Extends(ListItemBase, text, value);
         
    // ...
     
    }


        对于基类,会执行new ListItemBase(text, value);这样的操作来生成基类的实例。

        附加继承法的缺陷:

        从目前我的使用来看,如果不使用override技术来重写方法,然后还在新的方法中去调用基类的方法(这个技术我会以后再讲,因为它不影响也不属于我们今天讨论的继承方式的这个话题)的话。附加法基本没有缺陷,一定要说有就的话就是:使用一个for( in )循环来进行基类的导入,语法上很ugly:(

        附加继承法的示例:

     document.write('附加继承法:<br>'); 
     
    var arrayList41 = new ArrayList04();
     arrayList41.Add('a');
     arrayList41.Add('b');
     arrayList41.foo();
     
    var arrayList42 = new ArrayList04();
     arrayList42.Add('a');
     arrayList42.Add('b');
     arrayList42.Add('c');
     arrayList42.foo();


        示例运行结果为:

     附加继承法:
     [class ArrayList04]: 
    2: a,b
     [class ArrayList04]: 
    3: a,b,c


        小结:附加继承法是看起来最不像继承,但却是实际使用中最sexy(PS:这是我们boss对好代码的称呼)的解决方案。其override也非常的清晰明了,只要在this.Extends(BaseClass);语句后有同名的方法被导入子类,就会自动覆盖从基类中导入的方法,实现override的效果。

        使用场景:anywhere, anytime, anybody...

        这话似乎说大了,完美的东西一定是没有的,附加继承法也是有缺陷的,只不过这个缺陷不属于继承这个范畴,而是对其它OO编程特性的模拟中出现的问题,以后再谈。再唐僧一下:光是类的继承和使用,附加继承法是没有任何问题的。

        总算完成了JScript模拟面向对象编程中实现继承的各种研究。

  • 相关阅读:
    软件测试的术语及解释总结
    测试工具
    数据库客户端连接
    应用程序发生错误怎么修改
    软件测试容易忽略的缺陷
    LR中错误解决方法
    LR结果分析2
    LR结果分析
    Hive
    zookeeper
  • 原文地址:https://www.cnblogs.com/pricks/p/1667185.html
Copyright © 2011-2022 走看看