一篇文章让你搞清楚 JavaScript 继承的本质、prototype
、__proto__
、constructor
都是什么。javascript
不少小伙伴表示不明白 JavaScript 的继承,说是原型链,看起来又像类,到底是原型仍是类?各类 prototype
、__proto__
、constructor
内部变量更是傻傻搞不清楚。其实,只要明白继承的本质就很能理解,继承是为了代码复用。复用并不必定得经过类,JS 就采用了一种轻量简明的原型方案来实现。Java/C++ 等强类型语言中有类和对象的区别,但 JS 只有对象。它的原型也是对象。只要你彻底抛开面向对象的继承思路来看 JS 的原型继承,你会发现它轻便但强大。html
prototype
class
__proto__
前面咱们讲,继承的本质是为了更好地实现代码复用。再仔细思考,能够发现,这里的「代码」指的必定是「数据+行为」的复用,也就是把一组数据和数据相关的行为进行封装。为何呢?由于,若是只是复用行为,那么使用函数就足够了;而若是只是复用数据,这使用 JavaScript 对象就能够了:java
const parent = { some: 'data', } const child = { ...parent, uniq: 'data', }
所以,只有数据+行为(已经相似于一个「对象」的概念)的封装,才是继承技术所必须出现的地方。为了知足这样的代码复用,一个继承体系的设计须要支持什么需求呢?git
「支持私有数据」,这个基本全部方案都没实现,此阶段咱们能够不用纠结;而「增长新成员的能力」,基本全部的方案都能作到,也再也不赘述,主要来看前四点。github
prototype
JavaScript 的继承有多种实现方式,具体有哪些,推荐读者可阅读:[JavaScript 语言精粹][]一书 和 这篇文章。这里,咱们直接看一版比较优秀的实现:浏览器
function Animal(name) { this.name = name this.getName = function() { return this.name } } function Cat(name, age) { Animal.call(this, name) this.age = age || 1 this.meow = function() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` } } const cat = new Cat('Lily', 2) console.log(cat.meow()) // 'Lilyeowww~~~~~, I'm 2 year(s) old'
这个方案,具有增添新成员的能力、调用被继承对象函数的能力等。一个比较重大的缺陷是:对象的全部方法 getName
meow
,都会随每一个实例生成一份新的拷贝。这显然不是优秀的设计方案,咱们指望的结果是,继承自同一对象的子对象,其全部的方法都共享自同一个函数实例。数据结构
怎么办呢?想法也很简单,就是把它们放到同一个地方去,而且还要跟这个「对象」关联起来。如此一想,用来生成这个「对象」的函数自己就是很好的地方。咱们能够把它放在函数的任一一个变量上,好比:函数
Animal.functions.getName = function() { return this.name } Cat.functions.meow = function() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` }
但这样调用起来,你就要写 animal.functions.getName()
,并不方便。不要怕,JavaScript 这门语言自己已经帮你内置了这样的支持。它内部所用来存储公共函数的变量,就是你熟知的 prototype
。当你调用对象上的方法时(如 cat.getName()
),它会自动去 Cat.prototype
上去帮你找 getName
函数,而你只须要写 cat.getName()
便可。兼具了功能的实现和语法的优雅。post
最后写出来的代码会是这样:this
function Animal(name) { this.name = name } Animal.prototype.getName = function() { return this.name } function Cat(name, age) { Animal.call(this, name) this.age = age || 1 } Cat.prototype = Object.create(Animal.prototype, { constructor: Cat }) Cat.prototype.meow = function() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` }
请注意,只有函数才有 prototype
属性,它是用来作原型继承的必需品。
class
然鹅,上面这个写法仍然并不优雅。在何处呢?一个是 prototype
这种暴露语言实现机制的关键词;一个是要命的是,这个函数内部的 this
,依靠的是做为使用者的你记得使用 new
操做符去调用它才能获得正确的初始化。可是这里没有任何线索告诉你,应该使用 new
去调用这个函数,一旦你忘记了,也不会有任何编译期和运行期的错误信息。这样的语言特性,与其说是一个「继承方案」,不如说是一个 bug,一个不该出现的设计失误。
而这两个问题,在 ES6 提供的 class
关键词下,已经获得了很是妥善的解决,尽管它叫一个 class,但本质上实际上是经过 prototype 实现的:
class Animal { constructor(name) { this.name = name } getName() { return this.name } } class Cat extends Animal { constructor(name, age) { super(name) this.age = age || 1 } meow() { return `${this.getName()}eowww~~~~~, I'm ${this.age} year(s) old` } }
new
操做符,编译器和运行时都会直接报错。为何呢,咱们将在[下一篇文章][]讲解extends
关键字,会使解释器直接在底下完成基于原型的继承功能如今,咱们已经看到了一套比较完美的继承 API,也看到其底下使用 prototype
存储公共变量的地点和原理。接下来,咱们要解决另一个问题:prototype
有了,实例对象应该如何访问到它呢?这就关系到 JavaScript 的向上查找机制了。
__proto__
function Animal(name) { this.name = name } Animal.prototype.say = function() { return this.name } const cat = new Animal('kitty') console.log(cat) // Animal { name: 'kitty' } cat.hasOwnProperty('say') // false
看上面 👆 一个最简单的例子。打出来的 cat
对象自己并无 say
方法。那么,被实例化的 cat
对象自己,是怎样向上查找到 Animal.prototype
上的 say
方法的呢?若是你是 JavaScript 引擎的设计者,你会怎样来实现呢?
我拍脑壳这么一想,有几种方案:
Animal
中初始化实例对象 cat
时,顺便存取一个指向 Animal.prototype
的引用Animal
中初始化实例对象时,记录其「类型」(也便是 Animal
)// 方案1 function Animal(name) { this.name = name // 如下代码由引擎自动加入 this.__prototype__ = Animal.prototype } const cat = new Animal('kitty') cat.say() // -> cat.__prototype__.say() // 方案2 function Animal(name) { this.name = name // 如下代码由引擎自动加入 this.__type__ = Animal } const cat = new Animal('kitty') cat.say() // -> cat.__type__.prototype.say()
究其实质,其实就是:实例对象须要一个指向其函数的引用(变量),以拿到这个公共原型 prototype
来实现继承方案的向上查找能力。读者若是有其余方案,不妨留言讨论。
无独有偶,这两种方案,在 JavaScript 中都有实现,只不过变量的命名与咱们的取法有所差别:第一种方案中,实际的变量名叫 __proto__
而不是 __prototype__
;第二种方案中,实际的变量名叫 constructor
,不叫俗气的 __type__
。实际上,用来实现继承、作向上查找的这个引用,正是 __proto__
;至于 constructor,则另有他用。不过要注意的是,尽管基本全部浏览器都支持 __proto__
,它并非规范的一部分,所以并不推荐在你的业务代码中直接使用 __proto__
这个变量。
从上图能够清楚看到,prototype
是用来存储类型公共方法的一个对象(正所以每一个类型有它基本的方法),而 __proto__
是用来实现向上查找的一个引用。任何对象都会有 __proto__
。Object.prototype
的 __proto__
是 null,也便是原型链的终点。
再加入 constructor 这个东西,它与 prototype
、__proto__
是什么关系?这个地方,说复杂就很复杂了,让咱们尽可能把它说简单一些。开始以前,咱们须要查阅一下语言规范,看一些基本的定义:
__proto__
)][specification: overview]prototype
对象,用以实现原型式继承,做属性共享用 这里说明了什么呢?说明了构造函数是函数,它比普通函数多一个 prototype
属性;而函数是对象,对象都有一个原型对象 __proto__
。这个东西有什么做用呢?
上节咱们深挖了用于继承的原型链,它连接的是原型对象。而对象是经过构造函数生成的,也就是说,普通对象、原型对象、函数对象都将有它们的构造函数,这将为咱们引出另外一条链——
在 JavaScript 中,谁是谁的构造函数,是经过 constructor
来标识的。正常来说,普通对象(如图中的 cat
和 { name: 'Lin' }
对象)是没有 constructor
属性的,它是从原型上继承而来;而图中粉红色的部分便是函数对象(如 Cat
Animal
Object
等),它们的原型对象是 Function.prototype
,这没毛病。关键是,它们是函数对象,对象就有构造函数,那么函数的构造函数是啥呢?是 Function
。那么问题又来了,Function
也是函数,它的构造函数是谁呢?是它本身:Function.constructor === Function
。由此,Function
便是构造函数链的终结。
上面咱们提到,constructor
也能够用来实现原型链的向上查找,而后它却别有他用。有个啥用呢?通常认为,它是用以支撑 instanceof
关键字实现的数据结构。
好了,是时候进入最烧脑的部分了。前面咱们讲了两条链:
Object.prototype
,终结于 null
,没有循环Function
把这两条链结合到一块儿,你就会看到一条双螺旋 DNA这几张你常常看到却又看不懂的图:
图都是引用自其它文章,点击图片可跳转到原文。其中,第一篇文章 [一张图理解 JS 的原型][] 是我见过解析得最详细的,本文的不少灵感也来自这篇文章。
理解了上面两条链之后,这两个全图实际上就不难理解了。分享一下,怎么来读懂这个图:
constructor
都会指向它们的构造函数;而构造函数也是对象,它们最终会一级一级上溯到 Function
这个构造函数。Function
的构造函数是它本身,也即此链的终结;Function
的 prototype
是 Function.prototype
,它是个普通的原型对象;__proto__
都会指向其构造函数的原型对象 [Class].prototype
;而全部原型对象,包括构造函数链的终点 Function.prototype
,都会最终上溯到 Object.prototype
,终结于 null。也便是说,构造函数链的终点 Function
,其原型又融入到了原型链中:Function.prototype -> Object.prototype -> null
,最终抵达原型链的终点 null
。至此这两条契合到了一块儿。
总结下来,能够归纳成这几句话:
Function
,包括 Function
本身Object.prototype
,包括 Function.prototype
,终止于 null
这里还有最后一个所谓「鸡生蛋仍是蛋生🐔」的问题:是先有 Object.prorotype
,仍是先有 Function
?若是先有前者,那么此时 Function
还不在,这个对象又是由谁建立呢?若是先有后者,那么 Function
也是个对象,它的原型 Function.prototype.__proto__
从哪去继承呢?这个问题,看似无解。但从 这篇文章:从__proto__和prototype来深刻理解JS对象和原型链 中,咱们发现了一个合理的解释,那就是:
Object.prototype
是个神之对象。它不禁Function
这个函数构造产生。
证据以下:
Object.prototype instanceof Object // false Object.prototype instanceof Function // false Object.prototype.__proto__ === Function.prototype // false
JS 对象世界的构造次序应该是:Object.prototype
-> Function.prototype
-> Function
-> Object
-> ...
讲到这里,我想关于 JavaScript 继承中的一些基本问题能够解释清楚了:
JavaScript 继承是类继承仍是原型继承?不是使用了 new 关键字么,应该跟类有关系吧?
是彻底的原型继承。尽管用了 new
关键字,但其实只是个语法糖,跟类没有关系。JavaScript 没有类。它与类继承彻底不一样,只是长得像。比如雷锋和雷峰塔的关系。
prototype
是什么东西?用来干啥?
prototype
是个对象,只有函数上有。它是用来存储对象的属性(数据和方法)的地方,是实现 JavaScript 原型继承的基础。
__proto__
是什么东西?用来干啥?
__proto__
是个指向 prototype
的引用。用以辅助原型继承中向上查找的实现。虽然它获得了全部浏览器的支持,但并非规范所推荐的作法。严谨地说,它是一个指向 [[Prototype]]
的引用。
constructor
是什么东西?用来干啥?
是对象上一个指向构造函数的引用。用来辅助 instanceof
等关键字的实现。
🐔生蛋仍是蛋生🐔?
神生鸡,鸡生蛋。
__proto__
][]