一张图带你搞懂Javascript原型链关系

在某天,我听了一个老师的公开课,一张图搞懂了原型链。

老师花两天时间理解、整理的,他讲了两个小时我们当时就听懂了。
今天我把他整理出来,分享给大家。也让我自己巩固加深一下。

就是这张图:

为了更好的图文对照,我为每条线编了标号,接下来的细节讲解,都会用到这张图里的编号:

为了你更好的对照阅读,你可以单独打开这张图片,然后对比着文章看。 当然,我后边也会贴心的把对应区域截小图贴在文案附近。

前置知识

在对这张图进行详细拆解前,我们先来说几个前置的基础知识。以便后续更好的理解。

  • Function、Object、Array、String、Symbol等这些都是JavaScript的内建函数,也叫原生函数(js创造时,他们就存在的,是js内部提供的)
  • prototype:原型对象;
  • __proto__:隐式原型、对象的私有属性;
  • 所有的函数都是Function构造出来的,包括Object等原生函数。可以说,每个函数都是Function类型的实例。
  • 函数实际上是对象,但比较特殊,我们叫做函数对象
  • 每个函数被创造出来时都有一个prototype,表示该函数的原型。他就是原型对象
  • 每个对象身上都有一个私有属性__proto__,指向该对象的构造函数的原型对象。函数作为对象也有__proto__
  • prototype是一个对象,由Object构造出来的。所以他身上也有__proto__,永远指向对象的构造函数Object的原型(即:Object.prototype)
  • 函数都是被Function构造出来的,所以每个函数的__proto__都指向Function的原型(即:Function.prototype)
  • Object.prototype的__proto__不能再指向自身无限循环,所以指向null
  • Function.__proto__指向自身原型。因为Function没人构造,“生下来”就有。

如下图:
函数a,既有prototype、也有__proto__

内建函数Object,既有prototype、也有__proto__

对象身上就只有__proto__

口诀提炼

为了更好的掌握,我把相关的知识点汇总成下列几条口诀。接下来的剖析中都会用到。

  1. 函数是Function构造出来的
  2. 一切函数都是对象
  3. 只要是函数对象,就会有原型prototype和隐式原型__proto__两个属性。
  4. 普通对象身上只有__proto__,没有prototype
  5. 实例化对象的__proto__都指向构造函数的prototype
  6. 所有函数的prototype都指向自身prototype
  7. 所有prototype的__proto__都指向Object.prototype(Object的除外)
  8. 所有函数对象的__proto__都指向Function.prototype(包括Function自身)
  9. 对象身上都有constructor指向函数自身

注意:这里不考虑原型链指向修改、Object.create(null)这些特殊情况

剖析一张图

接下来我们根据基础知识和口诀,正式来看图中的每一个细节

图例:

观察一个图之前,我们先看他的图例

右边表示节点的类型:

绿色方块:表示普通对象,比如平时创建的对象obj {}、arr **[]**等
红色方块:表示函数对象,也就是函数。他是一种特殊的对象。

左边表示箭头的指向:

绿色箭头:表示用 new + 构造函数调用 的方式创建实例化对象
白色箭头:表示当前节点的prototype原型对象的指向
蓝色箭头:表示当前节点的__proto__私有属性的指向

详情

Function

我们先看最右边的Function(图中色块1)。

他是js的内部函数,你打印Function会得到标示着“本地代码”的结果。

也就是说他是js一开始就有的。

而伴随他出生的就是他的原型: Function.prototype。(图中色块2)
prototype是函数特有的标志,每个函数被创建出来,身上就有一个prototype的属性,表示自己的原型对象。
根据口诀:所有函数的prototype都指向自身prototype。
也就是说,Function.prototype指向Function原型。
所以 Function.prototype === Function.prototype(图中线条a)

然后说下比较特殊的Function.__proto__

因为Function他是个函数,函数又是一种特殊的对象(函数类对象,又叫函数对象)。所以作为对象身上特有的标志__proto__,在Function身上也有一个。
另外,任何对象都可以理解为实例化对象,所以我们总结出口诀:实例化对象的__proto__都指向构造函数的prototype所有prototype的__proto__都指向Object.prototype(Object的除外)
如:

const obj = new Object() // 或 const obj = {} 的字面量写法

obj.__proto__ === Object.prototype // true

// 实例化对象obj,其隐式原型__proto__指向构造函数Object的原型

所以,Function.__proto__本来也应该指向Function的构造函数的原型。
但是因为Function比较特殊,他是祖宗级别的函数,是JS中万物开天辟地就有的,不能说谁把他构造出来的,
因此Function的__proto__的指向就比较特殊,他没有自己的构造函数,于是就指向了自己的原型。
于是Function.__proto__指向自己的原型Function.prototype。
所以 Function.__proto__ === Function.prototype(图中线条b)

这是原型链中第一个特殊点
口诀:所有函数对象的__proto__都指向Function.prototype(包括Function自身)

扩展:

原型对象prototype身上都有constructor属性,指回构造函数自身。
所以 Function.prototype.constructor === Function

Object

再说Object。(图中色块3)
我们平时见过这种创建函数的书写形式:

const obj = new Object()

可见Object是一个函数。但同时函数又是一个对象。所以Object就是一个函数对象。
只要是函数对象,就会有原型prototype和隐式原型__proto__两个属性。

我们先看Object.prototype。(图中色块4)
Object作为一个函数,他就有自己的原型:Object.prototype。
根据口诀:所有函数的prototype都指向自身prototype
所以 Object.prototype === Object.prototype(图中线条d)

而对于Object.__proto__ 我们可以这样理解:
Object作为一个函数,他是Function构造出来的。形似下面这种写法:(图中线条c)

const Object = new Function()

因此可以说Object是实例化函数对象。
根据口诀:一切 实例化对象的__proto__都指向构造函数的prototype函数是Function构造出来的
所以 Object.__proto__ === Function.prototype。(图中线条f)

原型的原型

我们来分析下两个内置函数的原型的原型:

先看Function.prototype.__proto__
Function.prototype作为Function的原型对象,他就是一个普通对象,但凡普通对象就都是Object构造出来的,
根据口诀:实例化对象的__proto__都指向构造函数的prototype所有prototype的__proto__都指向Object.prototype(Object的除外)
所以所有prototype对象的__proto__都指向构造函数Object的原型。包括Function函数的原型的隐式原型,也指向Object的原型。
所以 Function.prototype.__proto__ === Object.prototype。(图中线条g)

再看Object.prototype.__proto__
Object.prototype作为是一个普通对象,他的隐式原型__proto__也本应该指向构造函数的原型。
但由于prototype对象都是Object函数构造的,按照上边的规则,Object.prototype.__proto__也本应该指向Object.prototype。但是这么死循环的指没完没了了不是,还没有意义。
所以这里是原型链中第二个特殊点:让Object.prototype的原型指向null,好结束这段轮回。
也就是 Object.prototype.__proto__ === null。(图中线条e)

口诀:所有prototype的__proto__都指向Object.prototype(Object的除外)

好在,Object.prototye的构造函数还是诚实的,知道自己的祖宗是谁,于是他的constructor属性还是Ojbect。
Object.prototype.constructor === Object

自定义函数

我们都知道,平时我们用字面量的形式创建一个对象、数组、function,
其实都是new Object()、或 new Array()、new Function 这样的形式创建的。(图中线条h)

var obj = {};  // 类似写法 var obj = new Object();
var arr = [];  // 类似写法 var arr = new Array();
function Person({}  // 类似写法 var Person = new Function(){}

所以对象、数组、函数这些都是实例化对象。
对象、数组稍后再谈,他们就是自定义对象。(图中色块7)
先说我们创建的函数 — — 自定义函数。(图中色块5)

「自定义函数」和Object性质一样,都是函数对象。只不过自定义函数的名字是我们用户自定义的,比如Person、Animal、clickHandle等。而Object、Array等是JS内部原生提供的。
但记住口诀:只要是函数对象,就会有原型prototype和隐式原型__proto__两个属性

先说自定义函数.prototype(图中色块6)。
前边说过,所有函数的prototype都指向自身prototype,原型上边的constructor再指回函数自身。

所以 Person.prototype === Person.prototype(图中线条i)

再说自定义函数.prototype.__proto__
自定义函数的原型作为普通对象,由Object构造出来,其原型__proto__肯定指向Object.prototype。原理同Function.prototype。
根据口诀:实例化对象的__proto__都指向构造函数的prototype所有prototype的__proto__都指向Object.prototype(Object的除外)
所以 自定义函数.prototype.__proto__ === Object.prototype。(图中线条J)

再说自定义函数.__proto__
根据口诀:实例化对象的__proto__都指向构造函数的prototype
因此,实例化对象(这里的自定义函数)的__proto__就指向构造函数的原型。根据口诀:函数是Function构造出来的、所有函数对象的__proto__都指向Function.prototype(包括Function自身)。所有自定义函数的构造函数是Function,他的原型也就是右边的Function.prototype。
自定义函数.__proto__ === Function.prototype。(图中线条k)

自定义对象

说清楚了自定义函数,我们再来说自定义对象。(图中色块7)
比如obj、arr这样的对象,他们和我们平时“new + 构造函数()”得到的实例化对象一样:(图中线条L)

const object = new Object()
const person = new Person()

以这个person为例,说一下图中绿色块7:自定义对象

既然叫「自定义对象」,那他肯定就只是一个对象。
对象就好说了,普通对象身上只有__proto__,而且普通对象(实例化对象)的__proto__指向构造函数的prototype。
根据口诀:实例化对象的__proto__指向构造函数的prototype
即自定义对象.__proto__ 指向 自定义对象的构造函数(即自定义函数)的prototype
所以 person.__proto__ === Person.prototype。(图中线条m)

于是图中(实例化)自定义对象.__proto__ 指向了上边自定义函数原型。

至此,这张图我们都过了一遍。

总结

  • Function.__proto__ === Function.prototype【特殊】
  • Function.prototype === Function.prototype
  • Function.prototype.constructor === Function
  • Function.prototype.__proto__ === Object.prototype
  • Object.__proto__ === Function.prototype。
  • Object.prototype.__proto__ === null 【特殊】
  • Person.__proto__ === Function.prototype
  • Person.prototype.__proto__ === Object.prototype
  • person.__proto__ === Person.prototype

原型链

由于原型对象prototype本身是一个对象,因此,他也有隐式原型__proto__。隐式原型指向的规则不变,指向构造函数的原型;
这样一来,原型 -> 隐式原型、隐式原型 -> 原型。
从某个对象出发,依次寻找隐式原型的指向,将形成一个链条,该链条叫做原型链
在查找对象成员时,若对象本身没有该成员,则会到原型链中查找。

在上图和知识总结中我们看到:
自定义对象的__proto__指向自定义函数的原型。
而自定义函数的原型也是一个对象,他虽然在函数一生下来就有了,但是他作为对象,也是Object函数对象构建的。因此自定义函数原型身上的__proto__指向Object的原型对象。
而Object.prototype又指向null。
观察发现这最左边的一条居然形成了一个链式指向:自定义对象 -> 自定义函数的原型 -> Object原型 -> null
当我们在最低部的自定义对象身上寻找一个属性或方法找不到的时候,JS就会沿着这条原型链向上查找,若找到就返回,直到null还查不到就返回undefined

同样的,函数 -> Function原型 -> Object原型 -> null, 也形成了原型链。当我们在函数身上调用一个方法或属性时,根据原型链的查找规则,会一直层层向上查找到null。

这也就是为什么,call、apply、bind这些函数是定义在Function原型身上的,我们也能用Person.call、Person.apply这样调用;hasOwnProperty、isPrototypeOf这些函数是定义在Object原型身上的,我们也能用Person.isPrototypeOf、obj.hasOwnProperty这样使用了。

function Person({
  console.log('我是Person函数');
}
let obj = new Object()
let person = new Person()

console.log(person.hasOwnProperty('a')); 
// 原型链查找:person -> person.__proto__(即Person.prototype) -> Person.prototype.__proto__ (即Object.prototype) 找到hasOwnProperty函数,执行调用
console.log(Person.call());
// 原型链查找:Person -> Person.__proto__(即Function.prototype) 找到call函数,执行调用
console.log(obj.xxx)
// 原型链查找:obj -> obj.__proto__(即 Object.prototype) -> null 没找到,返回undefined

知识点扩展

函数对象和普通对象

普通对象是通过 new 函数() 创建/构造的
函数对象是通过 new Function() 构造的

所有对象都是通过 new 函数() 的方式创建的

  • 该函数叫做构造函数;
  • 创建的对象被称作实例化对象
  • 对象赋值给变量后,变量中保存的是地址,地址指向对象所在的内存。

函数也是一个对象,他是通过 new Function() 创建的

原型对象 prototype

原型prototype的本质:对象。
prototype又称作原型对象。
原型对象也有一个自己的原型对象:__proto__
所有的函数都有原型属性prototype
默认情况下,prototype是一个Object对象。也就是说由Object构造函数创建,其原型指向Object的prototype。
prototype中默认包含一个属性:constructor,该属性指向函数对象本身
prototype中默认包含一个属性:__proto__,该属性指向构造函数的原型(默认情况是Object.prototype)

隐式原型 __proto__

所有的对象都有隐式原型:__proto__属性
隐式原型是一个对象,指向创建该对象的构造函数的原型 prototype
在查找对象成员时,若对象本身没有该成员,则会到隐式原型中查找。
层层向上知道Object.prototype。若到null还找不到则返回undefined。

__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。
来自《es6》<http://es6.ruanyifeng.com/#docs/class>

隐式原型和原型出现的根本原因:

js没有记录类型的元数据。因此,js只能通过对象的隐式原型找到创建他的函数的原型,从而确定其类型。

特殊的两个情况

Function的隐式原型指向自己的原型
Object原型的隐式原型指向null

两个固定情况

所有函数的隐式原型,都指向Function的原型(包括Function函数自身)
所有函数原型的隐式原型,都指向Object的原型。(不包括Object原型对象自身)

constructor

原型中的constructor指向函数本身:

思考

Function原型上都有什么?

执行下列代码,创建一个普通该函数。

function a(){}

观察window.a在控制台的打印结果,展开a.__proto__,得到Function.prototype的所有默认属性:

图中可以看到,a.prototype.__proto__,即Function的原型中:

  • 有函数方法:call、apply、bind、toString、constructor、Symbol;(标志性就是call、apply、bind这仨)
  • 有属性:arguments、caller、以及这俩属性的getter和setter;
  • 最后还有对象:__proto__指向他的构造函数原型(也就是Object.prototype)

Object原型上都有什么?

有函数方法:hasOwnProperty、isPrototypeOf、propertyIsEnumerable、toLocaleString、toString、valueOf、以及constructor
特殊的还有:get __proto__、set __proto__,估计是为了返回null给拦截的。
标志就是get __proto__、set __proto__这俩

其他探索问题

数组函数的原型上都有什么?
自定义函数的原型上都有什么?

练手面试题

最后来两道面试题,欢迎评论区一起探讨:

让我们一起携手同走前端路, 关注公众号【前端印记】即可。

posted @ 2021-08-09 11:24  xing.org1^  阅读(1301)  评论(5编辑  收藏  举报