本文讲述JavaScript中类继承的实现方式,并比较实现方式的差异。
一、何为继承
继承,是子类继承父类的特征和行为,使得子类对象具有父类的实例域和方法。
继承是面向对象编程中,不可或缺的一部分。
1.1 优点
-
减少代码冗余
父类可以为子类提供通用的属性,而不必因为增加功能,而逐个修改子类的属性 -
代码复用
同上 -
代码易于管理和扩展
子类在父类基础上,可以实现自己的独特功能
1.2 缺点
-
耦合度高
如果修改父类代码,将影响所有继承于它的子类 -
影响性能
子类继承于父类的数据成员,有些是没有使用价值的。但是,在实例化的时候,已经分配了内存。所以,在一定程度上影响程序性能。
二、例子
例子以图书馆中的书入库归类为例。
以下是简化后的父类Book
(也可称为基类)。
目的是通过继承该父类,产出Computer
(计算机)子类。
并且,子类拥有新方法say
,输出自己的书名。
function Book(){
this.name = ''; // 书名
this.page = 0; // 页数
this.classify = ''; // 类型
}
Book.prototype = {
constructor: Book,
init: function(option){
this.name = option.name || '';
this.page = option.page || 0;
this.classify = option.classify || '';
},
getName: function(){
console.log(this.name);
},
getPage: function(){
console.log(this.page);
},
getClassify: function(){
console.log(this.classify);
}
};
接下来会讲解子类Computer
几种继承方式的实现和优化方法。开始飙车~
三、实例式继承
function Computer(){
Book.apply(this, arguments);
}
Computer.prototype = new Book();
Computer.prototype.constructor = Computer;
Computer.prototype.init = function(option){
option.classify = 'computer';
Book.prototype.init.call(this, option);
};
Computer.prototype.say = function(){
console.log('I\'m '+ this.name);
}
3.1 调用父类构造器进行初始化
function Computer(){
Book.apply(this, arguments);
}
Computer
的构造函数里,调用父类的构造函数进行初始化操作。使子类拥有父类一样的初始化属性。
3.2 将父类的原型传递给子类
Computer.prototype = new Book();
使用new操作符对父类Book
进行实例化,并将实例对象赋值给子类的prototype
。
这样,子类Computer
就可以通过原型链访问到父类的属性。
3.3 缺点
父类
Book
的构造函数被执行了2次- 一次是在
Computer
的构造函数里Book.apply(this, arguments);
- 一次是在
Computer.prototype = new Book();
- 一次是在
这种模式,存在一定的性能浪费。
- 父类实例化无法传参
Computer.prototype = new Book();
,这种实例化方式,无法让Book
父类接收不固定的参数集合。
四、原型式继承
function Computer(){
Book.apply(this, arguments);
}
Computer.prototype = Object.create(Book.prototype);
Computer.prototype.constructor = Computer;
Computer.prototype.init = function(option){
option.classify = 'computer';
Book.prototype.init(option);
};
Computer.prototype.say = function(){
console.log('I\'m '+ this.name);
}
这里的改进:是使用Object.create(Book.prototype)
。它的作用是返回一个继承自原型对象Book.prototype
的新对象。且该对象下的属性已经初始化。
用Object.create
生成新对象,并不会调用到Book
的构造函数。
这种方式,也可以通过原型链实现继承。
五、Object.create的简单版兼容
由于低版本的浏览器是不支持Object.create
的。所以这里简单介绍下兼容版本:
Object.create = function(prototype){
function F(){}
F.prototype = prototype;
return new F();
}
原理是定义一个空的构造函数,然后修改其原型,使之成为一个跳板,可以将原型链传递到真正的prototype。
六、函数化继承
上述两种实现方式,都存在一个问题:不存在私有属性
和私有方法
。也就是说,存在被篡改的风险。
接下来就用函数化来化解这个问题。
function book(spec, my){
var that = {};
// 私有变量
spec.name = spec.name || ''; // 书名
spec.page = spec.page || 0; // 页数
spec.classify = spec.classify || ''; // 类型
var getName = function(){
console.log(spec.name);
};
var getPage = function(){
console.log(spec.page);
};
var getClassify = function(){
console.log(spec.classify);
};
that.getName = getName;
that.getPage = getPage;
that.getClassify = getClassify;
return that;
}
function computer(spec, my){
spec = spec || {};
spec.classify = 'computer';
var that = book(spec, my);
var say = function(){
console.log('I\'m '+ spec.name);
};
that.say = say;
return that;
}
var Ninja = computer({name: 'JavaScript忍者秘籍', page: 350});
函数化的优势,就是可以更好地进行封装和信息隐藏。
也许有人疑惑为什么用以下这种方式声明和暴露方法:
var say = function(){
console.log('I\'m '+ spec.name);
};
that.say = say;
其实是为了保护对象自身的完整性。即使that.say
被外部篡改或破坏掉,function computer
内部的say
方法仍然能够正常工作。
另外,解释下that
、spec
和my
的作用:
-
that
是一个公开数据存储容器,暴露出去的数据接口,都放到这个容器 -
spec
是用来存储创建新实例所需的信息,属于实例之间共同编辑的数据 -
my
是用来存储父类、子类之间共享的私密数据容器,外部是访问不到的。
七、ES6继承
最后,看下现代版ES6的类继承。不禁感慨以前的刀耕火种,是多么折磨人🌚
class Book {
constructor(){
this.name = ''; // 书名
this.page = 0; // 页数
this.classify = ''; // 类型
}
init(option) {
this.name = option.name || '';
this.page = option.page || 0;
this.classify = option.classify || '';
}
getName() {
console.log(this.name);
}
getPage (){
console.log(this.page);
}
getClassify (){
console.log(this.classify);
}
}
class Computer extends Book{
constructor(...args){
super(...args);
}
init(option) {
super.init(option);
this.classify = 'computer';
}
say() {
console.log('I\'m '+ this.name);
}
}
结语
虽然ES5终究会被淘汰,但是了解下其工作原理,还是很有必要。因为很多源码还是有用到里面的模式。
附带的价值就是,ES5的继承玩到飞起,ES6的继承就是小菜一碟。