[学习笔记]原型与原型链

157 阅读7分钟

前言

原型与原型链是我们深入学习JavaScript不可缺少的一部分,最近在学习原型和原型链的过程中总有一种似懂非懂的状态(看完文章和视频后觉得自己懂了,但是想自己复述一遍好像又差了些什么),所以写下这篇文章来理顺一下自己的思路

prototype与__proto__属性

普通对象和函数对象

js将对象分为普通对象和函数对象两种

  • 普通对象
    1. 使用对象字面量创建的对象,如: var obj1 = {}
    2. 使用Object构造函数创建的对象,如: var obj2 = new Object()
  • 函数对象
    1. 使用函数声明创建的函数对象,如: function foo1() {}
    2. 使用函数字面量创建的函数对象,如: var foo2 = function() {}
    3. 使用Function构造函数创建的函数对象,如: var foo3 = new Function()
    4. js内置函数,如: Function、Object、Array、Date、Number、Boolean...

为了方便我们理解,下面我们所指的函数对象均是构造函数

prototype属性

  • 只有函数对象才有prototype属性,我们叫其为显式原型

    下面我们用代码来验证

      // 通过两种方式创建普通对象
      var obj1 = {}
      var obj2 = new Object()
    
      // 它们的prototype属性为undefined
      console.log(obj1.prototype)
      console.log(obj2.prototype)
    
      // 通过三种方式创建函数对象
      function Foo1(){}
      var Foo2 = function(){}
      var Foo3 = new Function()
      console.log(Foo1.prototype)
      console.log(Foo2.prototype)
      console.log(Foo3.prototype)
    
      // 内置函数有没有prototype属性?
      console.log(Array.prototype)
      console.log(Object.prototype)
      console.log(Date.prototype)
      console.log(Number.prototype)
    

prototype.png

通过代码验证,我们可以看到普通对象是没有prototype属性的,而函数对象是拥有prototype属性的

原型(原型对象)

在我们创建函数对象时,浏览器引擎会自动帮我们往函数对象中添加一个prototype属性,它指向一个object对象,我们叫这个对象为原型(原型对象)

我们通过画图来理解 原型对象的创建.drawio.png

constructor

我们创建函数对象的同时也与其关联了一个原型对象,而这个原型对象会自动获得一个constructor属性包含一个指针指向我们创建的函数对象所在的位置

我们看代码

  function Person(name, age) {
    this.name = name
    this.age = age
  }
  console.log(Person.prototype)
  console.log(Person.prototype.constructor === Person);

image.png

构造函数和原型对象直接的关系.drawio (1).png

构造函数与原型对象之间的关系

构造函数和原型对象直接的关系.drawio.png

我们通过prototype属性说明了构造函数与原型之间的关系,但构造函数与实例对象之间又有着什么样的关系呢?这就要用到接下来要讲的 __proto__属性

__proto__属性

所有对象都具有__proto__属性,我们叫其为隐式原型

实例对象没有prototype属性只有__proto__属性

  function Person(name, age) {
    this.name = name
    this.age = age
  }

  var person1 = new Person("LDec.27", 18)
  console.log(person1.prototype) // undefined
  console.log(person1.__proto__)

image.png 看到这里你可能会问了Person构造函数不是Function构造函数的实例对象吗? 为什么它有prototype属性呢?

其实这里指的实例对象指的是无法通过new操作符创建实例对象的实例对象

这句话看上去可能会很绕口,我们用代码的形式来解读一下

  1. Person构造函数是Function构造函数的实例对象,所以我们可以使用new操作符来通过Function构造函数来创建一个名为Person的实例对象

  var Person = new Function("name", "age", "this.name = name;\n this.age = age;")
  console.log(Person);
  console.log(Person.prototype);
  console.log(Person.__proto__);
  

image.png 2. 我们再用Function的实例对象Person创建一个名为person1的实例对象

  var Person = new Function("name", "age", "this.name = name;\n this.age = age;")
  var person1 = new Person("LDec.27", 18)
  console.log(person1);
  console.log(person1.prototype);
  console.log(person1.__proto__);

image.png 其实在这一步我们就能看到Person的实例对象person1就已经不具有prototype属性了

  1. 我们再用Person的实例对象person1创建一个名为p1的实例对象
  var Person = new Function("name", "age", "this.name = name;\n this.age = age;")
  var person1 = new Person("LDec.27", 18)
  var p1 = new person1("xxx",18)

image.png

我们可以看到Person的实例对象person1是不具有创建实例对象的功能的,所以person1是无法通过new操作符创建实例对象的实例对象,所以它没有prototype属性

实例对象的隐式原型等于其构造函数的显式原型

实例对象的__proto__属性的值指向它的构造函数的prototype属性的值

实例对象的隐式原型等于其构造函数的显式原型

  function Person(name, age) {
    this.name = name
    this.age = age
  }

  var person1 = new Person("LDec.27", 18)
  console.log(person1.__proto__ === Person.prototype) // true

image.png

所以我们可以这样理解:

在创建实例对象时,实例对象会自动获得一个__proto__属性,其会指向其构造函数的原型

画图表示如下:

构造函数和原型对象直接的关系.drawio (3).png

构造函数和实例对象之间的关系

构造函数和实例对象间的关系.drawio.png

原型链

我们先看一段代码

  function Person(name, age) {
    this.name = name
    this.age = age
  }
  var person1 = new Person('lyl', 18)
  console.log(person1); // Person {name: 'lyl', age: 18}
  console.log(person1.hobby); // undefined
  console.log(person1.food); // undefined
  // 向Person的原型上添加属性
  Person.prototype.hobby = "basketball"
  Person.prototype.food = "meat"
  console.log(person1.hobby); // basketball
  console.log(person1.food); // meat

我们发现第一次我们查找person1上的hobby和food属性结果是undefined, 我们在Person的原型上添加了hobby和food两个属性后再查找person1上的hobby和food属性结果就变成了basketball和meat

我们可以将这种查找过程拆解:

  1. 现在person1自身查找,只有name和age属性没有找到hobby和food属性

image.png 2. 通过隐式原型找到其构造函数的原型对象,在原型对象里面找有没有hobby和food属性,若有则输出 image.png 3. 若还没有则到隐式原型的隐式原型中查找

  • 何为隐式原型的隐式原型?

实例对象有隐式原型__proto__,其指向构造函数的原型对象,而原型对象是一个由Object构造函数创建的实例对象,既然是实例对象那么就有__proto__属性,所以隐式原型的隐式原型就是原型对象的隐式原型

image.png 所以Person原型对象的隐式原型就是Object构造函数的显式原型它上面有toString方法

  console.log(person1.toString()) // [object Object]

image.png

用画图来理解:

构造函数和原型对象直接的关系.drawio (4).png

给原型链下个定义

通过隐式原型对对象的属性进行查找的一条查找路径叫做原型链所以原型链也可以叫做隐式原型链

原型链的尽头

因为创建函数对象的同时会有一个原型对象与之关联,这个原型对象是由Object构造函数创建的,所以无论我们创建一个什么样的函数对象,其原型对象的隐式原型都会指向Object构造函数的显式原型Object.prototype

image.png

既然走到最后都原型对象的隐式原型都是Object.prototype,那么Object.prototype的隐式原型不就是原型链的尽头了吗?

image.png

所以原型链的尽头为null

原型链的作用

  • 原型链是用来查找对象上的属性的(与作用域链的作用类似)
  • 原型链的查找规则
  1. 现在对象本身查找,若有则输出
  2. 若对象本身没有,则到其隐式原型中查找,若有则输出
  3. 若实例对象的隐式原型中仍然没有,则在其隐式原型中的隐式原型中查找(构造函数的原型对象中的隐式原型)
  4. 按照上述步骤直至找到原型链的尽头

易错点

Function.prototype === Function. __proto__吗?

判断显式原型与隐式原型是否相等我们只需要记住实例对象的隐式原型等于其构造函数的显式原型

这里实例对象为Function,它是通过Function构造函数创建的,所以返回的是true

Function.prototype === Object.__proto__吗?

同上这里实例对象为Object,它是通过Function构造函数创建的,所以返回的是true

总结

  1. 所有对象都有__proto__属性,只有函数对象才有prototype属性
  2. 实例对象(不具有创建实例对象功能的实例对象)没有prototype属性
  3. 实例对象的隐式原型等于构造函数的显式原型
  4. 原型链的尽头是null

参考文章

# 轻松理解JS 原型原型链

# JavaScript 深入之从原型到原型链

# 面不面试的,你都得懂原型和原型链

# 尚硅谷JavaScript高级教程(javascript实战进阶)