• 【转载】Javascript原型继承-学习笔记


    阮一峰这篇文章写的很好

    http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html

    笔记如下:

    一直很难理解Javascript语言的继承机制。
    它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。
    读到法国程序员Vjeux的解释,才恍然大悟,完全明白了Javascript为什么这样设计。

    一、从古代说起

    Javascript里面所有的数据类型都是对象(object),这一点与Java非常相似。但是,他随即就遇到了一个难题,到底要不要设计"继承"机制呢?

    二、Brendan Eich的选择

    Brendan Eich最后还是设计了"继承"。
    但是,他不打算引入"类"(class)的概念,因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,
    这好像有点太正式了,而且增加了初学者的入门难度。 C
    ++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,
    而是构造函数。 例来说,现在有一个叫做DOG的构造函数,表示狗对象的原型。   function DOG(name){     
    this.name = name;   } 对这个构造函数使用new,就会生成一个狗对象的实例。   var dogA = new DOG('大毛');   alert(dogA.name); // 大毛

    三、new运算符的缺点

    用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

    四、prototype属性的引入

    Brendan Eich决定为构造函数设置一个prototype属性。

    这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。

    实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

    属性放在prototype对象里,是两个实例对象共享的。只要修改了prototype对象,就会同时影响到两个实例对象。

    五、总结 

    由于所有的实例对象共享同一个prototype对象,那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象一样。

    这就是Javascript继承机制的设计思想。下面三篇文章,介绍了继承机制的具体应用方法:

      * 《Javascript面向对象编程(一):封装》

      * 《Javascript面向对象编程(二):构造函数的继承》

      * 《Javascript面向对象编程(三):非构造函数的继承》

    笔记如下:

    一. 封装

    为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。

    所谓"构造函数",其实就是一个普通函数,但是内部使用了this变量

    对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。

    function Cat(name,color){
        this.name=name;
        this.color=color;
      }
    
    var cat1 = new Cat("大毛","黄色");
      var cat2 = new Cat("二毛","黑色");
      alert(cat1.name); // 大毛
      alert(cat1.color); // 黄色

    Javascript还提供了一个instanceof运算符,验证原型对象与实例对象之间的关系。

    alert(cat1 instanceof Cat); //true
    alert(cat2 instanceof Cat); //true

    构造函数模式的问题

    构造函数方法很好用,但是存在一个浪费内存的问题。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。

    Prototype模式

    Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

    这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。

    这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。

      function Cat(name,color){
        this.name = name;
        this.color = color;
      }
      Cat.prototype.type = "猫科动物";
      Cat.prototype.eat = function(){alert("吃老鼠")};

    还有一种写法:

    function DOG(name){
        this.name = name;
      }
      DOG.prototype = { species : '犬科' };
    
      var dogA = new DOG('大毛');
      var dogB = new DOG('二毛');
    
      alert(dogA.species); // 犬科
      alert(dogB.species); // 犬科

    Prototype模式的验证方法

    为了配合prototype属性,Javascript定义了一些辅助方法,帮助我们使用它。

    isPrototypeOf()

    这个方法用来判断,某个proptotype对象和某个实例之间的关系。

    alert(Cat.prototype.isPrototypeOf(cat1)); //true

    hasOwnProperty()

    每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

    alert(cat1.hasOwnProperty("name")); // true

    alert(cat1.hasOwnProperty("type")); // false

    in运算符

    in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。

    alert("name" in cat1); // true

    in运算符还可以用来遍历某个对象的所有属性。

    for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }

    二. 构造函数的继承

    要介绍的是,对象之间的"继承"的五种方法

    一、 构造函数绑定

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

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

    二、 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 = new Animal();"这一行,
    Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。   alert(Cat.prototype.constructor
    == Animal); //true 更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。   alert(cat1.constructor == Cat.prototype.constructor); // true 因此,在运行"Cat.prototype = new Animal();"这一行之后,cat1.constructor也指向Animal!   alert(cat1.constructor == Animal); // true 这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。
    这就是第二行的意思。 这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,   o.prototype
    = {}; 那么,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。   o.prototype.constructor = o;

    三、 直接继承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

    四、 利用空对象作为中介

    由于"直接继承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

    封装成一个函数:

    function extend(Child, Parent) {
    
        var F = function(){};
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
        Child.uber = Parent.prototype;
      }
    
    使用的时候,方法如下
      extend(Cat,Animal);
      var cat1 = new Cat("大毛","黄色");
      alert(cat1.species); // 动物
    
    这个extend函数,就是YUI库如何实现继承的方法

    另外,说明一点,函数体最后一行 Child.uber = Parent.prototype; 意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

    五、 拷贝继承

    可以换一种思路,纯粹采用"拷贝"方法实现继承。

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

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

    使用的时候,这样写:

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

    三. 非构造函数的继承

    这个系列的第一部分介绍了"封装",第二部分介绍了使用构造函数实现"继承"。

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

    比如,现在有一个对象,叫做"中国人"。
      var Chinese = {
        nation:'中国'
      };
    还有一个对象,叫做"医生"。
      var Doctor ={
        career:'医生'
      }
    请问怎样才能让"医生"去继承"中国人",也就是说,我怎样才能生成一个"中国医生"的对象?

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

    二、object()方法

    一个object()函数,可以做到这一点。

      function object(o) {
        function F() {}
        F.prototype = o;
        return new F();
      }
    这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
    
    使用的时候,第一步先在父对象的基础上,生成子对象:
      var Doctor = object(Chinese);
    
    然后,再加上子对象本身的属性:
      Doctor.career = '医生';
    
    这时,子对象已经继承了父对象的属性了。
      alert(Doctor.nation); //中国

    三、浅拷贝

    除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。

    下面这个函数,就是在做拷贝:
      function extendCopy(p) {
        var c = {};
        for (var i in p) { 
          c[i] = p[i];
        }
        c.uber = p;
        return c;
      }
    使用的时候,这样写:
    var Doctor = extendCopy(Chinese);
      Doctor.career = '医生';
      alert(Doctor.nation); // 中国

    但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

    四、深拷贝

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

    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;
      }

    使用的时候这样写:

    var Doctor = deepCopy(Chinese);
    
    现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:
      Chinese.birthPlaces = ['北京','上海','香港'];
      Doctor.birthPlaces.push('厦门');
    
    这时,父对象就不会受到影响了。

    alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门

    alert(Chinese.birthPlaces); //北京, 上海, 香港

    目前,jQuery库使用的就是这种继承方法

    (完)

  • 相关阅读:
    轻量级数据库sqlite的使用
    Integer引发的思考
    css限制显示行数
    数据库 chapter 17 数据仓库与联机分析处理技术
    数据库 chapter 15 对象关系数据库系统
    数据库 chapter 16 XML数据库
    数据库 chapter 14 分布式数据库系统
    数据库 chapter 11 并发控制
    数据库 chapter 12 数据库管理系统
    数据库 chapter 13 数据库技术新发展
  • 原文地址:https://www.cnblogs.com/charlesblc/p/6014457.html
Copyright © 2020-2023  润新知