继承是我们前端必须熟悉的一个知识点。可依旧有很多前端对继承的实现和应用没有一个整体的把握。追其原因无非有二:
无论由于哪一个原因,建议请尽快弄懂继承的实现和应用,否则你可能会如同你的表情包一样——流下了没有技术的泪水。
接下来我会尽我所能讲清楚继承这个概念,并结合相关经典图文做辅助解释。
在讲 ECMAScript 继承的概念之前,我先说下类和原型的概念。
讲 ECMAScript 继承的概念之前,我先说下类的概念。(如果接触过 Java 或者是 C++ 的话,我们就知道 Java(C++)的继承都是基于类的继承)。
类: 是面向对象(Object Oriented)语言实现信息封装的基础,称为类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。 类: 是描述了一种代码的组织结构形式,一种在软件中对真实世界中问题领域的建模方法。
类的概念这里我就不再扩展,感兴趣的同学可以自行查阅书籍。接下来我们重点讲讲原型以及原型链。
JavaScript 这门语言没有类的概念,所以 JavaScript 并非是基于类的继承,而是基于原型的继承。(主要是借鉴 Self 语言原型( prototype
)继承机制)。
注意:ES6 中的 class 关键字和 OO 语言中的类的概念是不同的,下面我会讲到。ES6 的 class 其内部同样是基于原型实现的继承。
JavaScript 摒弃类转而使用原型作为实现继承的基础,是因为基于原型的继承相比基于类的继承上在概念上更为简单。首先我们明确一点,类存在的目的是为了实例化对象,而 JavaScript 可以直接通过对象字面量语法轻松的创建对象。
每一个函数,都有一个 prototype
属性。 所有通过函数 new
出来的对象,这个对象都有一个 __proto__
指向这个函数的 prototype
。 当你想要使用一个对象(或者一个数组)的某个功能时:如果该对象本身具有这个功能,则直接使用;如果该对象本身没有这个功能,则去 __proto__
中找。
prototype
[显式原型]
prototype
是一个显式的原型属性,只有函数才拥有该属性。 每一个函数在创建之后都会拥有一个名为prototype
的属性,这个属性指向函数的原型对象。( 通过Function.prototype.bind
方法构造出来的函数是个例外,它没有prototype
属性 )。
prototype
是一个指针,指向的是一个对象。比如 Array.prototype
指向的就是 Array 这个函数的原型对象。
在控制台中打印 console.log(Array.prototype)
里面有很多方法。这些方法都以事先内置在 JavaScript 中,直接调用即可。上面我标红了两个特别的属性 constructor
和 __proto__
。这两个属性接下来我都会讲。
我们现在写一个 functionnoWork(){}
函数。
当我写了一个 noWork
这个方法的时候,它自动创建了一个 prototype
指针属性(指向原型对象)。而这个被指向的原型对象自动获得了一个 constructor
(构造函数)。细心的同学一定发现了: constructor
指向的是 noWork
。
noWork.prototype.constructor === noWork // true
// 一个函数的原型对象的构造函数是这个函数本身
tips: 图中打印的 Array 的显式原型对象中的这些方法你都知道吗?要知道数组也是非常重要的一部分哦 ~ 咳咳咳,这是考试重点。
__proto__
[隐式原型]prototype
理解起来不难, __proto__
理解起来就会比 prototype
稍微复杂一点。不过当你理解的时候你会发现,这个过程真的很有趣。下面我们就讲讲 __proto__
。
其实这个属性指向了 `[[prototype]]`,但是 `[[prototype]]` 是内部属性,我们并不能访问到,所以使用 `__proto__` 来访问。
我先给个有点绕的定义:
__proto__
指向了创建该对象的构造函数的显式原型。
我们现在还是使用 noWork
这个例子来说。我们发现 noWork
原型对象中还有另一个属性 __proto__
。
我们先打印这个属性:
我们发现这个 __proto__
指向的是 Object.prototype
。
我听到有人在问为什么?
__proto__.constructor
指向的是 Object
。__proto__.constructor
指向的是 Object.prototype.constructor
。__proto__
指向的是 Object.prototype
。
至于为什么是指向 Object? 因为所有的引用类型默认都是继承 Object 。
作用
noWork
这个对象中的 toString()
属性时,在 noWork
中找不到,就会沿着 __proto__
依次查找。当我们使用 new 操作符时,生成的实例对象拥有了 __proto__
属性。即在 new 的过程中,新对象被添加了 __proto__
并且链接到构造函数的原型上。
new 的过程
Function.__proto__===Function.prototype
难道这代表着 Function
自己产生了自己? 要说明这个问题我们先从 Object
说起。
我们知道所有对象都可以通过原型链最终找到 Object.prototype
,虽然 Object.prototype
也是一个对象,但是这个对象却不是 Object
创造的,而是引擎自己创建了 Object.prototype
。 所以可以这样说:
所有实例都是对象,但是对象不一定都是实例。
接下来我们来看 Function.prototype
这个特殊的对象:
打印这个对象,会发现这个对象其实是一个函数。我们知道函数都是通过 newFunction()
生成的,难道 Function.prototype
也是通过 newFunction()
产生的吗?这个函数也是引擎自己创建的。
首先引擎创建了
Object.prototype
,然后创建了Function.prototype
,并且通过__proto__
将两者联系了起来。
这就是为什么 Function.prototype.bind()
没有 prototype
属性。因为 Function.prototype
是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype
属性。
对于为什么
Function.__proto__
会等于Function.prototype
? 我看到的一个解释是这样的: 其他所有的构造函数都可以通过原型链找到Function.prototype
,并且functionFunction()
本质也是一个函数,为了不产生混乱就将functionFunction()
的__proto__
联系到了Function.prototype
上。