【JS】JavaScript中的六种继承方式总结

前言

对于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

此时,子类实例有父类构造函数中的实例属性/方法,子类原型对象没有父类构造函数中的实例属性/方法,但是有父类原型对象上的属性/方法,为什么不用这种方式呢?
希望有看到这里知道答案的可以解答下啦,先谢谢啦(^▽^)

参考链接

  1. http://www.cnblogs.com/humin/p/4556820.html
  2. http://www.ayqy.net/blog/%E9%87%8D%E6%96%B0%E7%90%86%E8%A7%A3js%E7%9A%846%E7%A7%8D%E7%BB%A7%E6%89%BF%E6%96%B9%E5%BC%8F/
  3. 《JavaScript高级程序设计》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值