成长脚印-专注于互联网发展
【读书笔记】之Javascript继承模式深入分析
post by:天之骄子 2013-9-2 0:39

    说到Javascript的继承,总结大家对“继承”的实现方法,可能已经达到十多种了(《Javascript面向对象编程指南》列举出了12种方法),但如果真正理解了继承的本质,便会发现真正意义上的继承,可能就只剩没两个了,故,在前面对继承打了引号。

一、什么是继承?

先来看看《Javascript王者归来》中对继承的定义:
    前面已经说过,如果两个类都是同一个实例的类型,那么它们之间存在着某些关系,我们把同一个实例的类型之间的泛化关系称为“继承”。
    这很容易理解,例如,“白马”是一种“马”,“白马”和“马”之间的关系就是继承关系。“一匹白马”是“白马”的一个实例,“白马”是“一匹马”的类型,而“马”同样是“一匹马”的类型,“马”是“白马”的泛化,所以“白马”继承自“马”。
    一旦确定了两个类的继承关系,就至少意味着三层含义,
    一是,子类的额实例可以共享父类的方法,
    二是,子类可以覆盖父类的方法或者扩展新的方法,
    三是,子类和父类都是子类实例的“类型”。

    在Javascript中,并不直接从文法上支持继承,换句话说,Javascript没有实现“继承”的语法,从这个意义上来说,Javascript并不是直接的面向对象的语言。
    在Javascript中,继承是通过模拟的方法来实现的。

上述对继承的定义,很好诠释了继承的本质,只有深刻理解这三层含义,才能透彻理解继承。


二、继承的实现层面

        和众多的图书中对继承方法的汇总一样,很多人都只是对N种实现方法的堆砌,所以让人阅读起来感觉很混乱,看时懂了,看完迷糊。所以在列举方法之前,需要先区分这些方法的实现层面。继承的实现层面有如下两种:

        一、构造函数层面的实现方法(比如组合继承)
        二、对象层面的实现方法(比如深浅拷贝)


        后面会分析各种“继承”方式的优缺点,最后会总结出哪些才是实现了真正意义上的继承。


三、JavaScript中的值类型和引用类型(可参考文章JavaScript值类型和引用类型

        看到这儿你可能会问,写继承怎么说到这两个概念去了,跑题了啊!但提醒你的是,这两个概念会在后面对某些继承方式缺点的描述中多次提到,所以也必须透彻理解

1、JavaScript值类型和引用类型有哪些

    (1)值类型:数值、布尔值、null、undefined。
    (2)引用类型:对象、数组、函数。

2、如何理解值类型和引用类型及举例

    (1)值类型理解:变量的交换等于在一个新的地方按照连锁店的规范标准(统一店面理解为相同的变量内容)新开一个分店,这样新开的店与其它旧店互不相关、各自运营。
    (2)引用类型理解:变量的交换等于把现有一间店的钥匙(变量引用地址)复制一把给了另外一个老板,此时两个老板同时管理一间店,两个老板的行为都有可能对一间店的运营造成影响。


四、继承的实现方式汇总

构造函数层面
    1、原型链继承
    2、借用构造函数(伪造对象/经典继承)
    3、组合继承(伪经典继承/类抄写)

对象层面
    4、原型式继承
    5、寄生式继承
    6、寄生组合式继承
    7、实例继承


五、各继承方式分析

1、原型链继承(关于prototype可参考上一篇【读书笔记】之Javascript prototype(JS原型)深入理解)
实现方式很简单:
 function SuperType() {
      this.flag = true;
 }

 SuperType.prototype.getSuperFlag = function() {
      return this.flag;
 };

 function SubType() {
      this.subFlag = false;
 }

 //继承了SuperType
 SubType.prototype = new SuperType();
 
 //扩展了自己的方法
 SubType.prototype.getSubFlag = function() {
      return this.subFlag;
 };

 var st = new SubType();

原型链继承的优点:强大、实现起来方便快捷
当然缺点也是明显的:

        第一个问题是,包含引用类型值的原型属性会被所有实例共享,改变一个实例的引用值类型属性,会直接影响到其它实例。而这也正是为什么要在构造函数中,而不是原型中定义属性的原因。
看看下图就知道了:

        你发现了什么没有?
        当改变子类的实例st1“自己的”colors数组属性时,其它的实例的colors也变成一样的了,其实这个colors数组属性并不是真正属于自己,而是对 SubType原型中该数组(SubType.prototype.colors)的引用(C语言中的指针了解吧,st1.colors -> SubType.prototype.colors <- st2.colors),子类的实例的colors属性都引用于这个数组,    所以修改后直接影响到其它所有实例的该属性。修改name属性也是同样的道理。

        第二个问题是:
在创建子类型的实例时,不能通过new SubType(args)的方式向超类型的构造函数中传递参数。
看看下图:


        第三个问题是:无法直接实现多重继承(比如继承自两个或三个超类)

2、借用构造函数(伪造对象/经典继承)

        正是由于原型链继承方式中,对引用值类型的数据处理的缺点,所以才出现了这个借用构造函数的继承方式。
        这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。 别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用applay()和call()方法也可以在(将来)新创建的对象上执行构造函数,如下所示:
 function SuperType() {
      this.colors = ['red', 'blue', 'green'];
 }

 function SubType() {
      SuperType.call(this);  //调用SuperType()构造函数,并将当前this传入
 }

 var st1 = new SubType();
 st1.colors.push('black'); 
 alert(st1.colors);  //'red,blue,green,black'

 var st2 = new SubType();
 alert(st2.colors);  //'red,blue,green'

看看下图:

        子类的实例st1和st2的colors属性互不干扰了。这到底是为嘛呢?
        SubType实例化时,比如st1,在SubType中this指向这个st1,SuperType.call(this)相当于,st1.colors=['red', 'blue', 'green'];同理,st2也是如此,所以st1和st2各自有了真正属于自己的colors属性而不是引用自原型中的。

借用构造函数的优点:

        一是,解决了原型链继承中共享原型引用值类型属性的问题;
        二是,相对于原型链而言,有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数
看下图:

缺点也很明显:
        并没有实现共享,SubType中的方法根本得不到复用,而且,在超类型的原型中定义的方法,对子类型而言也是不可见的。(见图片中超类原型中的name属性) 结果所有类型都只能使用构造函数模式。 考虑到这些问题,借用构造函数技术也是很少单独使用的。

        (关于第二个优点这个地方为什么没有引用《Javascript高级程序设计》中的例子,个人觉得《高级程序设计》中这个地方完全没有把这个优点讲解明白,相反让人很疑惑很费解)

3、组合继承


        组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。 其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。 这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。
 function SuperType(name) {
      this.name = name;
      this.colors = ['red', 'blue', 'green'];
 }

 SuperType.prototype.sayName = function() {
      console.log(this.name);
 };

 function SubType(name,age) {
      SuperType.call(this, name);
      this.age = age;
 }

 SubType.prototype = new SuperType();

 var st1 = new SubType('大毛',23);
 st1.colors.push('black');
 
 var st2 = new SubType('二毛',22);

看下图:

        可以看到组合继承优势很明显了,规避了前两种方法(原型链继承、借用构造函数)的缺点,整合了前两种方法的优势。不再共享引用值类型,所以各自不会干扰,同时共享了超类原型中的方法。个人觉得这个才应该是经典。

4、原型式继承


        前面谈到的几种继承方式都是构造函数层面实现的继承,下面集中属于对象层面的继承了
        原型式继承,这种方法并没有使用严格意义上的构造函数。 它借助原型可以基于已有对象创建新对象,同时还不必因此创建自定义类型。 为了达到这个目的,给出如下函数:
 
 function extend(o) {
      function F () {}
      F.prototype = o;
      return new F();
 }

         在extend()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。 从本质上讲,object()对传入其中的对象执行了一次浅复制

        未完待续,明日再续~
评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容