探究
我觉得学习一个东西最重要的是搞懂三样东西
What/How/Why
What
什么是原型?什么是原型链?什么是继承?
原型
在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个” [[Prototype]]”内部属性,这个属性所对应的就是该对象的原型。
“[[Prototype]]”作为对象的内部属性,是不能被直接访问的。所以为了方便查看一个对象的原型,Firefox和Chrome中提供了”proto“这个非标准(不是所有浏览器都支持)的访问器(ECMA引入了标准对象原型访问器”Object.getPrototype(object)”)。
用一句话来概括就是: 原型是一个属性可以被继承的对象.原型也可以有自己的原型.
原型链
原型也可以有自己的原型, 而一个个原型通过prototype链接起来的就叫做原型链
继承
儿子可以通过继承获得父亲的属性或者方法;
why
为什么会有原型/原型链和继承?
摘自JavaScript高级程序设计:
继承是OO语言中的一个最为人津津乐道的概念.许多OO语言都支持两种继承方式: 接口继承 和 实现继承 .接口继承只继承方法签名,而实现继承则继承实际的方法.由于js中方法没有签名,在ECMAScript中无法实现接口继承.ECMAScript只支持实现继承,而且其
实现继承
主要是依靠原型链来实现的.
How
原型
怎么找到原型?
我们打开浏览器, 打开控制台(F12), 可以直接输入 const obj = {}
, 我们就可以把他直接点开.发现里面有一个__proto__
其实我们输入的这个对象, 是一个Object
构造函数的实例. 我们可以通过实例的__proto__
属性找到他的原型;
原型链
怎么找到原型链?
在找之前我们先了解一个原型与构造函数与实例直接的关系.
假定我们有一个构造函数 function A(){}
, 我们称他为A
我们有一个实例B, 他是这样来的: const B = new A()
, 我们称他为B
A的原型为C
接下里我们写一个等式来很好的展示他们之间的关系
A = prototype => C = construct => A = new => B = __proto__
=> C
解释一下: A有一个属性prototype
指向A的原型 C, 而C的属性construct
指向A, A通过new方法可以创建实例B, 而B有一个属性__proto__
指向了C(A的原型)
OK, 理解完上面的东西之后, 我们再加一个知识点, A的原型C, 也是C的原型的一个实例.
先别懂没懂, 我们看一下实战.
书接上文中的原型, 我再复制一下, 方便阅读
我们打开浏览器, 打开控制台(F12), 可以直接输入
const obj = {}
, 我们就可以把他直接点开.发现里面有一个__proto__
其实我们输入的这个对象, 是一个Object
构造函数的实例. 我们可以通过实例的__proto__
属性找到他的原型;
我们接着点开__proto__
,发现里面有一堆”花里胡哨”的东西, 我们找到了其中一个很眼熟的”靓仔” -> construct
;
根据之前的了解
A = prototype => C = construct => A = new => B =
__proto__
=> C
我们点开的这个construct
其实那个空对象的构造函数Object
, 那么他应该有一个prototype
指向了他的原型.
我们找找, 确实发现了有这么一个属性(prototype
); 那么点开之后应该与最早的空对象的__proto__
的内容一致. 根据等式可得;
总结一下
对象的 __proto__
属性指向原型, __proto__
将对象和原型连接起来组成了原型链
接下来进入我们的重头戏继承
继承
其实继承又分为好多种: 原型链继承, 构造函数继承, 组合继承和寄生组合继承;
原型链继承
1 | function A(){ |
我们定义了两个构造函数, A和B; 根据之前的知识, 我们让B的原型指向了构造函数A创造出来的实例(new A()
); 然后我们用构造函数B创造了实例b1, 往 继承来的属性array 增加一个字符串”d”; 然后我们输出一下, 按照我们的逾期的多了一个”e”的情况下, 还有”a,b,c”;
接下来就有些问题了. 我们按照惯例, 通过构造函数B, 创造了b2. b2确实也有了继承来的属性”array”, 可是这个array并不是我们想要的初始的["a","b","c"]
; 我们放弃探究, 继续往这个array
里面增加一个e
, 输出一下b1
的属性array
. 确实是可以增加这个e
;
可是!
当我们输出b1.array
时, 发现b1也被改了, 和之前诡异的b2多出一个d
一样.
探究发现这个由于我们用的是引用类型, 所以他们存储的是指向同一个内存的内存地址; 所以才会发生这些问题. 这也暴露了原型链继承时的一个问题.引用类型带来的毛病;
其次, 我们发现我们的B的原型指向A的构造函数时不能传递参数, 这也的话有点呆板
综上原型链的继承在日常中是很少用到的
优点
子类可以通过原型链的查找,实现父类的属性公用与子类的实例
缺点
- 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类
- 谨慎定义方法,以免定义方法也继承对象原型的方法重名
- 无法直接给父级构造函数使用参数
构造函数继承
1 | function A(){ |
我们奇迹般的发现, 构造函数的继承解决了我们之前发现的JavaScript引用问题.那函数传参的问…
啪!
停, 我们现在来传递一下参数!
我们把上面的例子修改一下
1 | function A(name){ |
这样的话我们解决了原型链继承的两个问题(1.无法传参, 2.js引用问题);
该不会有人认为这样就很完美了吧?不会吧!不会吧!
我们冷静下来想想看, 其实这个传递参数是几乎算是写死的. 还不如直接在A里面直接写死好了, 根本就无法复用嘛; 而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结 果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。
优点
- 解决了原型链继承的 引用类型操作问题
- 解决了父类传递参数问题
缺点
- 仅仅使用借用构造函数模式继承,无法摆脱够着函数。方法在构造函数中定义复用不可谈
- 对于超类的原型定义的方法对于子类是不可使用的,子类的实例只是得到了父类的this绑定的属性 考虑到这些缺点,单独使用借用构造函数也是很少使用的
组合继承
1 | function A(name){ |
在这个例子中, 构造函数A有两个内置属性name
和array
. A的原型定义了一个方法sayName
; 构造函数B在调用A的构造函数时传入了一个参数name
, 然后又给B的原型上定义了一个方法sayAge
. 这样一来, 就可以让两个B的实例分别拥有自己的属性, 当然也包括了array, 还可以拥有相同的方法.
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继 承模式。而且(关键),instanceof
和 isPrototypeOf()
也能够用于识别基于组合继承创建的对象。
优点
- 解决了原型链继承引用类型的实例操作导致引用改变
- 解决了借构造函数继承方式的,父类原型子类实例可以使用
缺点
父类的构造函数被实例换了两次 * 实例会有父类的构造函数的一些this属性、子类的构造函数(prototype)上也有一份实例的上有的属性
原型式继承
道格拉斯·克罗克福德在 2006 年写了一篇文章,题为 Prototypal Inheritance in JavaScript (JavaScript 10 中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的 构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为 了达到这个目的,他给出了如下函数。
1 | function object(o){ |
看起来非常简单
先在object
函数内部创建一个临时的构造函数F
, 然后将传入的这个对象o
作为这个临时构造函数的原型, 最后返回这个临时构造函数的实例.
简单来说就是object
对传入的对象进行了浅复制.
浅拷贝和深拷贝可以自行Google一下, 这算一个重要的知识点.
举个🌰
1 | function object(o){ |
有没有似曾相识的感觉….原型链继承的老毛病来了…
不过他也不是没有优点的.
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式 继承是完全可以胜任的。
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()与 object()方法的行为相同。有兴趣的话可以自己了解一下
优点
再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分
缺点
- 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类
- 谨慎定义方法,以免定义方法也继承对象原型的方法重名
- 无法直接给父级构造函数使用参数
寄生继承
寄生继承和原型式继承紧密相关, 同样也是由克罗克福德推而广 之的.寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该 函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。以下代码示范了寄 生式继承模式。
上代码
1 | function object(o){ |
在这个例子中,createAnother()函数接收了一个参数,也就是将要作为新对象基础的对象。然 后,把这个对象(original)传递给 object()函数,将返回的结果赋值给 clone。再为 clone 对象 添加一个新方法 sayHi(),最后返回 clone 对象。
1 | var person = { |
这个例子中的代码基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi()方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示 范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
使用寄生继承, 由于不能做到函数复用而降低效率, 这点和构造函数继承很像
优点
- 再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分
- 可以给备份的对象添加一些属性
缺点
类似构造函数一样,创建寄生的方法需要在clone对象上面添加一些想要的属性,这些属性是放在clone上面的一些私有的属性
寄生组合继承(终极方案
前面说过,组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的 问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子 类型构造函数时重写这些属性。再来看一看下面组合继承的例子。
1 | function A(name){ |
第一次调用A构造函数的时候, B的原型上就拥有了A的两个属性和一个方法;
第二次调用A的构造函数时, 为B的实例创建了A的两个的属性和一个方法, 于是实例的两个属性和一个方法就屏蔽了原型上的实例和方法;
是不是感觉有点点浪费, 不过好在我们已经找到解决方案—>寄生组合继承
举一个非常简单的🌰, 并且我尽量逐字逐句的讲清楚
1 | function object(o){ |
基本思想:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。实际上是在组合继承的基础上,用超类型原型的副本代替调用超类型的构造函数给子类型指定原型。
本质上,是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
优点:只调用了一次超类型的构造函数,并且因此避免了在子类型的原型上创建不必要的、多余的属性。与此同时,原型链还能保持不变。因此,还能够正常使用instanceof
和isPrototypeOf()
。普遍认为寄生组合式继承是引用类型最理想的继承方式。
优点
- 在少一次实例化父类的情况下,实现了原型链继承和借用构造函数
- 减少了原型链查找的次数(子类直接继承超类的prototype,而不是父类的实例)
缺点
暂无
class继承
1 | class Parent{ |
说明:核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super。
class 的本质还是函数,这种表达不过是一种语法糖。
这里就不展开了.