【前端词典】继承(一) - 原型链你真的懂吗?

写在前言前面的话

虽然说标题是继承,但继承这块的涉及的知识点不只仅只是继承,因此这块我会分红两个部分来说:前端

  • 第一部分主讲原型以及原型链
  • 第二部分主讲继承的几种方式

分两节讲有如下两个缘由:面试

  1. 须要了解继承必须对原型和原型链有深入的了解,分开讲好消化
  2. 最近孩子快出生,须要更多的时间翻阅资料来保证文章的质量

前言

继承于咱们前端来讲绝对是很是熟悉也必须熟悉的一个高频必懂知识点。熟悉到只要是面试必定会有关于继承的问题;并且源码中继承的使用也随处可见。数组

可依旧有不少前端对继承的实现和应用没有一个总体的把握。追其缘由无非有二:bash

  1. ECMAScript 继承的实现方法区别于其余基于类的实现继承的面向对象(Object Oriented)语言。
  2. 工做中即便对如何实现继承只知其一;不知其二,也一点都不耽误写逻辑代码。

不管因为哪个缘由,建议请尽快弄懂继承的实现和应用,不然你可能会如同你的表情包同样——流下了没有技术的泪水。微信

接下来我会尽我所能讲清楚继承这个概念,并结合相关经典图文作辅助解释。函数

在讲 ECMAScript 继承的概念以前,我先说下原型的概念。源码分析

类与原型

讲 ECMAScript 继承的概念以前,我先说下类的概念。(若是接触过 Java 或者是 C++ 的话,咱们就知道 Java(C++)的继承都是基于类的继承)。post

类: 是面向对象(Object Oriented)语言实现信息封装的基础,称为类类型。每一个类包含数听说明和一组操做数据或传递消息的函数。类的实例称为对象this

类: 是描述了一种代码的组织结构形式,一种在软件中对真实世界中问题领域的建模方法。spa

类的概念这里我就再也不扩展,感兴趣的同窗能够自行查阅书籍。接下来咱们重点讲讲原型以及原型链

原型

JavaScript 这门语言没有类的概念,因此 JavaScript 并不是是基于类的继承,而是基于原型的继承。(主要是借鉴 Self 语言原型(prototype)继承机制)。

注意:ES6 中的 class 关键字和 OO 语言中的类的概念是不一样的,下面我会讲到。ES6 的 class 其内部一样是基于原型实现的继承。

JavaScript 摒弃转而使用原型做为实现继承的基础,是由于基于原型的继承相比基于的继承上在概念上更为简单。首先咱们明确一点,存在的目的是为了实例化对象,而 JavaScript 能够直接经过对象字面量语法轻松的建立对象。

每个函数,都有一个 prototype 属性。 全部经过函数 new 出来的对象,这个对象都有一个 __proto__ 指向这个函数的 prototype。 当你想要使用一个对象(或者一个数组)的某个功能时:若是该对象自己具备这个功能,则直接使用;若是该对象自己没有这个功能,则去 __proto__ 中找。

1. prototype [显式原型]

prototype 是一个显式的原型属性,只有函数才拥有该属性。
每个函数在建立以后都会拥有一个名为 prototype 的属性,这个属性指向函数的原型对象。( 经过 Function.prototype.bind 方法构造出来的函数是个例外,它没有 prototype 属性 )

prototype 是一个指针,指向的是一个对象。好比 Array.prototype 指向的就是 Array 这个函数的原型对象。

在控制台中打印 console.log(Array.prototype) 里面有不少方法。这些方法都以事先内置在 JavaScript 中,直接调用便可。上面我标红了两个特别的属性 constructor__proto__。这两个属性接下来我都会讲。

咱们如今写一个 function noWork(){} 函数。

当我写了一个 noWork 这个方法的时候,它自动建立了一个 prototype 指针属性(指向原型对象)。而这个被指向的原型对象自动得到了一个 constructor (构造函数)。细心的同窗必定发现了: constructor 指向的是 noWork

noWork.prototype.constructor === noWork     // true
复制代码

一个函数的原型对象的构造函数是这个函数自己

如图:

tips: 图中打印的 Array 的显式原型对象中的这些方法你都知道吗?要知道数组也是很是重要的一部分哦 ~ 咳咳咳,这是考试重点。

2. __proto__[隐式原型]

prototype 理解起来不难,__proto__ 理解起来就会比 prototype 稍微复杂一点。不过当你理解的时候你会发现,这个过程真的颇有趣。下面咱们就讲讲 __proto__

其实这个属性指向了 `[[prototype]]`,可是 `[[prototype]]` 是内部属性,咱们并不能访问到,因此使用 `__proto__` 来访问。

我先给个有点绕的定义:

__proto__ 指向了建立该对象的构造函数的显式原型。

咱们如今仍是使用 noWork 这个例子来讲。咱们发现 noWork 原型对象中还有另外一个属性 __proto__

咱们先打印这个属性:

咱们发现这个 __proto__ 指向的是 Object.prototype

我听到有人在问为何?

  1. 由于这个 __proto__.constructor 指向的是 Object
  2. 咱们知道:一个函数的原型对象的构造函数是这个函数自己
  3. 因此这个 __proto__.constructor 指向的是 Object.prototype.constructor
  4. 进而 __proto__ 指向的是 Object.prototype

如图:

咱们来验证一下:

至于为何是指向 Object? 由于全部的引用类型默认都是继承 Object 。

做用

  1. 显式原型:用来实现基于原型的继承与属性的共享。
  2. 隐式原型:构成原型链,一样用于实现基于原型的继承。 举个例子,当咱们使用 noWork 这个对象中的 toString() 属性时,在noWork 中找不到,就会沿着 __proto__ 依次查找。

3. new 操做符

当咱们使用 new 操做符时,生成的实例对象拥有了 __proto__属性。即在 new 的过程当中,新对象被添加了 __proto__ 而且连接到构造函数的原型上。

new 的过程

  1. 新生成了一个对象
  2. 连接到原型
  3. 绑定 this
  4. 返回新对象

Function.__proto__ === Function.prototype

难道这表明着 Function 本身产生了本身? 要说明这个问题咱们先从 Object 提及。

咱们知道全部对象均可以经过原型链最终找到 Object.prototype ,虽然 Object.prototype 也是一个对象,可是这个对象却不是 Object 创造的,而是引擎本身建立了 Object.prototype 因此能够这样说:

全部实例都是对象,可是对象不必定都是实例。

接下来咱们来看 Function.prototype 这个特殊的对象:

打印这个对象,会发现这个对象实际上是一个函数。咱们知道函数都是经过 new Function() 生成的,难道 Function.prototype 也是经过 new Function() 产生的吗?这个函数也是引擎本身建立的。

首先引擎建立了 Object.prototype ,而后建立了 Function.prototype ,而且经过 __proto__ 将二者联系了起来。

这就是为何 Function.prototype.bind() 没有 prototype 属性。由于 Function.prototype 是引擎建立出来的对象,引擎认为不须要给这个对象添加 prototype 属性。

对于为何 Function.__proto__ 会等于 Function.prototype ? 我看到的一个解释是这样的:
其余全部的构造函数均可以经过原型链找到 Function.prototype ,而且 function Function() 本质也是一个函数,为了避免产生混乱就将 function Function()__proto__ 联系到了 Function.prototype 上。

继承的几种方式

未完待续

参考

  1. 《JavaScript 高级程序设计》
  2. 《你不知道的 JavaScript - 上》
  3. 《JavaScript 语言精粹》
  4. ~zepto设计和源码分析

前端词典系列

《前端词典》这个系列会持续更新,每一期我都会讲一个出现频率较高的知识点。但愿你们在阅读的过程中能够斧正文中出现不严谨或是错误的地方,本人将不胜感激;若经过本系列而有所得,本人亦将不胜欣喜。

若是你以为个人文章写的还不错,能够关注个人微信公众号,公众号里会提早剧透呦。

你也能够添加个人微信 wqhhsd, 欢迎交流。

下期预告

【前端词典】继承(二) - “回”的几种写法

传送门

  1. 【前端词典】和媳妇讲代理后的意外收获
  2. 【前端词典】滚动穿透问题的解决方案
  3. 继承(一) - 原型链你真的懂吗?