1.背景介绍
继承,指一个对象直接使用另一对象的属性和方法。
JS里常用的有如下两种继承方式:
原型链继承(对象间的继承)
类式继承(构造函数间的继承)
2.知识剖析
JavaScript语言的对象体系,不是基于“类”,而是基于构造函数(constructor)和原型(prototype)。
* 原型对象:只要创建一个新函数,就会根据特定的规则为该函数创建一个prototype属性指向其原型对象,默认情况下原型对象会自动获得一个constructor属性,该属性包含一个指向prototype属性所在函数的指针。
Function.prototype.constructor === Function //true
* 构造函数:本身是一个函数,出于创建特定类型新对象的目的而定义的,内部使用this变量,需要和new配合使用来创建实例,this变量会绑定在实例对象上。
demo:
var f = function(){}
f.prototype.constructor === f //true
3.常见问题
常见的继承方式有哪些?如何实现?
4.解决方案
方法一:原型链
原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针。如果:我们让原型对象A等于另一个类型B的实例,那么原型对象A就会有一个指针指向B的原型对象,相应的B的原型对象中保存着指向其构造函数的指针。假如B的原型对象又是另一个类型的实例,那么上述的关系依旧成立,如此层层递进,就构成了实例与原型的链条。
注意: 通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做会重写原型链。
问题: 原型链很强大,可以利用它来实现继承,但是也有一些问题,主要的问题还是包含引用类型值的原型属性会被所有实例共享。因此我们在构造函数中定义实例属性。但是在通过原型来实现继承时,原型对象其实变成了另一个类型的实例。于是原先定义在构造函数中的实例属性变成了原型属性了。
原型链的另一个问题是: 在创建子类型的实例时,不能在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
方法二:构造函数
为了解决原型中包含引用类型值带来的一些问题,引入了借用构造函数的技术。这种技术的基础思想是:在子类型构造函数的内部调用超类型构造函数。
<!-- Parent.call(this)在新创建的Child实例环境下调用Parent构造函数。这样,就在新的Child对象上,此处的kid1和kid2对象上执行Parent()函数中定义的对象初始化代码。这样,每个Child实例就都会具有自己的friends属性的副本了。 -->
借用构造函数的方式可以在子类型的构造函数中向超类型构造函数传递参数。
构造函数模式的问题: 在于方法都在构造函数中定义,函数复用无从谈起,因此,借用构造函数的模式也很少单独使用。
方法三:组合继承
组合继承指的是将原型链和借用构造函数的技术组合在一块,从而发挥二者之长。即:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
<!-- Person构造函数定义了两个属性:name和friends。Person的原型定义了一个方法sayName()。Child构造函数在调用Parent构造函数时,传入了name参数,紧接着又定义了自己的属性age。然后将Person的实例赋值给Child的原型,然后又在该原型上定义了方法sayAge().这样,两个不同的Child实例既分别拥有自己的属性,包括引用类型的属性,又可以使用相同的方法了。 -->
组合继承避免了原型链和构造函数的缺陷,融合了他们的有点,成为JavaScript中最常用的继承模式。而且,instanceOf和isPropertyOf()也能够识别基于组合继承创建的对象。
方法四:原型式继承
这是另一种继承,没有严格意义上的构造函数。思路是:借助原型可以基于已有的对象创建新对象,同时还不必要创建自定义类型。
在object()函数内部,先创建一个临时的构造函数,然后将传入的对象作为构造函数的原型,最后返回这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
方法五:寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,与寄生式构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
使用寄生式继承来为对象添加函数,会由于不能做到函数的复用而降低效率;这一点和构造函数继承模式类似。
方法六:寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链来继承方法。寄生组合式继承的基本思路:不必为了指定子类型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(child, parent){
var prototype = object(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
child.prototype = prototype; // 指定对象
}
5.编码实战
6.扩展思考
寄生组合式继承和组合式继承,两者有什么区别?
组合继承最大的问题就是无论什么情况下,都会调用俩次超类型构造函数Parent():一次是在创建子类型原型的时候( Parent.call(this,name); ),另一个是在子类型构造函数内部( Child.prototype = new Parent(); )。
寄生组合式继承的基本思路:不必为了指定子类型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
7.参考文献
参考一:JavaScript高级程序设计(第3版)
参考二:JS实现继承的几种方式详述(优快云)————https://blog.youkuaiyun.com/liuyan19891230/article/details/50774439
参考三:JS继承的6种方式(优快云)————https://blog.youkuaiyun.com/hhthwx/article/details/78095944
参考四:js继承的常用方法————https://www.cnblogs.com/chenwenhao/p/6980401.html
8.更多讨论
Q1:继承一般用到什么地方?
A :通常在一般的项目里不需要,因为应用简单,但你要用纯js做一些复杂的工具或框架系统就要用到了,比如webgis、或者js框架如jquery、angular、vue什么的,不然一个几千行代码的框架不用继承得写几万行,甚至还无法维护。
Q2:如何确定原型和实例之间的关系?
A:两种方法:第一种是使用instanceOf操作符,第二种是使用isPrototypeOf()方法。
eg:
console.log(person instanceOf Child);//true
console.log(Child.prototype.isPrototypeOf(instance));//true
Q3:什么是浅复制?
A :浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。