理解构造函数(`constructor`)、显式原型(`prototype`)、隐式原型(`__proto__`)、原型链

142 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

跟着文章,动手画一画图,一定可以理解什么是构造函数(constructor)、显式原型(prototype)、隐式原型(__proto__)、原型链

构造函数(constructor)

JavaScript语言使用构造函数(constructor)作为对象的模版,所谓“构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。

意义:使用对象字面量创建一系列同一类型的对象时,这些对象可能具有一些相似的特征(属性)和行为(方法),此时会产生很多重复的代码,把这些重复的特征(属性)和行为(方法)抽象出来,做成构造函数,可以实现代码复用。

使用:用new关键字来进行调用,一般首字母要大写,constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

创建一个构造函数
function Person(name){
  this.name=name
}
const zhansanObj = new Person('张三')
const lisiObj = new Person('李四')

上面Person就是zhansanObjlisiObj的构造函数,zhansanObjlisiObj是构造函数创建出来的函数对象[Function: Object]凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象

注:ObjectFunction是内置的函数对象,JS自带的,这两是特殊的,后面会讲到

查看创建的对象

console.log(zhansanObj);

image-20221122143849304.png

查看创建该对象的构造函数
console.log(zhansanObj.constructor);
// 得到下面这个函数
ƒ Person(name) {
  this.name = name
}

注:对于引用类型来说 constructor 属性值是可以修改的,但是对于基本类型来说是只读的,这说明,依赖一个引用对象的 constructor 属性,并不是安全的。另外**nullundefined 是没有 constructor 属性的。**

函数也是对象
console.log(Person.constructor)
console.log(Function.constructor)
console.log(Object.constructor)

让我们来思考一下,这三个分别会打印什么?

  • Person是一个声明函数,那这个函数有没有对应的构造函数呢?如果有是什么?
  • 前面说到Function是js的内置函数对象,那这个函数对象有没有对应的构造函数呢?如果有是什么?
  • Object也是js的内置函数对象,那这个函数对象有没有对应的构造函数呢?如果有是什么?

这三个打印出来的都是ƒ Function() { [native code] },也就是Function对象

  • Person的构造函数是Function,说明函数是Function的实例对象,即function Person(){ ... } 其实是 var Person= new Function(...),另外let a = {} 其实是 let a = new Object() 的语法糖,let a = [] 其实是 let a = new Array() 的语法糖
  • Function的构造函数是它自己,说明Function是构造函数的源头
  • Object的构造函数也是Function,说明对象由Function创建
画个图

image-20221122153115095.png

所以constructor属性其实就是一个拿来保存自己构造函数引用的属性,没有其他特殊的地方。

注意: constructor属性不一定是对象本身的属性,这里只为方便理解将其泛化成对象本身属性,所以用虚线框,真正的constructor属性藏在哪呢,等会儿讲

显示原型(prototype)

所有的函数都有一个prototype(显式原型)属性,属性值是一个普通对象。对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器的prototype属性上,而非对象实例本身(所以每个实例能使用原型上的方法和属性而且不会占内存)

当我们创建一个函数时,prototype 属性就被自动创建了,还可以在prototype对象上定义其他属性

function Person(name){
  this.name=name
}
Person.prototype.sayHi = function sayHi(){
    console.log("你好")
}

image-20221122165851943.png

从上面的图,我们发现几点

  • 原型上默认就有一个属性constructor,是不是就是前面图中虚线框中的constructor
  • 我们可以在原型上自定义属性和方法,这个会共享
  • 构造函数PersonPerson.prototype指向原型,Person.prototype.constructor指向构造函数Person;如下图

image-20221122162450234.png

这就是一个循环引用,即Person.prototype.constructor === Person

image-20221122162831301.png

所以按照上面推断出的,我们前面画的图应该变成这样

image-20221122170235110.png

看完图,所以虚线框constructor到底是啥?至少我们知道它和显式原型上的constructor并不是一回事,继续往下看,弄明白虚线框的constructor到底是啥

隐式原型(__proto__)

每个实例对象都有一个隐式原型(__proto__),它指向创建该对象的构造函数的原型,也就是指向构造函数的prototype属性

function Person(name){
  this.name=name
}
Person.prototype.sayHi = function sayHi(){
    console.log("你好")
}
const zhansanObj = new Person('张三')

new Person('张三')时,__proto__ 就被自动创建,打印看看

image-20221122170920424.png

zhansanObj.__proto__等于Person.prototype

那么前面我们发的图就要再变一变

image-20221122171326543.png

image-20221122172357673.png

看完这张图,我们再想一下,zhansanObj.__proto__等于Person.prototype,所以zhansanObj.__proto__.constructor等于Person.prototype.constructor等于构造函数Person,所以虚线框里的constructor就是zhansanObj.__proto__.constructor,也就是Person.prototype.constructor,套娃结束....

image-20221122175323144.png

看完上面的图,有没有以下几个疑问?

  • 之前我们虚线框里的constructor,使用zhansanObj.constructor表示的,然后你又告诉我这个其实是zhansanObj.__proto__.constructor这不就说明zhansanObj.constructor就是zhansanObj.__proto__.constructor么?为什么?

    答:这是JS的内部操作了,当一个实例对象找不到某个属性的时候,JS就会去它的原型对象上去找是否有相关的共享属性或方法,虽然zhansanObj实例上没有constructor属性,但是原型对象上有,所以可以直接使用。后面还涉及原型链的知识,等会看完你就懂了;

  • prototype也是一个对象,这个对象上有没有__proto__

    答:是的,函数内的prototype其实就是一个普通的对象,并且默认都是Object对象的实例

    Person.prototype.__proto__.constructor打印出ƒ Object() { [native code] },所以Person.prototype.__proto__.constructor===Object或者Person.prototype.__proto__.constructor===Object.prototype.constructor

    Person.prototype.__proto__.__proto__打印出null

答完这两个疑问,我们的图又要变一变了

image-20221123091040466.png

由图可知,

  • 所有函数的__proto__指向他们的原型对象

  • 最后一个prototype对象是Object函数内的prototype对象

    Object函数是所有对象通过原型链追溯到最根的构造函数

  • Object函数的prototype中的__proto__指向null

    如果沿着原型链寻找不到某一属性,这个null其实就是个跳出条件

接下来,看一下什么是原型链

原型链

每个对象都有一个原型对象,通过__proto__指向上一个原型,并从中继承方法和属性,同时被指向的原型对象也可能有原型,这样一层一层,最终指向null,这种关系被称为原型链。根据定义,null 没有原型,并作为原型链中的最后一个环节。

接下来,用不同的箭头颜色,标注出原型链

image-20221123095228448.png

  • 第一条原型链:zhansanObj.__proto__.__proto__.__proto__
  • 第二条原型链:lisiObj.__proto__.__proto__.__proto__
  • 第三条原型链:Person.__proto__.__proto__.__proto__
  • 第四条原型链:Function.__proto__.__proto__.__proto__
  • 第五条原型链:Object.__proto__.__proto__.__proto__

这些原型链最终都是指向null,看到这里应该就已经懂了原型链是什么了,通过__proto__指针指向上一个原型,一层层,直到null。

当试图得到一个对象的属性时,先在这个对象本身找,找不到就去它的原型中找,找不到就继续往上找,直到找到匹配的属性或到达原型链尾部也没找到返回null

写个new

学完这一些,可以试着实现以下new操作符的功能,我们来分析一下new操作符做了什么

  1. new返回一个实例对象
  2. 实例对象的__proto__指向构造函数的原型
  3. this指向实例对象
function Person(name) {
  this.name = name
}
​
function myNew(fn, ...args) {
  const obj = {} // 创建一个实例对象
  obj.__proto__ = fn.prototype //实例对象的`__proto__`指向构造函数的原型
  fn.call(obj, ...args) // this指向实例对象
  return obj // 返回一个实例对象
}
​
let wangwuObj = myNew(Person, '王五')

是不是再也不怕面试官让你手写实现new了

总结

  • 构造函数,就是专门用来生成实例对象的函数,它就是对象的模板,描述实例对象的基本结构。
  • ObjectFunction是内置的函数对象,JS自带的,记住这两个特殊的家伙
  • 凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象
  • 所有的函数都有一个prototype(显式原型)属性,属性值是一个普通对象
  • 每个对象都有一个隐式原型(__proto__),它指向创建该对象的构造函数的原型,也就是指向构造函数的prototype属性
  • 每个对象都有一个原型对象,通过__proto__指向上一个原型,并从中继承方法和属性,同时被指向的原型对象也可能有原型,这样一层一层,最终指向null,这种关系被称为原型链
  • 当试图得到一个对象的属性时,先在这个对象本身找,找不到就去它的原型中找,找不到就继续往上找,直到找到匹配的属性或到达原型链尾部也没找到返回null,这就是原型链向上查找,instanceof的原理