我最早掌握的在js中实现继承的方法是在xx学到的混合原型链和对象冒充的方法,在工作中,只要用到继承的时候,我都是用这个方法实现。它的实现简单,思路清晰:用对象冒充继承父类构造函数的属性,用原型链继承父类prototype 对象的方法,满足我遇到过的所有继承的场景。正因如此,我从没想过下次写继承的时候,我要换一种方式来写,直到今天晚上看了三生石上关于javascript继承系列的文章(出的很早,现在才看,真有点可惜),才发现在js里面,继承机制也可以写的如此贴近java这种后端语言的实现,确实很妙!所以我想在充分理解他博客的思路下,实现一个自己今后用得到的一个继承库。
1. 混合方式实现及问题
了解问题之前,先看看它的具体实现:
- Hide code //父类构造函数 function Employee(name, salary) { //实例属性:姓名 this.name = name; //实例属性:薪资 this.salary = salary; } //通过字面量对象设置父类的原型,给父类添加实例方法 Employee.prototype = { //由于此处添加实例方法时也是通过修改父类原型处理的, //所以必须修改父类原型的constructor指向,避免父类实例的constructor属性指向Object函数 constructor: Employee, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } } //子类构造函数 function Manager(name, salary, percentage) { //对象冒充,实现属性继承(name, salary) Employee.apply(this, [name, salary]); //实例属性:提成 this.percentage = percentage; } //将父类的一个实例设置为子类的原型,实现方法继承 Manager.prototype = new Employee(); //修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数 Manager.prototype.constructor = Manager; //给子类添加新的实例方法 Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; } var e = new Employee('jason', 5000); var m = new Manager('tom', 8000, 0.15); console.log(e.toString()); //jason's salary is 5000. console.log(m.toString()); //tom's salary is 9200. console.log(m instanceof Manager); //true console.log(m instanceof Employee); //true console.log(e instanceof Employee); //true console.log(e instanceof Manager); //false
从结果上来说,这种继承实现方式没有问题,Manager的实例同时继承到了Employee类的实例属性和实例方法,并且通过instanceOf运算的结果也都正确。但是从代码组织和实现细节层面,这种方法还有以下几个问题:
1)代码组织不够优雅,继承实现的关键部分的逻辑是通用的,都是如下结构:
- Hide code //将父类的一个实例设置为子类的原型,实现方法继承 SubClass.prototype = new SuperClass(); //修改子类原型的constructor指向,避免子类实例的constructor属性指向父类的构造函数 SubClass.prototype.constructor = SubClass; //给子类添加新的实例方法 SubClass.prototype.method1 = function() { } SubClass.prototype.method2 = function() { } SubClass.prototype.method3 = function() { }
这段代码缺乏封装。另外在添加子类的实例方法时,不能通过SubClass.prototype = { method1: function() {} }这种方式去设置,否则就把子类的原型整个又修改了,继承就无法实现了,这样每次都得按SubClass.prototype.method1 = function() {} 的结构去写,代码看起来很不连续。
解决方式:利用模块化的方式,将通用的逻辑封装起来,对外提供简单的接口,只要按照约定的接口调用,就能够简化类的构建与类的继承。具体实现请看后面的内容介绍,暂时只能提供理论的说明。
2)在给子类的原型设置成父类的实例时,调用的是new SuperClass(),这是对父类构造函数的无参调用,那么就要求父类必须有无参的构造函数。可是在javascript中,函数无法重载,所以父类不可能提供多个构造函数,在实际业务中,大部分场景下父类构造函数又不可能没有参数,为了在唯一的一个构造函数中模拟函数重载,只能借助判断arguments.length来处理。问题就是,有时候很难保证每次写父类构造函数的时候都会添加arguments.length的判断逻辑。这样的话,这个处理方式就是有风险的。要是能把构造函数里的逻辑抽离出来,让类的构造函数全部是无参函数的话,这个问题就很好解决了。
解决方式:把父类跟子类的构造函数全部无参化,并且在构造函数内不写任何逻辑,把构造函数的逻辑都迁移到init这个实例方法,比如前面给出的Employee和Manager的例子就能改造成下面这个样子:
- Hide code //无参无逻辑的父类构造函数 function Employee() {} Employee.prototype = { constructor: Employee, //把构造逻辑搬到init方法中来 init: function (name, salary) { this.name = name; this.salary = salary; }, getName: function () { return this.name; }, getSalary: function () { return this.salary; }, toString: function () { return this.name + '\'s salary is ' + this.getSalary() + '.'; } }; //无参无逻辑的子类构造函数 function Manager() {} Manager.prototype = new Employee(); Manager.prototype.constructor = Manager; //把构造逻辑搬到init方法中来 Manager.prototype.init = function (name, salary, percentage) { //借用父类的init方法,实现属性继承(name, salary) Employee.prototype.init.apply(this, [name, salary]); this.percentage = percentage; }; Manager.prototype.getSalary = function () { return this.salary + this.salary * this.percentage; };