• home > webfront > ECMAS > javascript >

    JavaScript OPP编程分析:构造函数实现继承于非构造函数继承

    Author:zhoulujun@live.cn Date:

    JavaScript到底是怎么样实现面向对象的一些特征的呢 ?构造函数 call 绑定、prototype模式、直接继承prototype、拷贝继承 ,这些继承的原理是啥?

    在alloyteam博客看到一篇关于JavaScript继承的文章《 JavaScript-数据结构和算法(程序=数据结构+算法)》 ,自己再来重复一遍轮子

    这些将JavaScript作为一种面向对象的语言进行编程,那么JavaScript到底是怎么样实现面向对象的一些特征的呢,首先,我们来看看JavaScript怎么样来定义一个构造函数。

    借用构造函数实现继承

    JavaScript中借用构造函数实现继承的代码

    //首先创建父类 
    function Person(name, age, address) { 
        this.name = name; 
        this.age = age; 
        this.address = address; 
    } 
    //创建子类 
    function Student(score) { 
        this.score = score; 
        //可以用call方法或者是apply方法调用函数的构造函数 
        //调用父类的构造函数,通过call方法调用Person类的构造函数。
        //这样就会在student中初始化Person对象,student也就有了Person的属性的副本 
        Person.call(this,"zhangsan",22,"中国北京!"); 
    } 
    
    var student = new Student(100); 
    alert(student.address + student.score + "分");

    如何生成一个"继承"多个对象的实例。 

    比如,现在有一个"动物"对象的构造函数 和 一个"猫"对象的构造函数, 

    function Animal(){
      this.species = "动物";
    }
    function Cat(name,color){
      this.name = name;
      this.color = color;
    }

    怎样才能使"猫"继承"动物"呢? 

    1. 构造函数绑定 

    最简单的方法,大概就是使用call或apply方法,将父对象的构造函数绑定在子对象上,也就是在子对象构造函数中加一行: 

    function Cat(name,color){
      Animal.apply(this, arguments);
      this.name = name;
      this.color = color;
    }
    var cat1 = new Cat("大毛","黄色");
    alert(cat1.species); // 动物

    2. 直接继承prototype

    由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。 

    现在,我们先将Animal对象改写: 

    function Animal() {}
    Animal.prototype.species = "动物";

    然后,将Cat的prototype对象,然后指向Animal的prototype对象,这样就完成了继承。 

    Cat.prototype = Animal.prototype;
    Cat.prototype.constructor = Cat;
    var cat1 = new Cat("大毛","黄色");
    alert(cat1.species); // 动物

    与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。 

    所以,上面这一段代码其实是有问题的。请看第二行 

    Cat.prototype.constructor = Cat; 

    这一句实际上把Animal.prototype对象的constructor属性也改掉了! 

    alert(Animal.prototype.constructor); // Cat 

    3. prototype模式 

    更常见的做法,则是使用prototype属性。
    如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。

    Cat.prototype = new Animal();
    Cat.prototype.constructor = Cat;
    var cat1 = new Cat("大毛","黄色");
    alert(cat1.species); // 动物

    代码的第一行,我们将Cat的prototype对象指向一个Animal的实例。

    Cat.prototype = new Animal();

    它相当于完全删除了prototype 对象原先的值,然后赋予一个新值。但是,第二行又是什么意思呢?但是,第二行又是什么意思呢?

    Cat.prototype.constructor = Cat;

    原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数。也就是说,Cat.prototype 这个对象的constructor属性,是指向Cat的。
    我们在前一步已经删除了这个prototype对象原来的值,所以新的prototype对象没有constructor属性,所以我们必须手动加上去,否则后面的"继承链"会出问题。这就是第二行的意思。
    总之,这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,

    o.prototype = {};

    那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。

    o.prototype.constructor = o;


    4. 利用空对象作为中介

    由于"直接继承prototype"存在上述的缺点,所以可以利用一个空对象作为中介。 

    var F = function(){};
    F.prototype = Animal.prototype;
    Cat.prototype = new F();
    Cat.prototype.constructor = Cat;

    F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。 

    alert(Animal.prototype.constructor); // Animal 

    5. prototype模式的封装函数 

    我们将上面的方法,封装成一个函数,便于使用。 

    function extend(Child, Parent) {
      var F = function(){};
      F.prototype = Parent.prototype;
      Child.prototype = new F();
      Child.prototype.constructor = Child;
      Child.uber = Parent.prototype;
    }

    这个extend函数,就是YUI库如何实现继承的方法。 

    另外,说明一点。函数体最后一行 

    Child.uber = Parent.prototype; 

    意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。这等于是在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。 

    6.拷贝继承 

    上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用"拷贝"方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗? 

    首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

    function Animal(){}
    Animal.prototype.species = "动物";

    然后,再写一个函数,实现属性拷贝的目的。

    function extend2(Child, Parent) {
      var p = Parent.prototype;
      var c = Child.prototype;
      for (var i in p) {
        c[i] = p[i];
      }
      c.uber = p;
    }

    这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。 

    今天是最后一个部分,介绍不使用构造函数实现"继承"。 

    什么是"非构造函数"的继承? 

    比如,现在有一个对象,叫做"中国人"。 还有一个对象,叫做"医生"。 

    var Chinese = {
      nation:'中国'
    }; 
    var Doctor ={
      career:'医生'
    }

    请问怎样才能让"医生"去继承"中国人",也就是说,我怎样才能生成一个"中国医生"的对象? 

    这里要注意,这两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现"继承"。 

    object()方法 

    json格式的发明人Douglas Crockford,提出了一个object()函数,可以做到这一点。 

    function object(o) {
      function F() {}
      F.prototype = o;
      return new F();
    }

    这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。 

    使用的时候,第一步先在父对象的基础上,生成子对象:

    var Doctor = object(Chinese); 
    Doctor.career = '医生'; // 然后,再加上子对象本身的属性,这时,子对象已经继承了父对象的属性了。
    alert(Doctor.nation); //中国

    我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。 

    深拷贝 

    所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。 

    function deepCopy(p, c) {
      var c = c || {};
      for (var i in p) {
        if (typeof p[i] === 'object') {
          c[i] = (p[i].constructor === Array) ? [] : {};
          deepCopy(p[i], c[i]);
        } else {
          c[i] = p[i];
        }
      }
      return c;
    }


    可以看一遍《JavaScript继承的实现方式:原型语言对象继承对象原理剖析


    转载本站文章《JavaScript OPP编程分析:构造函数实现继承于非构造函数继承》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js/2016_0315_7707.html