掘金 后端 ( ) • 2024-05-27 10:17

原型可以说是JavaScript中最抽象最复杂的概念了,但是身为对象中总要的组成,对对象原型的理解又是重要的,这里我将具体的讲解prototype  proto constructor三者之间的关系和子类与父类的继承

prototype  proto  constructor三角恋的相爱相杀

1. prototype: 存在与构造函数的身上,它是一个对象(称为“原型对象”)。这个原型对象用于存储所有通过该函数构造的实例将要继承的共享属性和方法。

2.  proto :存在于所有对象(除了 null)上,无论是普通对象还是函数对象。它是一个非标准但被广泛支持的访问器,用于访问或设置对象的内部 __proto__属性,即该对象的原型对象。

3.  Constructor :存在于原型对象上。通常,当一个函数被用作构造函数时,它的 prototype 对象会自动获得一个 constructor 属性,该属性指向构造函数本身。

这些概念是不是还有点抽象,具体而言就是,prototype是构造函数找到原型对象的方法,而__proto__是实例对象找到原型函数的方法,而constructor是实例对象找到构造函数的方法。

如图关系

image.png

因此当我们使用构造函数创建实例化对象时:

  *//构造函数*

    function Constructor(name){

      this.name = name;

    }

    *//将函数放置在原型对象身上*

    Constructor.prototype.sayName = function(){

      console.log(this.name);

    }

    *//实例化*

    var obj = new Constructor('小明');

    obj.sayName();   *//小明*

    *//判断关系*

    console.log(obj.__proto__===Constructor.prototype);   *//true*

    console.log(obj.constructor===Constructor);   *//true*

当一个实例化对象调用原型对象身上的函数时,首先,JavaScript 引擎会在实例对象自身的属性中查找该函数。如果实例对象上有同名的属性或方法,那么这个属性或方法会被优先使用。

如果在实例对象上没有找到所需的属性或方法,引擎会继续向上遍历该实例的原型链,引擎会检查构造函数的 prototype 对象上是否有该函数。

如果构造函数的 prototype 对象上也没有找到该函数,引擎会继续沿着原型链向上,即检查构造函数的 prototype 对象的 [[Prototype]](即构造函数的原型的原型),这个过程会一直重复,直到:

找到函数:在原型链中的某个环节找到了该函数,引擎则使用找到的第一个匹配的函数。

或者到达原型链顶端:如果搜索到 Object.prototype(所有对象的最终原型)并且仍然没有找到该函数,此时认为该函数不存在于实例的可访问范围,会导致 undefined 或抛出错误(如果尝试调用未定义的函数)。


因此这里我们再抛出两个例子来理解这个关系:

console.log(Constructor.prototype.proto===Function.prototype);//false

console.log(Constructor.prototype.proto===Object.prototype);//true

结果写明了,因为Constructor上面没有继承其他对象,因此它的原型对象的原型就是Object.prototype。同时也告诉我们,即使是最上层对象的原型对象的原型不会是Function.prototype。


子类的继承

其实在上面的解释中就已经将子类继承父类后,子类是怎么调用父类函数的的路径。那么子类是怎么继承到父类的呢?

*//* *创建父类*

  function Constructor(){}

  Constructor.prototype = {

    constructor:Constructor, *//指向自身*

    say:function(){ console.log('hello world')} *//* *函数* *打印*

  }

  *//* *创建Child* *作为Constructor子类*

  function Child(){}

这里我们使用Object.create()将child的原型对象改为Constructor的原型对象。

 Child.prototype=Object.create(Constructor.prototype)

 Child.prototype.constructor=Child *//指向自身*

这时候我们可以通过Child实例化对象去调用Constructor上的函数

 let son=new Child()

  son.say()  //打印  hello world

继续实验 在这个基础上我们再创建一个孙类,作为Child的子类,并且给Child再添加上一个函数:

*//作为父类*

  function Constructor(){}

  Constructor.prototype = {

    constructor:Constructor, *//指向自身*

    say:function(){ console.log('hello world')} *//打印*

  }

  *//作为Constructor子类*

  function Child(){}

  *//继承Consturctor*

  Child.prototype=Object.create(Constructor.prototype)

  Child.prototype.constructor=Child

  Child.prototype.saying=function(){

    console.log('saying')
  }

*//作为child的子类:*

function grandChild(){}

*//继承Child*

grandChild.prototype=Object.create(Child.prototype)

grandChild.prototype.constructor=grandChild

let son=new Child()

  son.say()

  let kid=new grandChild()

  kid.say()

  kid.saying()

  *//这里我们再实验一下Constructor能不能调用到子类函数*

  let parent=new Constructor()

  parent.saying()

调式结果为:

image.png

这里我们可以看到,前三个输出是没有问题的,而第四个说明了parent.saying不是一个函数,说明了父类实例对象是无法调用到子类实例对象的。


既然子类的原型链被修改为父类的原型链上,那么子类的prototype是不是会等于父类的prototype呢?

答案是:false

继续实验

console.log(Object.prototype===Constructor.prototype)

console.log(Child.prototype===Constructor.prototype)

console.log(grandChild.prototype===Child.prototype)

console.log(grandChild.prototype===Constructor.prototype)

image.png

对于这里的理解,尽管 Child.prototype 的原型链指向了 Constructor.prototype,使得 Child 的实例能够访问到 Constructor 的原型上的方法和属性,但 Child.prototype 和 Constructor.prototype 本身并不是同一个对象。同样的,就算Constructor上面已经没有了父类,但是它于最大的原型对象Object.prototype也不会相等。

如图解:

image.png

这里我的理解为:虽然三个构造函数的原型对象绑定在了一起,但是 他们自身的prototype仍然有着差别用来记录自身的特点,就像Constuctor是无法访问到Child的函数一样的,通过某些被记录的属性而实现的自下而上访问父类函数,而父类无法访问子类函数。

这里可能解释的还不够清晰,希望评论区大佬解惑。