JavaScript中的原型和原型链

92 阅读5分钟

 前言

JS中的原型和原型链应该算是比较经典的话题了,很多人只了解了其表面(在创建构造函数的时候使用prototype属性声明公共方法),但是在面试中也会经常问到其中的原理,这时候就答不出来了(比如我),

先来看一下我自己对原型链的理解画出来的一张图解:

相信第一眼看到这张图的人,都会觉得特别繁琐,但是当你理解了原型和原型链后,就会觉得不过如此,而且画图是特别有助于理解的!

1. 使用构造函数来创建对象

先创建一个构造函数,并使用new运算符创建一个对象

  function Person(){
      this.name='make'
  }
  let person = new Person();
  console.log(person)

这个例子就是使用构造函数创建了一个对象,作为JS的使用者相信大家都会

2. prototype

prototype属性是函数对象特有的属性,比如在Person构造函数的prototype属性上添加一个message属性,并且创建两个Person对象:

 function Person(){
      this.name='make'
  }
  Person.prototype.message='hello world'
  let person1 = new Person()
  let person2 = new Person()
  console.dir(Person)
  console.log(person1.message)   // 'hello world'
  console.log(person2.message)   // 'hello world'

可以看到Person函数对象上的prototype属性有一个message属性为'hello world'

并且在两个实例对象person1和person2上都能访问到

那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?

其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,并且同一个构造函数的所有实例对象共享这个原型。

那么我们该怎么表示实例与原型之间的联系呢?往下看

3. proto

每一个JavaScript对象上都会有一个__proto__属性(隐式属性),这个__proto__属性就指向了该对象的原型,下面用一个例子来验证:

function Person(){
      this.name='make'
  }
  Person.prototype.message='hello world'
  let person1 = new Person()
  let person2 = new Person()
  console.log(person1.__proto__===person2.__proto__)  // true
  console.log(person2.__proto__===Person.prototype)   // true
  console.log(person1.__proto__)

这个例子证明了同一个构造方法下的所有实例对象共享一个原型对象,并且实例的原型就等于构造方法的prototype。

并且在原型对象上可以看到有一个自带的constructor属性指向的构造方法。

4. 实例与原型

当读取实例对象的一个属性,会先在这个实例对象身上查找有没有该属性,如果没有的话,就在实例对象的原型身上查找找,如果原型上还是没有,就在原型的原型上查找,直到查找到最顶层为止。

function Person(){
      this.name='make'
      this.message='hello make'
  }
  Person.prototype.message='hello world'
  Person.prototype.__proto__.message='hello'
  let person = new Person()
  console.log(person.message)     // hello make

在这个例子中,先是在构造方法中定义了一个message属性,原型上面也定义了message属性,原型的原型上面也定义了message属性。

打印的message值是位于构造方法上的message属性也就是实例对象上的属性

把这个实例属性注释掉,看看会有什么结果

  function Person(){
      this.name='make'
      // this.message='hello make'
  }
  Person.prototype.message='hello world'
  Person.prototype.__proto__.message='hello'
  let person = new Person()
  console.log(person.message)     // hello world

打印的是原型对象上的message属性值

那再把原型对象上的message注释掉呢

 function Person(){
      this.name='make'
      // this.message='hello make'
  }
  // Person.prototype.message='hello world'
  Person.prototype.__proto__.message='hello'
  let person = new Person()
  console.log(person.message)     //hello

当然也不用想,肯定是原型的原型上面的message属性

如果全部都注释掉,那就只有undefined了,因为查找到最后也没有找到message这个属性 

通过上面的例子,我们证实了读取一个对象上某个属性的查找顺序,这种查找方式就形成了一种链式查找过程,也就是叫做原型链

5. 原型链

我们都知道了读取对象属性是按照原型链的顺序查找,众所周知,在JS中,无论是函数还是数组对象,都是基于Object对象来实现的,这也就说明了所有复杂数据类型的原型链上一定存在Object.prototype,那么Object.prototype的原型是什么呢?,我们来打印一下:

  console.log(Object.prototype.__proto__) 

image.png 结果是null。

那我们知道了,原型链的尽头是null,所以Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。

所以查找属性的时候查到 Object.prototype 就可以停止查找了。 

到此就分析完了。

总结

constructor

首先是 constructor 属性,我们看个例子:

function Person() {
 
}
var person = new Person();
console.log(person.constructor === Person); // true

当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:

person.constructor === Person.prototype.constructor

proto

其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。

知识点归纳

  1. 所有的引用类型(数组、函数、对象)可以自由扩展属性(除null以外)。
  2. 所有的引用类型都有一个’_ _ proto_ _'属性(也叫隐式原型,它是一个普通的对象)。
  3. 所有的函数都有一个’prototype’属性(这也叫显式原型,它也是一个普通的对象)。
  4. 所有引用类型,它的’_ _ proto_ _'属性指向它的构造函数的’prototype’属性。
  5. 当试图得到一个对象的属性时,如果这个对象本身不存在这个属性,那么就会去它的’_ _ proto_ _'属性(也就是它的构造函数的’prototype’属性)中去寻找。

参考  一文搞懂JavaScript中原型与原型链