zoukankan      html  css  js  c++  java
  • 复习面向对象 -- 继承

      本文是面向对象第三部分--继承,相对于前两个,篇幅过长,理解稍微难点,不过多思考多敲敲,会一下子茅塞顿开,就懂了,不太懂面向对象-创建对象的,可以看这篇文章,传送门,不太懂面向对象-原型与原型链的,可以看这篇文章,传送门

    面向对象继承

      许多语言都支持两种继承方式:接口继承实现继承
      接口继承只继承方法签名(方法签名由方法名称和一个参数列表(方法的参数的顺序和类型)组成,java所属)。
      而js中的函数没有签名,所以无法实现接口继承,只能实现实现继承,而实现继承主要依靠原型以及原型链实现的

      高程中写到:假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念

      js的继承是依据原型以及原型链来实现的,回顾前几节的知识,可以得知,一个构造函数创建出来的实例,都可以访问到该构造函数的的属性,方法,还有构造函数的原型的属性以及方法。

      首先 先了解构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。通俗的说:实例通过内部指针可以访问到原型对象,原型对象通过constructor指针,找到其构造函数。

      那么js中的继承的基本思路就是利用原型以及原型链的特性,改变内部指针的指向,进而实现继承。

     原型链继承:

      js所有继承都基于原型链继承。

    function Animal(){
        this.type = 'animal';
    }
    Animal.prototype.feature = function(){
        alert(this.type);
    }
    function Cat(name,color){
        this.name = name;
        this.color = color;
    }  
    Cat.prototype = new Animal();
    
    var tom = new Cat('tom','blue');
    console.log(tom.name);  // 'tom'
    console.log(tom.color); // 'blue'
    console.log(tom.type);  // 'animal'
    tom.feature()    // 'animal'    

      这个例子中有创建了两个构造函数,Animal构造函数有一个type属性和feature方法。Cat构造函数有两个属性:name和color。实例化了一个Animal对象,并且挂载到了Cat函数的原型上,相当于重写了Cat的原型,所以Cat函数拥有Animal函数的所有属性和方法。
      然后又Cat函数new出一个tom对象,tom对象拥有Cat函数的属性和方法,因此也拥有Animal的属性和方法。

      通过上面的例子,可以总结:通过改变构造函数的原型,进而实现继承。

      ps:
      1 如果在继承原型对象之前,产生的实例,其内部指针还是只想最初的原型对象。

    function Animal(){
        this.type = 'animal';
    }
    Animal.prototype.feature = function(){
        alert(this.type);
    }
    function Cat(name,color){
        this.name = name;
        this.color = color;
    }
    Cat.prototype.type = 'cat';
    Cat.prototype.feature = function(){
        alert(this.type);
    }    
    var tom = new Cat('tom','blue');
    Cat.prototype = new Animal();// 先实例化对象,再重写原型,结果指针还是指向最初的原型
    console.log(tom.name);  // 'tom'
    console.log(tom.color); // 'blue'
    console.log(tom.type);  // 'cat'  ----- 是最初的type
    tom.feature()    // 'cat'

      从打印结果来看:new出来的tom对象,在Cat.prototype重写原型之后,依然还是指向没重写的原型上。

      2 在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。

    function Animal(){
        this.type = 'animal';
        this.size = 'small'
    }
    Animal.prototype.feature = function(){
        alert(this.type);
    }
    function Cat(name,color){
        this.name = name;
        this.color = color;
    }  
    Cat.prototype = new Animal();
    // 使用字面量添加新方法,会导致上一行代码无效
    Cat.prototype = {
        type:'cat',
        feature:function(){
            alert(this.type)
        }
    }
    var tom = new Cat('tom','blue');
    console.log(tom.name);  // 'tom'
    console.log(tom.color); // 'blue'
    console.log(tom.type);  // 'cat'
    tom.feature()    // 'cat'  
    console.log(tom.size)  // undefined   ----- tom拿不到size属性

      由打印结果可知:tom这个对象拿不到将继承的size属性,所以用字面量添加属性或方法,会切断将继承的与实例之间的联系。

      原型链继承并不是完美的,用原型链实现继承,有一定的问题存在

      1 首先 值类型与引用类型在参数传递时,方式是不一样的

       值类型 将值本身拷贝一份赋值给其他变量,若该值发生变化,也不会影响到其他变量。
       引用类型 将指针(内存中的地址)拷贝一份 赋值给其他变量;若内存中的地址内容发生改变,其他变量内的内容也会发生变化。

    var a = 11;
    var b = a;
    
    console.log(a,b) // 11 11
    b = 22;
    console.log(a,b) // 11 22
    
    
    var obj ={
        name:'peter',
        age:18
    };
    var m = obj;
    console.log(m) // {name: "peter", age: 18}
    m.name = 'tom';
    m.age = 22;
    console.log(obj) // {name: "tom", age: 22}

      同样的道理,在原型链继承中,包含引用类型值的原型属性会被所有实例共享,如果某一个实例更改了属性或方法,会影响到原型属性,进而影响所有的实例。

    function Animal(){
        this.type = 'animal';
        this.size = ['large','small'];
    }
    Animal.prototype.feature = function(){
        alert(this.type);
    }
    function Cat(name,color){
        this.name = name;
        this.color = color;
    }  
    Cat.prototype = new Animal();
    
    var tom = new Cat('tom','blue');
    var peter = new Cat('peter','yellow')
    console.log(tom.size);  // ["large", "small"]
    tom.size.push('middle');
    console.log(tom.size);  // ["large", "small", "middle"]
    console.log(tom.size);  // ["large", "small", "middle"]

      通过打印结果可知,在一个实例添加一个颜色时,同时也影响了其他实例。

      2 在高程上说还有个问题,在创建子类型(Cat)的实例时,不能向超类型(Animal)的构造函数中传递参数。意思就是没有办法在不影响所有对象实例的情况下,给超类型(Animal)的构造函数传递参数

    function Animal(type){
        this.type = type;
        this.size = ['large','small'];
    }
    Animal.prototype.feature = function(){
        alert(this.type);
    }
    function Cat(type,name,color){
        this.name = name;
        this.color = color;
    }  
    Cat.prototype = new Animal();
    
    var tom = new Cat('animal','tom','blue');
    console.log(tom.type); // undefined

      当给被继承的构造函数传参数时,发现为undefined,所以原型链继承无法传参数。

      因此原型链继承有这两个问题,所以在实践中很少使用原型链继承。

     借用构造函数继承

      借用构造函数继承的基本思想:就是在子类型构造函数的内部使用 apply() 和 call() 方法调用超类型构造函数里的属性或方法。

      ps:函数只不过是在特定环境中执行代码的对象,因此通过使用 apply() 和 call() 方法也可以在(将来)新创建的对象上执行构造函数。

    function Animal(name){
        this.name = name;
        this.size = ["large", "small"];
    }
    Animal.prototype.say = function(){
        alert(this.name);
    }
    function Cat(name,age){
        Animal.call(this,name);
        this.age = age;
    
    }
    var tom = new Cat('tom',18);
    var peter = new Cat('peter',22);
    console.log(tom); // {name: "tom", size: Array[2], age: 18};
    console.log(peter); // {name: "peter", size: Array[2], age: 22};
    
    tom.size.push('middle');
    console.log(tom.size);  // ["large", "small", "middle"]
    console.log(peter.size); // ["large", "small"]
    
    tom.say();  // Uncaught TypeError: tom.say is not a function

      由上述打印的结果,我们可以得出以下结论:

       1 可以往超类型(Animal)传参数,Animal可以接受一个参数,将参数赋值给一个属性,所以在Cat构造函数内部调用Animal时,就是给Cat的实例设置该属性。

       ps:为了确保Animal 构造函数不会重写Cat的属性,可以在调用超类型构造函数后,再添加应该在子类型中。

    function Cat(name,age){
        Animal.call(this,name);
        this.age=age;
        this.name = 'jerry'; // 如果该属性写在Animal.call(this,name);之前的话,没有作用,还是被调用的给覆盖了
    }

      2 因为是子类型调用超类型,所以每个子类型调用的都是超类型的初始化的属性或内部方法。每个实例都互不影响。
      3 超类型的原型上的方法对子类型不可见,不可被调用。

      借用构造函数继承最主要的就是子类型利用call或apply调用超类型的方式去使用该方法或属性。但是很显然也有缺点:

       1 由于每个子类型声明自己属性或方法,而且别人不能使用,所以不能复用。
       2 无法调用超类型的原型上的方法。

     

     组合继承

      组合继承是采用了原型链继承和借用构造函数继承的方式。
      其基本思想就是:原型链继承实现对原型属性和方法的继承,借用构造函数继承实现对实例属性的继承。

    function Animal(name){
        this.name = name;
        this.size = ["large", "small"];
    }
    Animal.prototype.say = function(){
        alert(this.name);
    }
    function Cat(name,age){
        Animal.call(this,name); // 调用Animal的属性
        this.age = age;
    
    }
    Cat.prototype = new Animal(name); // 调用Ainaml的原型上的方法。
    Cat.prototype.constructor = Cat;  // 保证Cat的原型上的构造器对象还是指向Cat。
    Cat.prototype.skill = function(){
        alert('running');
    }
    var tom = new Cat('tom',18);
    var peter = new Cat('peter',22);
    console.log(tom); // {name: "tom", size: Array[2], age: 18};
    console.log(peter); // {name: "peter", size: Array[2], age: 22};
    
    tom.size.push('middle');
    console.log(tom.size);  // ["large", "small", "middle"]
    console.log(peter.size); // ["large", "small"]
    
    tom.say(); // tom
    peter.say(); // peter

      上面的例子:在Cat构造函数里使用call调用Animal里属性,并且在Cat的原型上实例化Animal,进而调用Animal原型上的方法。

      ps:子类型扩展方法时要放在原型链继承之后,因为原型链继承后,重写了其constructor属性,导致没继承前的属性或方法失效。

    Cat.prototype = new Animal(name); // 调用Ainaml的原型上的方法。
    Cat.prototype.constructor = Cat;  // 保证Cat的原型上的构造器对象还是指向Cat。
    Cat.prototype.skill = function(){   // 该一步要放在调用Animal原型方法之后,如果放在前面,会导致其skill方法失效。
        alert('running');
    }

      这样融合两者的优点,摒弃了缺点。成为最常用的继承方式。但是也有一个不足:无论什么情况下都会调用两次超类型构造函数:1 在创建子类型原型的时候,2 在子类型构造函数内部。

     原型式继承

      原型式继承是道格拉斯.克罗克福德提出的继承方式,其基本思想是:借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型。主要函数如下:

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

      这个函数主要是在内部创建了一个构造函数,该构造函数的原型就是传来的对象(继承),并且返回这个构造函数的实例。
      这种方式的要求是必须有一个对象,作为另一个对象的基础,通过对象的浅拷贝的方式,创建这个对象的副本作为新对象使用,在该基础上进行修改。

    var peter = {
        name:'peter',
        age:18,
        say:function(){
            alert(this.name);
        }
    }
    var tom = object(peter);
    console.log(tom);  // F {}
    tom.say();  // peter
    
    tom.name = 'tom';
    tom.age = 22;
    console.log(tom);  // F {name: "tom", age: 22}
    tom.say();  // tom

      由上述结果可知,新创建的对象,返回的是对象,但是打印tom.say(),出现的是peter,说明新对象调用了原型上的属性和方法。随后在修改后新对象的属性时,会返回新的属性和方法。
      但是显然易见:此模式就是新对象是利用原要继承的对象挂载到原型上的原理,去使用原型上的属性和方法,然后修改其属性和方法。同样如果不修改属性值,会被所有实例共享。

      主要适用于:在没必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,可以使用原型式继承。

      在es5出来一个新方法,可以替代克罗克福德创建的函数: Object.create(),前提条件只传一个参数情况下。

    var tom = Object.create(peter);
    console.log(tom);  // F {}
    tom.say();  // peter
    
    tom.name = 'tom';
    tom.age = 22;
    console.log(tom);  // F {name: "tom", age: 22}
    tom.say();  // tom

      tip:Object.create()

       接受两个参数:
       1.必需。 要用作原型的对象。可以为 null。
       2.可选。 包含一个或多个属性描述符的 JavaScript 对象。“数据属性”是可获取且可设置值的属性。
        数据属性描述符包含 value 特性,以及 writable、enumerable 和 configurable 特性。如果未指定最后三个特性,则它们默认为 false。
       返回值:一个具有指定的内部原型且包含指定的属性(如果有的话)的新对象。

    寄生式继承

      寄生式继承也是克罗克福德提出的,并在原型式继承上进行推广的。其基本思想就是:即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象。
      该继承方式最大的特点就是封装成一个函数,在内部扩展对象的属性或方法

    function inherit(o){
        var clone = Object.create(o);  // 通过调用函数创建一个对象
        clone.type = 'people';    // 扩展对象属性或方法
        clone.say=function(){
            alert(this.name);
        }
        return clone;
    }
    var peter = {
        name:'peter'
    }
    var perterSon = inherit(peter);
    console.log(perterSon.type);    // people
    perterSon.say();  // peter

      inherit函数中返回一个新创建的对象,这个对象有扩展的方法和属性,另一个对象在调用这个方法时会继承扩展属性和方法。
      由此可见,扩展方法已经写死了,所以不能不复用,进而降低效率。

      使用情况:在主要考虑对象而不是自定义类型和构造函数的情况下,可以采用寄生式继承。

     寄生组合式继承

      在组合继承的方式中,有一个不足,就是多次调用超类型构造函数,为了避免这个情况,寄生组合式继承就出现了。
      其基本思路是:通过借用构造函数来继承属性,用原型链的混成形式来继承方法。不必为了指定子类型的原型而调用超类型的构造函数。
      最简单的说法:使用寄生式继承来继承超类型的原型,然后将结果指定给子类型的原型。

    function inheritPrototype(sub,supers){
        var clone = Object.create(supers.prototype);
        clone.constructor = sub;
        sub.prototype = clone;
    }

      这个函数实现了三个步骤:(先传两个参数,一个子类型构造函数sub,一个超类型构造函数super。)
       1 创建超类型原型的一个副本。
       2 为创建的副本添加constructor属性,从而弥补重写原型而失去的默认的constructor属性。
       3 将新创建的对象(副本)赋值给子类型的原型。

    function Animal(name){
        this.name = name;
        this.size = ["large", "small"];
    }
    Animal.prototype.say = function(){
        alert(this.name);
    }
    function Cat(name,age){
        Animal.call(this,name); 
        this.age = age;
    
    }
    inheritPrototype(Cat,Animal);
    Cat.prototype.skill = function(){
        alert('running')
    }
    var tom = new Cat('tom',18);
    console.log(tom); // Cat {name: "tom", size: Array(2), age: 18}
    tom.say();   // tom
    tom.skill();  // running

      由上述可以得知:也能实现组合继承所实现的方案,只不过只调用了一次超类型构造函数,提高了效率,同时原型链也能保持不变,能够使用instanceof和isPrototypeOf()方法,组合继承也是一样的。

    console.log(tom instanceof Cat) // true;
    console.log(tom instanceof Animal) // true

      tip:
      1 instanceof
       该运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。返回值是一个bool。

    obj instanceof Object // 实例obj在不在Object构造函数中

       意思就是:检测Object.prototype是否存在于参数obj的原型链上。

      2 isPrototypeOf()
       该函数用于指示对象是否存在于另一个对象的原型链中。如果存在,返回true,否则返回false。
       该方法属于Object对象,由于所有的对象都"继承"了Object的对象实例,因此几乎所有的实例对象都可以使用该方法。

    后记:
      在复习面向对象中,由于篇幅过长,将知识点分成了三部分,面向对象--创建对象面向对象--原型与原型链,面向对象--继承,对应的知识点我放在了github里,有需要的可以去clone学习,觉得好的话,给个star。
      文章有不对或者不理解的地方,请私信或者评论,一起讨论进步。

     
    参考资料:

      javascript高级程序设计(第三版)
  • 相关阅读:
    MySQL存储过程
    [转载]JDBC应该始终以PreparedStatement代替Statement
    Restlet入门例子 RESTful web framwork for java
    Cglib的使用方法(3)Mixin
    HDOJ_1220
    精华:OracleHelper类
    web.config详解
    ADO.NET结构
    字典树
    WCF、Net remoting、Web service概念及区别
  • 原文地址:https://www.cnblogs.com/sqh17/p/9664882.html
Copyright © 2011-2022 走看看