作为一门弱类型的编程语言,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!
复制代码
优点:
- 纯粹的继承关系,子类原型复制了父类的实例属性并加以拓展;
- 子类的原型继承了父类的原型属性,所以父类新添加的原型属性都可以使用;
- 简单易理解,结构清晰明了;
缺点:
- 难以实现多继承(多个父类实例赋值给子类原型的属性并不美观);
- 父类的实例属性值(并不是方法)存在于子类的原型中;对于构造函数来讲,最理想的状态就是属性存在实例中,方法存在于原型中;
- 在定义子类的原型时就要创建父类的实例(传参也在这一步完成);创建子类实例的时候父类的实例已经初始化完成,无法向父类传参;如果创建子类的参数是异步的就会造成困扰,除非子类实例定义一个与父类实例相同的参数进行覆盖,但显然这不是最好的方式,会造成子类实例和原型中出现相同的属性;
- 想要为子类新增原型方法,必须在继承了父类实例之后执行;
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!'
复制代码
优点:
- 初始化子类实例的时候,可以为父类传递参数;
- 父类的实例属性会被初始化在子类实例中的,并不会在原型中;
- 方便实现多继承;
缺点:
- 并没有真正的继承父类,只是复制了一份父类的实例属性到子类中;
- 没有继承父类的原型方法,子类原型的再上一层继承的仍然是 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!
复制代码
优点:
- 弥补了「构造继承」中不能继承原型方法的问题;
- 弥补了「原型链继承」中,在定义子类原型时就需要传参的问题,以及难以实现多继承问题;
缺点:
- 同样存在「原型链继承」中要为子类新增原型方法,需要在继承了父类实例之后执行的问题;
- 一次继承创建了两份父类实例,一份复制在子类实例中,一份在子类原型中,使用时子类实例覆盖了原型中的同名属性,增大了内存消耗;
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();
复制代码
优点:
- 非常干净的原型链,继承方式趋向于完美;
缺点:
- 实现方式较为繁琐和复杂;
以上就是我要介绍的 JS 实现继承的几种常用方式了,你们以为到这里就已经结束了吗?
ES6 出来已经有好长一段时间了,各浏览器对 ES6 语法和 API 的支持也越来越完善(就算还没支持的,也有 babel 这类工具让大家可提前使用 ES6 的各种新特性);
而 es6 本身的 calss 语法也实现了继承的特性,有空的同学可以自己去看看;
下面补充一点
其实 js 本身并没有继承,而所谓的原型继承可以称为怪异继承,但其本身并不是继承,只是通过 new 调用函数,强行将生成的对象和函数的 prototype 对象进行关联,可以实现对其方法的使用而已(详细解释请移步至《你不知道的 JavaScript 上卷》P146:5.2章 “类”)