JS原型链

79 阅读6分钟

prototype

每个函数在创建的时候,都会赋与一个prototype属性来保存由特定类型实例所使用的共享的属性,来看下一个例子。

      function Car(){}
      
      Car.prototype.say=function(){
        console.log("say Hi");
      }

      console.dir(Car);

我们创建了一个构造函数Car,并且在其prototype属性上添加了一个say方法,然后打印一些这个构造函数,结果如下图所示:

image.png

我们可以看到,在Car.prototype上添加上了一个say方法,这时候我们使用这个构造函数创建几个实例看看呢1

      function Car() {}

      Car.prototype.say = function () {
        console.log("say Hi");
      };

      console.dir(Car);

      const car1 = new Car();
      const car2 = new Car();

      car1.say();//say Hi
      car2.say();//say Hi

      console.log((car1.say === car2.say));//true

我们可以根据打印的结果看的出,两个实例都可以使用say方法,并且两个实例的say方法都是指向同一个地址的,所以之前说,保存在prototype上的属性会在这个函数创建出来的实例中共享。

[[prototype]]

接着上面的例子,不过这次我们打印一下其中一个实例,

      function Car() {}

      Car.prototype.say = function () {
        console.log("say Hi");
      };

      console.dir(Car);

      const car1 = new Car();
      const car2 = new Car();
      console.log(car1);

结果如下:
image.png

欸1看到这里可能有些朋友就会产生疑惑了,为什么我的实例上没有say方法,而这个实例却能使用呢?这就关乎到隐藏属性[[prototype]]

在使用构造函数创建一个实例的时候,这个实例内部的[[prototype]]指针就会被赋值为构造函数的prototype,并且一些主流浏览器会在对象上暴露__proto__属性,通过这个属性可以访问到对象的原型

这句话是什么意思呢?别急,来看下一个例子。

      function Car() {}

      Car.prototype.drive = function () {
        console.log("I am driving");
      };

      const car = new Car();
      console.log(car.__proto__);
      console.log(Car.prototype);
      console.log(car.__proto__===Car.prototype);

执行的结果如下图所示。

image.png

上面这个例子我们分别打印了实例的__proto__属性和构造函数Car的prototype属性,我们仔细一点可以发现,这两个打印结果是一摸一样的,然后我对这两个属性进行了严格相等结果为true,所以实例.__proto__构造函数.prototype是指向同一个地址的。

既然结果为这样了,博主在之前的某一篇文章中也讲到了[[Get]],所以就能解释为什么在实例上没有say这个方法却能使用了!这里有一张图帮助我们理解构造函数与实例之间的关系:

(啊哈,图不见了)

constructor

在《JavaScript高级程序设计》(第四版)中这样提到:当我们创建一个构造函数的时候,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。默认情况下,所有的原型对象自动获得一个名为constructor的属性,指回与之关联的构造函数。朋友们有get到吗?来,我们看下一个例子:

    function Car(){

    }

    Car.prototype.driving=function(){
      console.log("I am driving");
    }
    console.log(Car.prototype);
    console.log(Car.prototype.constructor===Car);//true

我们还是使用Car构造函数,并且打印了一下其prototype属性,结果如下图所示:

image.png

我们可以看到,在prototype对象里面有一个名为constructor的属性,并且它是指向构造函数Car的。我们做了一下比较,结果为true

这里我们在prototype上定义了一个driving方法,那么如果我们需要很多方法呢?难道得一个一个去添加吗?我们可不可以把构造函数写在一个对象中再赋值呢?

来看下面这个例子:

    function NewCar(){}
    
    NewCar.prototype={
      say:function(){
        console.log("say Hi");
      },
      driving:function(){
        console.log("I am driving");
      }
    }
    const newCar=new NewCar()
    newCar.say()//say Hi
    newCar.driving()//I am driving

上面这个例子我们把需要使用到的方法集中在一个对象中再总体赋值给构造函数的prototype,和之前一样,实例照样可以使用这两个方法。
但是:打印一下constructor

    console.log(NewCar.prototype.constructor===NewCar);//false

欸?这就有问题了,怎么之前明明相等的现在就不相等呢?如果细心的你把constructor打印一下会发现并不是指向NewCar而是指向Object了。欸?奇了怪了,怎么肥事呢?

解答

在我们不是使用添加方法的方式而是使用直接赋值一个新的对象的话,那么prototype指向了这个对象,这个对象肯定是Object类型的嘛,并且这个对象直接覆盖掉了原来的prototype对象,我们打印prototype会发现里面有之前的两个方法,因为是对象直接覆盖,所有prototype.__proto__就是指向Object.prototype的,然后会去其中找constructor属性,当然就是指向Object构造函数啦!

解决方法

    function NewCar(){}
    
    NewCar.prototype={
      constructor:NewCar,//重新指回构造函数
      say:function(){
        console.log("say Hi");
      },
      driving:function(){
        console.log("I am driving");
      }
    }

我们在覆盖掉之前的prototype对象之后,把constructor属性重新指向NewCar就ok了。

但是这种方法添加的constructor会为prototype属性创建一个可枚举的constrcutor,如果你想添加一个[[Enumerable]]为false的constructor可以使用Object.defineProperty来添加,这里就不做例子啦。

下面给上一张图来理解与constructor有关的联系:

qq_pic_merged_1605853540295.jpg

原型链

[[prototype]],prototype,constructor之间的关系就像一条链延伸到Object,看下面这个例子:

      function Star() {}
      Star.prototype.say = function () {
        console.log("say Hi");
      };
      const ldh = new Star();

      console.log(ldh.__proto__ === Star.prototype);
      console.log(ldh.__proto__.constructor === Star);
      console.log(Star.prototype.constructor === Star);
      //而prototype是一个对象
      console.log(Object.prototype.toString.call(Star.prototype)); //[[object Object]]
      //所以prototype的__proto__是指向Object.prototype的
      console.log(ldh.__proto__.__proto__ === Object.prototype);

      //
      console.log(ldh.__proto__.__proto__.constructor === Object);
      //那么Object的原型也是一个对象,那么其__proto__指向的是谁的prototype呢?
      console.log(Object.prototype.__proto__); //null 结果为null 到这里就结束啦!

我们使用了一个构造函数Star来测试,我们没有停留在只与Star相关的层面,我们知道__proto__是指向构造函数的prototype的,那么Star.prototype.__proto__是什么呢?例子中我们对prototype进行类型检测,因为是一个对象,所以这个对象的__proto__肯定是指向Object.prototype的,那么再往上呢?Object.prototype.__proto__又是指向哪里呢?结果是null,原型链的搜索也到此为止了

这里有一张图帮助大家理解:
1605853614842.jpg

得益于原型链,我们许多实例都可以使用定义在Object.prototype上的方法,例如toString()

补充—原型的动态性

因为从原型链上搜索值得过程是动态的,那么即使实例在原型修改之前存在了,但对原型上的修改也会在实例上反映出来,来看一下这个例子:

      function Car() {}
      const car = new Car();
     
      car.prototype.driving = function () {
        console.log("I am driving");
      };
      car.driving();//I am driving

我们在给原型上添加方法之前创建了对象,在其之后用这个对象调用这个方法,也是可以的。

修改 != 重写

重写整个原型会切断最初原型和构造函数的联系,但实例引用的仍然是最初的原型,记住实例只有指向原型的指针,并没有指向构造函数的指针,来看一个例子:

      function Car() {}
      const car = new Car();
     
      // Car.prototype.driving = function () {
      //   console.log("I am driving");
      // };
      //重写
      Car.prototype={
        constructor:Car,
        driving:function(){
          console.log("I am driving");
        }
      }
      car.driving();//typeError

再重写之后,因为重写后的原型上并没有driving方法,所以会报错,这里博主也尝试把constructor重新指向构造函数(但肯定也是不行).