目录
原型链继承
上一篇博客我们知道了原型和原型链,知道了构造函数在原型链中的地位,也知道了js对象可以从从原型继承属性和方法,而ES5中定义类是通过构造函数实现,所以ES5中其中一种继承方法就是以原型为基础的。
继承方法
通过原型链思想,让父类实例对象作为子类原型对象,子类即可继承父类的属性和方法
//es5继承
function Parent(){ //父类构造函数
this.name='张三';
}
Parent.prototype={ //父类原型对象
age:20,
get:function(){
console.log(this.name+"的年龄是"+this.age);
}
}
function Child(){} //子类构造函数
Child.prototype=new Parent(); //将父类的实例对象作为子类的原型对象
Child.prototype.constructor=Child; //原型对象的constructor指向子类
var Ch1=new Child(); //创建子类实例对象
//用子类去调用父类的属性和方法
console.log(Ch1.name); //张三
console.log(Ch1.age); //20
Ch1.get(); //张三的年龄是20
上例中,我们创建父类构造函数和父类原型对象,并为对象创建属性和方法。然后创建子类构造函数,将父类的实例对象作为子类的原型对象,创建子类实例对象后调用父类的属性和方法,成功输出父类的属性和方法,证明继承成功。
那么,如果我们为子类增加属性和方法,子类是否会影响到父类呢?
//为子类增加新属性和新方法
Child.prototype.gender='男';
Child.prototype.gets=function(){
console.log('性别:'+this.gender);
}
//子类成功输出
console.log(Ch1.gender); //男
Ch1.gets(); //性别:男
//测试父类能否调用子类属性和方法--不能
var Pa1=new Parent(); //创建父类实例对象
console.log(Pa1.gender); //undefined
Pa1.gets(); //Pa1.gets is not a function
//测试父类的原型能否调用子类属性和方法--不能
console.log(Parent.prototype.gender); //undefined
Parent.prototype.gets(); //Parent.prototype.gets is not a function
通过上述测试我们会发现,子类增加属性和方法不会影响到父类,而父类增加属性和方法,子类可以继承。
所以我们可以做出利用原型对象继承父类方法的原型链示意图:
优点
通过原型链继承,子类可以复用父类的公共函数,通过原型可以向构造函数内添加新属性和新方法
缺点
但是原型链方式继承也存在一定的缺点:
1.多个实例对引用类型的操作会被篡改,换种方法来讲就是当一个实例对引用类型属性进行修改,这种修改会被其他实例共享(不会影响到父类实例)
function Parent(){ //父类构造函数
this.name=['张三'];
}
function Child(){} //子类构造函数
Child.prototype=new Parent(); //将父类的实例对象作为子类的原型对象
Child.prototype.constructor=Child; //原型对象的constructor指向子类
var Ch1=new Child(); //创建子类实例对象1
Ch1.name.push("李四"); //利用array的内置函数push添加元素到数组的末尾
console.log(Ch1.name); //[ '张三', '李四' ]
//实例对象1对引用类型的修改影响到了其他实例对象
var ch2=new Child() //再创建一个子类实例对象2
console.log(ch2.name); //[ '张三', '李四' ]
//!子类实例变化不会影响到父类实例!
var Pa1=new Parent(); //创建父类实例对象
console.log(Pa1.name); //[ '张三' ]
2.创建子类实例的时候,无法向父类构造函数进行传参。
借用构造函数(经典继承)
在之前写过的函数详解博客中,有写过更改this指向,其中就包含apply()和call()两种方法。借用构造函数继承的方法就用到这两种方式。
继承方法
在子类构造函数的内部调用父类构造函数,通过使用apply()和call()方法,就可以在新创建的对象上执行构造函数。
function Parent(){
this.name=['张三'];
}
function Child(){
Parent.call(this); //call()方法
// Parent.apply(this); //apply()方法
}
var ch1=new Child(); //创建子类实例对象1
ch1.name.push('李四'); //利用array的内置函数push添加元素到数组的末尾
console.log(ch1.name); //[ '张三', '李四' ]
var ch2=new Child(); 创建子类实例对象2
console.log(ch2.name); //[ '张三']
通过上述测试我们可以发现,利用借用构造函数方法不仅可以做到继承,而且还解决了原型链继承中,子类实例修改引用类型属性被其他实例共享的问题。因为通过apply()和call()两种方法,实际上是在新创建的子类实例环境下调用父类构造函数,故子类每个实例会将父类中的属性复制一份。不仅如此,该继承方法还可以解决原型链第二个缺点子类无法向父类传参的问题:
//传参
function Parent(name){
this.name=name;
}
function Child(name){
Parent.call(this,name); //call()方法
// Parent.apply(this,[name]); //apply()方法
}
var ch1=new Child('李四'); //创建子类实例对象1,进行传参
console.log(ch1.name); //李四
var ch2=new Child('王五'); 创建子类实例对象2,进行传参
console.log(ch2.name); //王五
所以,综上所述,借用构造函数继承方法有两个优点,分别解决了原型链继承的两个缺点,但这种继承方式也有缺点:
优点
1.解决了当子类实例修改引用类型属性被其他子类实例共享的问题
2.子类可以向父类进行传参
缺点
1.不能复用父类的公共函数
//父类原型定义属性
Parent.prototype={
age:20
}
var ch1=new Child('李四'); //创建子类实例对象1,进行传参
console.log(ch1.name); //李四
console.log(ch1.age); //undefined 不能复用
2.每个子类都有父类实例函数的副本,影响性能
组合继承
组合继承实质上是将原型链和借用构造函数结合在一起。
继承方法
用原型链实现对原型属性和方法的继承,用借用构造函数计数实现对实例属性的继承。
//组合继承
function Parent(name){ //父类构造函数
this.name=name;
this.fruit=['apple','banana'];
}
Parent.prototype={ //父类原形函数
set:function(){
console.log(this.name+"喜欢的水果有"+this.fruit[2])
}
}
//构造函数
function Child(name,age){ //子类构造函数
//第二次调用Parent 继承属性
Parent.call(this,name);
this.age=age;
}
//构建原型链
//第一次调用Parent 继承原型方法
Child.prototype=new Parent(); //将父类的实例对象作为子类的原型对象
Child.prototype.constructor=Child(); //原型对象的constructor指向子类构造函数
var ch1=new Child('张三','20'); //实例对象1
ch1.fruit.push('pear'); //利用array的内置函数push添加元素到数组的末尾
console.log(ch1.name,ch1.age); //张三 20
console.log(ch1.fruit); //[ 'apple', 'banana', 'pear' ]
ch1.set(); //张三喜欢的水果有pear
var ch2=new Child('李四','22'); //实例对象2
ch2.fruit.push('lemon'); //利用array的内置函数push添加元素到数组的末尾
console.log(ch2.name,ch2.age); //李四 22
console.log(ch2.fruit); //[ 'apple', 'banana', 'lemon' ]
ch2.set(); //李四喜欢的水果有lemon
通过上述测试我们可以发现,组合继承结合了原型继承和构造函数方式继承的优点,在原型上定义方法实现函数复用,又能保证每个实例拥有自己的属性。
优点
1.可以做到父类方法复用
2.解决了当子类实例修改引用类型属性被其他子类实例共享的问题
3.子类可以向父类进行传参
缺点
无论在什么情况下,都需要执行两次父类构造函数,第一次是创建子类原型时:Child.prototype=new Parent(),第二次在子类构造函数内部:Parent.call(this,name),造成了不必要的浪费
原型式继承
要严格区分原型式继承与原型链继承
继承方法
借助原型根据已有的对象创建新对象(复制传入的对象到创建对象的原型上)
function fun(obj){ //创建构造函数fun
function Child(){}; //创建空构造函数Child
Child.prototype=obj; //Child原型对象设置为参数
return new Child(); //Child实例化后返回
}
首先创造一个函数fun,内部定义一个构造函数Child,将child原型对象设置为参数,之后将参数设置为一个对象,完成继承,然后将Child实例化后返回,则返回的就是一个实例化对象
var Parent={
name:'张三',
fruit:['apple','banana']
}
var Child1=fun(Parent);
Child1.name='李四';
Child1.fruit.push("grape");
console.log(Child1.name); //李四
console.log(Child1.fruit); //[ 'apple', 'banana', 'grape']
var Child2=fun(Parent);
Child2.name='王五';
Child2.fruit.push("orange");
console.log(Child2.name); //王五
console.log(Child2.fruit); //[ 'apple', 'banana', 'grape', 'orange' ]
由上述测试可以发现,其优缺点和原型链继承一样
注:还可以使用内置函数Object.create()来代替上述fun的实现,这里不多加赘述。
优点
简单
缺点
1.多个实例对引用类型的操作会被篡改,换种方法来讲就是当一个实例对引用类型属性进行修改,这种修改会被其他实例共享
2.创建子类实例的时候,无法向父类构造函数进行传参
寄生式继承
寄生式继承实则是在原型式继承的基础上,增强对象,返回构造函数
继承方法
寄生式继承可以理解为增加一种添加对象新属性和方法的一种方式
//寄生式继承
function fun(obj){
function Child(){};
Child.prototype=obj;
return new Child();
}
//封装一个jisheng函数
function jisheng(obj){
var copy=fun(obj);
copy.age=20;
copy.zimu=['a','b'];
copy.set=function(){
console.log('年龄:',this.age);
}
return copy;
}
var Parent={
name:'张三',
fruit:['apple','banana']
}
var Child1=jisheng(Parent);
//fun
Child1.name='李四';
Child1.fruit.push("grape");
console.log(Child1.name); //李四
console.log(Child1.fruit); //[ 'apple', 'banana', 'grape']
//jisheng
Child1.zimu.push('c');
console.log(Child1.zimu); //[ 'a', 'b', 'c' ]
console.log(Child1.age); //20
Child1.set(); //年龄: 20
var Child2=jisheng(Parent);
//fun
Child2.name='王五';
Child2.fruit.push("orange");
console.log(Child2.name); //王五
console.log(Child2.fruit); //[ 'apple', 'banana', 'grape', 'orange' ]
//jisheng
Child2.zimu.push('d');
console.log(Child2.zimu); //[ 'a', 'b', 'd' ]
console.log(Child2.age); //20
Child2.set(); //年龄: 20
通过上述测试可以发现,寄生式继承是在原型是继承的基础上,封装一个jisheng函数,对fun返回的函数进行增强,也就是新增方法(当然也可以新增属性),然后返回。
对于从fun和jisheng调用的属性和方法分别进行测试,我们发现,如果对从fun调用的引用类型属性进行修改,修改会被其他实例共享,但是对从jisheng调用的引用类型进行修改,修改不会被其他实例共享。
对于寄生函数,其实还有一个特点,就是关于从jisheng调用的方法,我们可以进行测试查看:
//测试两个子类中调用寄生函数属性和方法
console.log(Child1.age===Child2.age); //true
console.log(Child1.set===Child2.set); //false
//修改子类从寄生属性中获得的属性
Child1.age=22;
console.log(Child1.age); //22
Child1.set(); //年龄: 22
console.log(Child1.age===Child2.age); //false
我们发现,若两个子类分别调用jisheng函数内的属性和方法,它们的属性是相等的,但是方法不同,也就是说,实现了方法独立。
优点
1.jisheng函数部分解决了当子类实例修改引用类型属性被其他子类实例共享的问题
2.为构造函数新增属性和方法,增强函数
缺点
1.原型链中多个实例对引用类型的操作会被篡改,换种方法来讲就是当一个实例对引用类型属性进行修改,这种修改会被其他实例共享,这种仍然没有解决
2.无法传递参数
3.无法复用父类函数(jisheng函数只是增强函数,不是父类函数!)
4.每次创建对象都会创建一遍方法
寄生组合式继承
该方法是继承的最优方法,结合上述继承方法的缺点,优化了上述继承方法的缺点
继承方法
借用构造函数来继承属性,用原型链和寄生结合的方式继承父类原型
//寄生组合继承
function jisheng(Parent,Child){
var copy=Object.create(Parent.prototype); //创建父类原型的副本
Child.prototype=copy; //指定对象
copy.constructor=Child; //增强对象
}
//父类初始化实例属性和原型属性
function Parent(name){ //创建父类构造函数
this.name=name;
this.fruit=['apple','banana'];
}
Parent.prototype={
get:function(){
console.log(this.name+'喜欢的水果是'+this.fruit[2]);
}
}
//借用构造函数传递加强子类实例属性 可以进行传参
function Child(name){ //创建子类构造函数
Parent.call(this,name); //call()方法
// Parent.apply(this,[name]); //apply()方法
}
//父类原型指向子类
jisheng(Parent,Child);
ch1=new Child('张三'); //实例对象1
ch2=new Child('李四'); //实例对象2
ch1.fruit.push('pear');
ch2.fruit.push('lemon');
console.log(ch1.fruit); //[ 'apple', 'banana', 'pear' ]
console.log(ch2.fruit); //[ 'apple', 'banana', 'pear' ]
ch1.get(); //张三喜欢的水果是pear
ch2.get(); //李四喜欢的水果是lemon
通过Object.create()【上面原型式继承提到过,代替上述fun的实现】将父类构造函数原型复制为副本copy,然后将该副本作为子类构造函数的原型,给该副本增加constructor属性(因为前一步修改原型导致副本失去了默认的属性)。完成这一部分后,后面就和之前讲过的继承一样了,这里就不多加赘述。
通过上述测试我们发现,寄生组合式继承解决了之前集中继承方法的所有问题,所以是最优继承方式
优点
1.只调用了一次父类构造函数
2.可以实现父类方法复用
3.解决了当子类实例修改引用类型属性被其他子类实例共享的问题
4.子类可以向父类进行传参