JS继承的几种实现方式
继承是指子类继承父类的属性和方法,要实现继承,首先我们需要有一个父类
function Person (name){
//属性
this.name = name || 'person';
this.firends = ['James','Curry'];
//方法
this.speak = function (){
console.log('I can speak chinese!')
}
}
//原型方法
Person.prototype.skill= function (){
console.log('running')
}
var person = new Person;
person.speak(); //"speak"
person.skill(); //"running"
原型链继承
原型链继承的本质就是就是将父类的实例作为子类的原型对象
function Chinese() {
}
//继承Person
Chinese.prototype = new Person;
Chinese.name = "lianqi";
//添加新方法
Chinese.prototype.speakChinese = function (){
console.log('speak chinese')
}
//重写父类中的方法
Chinese.prototype.skill = function (){
console.log('jump')
}
console.log(chinese.name); //"lianqi"
chinese.speak(); //"speak"
chinese.speakChinese(); //"speak chinese"
chinese.skill(); //"jump"
console.log(chinese instanceof Chinese); //true
console.log(chinese instanceof Person); //true
子类有时需要重写父类中某个方法,或者需要添加子类中不存在的方法。不管怎么样给原型添方法的代码,一定要放在替换原型的语句之后。
还有一点需要注意,再通过原型链来实现继承时不能使用对象字面量创建原型方法,因为这样做会重写原型链,就不能调用父类的方法。如下面的例子
function Chinese() {
}
Chinese.prototype = {
eat : function (){
console.log('eat food')
}
}
var chinese = new Chinese();
console.log(chinese instanceof Chinese); //true
console.log(chinese instanceof Person); //false
chinese.eat(); //"eat food"
chinese.speak(); //报错"chinese.speak is not a function"
原型链虽然很强大,可以用它来实现继承,但也存在一些问题:
1.重写子类的原型 等于 父类的一个实例,(父类的实例属相变成子类的原型属性)如果父类包含引用类型的属性,那么子类所有实例都会共享该属性(包含引用类型的原型属性会被实例共享)
2.在创建子类实例时,不能向父类的构造函数传递参数
借用构造函数继承
借用构造函数继承的基本思想相当简单,即在子类构造函数的内部使用call()或apply()方法调用父类构造函数,这样子类的实例就都会具有属于自己的引用类型的属性,如下代码:
function Chinese(name) {
//使用call继承Person
Person.call(this,name);
}
var chinese = new Chinese('lianqi');
console.log(chinese.name); //"lianqi"
chinese.firends.push('Irving');
console.log(chinese.firends); // ["James", "Curry", "Irving"]
var chinese01 = new Chinese('Kobe');
console.log(chinese01.firends); // ["James", "Curry"]
借用构造函数继承的模式,解决了原型链继承中子类共享父类中引用类型属性的问题,而且创建子类实例时可以向父类的构造参数传递参数,还可以实现多继承(call多个父类对象)
借用构造函数继承的方式也存在问题:
1.方法都在构造函数中定义,因此函数复用就无从谈起了
2.子类实例只能继承父类的实例(私有)属性和方法,在父类原型中定义的方法,对子类是不可见的,因此不能继承父类的原型(公有)属性和方法
拷贝继承
拷贝继承的私有属性通过call来继承,公有属性通过函数extend()
来继承,拷贝继承实现了子类实例对父类的私有属性和公有属性的继承。
function extend(newObj,oldObj){
for (var attr in oldObj){
newObj[attr] = oldObj[attr]
}
}
function Chinese(){
//子类继承父类的私有属性和方法
Person.call(this)
}
//子类继承父类的公有属性和方法
extend(Chinese.prototype,Person.prototype);
var chinese = new Chinese('liaqi');
chinese.firends.push('Irving');
console.log(chinese.firends); // ["James", "Curry", "Irving"]
chinese.skill(); //"running"
拷贝继承也有它存在的问题:
1.效率较低,内存占用高(因为要拷贝父类的属性)
2.无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
组合继承
组合继承的原理是通过原型链实现对公有属性和方法的继承,通过借用构造函数来实现对私有属性和方法的继承。
function Chinese(name) {
//继承私有属性和方法
Person.call(this,name); //第二次调用父类Person()
}
//继承公有属性和方法
Chinese.prototype = new Person(); //第一次调用父类Person()
//手动设置子类的constructor
Chinese.prototype.constructor = Chinese;
Chinese.prototype.speakChinese = function (){
console.log('speak chinese')
};
var chinese = new Chinese();
chinese.firends.push('Irving');
chinese.speak(); //"speak"
chinese.speakChinese(); //"speak chinese"
console.log(chinese.firends); //["James", "Curry", "Irving"]
var chinese01 = new Chinese();
chinese.speak(); //"speak"
console.log(chinese01.firends); //["James", "Curry"]
组合继承可以向父类构造函数传参,避免了子类实例共享父类的引用类型的属性,每个子类实例还可以使用相同的方法。组合继承避免了原型链和借用构造函数的缺点,融合了他们的优点
组合继承的缺点是调用了两次父类构造函数
寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,然后返回这个对象。
function createPerson(original) {
//创建一个新对象
var obj = Object.create(original);
//给对象添加方法
obj.speakChinese = function (){
console.log('speak chinese')
}
//返回对象
return obj
}
var person = {
name: 'lianqi',
friends: ['James','Curry']
}
var chinese = createPerson(person);
chinese.speakChinese(); //"speak chinese"
createPerson()
函数将接受的参数作为新对象的基础返回新对象,新对象不仅拥有了person的属性和方法,还有了自己的方法speakChinese()
。在主要考虑对象而不是自定义类型活构造函数的情况下,寄生式继承是一种有用的模式。
寄生组合式继承
寄生组合式继承不会初始化两次调用父类构造函数,避免了组合继承的缺点,通过把父类原型寄生在一个新类的原型上,再赋值给子类原型。这样做的高效体现在只调用了一次父类构造函数,并且因此避免了在子类原型上创建不必要的、多余的属性
//创建子类
function Chinese(){
Person.call(this)
}
//创建一个新类,新类没有任何属性和方法
function Temp() {
}
//父类的原型属性和方法寄生在新类的原型
Temp.prototype = Person.prototype;
//再把新类的原型属性和方法赋值给子类的原型,新类就相当于一个临时中转站,这样就避免了调用创建子类实例调用两次父类构造函数
Chinese.prototype = Temp.prototype;
//为子类原型添加constructors属性,从而弥补因重写原型而失去的默认constructor属性
Chinese.prototype.constructor = Chinese;
var chinese = new Chinese();
chinese.speak(); //"speak"