js实现继承的几种方式和对比

作为一门弱类型的编程语言,JS 是通过构造函数的方式实现面向对象编程的,下面就来探讨一下 JS 的几种继承方式,并分析各种方式的优缺点;

父类

既然要实现继承,那么必须有一个父类,代码如下:

function Animal(name) {
    this.name = name;
}
Animal.prototype = {
    constructor: Animal,
    sleep: function() {
        console.log(this.name, 'is sleeping!');
    }
}
复制代码

由于方法是通用型的,个人觉得方法放到实例中是非常消耗性能的,毕竟创建创建一个实例,都会在内存空间中重新分配一部分空间用于存储方法,所以此篇文章默认方法都是放到原型中;

继承方式

1、原型链继承

父类的实例作为子类的原型

function Cat() {
    
}
Cat.prototype = new Animal('Cat');
Cat.prototype.constructor = Cat;    // 修复构造函数指向
var cat = new Cat();
console.log(cat.name);  // 'Cat'
cat.sleep();   // Cat is sleeping!
复制代码

优点:

  1. 纯粹的继承关系,子类原型复制了父类的实例属性并加以拓展;
  2. 子类的原型继承了父类的原型属性,所以父类新添加的原型属性都可以使用;
  3. 简单易理解,结构清晰明了;

缺点:

  1. 难以实现多继承(多个父类实例赋值给子类原型的属性并不美观);
  2. 父类的实例属性值(并不是方法)存在于子类的原型中;对于构造函数来讲,最理想的状态就是属性存在实例中,方法存在于原型中;
  3. 在定义子类的原型时就要创建父类的实例(传参也在这一步完成);创建子类实例的时候父类的实例已经初始化完成,无法向父类传参;如果创建子类的参数是异步的就会造成困扰,除非子类实例定义一个与父类实例相同的参数进行覆盖,但显然这不是最好的方式,会造成子类实例和原型中出现相同的属性;
  4. 想要为子类新增原型方法,必须在继承了父类实例之后执行;
2、构造继承

使用父类的构造器来增强子类的实例(将父类的实例赋予子类实例);

function Cat(name, age) {
    Animal.call(this, name);
    this.age = age || 0;
}
Cat.prototype = {
    constructor: Cat,
    eat: function() {
        console.log(this.name, 'is eating!');
    }
}
var cat = new Cat('Tom', 17)
console.log(cat.name);  // 'Tom'
cat.eat();  // 'Tom is eating!'
复制代码

优点:

  1. 初始化子类实例的时候,可以为父类传递参数;
  2. 父类的实例属性会被初始化在子类实例中的,并不会在原型中;
  3. 方便实现多继承;

缺点:

  1. 并没有真正的继承父类,只是复制了一份父类的实例属性到子类中;
  2. 没有继承父类的原型方法,子类原型的再上一层继承的仍然是 Object;
3、组合继承

原型链继承和构造继承两种方式的组合使用

function Cat(name) {
    Animal.call(this, name);
}
Cat.prototype = new Animal();   // 此处不用传参
Cat.prototype.constructor = Cat;
var cat = new Cat('Tom');
console.log(cat.name);  // 'Tom'
cat.sleep();    // Tom is sleeping!
复制代码

优点:

  1. 弥补了「构造继承」中不能继承原型方法的问题;
  2. 弥补了「原型链继承」中,在定义子类原型时就需要传参的问题,以及难以实现多继承问题;

缺点:

  1. 同样存在「原型链继承」中要为子类新增原型方法,需要在继承了父类实例之后执行的问题;
  2. 一次继承创建了两份父类实例,一份复制在子类实例中,一份在子类原型中,使用时子类实例覆盖了原型中的同名属性,增大了内存消耗;
4、寄生组合继承

通过寄生的方式,在继承时砍掉父类的实例属性,避免初始化两次父类实例的问题;

if(typeof Function.prototype.extend === 'undefined') {
    // 写法一
    Function.prototype.extend = function(Sup) {
        function O() {}
        O.prototype = Sup.prototype;
        this.prototype = new O();
        Object.defineProperty(this.prototype, 'constructor', {
            configurable: true,
            enumerable: false,
            writable: true,
            value: this
        });
    }
    // 写法二
    Function.prototype.extend = function(Sup) {
        var Sub = this;
        Sub.prototype = Object.create(Sup.prototype, {
            constructor: {
                configurable: true,
                enumerable: false,
                writable: true,
                value: Sub
            }
        });
    }
}
function Cat(name) {
    Animal.call(this, name);
}
Cat.extend(Animal);
var cat = new Cat('Tom');
console.log(cat.name);
cat.sleep();
复制代码

优点:

  1. 非常干净的原型链,继承方式趋向于完美;

缺点:

  1. 实现方式较为繁琐和复杂;

以上就是我要介绍的 JS 实现继承的几种常用方式了,你们以为到这里就已经结束了吗?


ES6 出来已经有好长一段时间了,各浏览器对 ES6 语法和 API 的支持也越来越完善(就算还没支持的,也有 babel 这类工具让大家可提前使用 ES6 的各种新特性);

而 es6 本身的 calss 语法也实现了继承的特性,有空的同学可以自己去看看;

下面补充一点

其实 js 本身并没有继承,而所谓的原型继承可以称为怪异继承,但其本身并不是继承,只是通过 new 调用函数,强行将生成的对象和函数的 prototype 对象进行关联,可以实现对其方法的使用而已(详细解释请移步至《你不知道的 JavaScript 上卷》P146:5.2章 “类”)

转载于:https://juejin.im/post/5b26175c51882574957a694a

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值