面向对象编程
继承
类式继承
function SuperClass(){
this.superValue = true
}
SuperClass.prototype.getSuperValue = function(){
return this.superValue
}
function SubClass(){
this.subValue = false
}
SubClass.prototype = new SuperClass()
SubClass.prototype.getSubValue = function(){
return this.subValue
}
var instance = new SubClass()
类式继承,就是子类通过其原型prototype对父类实例化,继承了父类。
分析一下instance的结构:
可以看到instance是SubClass的一个实例。
因为实例对象的__proto__属性指向的就是其构造函数的prototype属性,所以instance.proto === SubClass.prototype === SuperClass
instanceof用来判定对象是否是某个类的实例
我们可以用instanceof来判断对象和类之间的关系,instanceof是通过判断对象的prototype链来确定这个对象是否是某个类的实例,而不关心对象与类的自身结构。
因为某个对象的__proto__属性指向的是其构造函数的prototype属性。
所以从上面的测试可以看出,当某个对象/类是另一个类的实例,或者某个类的原型prototype是另一个类的实例时,就会满足instanceof的判定。另外instanceof的判定还会延伸至后者的父类或者祖先。
类式继承的缺陷
父类公有属性的引用类型会被共用
由于子类通过其原型prototype对父类的实例化,继承了父类。所以说如果父类中的共有属性要是引用类型,就会在子类中被所有实例共用。所以一个子类的实例更改子类原型从父类构造函数汇总继承来的共有属性就会直接影响到其他子类。
因为实例instance1和instance2的book属性指向的都是同一个地址,所以会相互影响;而title属性是通过值类型复制,所以相互独立。
无法初始化
因为子类的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,无法想父类传递参数,所以也无法对父类构造函数内的属性进行初始化。
构造函数继承——创建即继承,原型无法共用
上面的类式继承已经做到了子类可以继承父类的属性和方法,但是同样缺陷也很明显,就是继承的父类公有属性是引用类型的话,指向同一地址,实例会相互影响,并且无法进行初始化。而构造函数继承就能够完美解决这一问题。
// 构造函数继承
// 声明父类
function SuperClass(id){
// 引用类型共有属性
this.books = ['JS','html','css'];
// 值类型共有属性
this.id = id;
}
// f父类声明原型方法
SuperClass.prototype.showBooks = function(){
console.log(this.books)
}
// 声明子类
function SubClass(id){
SuperClass.call(this,id);
}
注意,SuperClass.call(this,id) 这条语句是构造函数式继承的精华,由于call这个方法可以改变函数的作用环境,因此在子类中,对SuperClass调用这个方法就是将子类的变量在父类中执行一遍,由于父类是给this绑定属性的,因此子类自然也就继承了父类的共有属性。但是这种类型的继承并没有涉及原型prototype,所以父类的原型方法自然不会被继承。
组合继承——类式继承和构造函数式继承合体
// 组合式继承
// 声明父类
function SuperClass(name){
this.name = name
this.books = ['html','css','js']
}
// 父类原型共有方法
SuperClass.prototype.getName = function(){
console.log(this.name)
}
// 声明子类
function SubClass(name,time){
// 构造函数式继承父类name属性
SuperClass.call(this,name)
// 子类中新增共有属性
this.time = time;
}
// 类式继承
SubClass.prototype = new SuperClass()
// 子类原型方法
SubClass.prototype.getTime = function(){
console.log(this.time)
}
var instance1 = new SubClass('js book',2021)
instance1.books.push('node.js')
console.log(instance1.books)
instance1.getName()
instance1.getTime()
var instance2 = new SubClass('css book',2022)
instance2.books.push('h5')
console.log(instance2.books)
instance2.getName()
instance2.getTime()
分析一下instance的构造:
组合继承模式将类式继承和构造函数式继承相结合,可以看到构造函数式继承将父类的属性复制到子类(每次执行构造函数式继承,相当于重新new一个父类实例,所以不会有相互覆盖的可能),但是不涉及到protype,所以不会继承父类的原型方法。所以接下来马上使用类式继承,将父类的实例赋值给子类的prototype,因为子类原型prototype上继承父类的共有属性(比如books)和之前子类执行构造函数时生成的books属性同名,所以instance.books不会取原型链上的books属性,但是能够取到原型链上的父类原型pototype上的方法。
综上,组合继承完美结合了类式继承和构造函数式继承的优点,并且消除了他们的缺点。但是这种继承也不是完全没有缺点,就是执行了2遍的父类构造函数。
那有没有办法只执行1遍父类的构造函数,以减少消耗呢?
方法肯定是有的,那就是使用终结继承大法——寄生组合式继承!
终极继承大法——寄生组合式继承
原型式继承——封装类式继承
在说明终极继承大法之前,我们先学习一下它的组成之一,原型式继承。
function inheritObject(o){
// 声明一个过渡函数对象
function F(){}
// 过渡对象的原型继承父对象
F.prototype = o
// 返回过渡对象的一个实例,该实例的原型继承了父对象
return new F()
}
可以看出原型式继承和类式继承十分相似,实际上它就是类式继承的一个封装。只不过这里的子类是以过渡对象的形式出现的。
寄生式继承——原型式继承的二次封装
// 声明基对象
var book = {
name:'js book',
alikeBook:['css book','html book']
}
// 原型式继承
function inheritObject(o){
// 声明一个过渡函数对象
function F(){}
// 过渡对象的原型继承父对象
F.prototype = o
// 返回过渡对象的一个实例,该实例的原型继承了父对象
return new F()
}
// 寄生式继承
function createBook(obj){
// 通过原型式继承创建新对象
var o = new inheritObject(obj)
// 扩展新对象
o.getName = function(){
console.log(this.name)
}
return o
}
var newBook = new createBook(book)
寄生式继承其实就是对实例化后的原型继承对象进行扩展。
寄生组合式继承——终极
// 原型式继承
function inheritObject(o){
// 声明一个过渡函数对象
function F(){}
// 过渡对象的原型继承父对象
F.prototype = o
// 返回过渡对象的一个实例,该实例的原型继承了父对象
return new F()
}
// 寄生组合式继承
function inheritPrototype(subClass,superClass){
// 复制一份父类的原型副本保存在变量中
var p = inheritObject(superClass.prototype)
// 修正因为重写子类原型导致子类的constructor属性被修改
p.constructor = subClass
subClass.prototype = p
}
// 定义父类
function SuperClass(name){
this.name = name
this.colors = ['#000000','#ffffff','#000fff']
}
// 定义父类原型方法
SuperClass.prototype.getName = function(){
console.log(this.name)
}
// 定义子类
function SubClass(name,time){
// 构造函数式继承
SuperClass.call(this,name)
// 子类新增属性
this.time = time
}
// 寄生式继承父类原型
inheritPrototype(SubClass,SuperClass)
// 子类新增原型方法
SubClass.prototype.getTime = function(){
console.log(this.time)
}
// 创建2个测试方法
var instance1 = new SubClass('js book',2021)
var instance2 = new SubClass('css book',2022)
instance1.colors.push('red')
console.log(instance1.colors)
console.log(instance2.colors)
instance2.getName()
instance2.getTime()
分析下instance1的结构:
寄生组合式继承替换掉类式继承,与构造函数式继承的结合堪称完美。首先构造函数式继承复制了父类的共有属性,然后寄生组合式继承只是将父类的原型prototype复制到了子类的prototype,并且还修正了子类constructor属性。
多继承
JavaScript不是只有一条原型链吗?怎么实现多继承呢?我们来看下下面的一段代码:
<script>
let extend = function(target,source){
// 遍历资源对象中的属性,复制到源对象中
for(let key in source){
target[key] = source[key]
}
}
let obj1 = {
a:'1',
b:'2',
c:'3'
}
let obj2 = {
a:'11'
}
extend(obj2,obj1)
console.log(obj2)
</script>
通过extend方法,我们成功将source中的属性成功复制到了target对象中,但是仅仅这样还不够,会出现几个问题:
1)如果target中有相同的属性,就会被source所覆盖
2)如果source中的某个属性是引用类型,就会导致target复制的该属性指向同一地址
3)extend方法没有涉及到prototype
解决这几个问题后,我们再考虑多继承的问题,不过是将source变成多个而已。
<script>
let extend = function(target,source){
// 遍历资源对象中的属性,复制到源对象中
for(let key in source){
// 当target对象中没有该属性时复制source对象中的属性
if(!target[key]){
// 如果source[key]是引用类型,则递归extend方法,避免复制该属性指向同一地址
if(source[key] instanceof Object){
// 避免递归时报错
target[key] = Object
extend(target[key],source[key])
}
target[key] = source[key]
}
}
}
let obj1 = {
a:'1',
b:'2',
c:{
d:'3',
e:{
f:'4',
g:'5'
},
h:[6,7,8]
}
}
let obj2 = {
a:'11'
}
extend(obj2,obj1)
console.log(obj2)
</script>
到这里,发现自己手写了一个深拷贝,解决了前2个问题,那prototype的问题呢?考虑到多继承和单继承不同,在此不做讨论,如果prototype也需要继承的话,可以参考extend方法。
接下来,我们需要扩展extend方法,将复制一个对象转为复制多个对象的属性。
<script>
function extend(target,source){
// 遍历资源对象中的属性,复制到源对象中
for(let key in source){
// 当target对象中没有该属性时复制source对象中的属性
if(!target[key]){
// 如果source[key]是引用类型,则递归extend方法,避免复制该属性指向同一地址
if(source[key] instanceof Object){
// 避免递归时报错
target[key] = Object
extend(target[key],source[key])
}
target[key] = source[key]
}
}
}
function mix(){
// 定义源对象
let target = arguments[0]
for(let i = 1;i < arguments.length;i++){
// 定义资源对象
let source = arguments[i]
extend(target,source)
}
return target
}
let obj1 = {
a:'11'
}
let obj2 = {
a:'1',
b:'2',
c:{
d:'3',
e:{
f:'4',
g:'5'
},
h:[6,7,8]
}
}
let obj3 = {
a:[1,2,3],
d:[4,5,6],
e:{
x:'001',
y:'001',
z:'003'
}
}
let obj = mix(obj1,obj2,obj3)
console.log(obj)
利用function的arguments属性去遍历传入参数,将第一个参数作为源对象,剩余参数作为资源对象进行属性的复制即可完成多继承。
下一章 工厂模式