1. 原型
我们应该相对独立地来看原型这个概念。
很多人会把原型看得很复杂,实际上它只是【能给其它对象提供共享属性的对象】。
好比郭德纲和郭麒麟。首先这俩都是人(对象),其次郭德纲给郭麒麟提供了相声教学等“共享属性”,所以郭德纲是一个“原型”。
1.1 隐式引用
- 在许多浏览器中,我们可以通过对象的
__proto__属性来访问该对象的原型。
(关于__proto__的历史问题我不展开讲了,有兴趣的可以看看第一篇参考文章。)
还是用郭德纲郭麒麟来举例子。我们假装郭德纲的儿子们关系网里都有名字叫__爸爸__的人,指向的是郭德纲。那我们访问郭麒麟.__爸爸__不就可以找到郭德纲了嘛?! - 我们可以通过设置对象的
__proto__来直接设置原型
这就好像郭德纲现在又生了个儿子,他需要在户口本上加上一些信息:新儿子.__爸爸__ = 郭德纲
2. 继承
对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为
__proto__)指向它的构造函数的原型对象(prototype )。(来自 MDN)
简单来说,继承意味着“爸爸有的我也有!”。而这一点在 JS 中是通过原型来实现的。
2.1 分类
2.1.1 显式继承
既然继承是两个对象之间的事情,那么我们手动给对象设置原型就可以实现继承。
1 | let gdg = { |
2.1.2 隐式继承(构造函数继承)
显示继承要求我们有 2 个对象,一个对象作为另一个对象的原型。
但隐式继承不用手动新建一个原型对象,只需通过“构造函数”就能快速实现继承。不过这原理还是__proto__,只不过它悄咪咪给你操作好了,我们使用的时候无痛当爹,察觉不出来而已。
每个函数,都默认有一个原型,可以通过 function.prototype 访问到,当我们使用 new 调用构造函数时,就默认继承了这个原型
1 | function Person (name, age) { |
我记得我有很长一段时间搞不清楚 prototype 和 __proto__。实际上继承本质上是两个对象之间的关系,中间可以没有构造函数。我们加上 Person 这个函数,但最终我们要用到的是 Person.prototype 这个对象。
这就好像 郭麒麟.__爸爸__ = 郭麒麟妈妈.老公(gql.__proto__ = GQLMM.prototype)
因此,隐式继承就是妈妈生了一个孩子,生的孩子.__爸爸__ = 妈妈.老公;显示继承有点像直接抱养一个孩子,直接把儿子.__爸爸__改掉就行了
另外,每个函数的 prototype 也是一个对象,既然是对象就有__proto__,这个属性是默认指向 Object.prototype 的。也就是说 func.prototype.__proto__ === Object.prototype。
有一道常见的面试题是“new 操作符做了什么工作”。实际上 new 背后的工作和显式继承的思路是一样的。我们尝试用显式继承的思路来写一个 new。大概是按以下三步走:1. 新建对象;2. 手动设置该新对象的__proto__为构造函数.protptype;3. 返回对象
1 | function _new() { |
当然 new 还是有别的特性的,以上代码只是最基础的。关于 new 操作符可能之后还会再展开写一篇文章。
2.2 方法
下面要说的继承的方法,其实基本是以显示继承和隐式调用的思想为基础的。以下方法按照 《Javascript 高级程序设计》一书分类。
2.2.1 原型链
这个实际上用的是隐式继承的方法
1 | function SuperType(){ |

2.2.1.1 问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引 用类型值的原型。包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原 型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
来自《Javascript 高级程序设计》
2.2.2 借用构造函数
关键在“借用”,我们在子构造函数中调用父构造函数
1 | function SuperType(){ |
这个和我们之前说的显示、隐式都不一样,他没有涉及到__proto__的变化。但确实实现了 SubType 实例中有 SuperType 相关的内容。通过使用 call()方法(或 apply()方法 也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。 这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果, SubType 的每个实例就都会具有自己的 colors 属性的副本了。
2.2.2.1 问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定 义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结 果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的
来自《Javascript 高级程序设计》
2.2.3 组合继承
结合前两种继承方法。用原型链去继承原型上一些共享的属性/方法,用构造函数来初始化实例属性。
2.2.4 原型式继承
这个关键就是基于已有的对象创建新对象。我们可以用显式也可以用隐式。
先看看显示继承:
1 | function create (prototype) { |
是不是和之前写的“new”特别像??改巴改巴就变成隐式继承了:
1 | function create (prototype) { |
ECMAScript 5 新增了 Object.create() 方法用于原型式继承。在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的
2.2.5 寄生式继承
这个寄生式继承,我理解就是原型式继承上再加点东西。我推荐和接下来的寄生组合式继承联系起来看。
2.2.6 寄生组合式继承
顾名思义,寄生组合式继承=寄生式继承+组合继承。为什么要有寄生组合式继承呢?明明其他方法已经很香了。因为它解决了组合继承中调用2次构造函数的问题。
当我们在讨论 2 个类(当然,JS 中就是个函数)之间的继承时,回顾我们之前提到的 5 种方法。首先不适合的是原型式继承,因为它适用于 2 个对象之间。其次原型链和借用构造函数各自都有一些缺点,组合继承结合了这两者,但它也有个缺点就是会调用 2 次父类的构造函数。那么“寄生组合式继承”就是通过寄生式继承的方法来解决了组合继承的这个缺点。
1 | // 寄生式继承 |
3. Object.prototype
这是一个十分特殊的对象
- 我们之前说过每个函数都默认有原型的,那么这个就是
Object函数的原型 - 这个是 JS 引擎在最开始定义的一个对象。我们可以把它想像成女娲,那么自然
- 女娲是没有原型的,即
Object.prototype.__proto__ === null - 所有没爸没妈的孩子都当作女娲的孩子。
1
2
3// 普普通通一对象
let a = { name: '没爸没妈' }
a.__proto__ === Object.prototype // true
- 女娲是没有原型的,即
4. 原型链
我们已经知道了,每个对象在【给其它对象提供共享属性】时就是原型的,且两者之前是通过 __proto__ 来隐式地关联起来的。那么假设有 A.__proto__ = B,B 虽然是 A 的原型,但它自身也可以有原型,也就是 B.__proto__ = C。如下图
graph LR A[A] --> |__proto__|B[B] B --> |__proto__|C[C] C --> |__proto__|D[...] D --> |__proto__|E[Object.prototype] E --> |__proto__|F[null]
这条链一直下去的话,最终会指向 Object.prototype
4.1 属性查找
问:假设“报菜名”是老郭家的独门绝技,现在郭麒麟想学,该怎么办?
答:1. 郭麒麟已经会了,那就自己再复习下;2.郭麒麟不会,那问他爸;3. 他爸也不会,那问他爷爷
同理。当我们访问对象的属性时,也会沿着原型链一直查找
5. 总结
如果你看完了文章还是一团乱麻,那么我自己总结了一些要点帮助记忆和理解
- 所有对象都有
__proto__ - 所有函数都默认有
prototype - 函数也是对象,所以有
__proto__ - 所有函数都继承自
Function