关于JavaScript原型与原型链

140 阅读7分钟
本文对一下几点进行探讨:

1.什么是原型
2.什么是原型链
3.继承的使用
4.圣杯模式
5.知识点补充

1 - 什么是原型?
    function Test() {
      this.message = '这是构造函数实例化的信息';
    }
    const p = new Test();
    console.log(p);

1.png

  • MDN的官方解释,每个实例对象都有一个私有属性,[[Prototype]],指向的是实例它的构造函数的原型对象。所以原型指的是每个实例化对象的私有属性[[Prototype]]。
  • 值得注意的一点是,我们平时字面量声明的对象,和new Object()实例化出来的对象是一致的,同样拥有私有属性[[Prototype]]。详情如下:
    // 方式1
    var obj1 = {
      message: '字面量声明方式'
    }

    // 方式2
    var obj2 = new Object();
    obj2.message = '实例化方式'

    console.log('obj1--->', obj1);
    console.log('obj2--->', obj2);
    //浏览器控制台 对比结构一致

3.png

2 - 什么是原型链?
    // 通过构造函数.prototype的方式可以访问或设置 该构造函数的原型对象
    Father.prototype.age = '36';

    function Father() {
      this.message = '这是父级构造函数'
    }

    //将构造函数Father实例化的对象作为 构造函数Son的原型对象
    Son.prototype = new Father();

    function Son() {
      this.message = '这是子级构造函数'
    }

    const son = new Son();
    console.log('查看实例化对象son的信息-------->', son);
    console.log('访问实例化对象son的age属性-------->', son.age);

    var objPrototype = Object.prototype;
    console.log('查看原型链的顶端Object的原型对象1', objPrototype);
    console.log('查看原型链的顶端Object的原型对象2', objPrototype.__proto__);

    console.log('查看是否相等----->', son.__proto__ === Son.prototype);
    /**
     * MDN的解释:
     * 遵循ECMAScript标准,son.[[Prototype]] 符号是用于指向 Son 的原型。
     * 从 ECMAScript 6 开始,[[Prototype]] 可以通过 
     * Object.getPrototypeOf() 和 Object.setPrototypeOf()访问器来访问。
     * 这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__。
     * 
     * 
     * 因为son.[[prototype]]会报错,所以这里我们使用 son.__proto__ 来访问 Son 的原型对象, 
     * */

5.png

其中一个有趣的现象,打印实例son可以发现,son的私有属性[[Prototype]]里面还存在着[[Prototype]],分析原因:

  • 我们在第一点原型的分析得出,每个实例对象都拥有着一个私有属性[[Prototype]],指向实例化它的构造函数的原型对象。
  • 观察代码,son.[[Prototype]] === Son.prototype,打印为true, 也就是说son的私有属性[[Prototype]]其实就是实例化son构造函数Son原型对象(prototype),这就可以解释为什么产生了[[Prototype]]里面保存着另一个[[Prototype]]的套娃现象。
  • 继续观察代码,套娃现象并不是无止境的,顶点为构造函数Object.prototype另一个有趣的现象,打印son.age,值为36,分析原因:
  • 正常来说,当访问对象中不存在的属性时,会得到undefined,但是现在控制台打印出了值为36。
  • 我们看看MDN的解释:当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
  • 也就是说,当实例son对象中不存在age属性时,系统会往son的私有属性[[Prototype]]里面寻找,当son的私有属性[[Prototype]]里面也不存在age属性时,会往son的私有属性[[Prototype]]里面的私有属性[[Prototype]]寻找,找到就会停止,找不到会一直寻找,直到套娃现象的顶端(构造函数Object.prototype)。 最后的总结:
  • 在JS中,通过__proto__,依次层层向上访问,直至构造函数Object.prototype这个顶端,而形成的这么一个链条,就称为原型链
  • 当访问一个对象的属性或方法时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾,这称为原型链继承
3 - 继承的使用。

一般来讲,我们使用继承的目标是,子类实例可以使用父类原型上的属性和方法,那么应该如何实现呢?下面从几个例子中看看不同继承方式的结果。

1.子类原型继承父类实例

    Father.prototype.age = 20;

    function Father() {
      this.message2 = "这是父亲的实例",
        this.hobby = {
          Monday: 'swim',
          Tuesday: 'run',
          Wednesday: 'read'
        }
    }
    const father = new Father();
    Son.prototype = father;

    function Son() {
      this.message = "这是孩子的实例"
    }
    const son = new Son();
    Son.prototype.age = 10;
    console.log('更改Son.prototype后打印father实例------------->', father);
    console.log('访问继承的属性------->', son.hobby);

10.png

从代码和控制台打印,可以看到,当子类继承父类的实例时,会产生一个问题:我们可以访问到father实例的属性,并且更改Son的原型时,father实例会被修改,明显就不是很合理。

2.子类原型继承父类原型

    Father.prototype.name = '张三'

    function Father() {
      this.message2 = "这是父亲的实例";
    }

    Son.prototype = Father.prototype;

    function Son() {
      this.message = "这是孩子的实例";
    }
    Son.prototype.name = '李四';
    const son = new Son();
    const father = new Father();
    console.log('访问继承的属性------->', father.name); // {name:李四}

子类原型继承父类原型,在son实例上操作时,同样会更改父类的原型对象,依旧不合理。

3.使用apply改变指向

    Father.prototype.name = '张三';
  
    function Father() {
      this.message = "这是父亲的实例";
      this.hobby = {
        Monday: 'swim',
        Tuesday: 'run',
        Wednesday: 'read'
      }
    }

    function Son() {
      Father.apply(this)
      this.message = '这是孩子的实例'
    }

    const son = new Son();
    console.log('打印son实例------>', son);
    console.log('访问son的name属性------>', son.name);

11.png

使用this指向,son实例拥有Father构造函数实例的属性和方法,但是son实例的私有属性指向的是Object,说明son并没有继承Father原型的属性和方法,严格说这并不是继承。

以上几种方法,都是在操作子类实例时父类原型受到了影响,那么有没有办法能够让子类继承父类的属性和方法且修改的同时,不会影响的父类原型呢?

4 - 圣杯模式。

主要思想是,构建一个缓冲构造函数(Buffer),以父类的原型当做Buffer的原型,以Buffer的实例当做子类的原型,因此,子类原型变动时,受到影响的只是Buffer的实例,而父类的原型不会受波及。

    Father.prototype.name = '张三';

    function Father() {
      this.message = "这是父亲的实例";
    }

    Buffer.prototype = Father.prototype;

    function Buffer() {};

    function Son() {
      this.message = "这是Son1的实例";
    }


    Son.prototype = new Buffer();
    Son.prototype.name = '李四';

    const son = new Son();
    const father = new Father();
    console.log('这是son实例--------->', son);
    console.log('访问son实例属性name--------->', son.name);
    console.log('这是father实例--------->', father);
    console.log('访问father实例属性name--------->', father.name);

12.png

同样的效果,也能使用Object.create()来实现。

    Father.prototype.name = '张三';

    function Father() {
      this.message = "这是父亲的实例";
    }

    const father = new Father();

    function Son() {
      this.message = "这是Son1的实例";
    }

    Son.prototype = Object.create(Father.prototype);
    Son.prototype.name = '李四';

    const son = new Son();

    console.log('这是son实例--------->', son);
    console.log('访问son实例属性name--------->', son.name);
    console.log('这是father实例--------->', father);
    console.log('访问father实例属性name--------->', father.name);

13.png

以上例子,子类既能访问父类原型上的属性和方法,又能修改自己原型上的属性且不影响所继承的父类原型,效果是不是会比较美一些?

5 - 知识点补充

1.继承后子类的constructor指向问题

    function Test() {}
    console.log('查看Test的实例-------->', new Test());


    function Father() {
      this.message = "这是父亲的实例";
    }

    const father = new Father();

    function Son() {
      this.message = "这是Son1的实例";
    }

    Son.prototype = Object.create(Father.prototype);
    const son = new Son();
    Son.prototype.constructor = Son;
    console.log('查看son实例----------->', son);
    console.log('查看father实例----------->', father);

15.png

在没有继承之前,Test的原型constructor指向的是Test本身。而使用继承以后,通过之前的例子可以发现,子类原型的constructor丢失,所以我们需要手动调整。

2.继承后分别修改子类原型的引用值和原始值

    Father.prototype.name = '张三';
    Father.prototype.hobby = {
      workingDay: 'run',
      weekend: 'watch movie'
    }

    function Father() {
      this.message = "这是父亲的实例";
    }

    const father = new Father();

    function Son() {
      this.message = "这是Son1的实例";
    }

    Son.prototype = Object.create(Father.prototype);
    Son.prototype.name = '李四';
    Son.prototype.hobby.weekend = 'swim';
    const son = new Son();

    console.log('查看son实例----------->', son.name);
    console.log('查看son实例----------->', son.hobby);
    console.log('查看father实例----------->', father.name);
    console.log('查看father实例----------->', father.hobby);

16.png

可以看到,修改原始值,不会影响父类原型,修改引用值,会影响父类原型。这个目前没有办法处理。

3.关于继承的缺点
走到这里,大家应该都明白了,子类继承父类,可以使用父类的属性和方法,但是有没有这么一种可能,就是子类一开始只是想使用其中一两个方法,但是父类缺一股脑地把所有东西都丢给了子类,仔细想想,这样子合理吗?科学吗?那么有没有办法,可以让用户自由挑选所需要的功能函数呢?

本文到这里就结束了,希望大家能有所收获。