本文讲述JavaScript中类继承的实现方式,并比较实现方式的差别。javascript
继承,是子类继承父类的特征和行为,使得子类对象具备父类的实例域和方法。
继承是面向对象编程中,不可或缺的一部分。java
减小代码冗余
父类能够为子类提供通用的属性,而没必要由于增长功能,而逐个修改子类的属性代码复用
同上代码易于管理和扩展
子类在父类基础上,能够实现本身的独特功能耦合度高
若是修改父类代码,将影响全部继承于它的子类影响性能
子类继承于父类的数据成员,有些是没有使用价值的。可是,在实例化的时候,已经分配了内存。因此,在必定程度上影响程序性能。例子以图书馆中的书入库归类为例。
如下是简化后的父类Book
(也可称为基类)。
目的是经过继承该父类,产出Computer
(计算机)子类。
而且,子类拥有新方法say
,输出本身的书名。es6
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); }
function Computer(){ Book.apply(this, arguments); }
Computer
的构造函数里,调用父类的构造函数进行初始化操做。使子类拥有父类同样的初始化属性。浏览器
Computer.prototype = new Book();
使用new操做符对父类Book
进行实例化,并将实例对象赋值给子类的prototype
。
这样,子类Computer
就能够经过原型链访问到父类的属性。app
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 = function(prototype){ function F(){} F.prototype = prototype; return new F(); }
原理是定义一个空的构造函数,而后修改其原型,使之成为一个跳板,能够将原型链传递到真正的prototype。优化
上述两种实现方式,都存在一个问题:不存在私有属性
和私有方法
。也就是说,存在被篡改的风险。
接下来就用函数化来化解这个问题。this
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的类继承。不由感慨之前的刀耕火种,是多么折磨人🌚
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的继承就是小菜一碟。