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

老师花两天时间理解、整理的,他讲了两个小时我们当时就听懂了。


今天我把他整理出来,分享给大家。也让我自己巩固加深一下。

就是这张图:

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

为了你更好的对照阅读,你可以单独打开这张图片,然后对比着文章看。

当然,我后边也会贴心的把对应区域截小图贴在文案附近。

前置知识

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

  • 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__这俩

其他探索问题

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

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

练手面试题

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

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

一张图带你搞懂Javascript原型链关系的更多相关文章

  1. 一张图搞懂javascript原型链

    js高级里面原型链对于新手来说并不友好,总的来说就是 任何函数都有自己的原型对象(prototype),任何实例对象都__proto__指向构造函数的原型 先来个最简单的原型三角关系 var fn = ...

  2. 一张图彻底搞懂JavaScript的==运算

    一张图彻底搞懂JavaScript的==运算 来源 https://zhuanlan.zhihu.com/p/21650547 PS:最后,把图改了一下,仅供娱乐 : ) 大家知道,==是JavaSc ...

  3. 轻松搞定javascript原型链 _proto_

    //如有错误或不同观点,欢迎批评与讨论! 首先,prototype出现的目的,是为了解决 代码重用 的问题 , prototype 相当于是在内存上划分出一个公共的区域, 专用于存放 实例化对象 的相 ...

  4. 一张图带你搞懂Node事件循环

    说一件重要的事儿:你还没关注公众号[前端印记],更多精彩内容等你探索-- 以下全文7000字,请在你思路清晰.精力充沛的时刻观看.保证你理解后很长时间忘不掉. Node事件循环 Node底层使用的语言 ...

  5. 一张图轻松搞懂javascript event对象的clientX,offsetX,screenX,pageX区别

    总是会被javascript的event对象的clientX,offsetX,screenX,pageX 弄得头晕,于是决定做个图来区分一下(画得我手那个酸呀....) 先总结下区别: event.c ...

  6. 40 张图带你搞懂 TCP 和 UDP

    前言 欢迎阅读「程序员cxuan」 的文章,从今往后,你就是我的读者了. 我的 github bestJavaer 已经收录此文章,目录在 https://github.com/crisxuan/be ...

  7. 来一轮带注释的demo,彻底搞懂javascript中的replace函数

    javascript这门语言一直就像一位带着面纱的美女,总是看不清,摸不透,一直专注服务器端,也从来没有特别重视过,直到最近几年,javascript越来越重要,越来越通用.最近和前端走的比较近,借此 ...

  8. 资料--JavaScript原型链

    JavaScript原型链 原文出处:https://www.cnblogs.com/chengzp/p/prototype.html 目录 创建对象有几种方法 原型.构造函数.实例.原型链 inst ...

  9. JavaScript原型链及其污染

    JavaScript原型链及其污染 一.什么是原型链? 1.JavaScript中,我们如果要define一个类,需要以define"构造函数"的方式来define: functi ...

随机推荐

  1. 深度解读MRS IoTDB时序数据库的整体架构设计与实现

    [本期推荐]华为云社区6月刊来了,新鲜出炉的Top10技术干货.重磅技术专题分享:还有毕业季闯关大挑战,华为云专家带你做好职业规划. 摘要:本文将会系统地为大家介绍MRS IoTDB的来龙去脉和功能特 ...

  2. npm ERR! Unexpected end of JSON input while parsing near '...'解决方法

    npm install时出现npm err! Unexpected end of JSON input while parsing near'...'错误 输入  npm cache clean -- ...

  3. SpringCloud-OAuth2(四):改造篇

    本片主要讲SpringCloud Oauth2篇的实战改造,如动态权限.集成JWT.更改默认url.数据库加载client信息等改造. 同时,这应该也是我这系列博客的完结篇. 关于Oauth2,我也想 ...

  4. 4、saltstack的使用

    官方文档地址:http://repo.saltstack.com/#rhel 4.1.saltstatck介绍: 用户要一致,这里使用的是root用户: 用于批量管理成百上千的服务器: 并行的分发,使 ...

  5. 应用CRM的自动化功能为什么备受推崇

    相信每个销售团队都遇到过这样的问题:在跟进新客户的时候顾此失彼,在跟踪一个客户的时候,转眼就忘记了另一个客户.这种情况很常见,但是每个新客户都有潜在的价值,我们该如何做,才能避免错失商机?CRM客户管 ...

  6. Spring缓存的注解关键词解释

    Spring缓存的注解关键词解释 @Cacheable支持缓存 @Cacheable可以标记在一个方法上,也可以标记在一个类上. 1.当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表 ...

  7. java swagger ui 添加header请求头参数

    我用到的swagger 主要有三款产品,swagger editor,swagger ui 和swagger codegen. swagger editor:主要是一个本地客户端,用来自己添加api, ...

  8. tf-gpu报错:ImportError: libcublas.so.10.0: cannot open shared object file: No such file or directory

    错误1:ImportError: libcublas.so.10.0: cannot open shared object file: No such file or directory 一般这种问题 ...

  9. OpenMVG 系列 (2):Image 和 Numeric

    OpenMVG 的功能模块由若干核心库组成,本文主要介绍 Image 和 Numeric 两个库 1  Image Image 库包含图像容器 Image<T>.图像IO读写函数 Read ...

  10. 一次性讲清楚spring中bean的生命周期之三:bean是如何实例化的

    在前面的两篇博文<一次性讲清楚spring中bean的生命周期之一:getSingleton方法>和<一次性讲清楚spring中bean的生命周期之二:FactoryBean的前世今 ...