你应该知道,JavaScript是一门基于原型链的语言,而咱们今天的主题 -- “继承”就和“原型链”这一律念息息相关。甚至能够说,所谓的“原型链”就是一条“继承链”。有些困惑了吗?接着看下去吧。javascript
要搞清楚如何在JavaScript中实现继承,咱们首先要搞懂构造函数,原型属性与实例对象三者之间的关系,让咱们先看一段代码:html
function Person(name, age) { var gender = girl // ① this.name = name // ② this.age = age } // ③ Person.prototype.sayName = function() { alert(this.name) } // ④ var kitty = new Person('kitty', 14) kitty.sayName() // kitty
让咱们经过这段代码澄清几个概念:java
Person
是一个“构造函数”(它用来“构造”对象,而且是一个函数),①处gender
是该构造函数的“私有属性”,②处的语句定义了该构造函数的“自有属性”;prototype
是Person
的“原型对象”(它是实例对象的“原型”,同时它是一个对象,但同时它也是构造函数的“属性”,因此也有人称它为“原型属性”),该对象上定义的全部属性(和方法)都会被“实例对象”所“继承”(咱们终于看到这两个字了,可是不要心急,咱们过一会才会谈论它);Person
的“实例对象”(它是由构造函数生成的一个实例,同时,它是一个对象),它能够访问到两种属性,一种是经过构造函数生成的“自有属性”,一种是原型对象能够访问的全部属性;对以上这些概念有清楚的认识,才能让你对JavaScript的“继承”与“原型链”的理解更加深入,因此务必保障你已经搞清楚了他们之间的关系。(若是没有,务必多看几遍,你能够找张纸写写画画,我第一次就是这么作的)浏览器
完全搞清楚了?那让咱们继续咱们的主题 -- “继承”。app
你是否以为奇怪,为何咱们的实例对象能够访问到构造函数原型属性上的属性(真是拗口)?答案是由于“每个对象自身都拥有一个隐式的[[proto]]
属性,该属性默认是一个指向其构造函数原型属性的指针”(其实我想说它是一个钩子,在对象建立时默认“勾住”了其构造函数的原型属性,可是我发现emoji竟然没有钩子的图标,因此...🤷🏻♂️,不过我仍是以为钩子更形象些...)。函数
当JavaScript引擎发现一个对象访问一个属性时,会首先查找对象的“自有属性”,若是没有找到则会在[[proto]]
属性指向的原型属性中继续查找,若是尚未找到的话,你知道其实原型属性也是一个对象,因此它也有一个隐式的[[proto]]
属性指向它的原型属性...,正如你所料,若是一直没有找到该属性,JavaScript引擎会一直这样找下去,直到找到最顶部构造函数Object
的prototype
原型属性,若是仍是没有找到,会返回一个undefined
值。这个不断查找的过程,有一个形象生动的名字“攀爬原型链”。this
如今你应该对“原型链”就是“继承链”这一说法有点感受了吧,让咱们暂时休息一下,对两个咱们遗漏的知识点补充说明:prototype
[[proto]]
属性prototype
[[proto]]
属性何为“隐式属性”呢?便是开发者没法访问却确实存在的属性,你可能会问,既然是隐式的,如何证实它的存在呢?问得好,答案是虽然JavaScript语言没有暴露给咱们这个属性,可是浏览器却帮助咱们能够获取到该属性,在Chorme中,咱们能够经过浏览器为对象添加的_proto_
属性访问到[[proto]]
的值。你能够本身试试在控制台中打印这个属性,证实我没有说谎。指针
prototype
还记的咱们以前提到JavaScript世界一条重要的概念吗?“每个对象自身都拥有一个隐式的[[proto]]
属性,该属性默认是一个指向其构造函数原型属性的指针”。其实与其对应的,还有一条重要的概念我须要在这里告诉你“几乎全部函数都拥有prototype
原型属性”。这两个概念确实很是重要,由于每当你搞混了构造函数,原型属性,实例对象之间的关系,以及JavaScript世界中的继承规则时,想一想这两个概念总能帮助你剥离迷雾,从新发现真相。code
由于他们真的很重要,因此我特别使用一个蓝色开头的列表再写一遍(保持耐心,朋友!)
[[proto]]
属性,该属性默认是一个指向其构造函数原型属性的指针;prototype
原型属性;至此,咱们搞清楚了构造函数,原型属性与实例对象三者的关系,相信我,理解清楚这三者的关系能让你以更清晰的视角去观察JavaScript的继承世界,而在下一章中,咱们将更进一步,直奔主题的阐述在JavaScript世界中如何实现继承,固然,还有背后的原理。
既然说了要直奔主题,咱们便直接开始对JavaScript世界中对象的继承方式展开说明。不过在那以前,让咱们再统一咱们对“继承”这一律念的认识:即咱们想要一个对象可以访问另外一个对象的属性,同时,这个对象还可以添加本身新的属性或是覆盖可访问的另外一个对象的属性,咱们实现这个目标的方式叫作“继承”。
而在JavaScript世界,实现继承的方式有如下两种:
看起来很合乎逻辑对吧,咱们可以针对“对象”,令一个对象继承另外一个对象,也可以转而针对建立对象的“构造函数”,以实现实例对象的继承。可是这里有个陷阱(你可能注意到了),对于一个已经定义的对象,咱们没法再改变其继承关系,咱们的第一种方式只能在“建立对象时”定义对象的继承对象。这是为何呢?答案是由于“咱们设置一个对象的继承关系,本质上是在操做对象隐式的[[proto]]
属性”,而JavaScript只为咱们开通了在对象建立时定义[[proto]]
属性的权限,而拒绝让咱们在对象定义时再修改或访问这一属性(因此它是“隐式”的)。很遗憾,在对象定义后改变它的继承关系确实是不可能的。
好了,是时候看看JavaScript世界中继承的主角了 -- Object.create()
(一)关于Object.create()
和对象继承
正如以前所说,Object.create()
函数是JavaScript提供给咱们的一个在建立对象时设置对象内部[[proto]]
属性的API,相信你已经清楚的知道了,经过修改[[proto]]
属性的值,咱们就能决定对象所继承的对象,从而以咱们想要的方式实现继承。
让咱们细致的了解一下Object.create()
函数:
var x = { name: 'tom', sayName: function() { console.log(this.name) } } var y = Object.create(x, { name: { configurable: true, enumerable: true, value: 'kitty', writable: true, } }) y.sayName() // 'kitty'
看到了吗,Object.create()
函数接收两个参数,第一个参数是建立对象想要继承的原型对象,第二个参数是一个属性描述对象(不知道什么是属性描述对象?看看我以前的这篇文章),而后会返回一个对象。
让咱们谈谈在调用Object.create()
时究竟发生了什么:
[[proto]]
属性的值;defineProperty()
方法,并将第二个参数传入该方法中;相信到这里你已经彻底明白了如何在建立对象时实现继承了,但这样的方法有不少局限,好比咱们只能在建立对象时设置对象的继承对象,又好比这种设置继承的方式是一次性的,咱们永远没法依靠这种方式创造出多个有相同继承关系的对象,而对于这种状况,咱们理所固然的要请出咱们的第二个主角 -- prototype
原型对象。
prototype
和构造函数继承还记得咱们以前反复说起构造函数,原型属性与实例对象的关系吧?咱们还强调了“几乎全部的函数都拥有prototype
属性”,如今就是应用这些知识的时候了,其实说到继承,构造函数生产实例对象的过程自己就是一种自然的继承。实例对象自然的继承着原型对象的全部属性,这实际上是JavaScript提供给开发者第二种(也是默认的)设置对象[[proto]]
属性的方法。
可是这种”自然的“继承方式缺点在于只存在两层继承:自定义构造函数的prototype
对象继承Object构造函数的prototype
属性,构造函数的实例对象继承构造函数的prototype
属性。而咱们有时想要更加灵活,知足需求,甚至是”更长“的原型链(或者说是”继承链“)。这是JavaScript默认的继承模式下没法实现的,但解决方式也很符合直觉,既然咱们没法修改对象的[[proto]]
属性,咱们就去修改[[proto]]
属性指向的对象 -- 原型对象。
咱们说过原型对象也是一个对象对吧?因此咱们就有了如下操做:
function Foo(x, y) { this.x = x this.y = y } Foo.prototype.sayX = function() { console.log(this.x) } Foo.prototype.sayY = function() { console.log(this.y) } function Bar(z) { this.z = z this.x = 10 } Bar.prototype = Object.create(Foo.prototype) // 注意这里 Bar.prototype.sayZ = function() { console.log(this.z) } Bar.prototype.constructor = Bar var o = new Bar(1) o.sayX() // 10 o.sayZ() // 1
相信你注意到了,我经过修改了构造函数Bar的原型属性,将其值设置为一个继承对象为Foo.prototype
的空对象,在以后,我又为在该对象添加了一些属性(注意到我添加的constructor
属性了吗?若是你不明白为何,你应该去了解一下我这么作的理由。)和方法。这样,构造函数Bar的实例对象就会在查询属性时攀爬原型链,从自有属性开始,途径Bar.prototype
,Foo.prototype
,最终到达Object.prototype
。这正是咱们想要的!太棒了!
绝不意外的,这种继承的方式被称为”构造函数继承“,在JavaScript中是一种关键的实现的继承方法,相信你已经很好的掌握了。
可是慢着,还有一个问题没有解决,让咱们回到刚才的代码,看看若是咱们在源代码上添加一条o.sayY()
会发生什么?答案是控制台会输出undefined
。
绝不意外对吧,毕竟咱们历来都没有定义过y属性。可是假如咱们也想让构造函数Bar的实例对象拥有构造函数Foo的设置的自有属性又该怎么办呢?答案是经过”构造函数窃取“技术,这将是咱们下一章也是最后一章要讨论的话题。
若是”窃取“所继承的构造函数的自有属性呢?答案是巧妙的使用.call()
和.apply()
方法,让咱们修改一下以前的代码:
function Foo(x, y) { this.x = x this.y = y } Foo.prototype.sayX = function() { console.log(this.x) } Foo.prototype.sayY = function() { console.log(this.y) } function Bar(z) { this.z = z this.x = 10 Foo.call(this, z, z) // 注意这里 } Bar.prototype = Object.create(Foo.prototype) Bar.prototype.sayZ = function() { console.log(this.z) } Bar.prototype.constructor = Bar var o = new Bar(1) o.sayX() // 1 o.sayY() // 1 o.sayZ() // 1
Done!咱们成功窃取了构造函数Foo的两个自有属性,构造函数Bar的实例对象如今也有了x和y的值!
虽然答案已经一目了然了,但仍是让我再解释一下这是怎么作到的:首先咱们知道构造函数也是函数,所以咱们能够像普通函数同样调用他,让咱们以单纯的函数视角看待构造函数Foo,它不过是往this
所指的对象上添加了两个属性,而后返回了undefined值,当咱们单纯调用该函数时,this
的指向为window
(不明白为何指向window,你能够阅读个人这篇文章)。可是经过call()
和apply()
函数,咱们能够人为的改变函数内this
指针的指向,因此咱们将构造函数内的this
传入call()
函数中,奇妙的事情发生了,原先为Foo函数实例对象添加的属性如今添加到了Bar函数的实例对象上!
“构造函数窃取”,我喜欢“窃取”这两个字,确实很巧妙。
太棒了 你终于看完了这篇文章,是否完全搞懂JavaScript中的继承了呢?但愿如此。
算是个奖励,我以前有将JavaScript中的继承知识总结为一张思惟导图,你能够点击这里查看。知识老是反复记忆才能真正掌握,但愿你能常回来看看。加油👊 !