前言
对于JS中的继承,一直都是不完全理解那种,最近找实习,面试中几乎必问JS的继承,故今天特地来整理下JS中的几种继承方式。
JS实现继承的六种方式
在说继承方式之前,我们先来定义下父类的代码以及一些约定的说法:
// 定义一个动物类,这是父类
function Animal (name) {
//私有属性,用不到,所以这里注释了
//var name = 'Animal';//私有基本属性
//var arr = [1]; //私有引用属性
//function sleep(){} //私有方法(引用属性)
// 实例属性
this.name = name || 'Animal';//实例基本属性
this.arr = [1]; //实例引用属性
this.sleep = function(){ //实例方法(引用属性)
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
一、原型链继承
核心:父类的实例充当子类的原型
举例:相关说明在代码中用注释说明了
function Cat(){} //子类方法
Cat.prototype = new Animal();//核心,Cat.prototype={name:'Animal',arr:[1],sleep: f()}
Cat.prototype.name = 'cat';//修改了Cat原型上的name属性值
var cat = new Cat(); //new了一个对象cat,没有本地(实例)属性,只有引用属性
console.log(cat.name); //cat
console.log(cat.eat('fish')); //cat正在吃fish
console.log(cat.sleep()); //cat正在睡觉
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
//问题来了
var cat2 = new Cat();//又new了一个Cat的对象
cat.arr.push(2);
console.log(cat.arr);//[1,2]
console.log(cat2.arr);//[1,2]
优点:
1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
2. 父类新增原型方法/原型属性,子类都能访问到
3. 简单,易于实现
缺点:
1. 修改了cat.arr,cat2.arr也改变了,因为来自原型对象的引用属性是所有实例所共享的;
可以这样理解:执行cat.arr.push(2);先对cat进行属性查找,找遍了实例属性(在本例中没有实例属性),没找到,就开始顺着原型链向上找,找到了cat的原型对象,发现有arr属性。于是给arr末尾插入了2,所以cat2.arr也变了。
2. 创建子类实例时,无法向父类构造函数传参
二、借用构造函数方式
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(跟原型没有任何关系)
举例:
function Cat(name){
Animal.call(this,name); //核心
}
var cat = new Cat('Tom');//new了个对象cat,cat={name:'Tom',arr:[1],function sleep: f()}
console.log(cat.name);//Tom
console.log(cat.sleep());//Tom正在睡觉
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
//问题来了
var cat2 = new Cat('Ford');
console.log(cat.sleep === cat2.sleep);//false
优点:
1. 解决了第一种方法的子类实例共享父类引用属性的问题
2. 创建子类实例时,可以向父类构造函数传参
缺点:
1. 实例并不是父类的实例,只是子类的实例
2. 只能继承父类的实例属性和方法,不能继承父类的原型属性和方法
3. 无法实现函数复用,每个子类实例都有父类的实例函数的副本,影响性能
三、组合继承(最常用)
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
举例:
function Cat(name){
Animal.call(this,name);//核心
}
Cat.prototype = new Animal();//核心
Cat.prototype.constructor = Cat;//需要修复构造函数的指向
// Test Code
var cat = new Cat('Tom');
console.log(cat.name);//Tom
console.log(cat.sleep());//Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
优点:
1. 弥补了第二种方法的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2. 既是子类的实例,也是父类的实例
3. 不存在引用属性共享问题
4. 创建子类实例时,可向父类构造函数传参
5. 函数可复用
缺点:
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)(其实是很好了,只是多消耗了一点内存)
四、原型式继承
核心:封装一个函数,该函数返回一个不含实例属性的对象,然后对这个对象逐步增强(逐步添加实例属性)
P.S. ES5中的Object.create()函数,内部就是原型式继承,IE9+支持。
举例:
function object(o){ //核心
//先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个实例。本质上来说,该函数对传入其中的对象执行了一次浅复制。
function F(){}
F.prototype = o;
return new F();
}
var Animal = {
name: 'Tom',
food: ['meat']
};
var cat = object(Animal);//创建了一个cat对象,没有实例属性,原型指向Animal
cat.name = 'Bob';
cat.food.push("fish");
var dog = object(Animal);//创建了一个cat对象,没有实例属性,原型指向Animal
dog.name = 'Alice';
dog.food.push("beef");
console.log(Animal.food); //"meat","fish","beef"
优点:
1. 从已有对象衍生出新对象,不需要创建自定义类型(本例中,从Animal衍生出cat和dog这两个对象,本质上来说,是对象的复制)
缺点:
1. 父类的引用属性会被所有实例共享
2. 无法实现代码复用(对象是现创建的,属性是现添的,都没有进行函数封装,如何复用?)
五、寄生式继承
核心:
寄生式继承和原型式继承是紧密相关的一种思路。寄生式继承就是给原型式继承穿了个马甲而已
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象
举例:
function createAnother(o){
var clone = object(o);//通过调用函数创建一个新对象,任何能够返回新对象的函数都适用于此模式(用Object.create()也可以)
clone.sayHi = function(){
console.log("hi");
};
return clone;
}
var Animal = {
name: 'Tom',
food: ['meat']
};
var cat = createAnother(Animal);;
cat.sayHi(); //"hi"
优点:
1. 主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承是一种有用的模式
缺点:
1. 用寄生式继承为对象添加函数,无法实现函数复用从而降低效率
六、寄生组合继承(最理想的)
核心:通过寄生方式,砍掉了子类原型对象上多余的那份父类实例属性,这样,在调用两次父类的构造函数的时候,就不会初始化两次实例方法/属性,避免了组合继承的缺点
举例:
function Cat(name){
Animal.call(this,name); //核心
}
(function(){ //核心
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
Cat.prototype.constructor = Cat;//需要修复构造函数的指向
})();
var cat = new Cat('Tom');
console.log(cat.name); //Tom
console.log(cat.sleep()); //Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
优点:
1. 只调用了一次父类构造函数,避免了在子类原型对象上面创建不必要的、多余的属性
2. 原型链保持不变,可以使用instanceof和isPrototypeOf()
缺点:
实现比较复杂
问题
在寄生组合继承中,如果把立即执行函数部分换成如下的方式,结果和寄生组合继承的结果是一样的。(对父类原型对象进行了复制,更像是复制而不是继承?)
function Cat(name){
Animal.call(this,name); //核心
}
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat = new Cat('Tom');
console.log(cat.name); //Tom
console.log(cat.sleep()); //Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
此时,子类实例有父类构造函数中的实例属性/方法,子类原型对象没有父类构造函数中的实例属性/方法,但是有父类原型对象上的属性/方法,为什么不用这种方式呢?
希望有看到这里知道答案的可以解答下啦,先谢谢啦(^▽^)