美文网首页js
原型、原型链及继承关系

原型、原型链及继承关系

作者: 寻梦人_b67c | 来源:发表于2019-05-10 17:06 被阅读0次

    原型:

    在讲原型关系之前给我们来看一张图片:

    原型关系图

    由图我们可知几个关系:

    • 每一个构造函数都有(原型)prototype指向它的原型对象。
    • 原型对象有constructor指向它的构造函数。
    • 构造函数可以通过new 的创建方式创建实例对象
    • 实例对象通过proto指向它的原型对象。
    • 原型对象也有自己的原型对象,通过proto指向。

    原型链

    如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性.如果还找不到则往原型的原型上找,这样一个层层查找形成的一个链式的关系被称为原型链。
    如图:


    image.png

    为了解释这个过程,用下面的例子做下说明:

    function Father(){
        this.property = true;
    }
    Father.prototype.getFatherValue = function(){
        return this.property;
    }
    function Son(){
        this.sonProperty = false;
    }
    //继承 Father
    Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
    Son.prototype.getSonVaule = function(){
        return this.sonProperty;
    }
    var instance = new Son();
    console.log(instance.getFatherValue());//true
    

    可见son实例对象找不到getFatherValue方法,只能前去Father原型那里去找,返回值为true。
    如果,对子类son进行改造:

    function Father(){
        this.property = true;
    }
    Father.prototype.getFatherValue = function(){
        return this.property;
    }
    function Son(){
        this.sonProperty = false;
        this.getFatherValue = function(){
        return this.sonProperty;
        }
    }
    //继承 Father
    Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
    Son.prototype.getSonVaule = function(){
        return this.sonProperty;
    }
    var instance = new Son();
    console.log(instance.getFatherValue());//false
    

    你会发现当子类里出现相同的方法时,则执行子类中的方法,也就验证了之前的实例对象查找引用属性的过程。

    确定原型和实例的关系

    使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种.

    第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点.

    console.log(instance instanceof Object);//true
    console.log(instance instanceof Father);//true
    console.log(instance instanceof Son);//true
    

    由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 因此, 这三个构造函数的结果都返回了true.

    第二种是使用 isPrototypeOf() 方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true, 如下所示.

    console.log(Object.prototype.isPrototypeOf(instance));//true
    console.log(Father.prototype.isPrototypeOf(instance));//true
    console.log(Son.prototype.isPrototypeOf(instance));//true
    

    原型链存在的问题。

    原型链并非十分完美, 它包含如下两个问题:

    • 问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
    • 问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
      有鉴于此, 实践中很少会单独使用原型链.

    为此,下面将有一些尝试以弥补原型链的不足.

    js 继承

    借用构造函数(经典继承)

    为解决原型链中上述两个问题, 我们开始使用一种叫做借用构造函数(constructor stealing)的技术(也叫经典继承).

    基本思路:就是在子类的构造函数里调用父类的构造函数。

    function Father(){
        this.colors = ["red","blue","green"];
        function hello() {
            console.log('hello world')
        }
    }
    function Son(){
        Father.call(this);//继承了Father,且向父类型传递参数
    }
    var instance1 = new Son();
    instance1.colors.push("black");
    console.log(instance1.colors);//"red,blue,green,black"
    
    var instance2 = new Son();
    console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的
    
    • 优点
      特别注意的是引用类型的值是独立的。
      很明显,借用构造函数一举解决了原型链的两大问题:
    1. 其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;
    2. 其二, 子类型创建时也能够向父类型传递参数.
    • 缺点
    1. 构造函数无法复用:如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题--方法都在构造函数中定义, 因此函数复用也就不可用了
    2. 超类型(如Father)中定义的方法,对子类型而言也是不可见的.(超类里的方法在子类里无法调用,比如hello方法就无法调用,亲测是这样的,有兴趣可以动手一试)

    考虑此,借用构造函数的技术也很少单独使用.

    原型继承

    该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型可以基于已有的对象创建新对象, 同时还不必因此创建自定义类型. 大意如下:

    基本思路:在create()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.

      function create(o){
        function Fn() {}
        Fn.prototype = o;
        return new Fn();
      }
    

    实质上就是对传入的实例o进行了一次浅拷贝。

    function Father(){
        this.colors = ["red","blue","green"];
    }
    
    let fa = new Father()
    var instance1 =create(fa);
    instance1.colors.push("black");
    console.log(instance1.colors);  // [ 'red', 'blue', 'green', 'black' ]
    
    var instance2 = create(fa);
    instance2.colors.push("white");
    console.log(instance2.colors); //[ 'red', 'blue', 'green', 'black', 'white' ]
    

    在此例中:instance1与instance的原型是同一个对象,当instance1操作原型的引用类型数值,也会影响到instance2。此时数据是共享的。
    再看下面这个例子:

    function Father(){
        this.colors = ["red","blue","green"];
    }
    
    var instance1 = create(new Father());
    instance1.colors.push("black");
    console.log(instance1.colors);  // [ 'red', 'blue', 'green', 'black' ]
    
    var instance2 = create(new Father());
    instance2.colors.push("white");
    console.log(instance2.colors); // [ 'red', 'blue', 'green', 'white' ]
    

    此时由于原型实例不是同一个,数据不在共享。

    在 ECMAScript5 中,通过新增 object.create() 方法规范化了上面的原型式继承.
    object.create() 接收两个参数:

    • 一个用作新对象原型的对象
    • (可选的)一个为新对象定义额外属性的对象

    关键点:原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式一样.

    组合继承

    组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。

    基本思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.

    如下例:

    function Father(name){
        this.name = name;
        this.colors = ["red","blue","green"];
    }
    Father.prototype.sayName = function(){
        alert(this.name);
    };
    function Son(name,age){
        Father.call(this,name);//继承实例属性,第一次调用Father()
        this.age = age;
    }
    Son.prototype = new Father();//继承父类方法,第二次调用Father()
    Son.prototype.sayAge = function(){
        alert(this.age);
    }
    var instance1 = new Son("louis",5);
    instance1.colors.push("black");
    console.log(instance1.colors);//"red,blue,green,black"
    instance1.sayName();//louis
    instance1.sayAge();//5
    
    var instance1 = new Son("zhai",10);
    console.log(instance1.colors);//"red,blue,green"
    instance1.sayName();//zhai
    instance1.sayAge();//10
    

    在这个例子中,类Son通过构造函数继承可以向父类Father传参,同时能够保证实例数据不被共享。同时通过原型继承可以复用父类的方法,两继承组合起来,各取所需。

    组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式. 而且, instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象.

    此处调用了两次父类的构造函数,后面的寄生式组合继承将会对这个问题进行优化。

    寄生式继承

    寄生式继承是与原型式继承紧密相关的一种思路。
    基本思路:寄生式继承的思路与(寄生)构造函数和工厂模式类似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象. 如下.

    function createAnother(original){
        var clone = create(original);//通过调用create函数创建一个新对象
        clone.sayHi = function(){//以某种方式来增强这个对象
            alert("hi");
        };
        return clone;//返回这个对象
    }
    

    直白点,所谓寄生式继承也就是在其他继承方式(构造继承、原型继承等)上增加新的功能,返回新的对象。

    寄生组合式继承

    前面讲过,组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足. 组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部. 寄生组合式继承就是为了降低调用父类构造函数的开销而出现的 .如下例:

    function extend(subClass,superClass){
        var prototype = create(superClass.prototype);//创建对象
        prototype.constructor = subClass;//增强对象
        subClass.prototype = prototype;//指定对象
    }
    

    下面我们来看下extend的另一种更为有效的扩展.

    // 把上面的 create 拆开,其实差不多。
    function extend(subClass, superClass) {
      var F = function() {};
      F.prototype = superClass.prototype;
      subClass.prototype = new F(); 
      subClass.prototype.constructor = subClass;
    
      subClass.superclass = superClass.prototype;
      if(superClass.prototype.constructor == Object.prototype.constructor) {
        superClass.prototype.constructor = superClass;
      }
    }
    
    

    扩展

    属性查找

    • hasOwnProperty:使用了原型链后, 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 但是仍然没有找到指定的属性,就会返回 undefined. 此时若想避免原型链查找, 建议使用 hasOwnProperty 方法. 因为 hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数.如下:
    console.log(instance1.hasOwnProperty('age'));//true
    
    • isPrototypeOf:对比而言isPrototypeOf 则是用来判断该方法所属的对象是不是参数的原型对象,是则返回true,否则返回false。
    console.log(Father.prototype.isPrototypeOf(instance1));//true
    

    instanceof && typeof

    instanceof 运算符是用来在运行时指出对象是否是构造器的一个实例, 例如漏写了new运算符去调用某个构造器, 此时构造器内部可以通过 instanceof 来判断.(java中功能类似)

    function f(){
      if(this instanceof arguments.callee)
        console.log('此处作为构造函数被调用');
      else
        console.log('此处作为普通函数被调用');
    }
    f();//此处作为普通函数被调用
    new f();//此处作为构造函数被调用
    

    new运算符

    new实质上做了三件事;

    var obj  = {};
    obj.__proto__ = F.prototype;
    F.call(obj);  //执行
    

    第一行,我们创建了一个空对象obj;
    第二行,我们将这个空对象的proto成员指向了F函数对象prototype成员对象;
    第三行,我们将F函数对象的this指针替换成obj,然后再调用F函数.

    我们可以这么理解: 以 new 操作符调用构造函数的时候,函数内部实际上发生以下变化:

    1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
    2. 属性和方法被加入到 this 引用的对象中。
    3. 新创建的对象由 this 所引用,并且最后隐式的返回 this。

    关于原型继承、构造函数继承(经典继承)里对于数据的操作

    在继承关系里,内部属性数值变不变,数据共不共享前面也有所介绍,但是不够具体。这块时常令人迷惑,决定单独拿出来讲讲:

    首先在继承关系里,原型继承与构造函数继承可以分成两个比较重要的继承关系,其他的继承都是在这基础上演变组合出来的,所以搞懂这两个继承关系中的数据变化,就差不多了。

    在讲区别之前我们先两个例子:

    function kk() {
        this.a = 3;
        this.k = {l: 5};
    }
    
    function j() {
        kk.call(this)
    }
    
    let m = new j();
    m.a = 9;
    m.k.l = 9;
    let n = new j();
    console.log(n.a, n.k.l);
    

    打印结果:


    image

    可见,原型kk的数据并没有改变,再看一个例子:

    function kk() {
        this.a = 3;
        this.k = {l: 5};
    }
    
    function j() {
    
    }
    j.prototype = new kk();
    
    let m = new j();
    m.a = 9;
    m.k.l = 9;
    let n = new j();
    console.log(n.a, n.k.l);
    

    打印结果:


    image

    你会发现:原型里a没变, k 变了。
    对比上例,a始终没变,k有所区别,究竟是什么原因呢?

    如果你的眼睛足够雪亮,会一眼看出上例是构造函数继承,下例是原型继承,它两的区别之前已经说过,构造函数继承数据不会共享,而原型继承会共享。于是你会说为什么a怎么不变,你又在忽悠人,哼!哈哈哈,抱歉,有没有看见a是基本数据类型,k是引用类型(<font color="red">引用类型包括:对象、数组、函数。</font>)啊,基本数据类型是指针的指向区别,引用类型是地址的指向区别。不了解这块可以看看这篇文章:https://segmentfault.com/a/1190000008472264

    使用权威指南6.2.2继承那块的一句话<font color="red">“如果允许属性赋值操作,它也总是在原始对象上创造属性或者对已有属性赋值,而不会修改原型链,在JavaScript里,只有查询属性才能感受到继承的存在,而设置属性则与继承无关”</font>。

    如何理解这句话?我想是指继承关系中属性在本身内部找不到的时候才会去原型里找,只是借用属性,但是并不会修改原型本身的属性值,这也就解释了基本数据类型始终不变的原因。而原型继承中由于使用的同一原型对象,里面的引用类型使用同一个地址,导致应用类型的数值是可以变化的。

    总结两点:

    • 原型的基本数据类型不会受影响
    • 在原型继承里,引用类型的属性会发生改变,在构造函数继承中不会受影响(地址不同)

    参考地址: https://juejin.im/post/58f94c9bb123db411953691b#heading-13

    相关文章

      网友评论

        本文标题:原型、原型链及继承关系

        本文链接:https://www.haomeiwen.com/subject/hvvvoqtx.html