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

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


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

就是这张图:

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

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

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

前置知识

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

  • 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. 在vue项目中使用scss,以及vscode适配scss语法(解决使用scss语法编辑器报错)

    项目搭建好之后 安装sass 依赖包 npm install --save-dev sass-loader //sass-loader依赖于node-sass npm install --save-d ...

  2. hive学习笔记之四:分区表

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  3. excel VBA根据一列的逗号隔开值分行

    Sub test1()    Dim h    Dim j As Integer    j = 0    Dim n1 As Integer '分行单元格在第几列    Dim m1 As Integ ...

  4. 对ES6中类class以及实例对象、原型对象、原型链之间关系的详细总结

    1. 类 ES6 中新增加了类的概念,可以使用 class 关键字声明一个类,之后用这个类来实例化对象.即类的用途:实例化对象. // 创建一个Person类 class Person { } // ...

  5. mapboxgl 互联网地图纠偏插件(二)

    前段时间写的mapboxgl 互联网地图纠偏插件(一)存在地图旋转时瓦片错位的问题. 这次没有再跟 mapboxgl 的变换矩阵较劲,而是另辟蹊径使用 mapboxgl 的自定义图层,重新写了一套加载 ...

  6. JS中的单例模式及单例模式原型类的实现

    单例模式 单例模式的定义: 保证一个类只有一个实例,并提供一个访问它的全局访问点 通过一个简单的例子来了解单例模式的作用: class Div { constructor() { return doc ...

  7. Vector ArrayList LinkedList

    三者都实现了List接口! Vector与ArrayList:采用顺序存储的方式,但是Vector是线程安全的,ArrayList是线程不安全的,按需使用: 当存储空间不足的时候,ArrayList默 ...

  8. python 常见面试问题

    https://blog.csdn.net/weixin_43789195/article/details/87469096 https://blog.csdn.net/qq_42642945/art ...

  9. ESP32-默认事件循环

    默认的事件循环是一个事件循环的系统应用发布和处理事件(例如,Wi-Fi无线事件). 基于ESP-IDF4.1 1 #include "esp_log.h" 2 #include & ...

  10. [转载]实现DDOS攻击自动封禁IP

    1 #!/bin/bash 2 ############################################################# 3 # File Name: ddos_ch ...