本文深入探讨JavaScript中继承的实现方式,从传统的混合原型链与对象冒充模式出发,分析其在代码组织、构造函数参数依赖及原型链长度上的缺陷。通过引入init方法、原型复制及方法代理等技术,逐步构建一个简洁高效的通用继承库,旨在提供类似Java的面向对象体验并优化执行性能。
JavaScript继承机制概述
文章围绕四个维度展开讨论:
•1. 混合模式实现及其缺陷
•2. 预期的调用接口
•3. 继承库的具体实现细节
•4. 结论
欢迎有兴趣的读者继续阅读后续内容。
我最初在xx处学到的JS继承方法是混合原型链与对象冒充技术,工作中涉及继承时均沿用此法。该方法逻辑简洁:通过对象冒充继承父类构造函数的属性,利用原型链继承父类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;
};
利用init方法完成构造功能,可确保在设置子类原型时(Manager.prototype = new Employee()),父类实例化操作不会出错。不足之处在于,调用类构造函数初始化实例时,必须在调用构造函数后手动调用init方法以执行实际构造逻辑:
- Hide code
var e = new Employee();
e.init('jason', 5000);
var m = new Manager();
m.init('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
若能将此init逻辑置于构造函数内部,则违背前述构造函数无参无逻辑原则。换个角度思考,该原则旨在确保实例化父类作为子类原型时,调用父类构造函数不出错。因此,可适度打破此原则,在类构造函数中添加少量且绝不会出错的逻辑来解决:
- Hide code
//添加一个全局标识initializing,表示是否正在进行子类的构建和类的继承
var initializing = false;
//可自动调用init方法的父类构造函数
function Employee() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
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() + '.';
}
};
//可自动调用init方法的子类构造函数
function Manager() {
if (!initializing) {
this.init.apply(this, arguments);
}
}
//表示开始子类的构建和类的继承
initializing = true;
//此时调用new Emplyee(),并不会调用Employee.prototype.init方法
Manager.prototype = new Employee();
Manager.prototype.constructor = Manager;
//表示结束子类的构建和类的继承,之后调用new Employee或new Manager都会自动调用init实例方法
initializing = false;
//把构造逻辑搬到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;
};
调用结果与前例一致。但此实现尚存一个小问题,即引入了全局变量initializing。若能消除该全局变量则更佳,这其实很容易解决,只需将类的构建与继承封装为模块,并将该变量置于模块内部即可。
3)在构造子类时,将子类原型设为父类的一个实例,这不符合语义。继承应发生于类与类之间,而非类与实例之间。之所以采用父类实例作为子类原型:
- Hide code
SubClass.prototype = new SuperClass();
完全是因为父类实例指向父类原型,而子类实例又指向子类原型,从而最终子类实例可通过原型链访问到父类原型上的方法。此做法虽能实现实例方法的继承,但不符合语义,且存在一个重大缺陷:它增加了原型链长度,导致子类调用父类方法时,必须通过原型链查找才能定位到父类方法。若继承层次较深,将对JS执行性能产生一定影响。
解决方案:在解决此问题前,先思考继承旨在解决的问题:从父类复用已有的实例属性与方法。在JavaScript面向对象编程中,一直遵循一个原则:实例属性均写在构造函数或实例方法内,实例方法写在原型上。换言之,类的原型按此原则而言,仅用于编写实例方法。因此,完全可在构建子类时,通过复制方式将父类原型的所有方法全部添加至子类原型上,无需将父类实例设为子类原型。此举可大幅缩短原型链长度,借助简短的copy函数,即可轻松改造前文代码:
- Hide code
//用来复制父类原型,由于父类原型上约定只写实例方法,所以复制的时候不必担心引用的问题
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
}
function Employee() {
this.init.apply(this, arguments);
}
Employee.prototype = {
constructor: Employee,
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() {
this.init.apply(this, arguments);
}
//将父类的原型方法复制到子类的原型上
Manager.prototype = copy(Employee.prototype);
//子类还是需要修改constructor指向,因为从父类原型复制出来的对象的constructor还是指向父类的构造函数
Manager.prototype.constructor = Manager;
Manager.prototype.init = function (name, salary, percentage) {
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
};
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); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
如此操作后,调用m.toString时实际调用的是Manager类自身原型上的方法,而非Employee类的实例方法,从而缩短了原型链上查找方法的距离。此做法在性能上具有显著优势,但缺点是通过原型链维持的继承关系已断裂,子类的原型与实例均无法再通过JS原生属性访问到父类原型,故调用console.log(m instanceof Employee)输出为false。然而,相较于性能优势,此点可视为次要问题:一是instanceOf运算在JS开发中几乎用不到,至少我未曾遇到;二是通过复制方式完全能够继承父类的实例方法,已达继承最大目的。
此方法另有一个额外好处,即解决了第2个问题最后提及的引入initializing全局变量问题。若采用复制方式,则在构建继承关系时无需调用父类构造函数,故无需在构造函数内先判断initializing再调用init方法,前文代码中已去除initializing变量的处理。
4)在子类构造函数与实例方法内若想调用父类构造函数或方法,显得较为繁琐:
- Hide code
function SuperClass() {}
SuperClass.prototype = {
constructor: SuperClass,
method1: function () {}
}
function SubClass() {
//调用父类构造函数
SuperClass.apply(this);
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function () {
//调用父类的实例方法
SuperClass.prototype.method1.apply(this, arguments);
}
SubClass.prototype.method2 = function () {}
SubClass.prototype.method3 = function () {}
每次都得靠apply借用方法来处理。要是能改成如下的调用就好用多了:
- Hide code
function SubClass() {
//调用父类构造函数
this.base();
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function() {
//调用父类的实例方法
this.base();
}
解决方案:若要在每个实例方法中,均能通过this.base()调用父类原型上相应方法,则this.base绝非固定方法,需在每个实例方法执行期间动态将this.base指定为父类原型的同名方法。能实现此功能的方式,唯有通过方法代理。前文Employee与Manager示例可改造如下:
- Hide code
//用来复制父类原型,由于父类原型上约定只写实例方法,所以复制的时候不必担心引用的问题
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
};
function Employee() {
this.init.apply(this, arguments);
}
Employee.prototype = {
constructor: Employee,
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() {
//必须在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型
//因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型
this.baseProto = Employee.prototype;
this.init.apply(this, arguments);
}
Manager.prototype = copy(Employee.prototype);
//子类还是需要修改constructor指向,因为从父类原型复制出来的对象的constructor还是指向父类的构造函数
Manager.prototype.constructor = Manager;
Manager.prototype.init = (function (name, func) {
return function () {
//记录实例原有的this.base的值
var old = this.base;
//将实例的this.base指向父类的原型的同名方法
this.base = this.baseProto[name];
//调用子类自身定义的init方法,也就是func参数传递进来的函数
var ret = func.apply(this, arguments);
//还原实例原有的this.base的值
this.base = old;
return ret;
}
})('init', function (name, salary, percentage) {
//通过this.base调用父类的init方法
//这个函数真实的调用位置是var ret = func.apply(this, arguments);
//当调用Manager实例的init方法时,其实不是调用的这个函数
//而是调用上面那个匿名函数里面return的匿名函数
//在return的匿名函数里,先把this.base指向为了父类原型的同名函数,然后在调用func
//func内部再通过调用this.base时,就能调用父类的原型方法。
this.base(name, salary);
this.percentage = percentage;
});
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); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
通过代理方式,解决了在实例方法内部通过this.base调用父类原型同名方法的问题。然而,在实际场景中,每个实例方法均可能需要调用父类实例,若每个实例方法都添加相同代码,显然会增加诸多麻烦。好在这部分逻辑相同,可将其抽象化,最终均置于模块内部,从而简化代理工作。
5)未考虑静态属性与静态方法。尽管静态成员无需继承,但在某些场景下,我们仍需静态成员,故需考虑静态成员应添加何处。
解决方案:由于JS原生不支持静态成员,故只能借助公共位置处理。最佳位置是添加至构造函数上:
- Hide code
var copy = function (source) {
var target = {};
for (var i in source) {
if (source.hasOwnProperty(i)) {
target[i] = source[i];
}
}
return target;
};
function Employee() {
this.init.apply(this, arguments);
}
//添加一个静态属性
Employee.idCounter = 1;
//添加一个静态方法
Employee.getId = function () {
return Employee.idCounter++;
};
Employee.prototype = {
constructor: Employee,
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
};
function Manager() {
this.baseProto = Employee.prototype;
this.init.apply(this, arguments);
}
Manager.prototype = copy(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.init = (function (name, func) {
return function () {
var old = this.base;
this.base = this.baseProto[name];
var ret = func.apply(this, arguments);
this.base = old;
return ret;
}
})('init', function (name, salary, percentage) {
this.base(name, salary);
this.percentage = percentage;
});
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); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
console.log(m.id); //2
console.log(e.id); //1
最后两行输出了正确的实例id,该id通过Employee类的静态方法生成。在Java面向对象编程中,子类与父类均可定义静态成员,调用时还存在覆盖问题。在JS中,受语言限制,自定义静态成员无法实现全面的面向对象功能,如上文所示,能为类提供公共属性与公共方法,已足够。
期望的调用方式与接口设计
2. 预期的调用接口
从第1部分分析可知,在JS中,类的构建与继承存在大量通用逻辑,完全可将其封装为独立模块,形成通用类库,以便工作中随时取用。该类库需完成所需功能(类的构建、继承及静态成员添加),同时使用需足够简洁便捷。在利用bootstrap的modal组件自定义alert、confirm和modal对话框一文中,我曾阐述过从组件期望调用方式反推组件实现的一些观点,当你明确所需之物时,方知如何创造。本文编写的继承组件亦采取此法实现,我先用前文Employee与Manager示例模拟调用此继承库的场景,通过预设的一些组件名称或接口名称及调用方式,尝试走通真实使用此继承库的流程。有了此目标,下一步只需据此要求实现即可,模拟如下:
- Hide code
//通过调用Class函数构造一个类
var Employee = Class({
//通过instanceMembers指定这个类的实例成员
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
//通过staticMembers指定这个类的静态成员
//静态方法内部可通过this访问其它静态成员
//在外部可通过Employee.getId这种方式访问到静态成员
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
this.base(name, salary);
this.percentage = percentage;
Manager.count++;
},
getSalary: function () {
return this.salary + this.salary * this.percentage;
}
},
//通过extend指定要继承的类
extend: Employee
});
从模拟结果来看,我期望的继承库对外提供的名称仅有Class、instanceMembers、staticMembers和extend,调用方式亦十分简单,只需传递参数给Class函数即可。接下来便按此目标,看看如何一步步根据第一部分罗列的那些问题及解决方式,将此库编写出来。
继承库的详细实现步骤
3. 继承库的具体实现细节
根据API名称、接口及前文第1部分提出的问题,此继承库需完成的功能有: 1)类的构建(关键:init方法)和静态成员处理; 2)继承关系的构建(关键:父类原型的复制); 3)父类方法的简化调用(关键:父类原型上同名方法的代理)。 故该库实现,可按这三点分三版开发。
1)第一版
在第一版中,仅需实现类的构架与静态成员添加功能即可,细节如下:
- Hide code
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用来判断是否为Object的实例
function isObject(o) {
return typeof (o) === 'object';
}
//用来判断是否为Function的实例
function isFunction(f) {
return typeof (f) === 'function';
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) && options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要构建的类的构造函数
function TargetClass() {
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
TargetClass.prototype = instanceMembers;
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();
此版核心代码在于类的构建与静态成员添加部分,其余代码仅提供一些提前可想到的赋值函数与变量(isObject、isFunction),并做一些参数合法性校验处理。添加静态成员的代码务必在设置原型的代码之前,否则存在原型被覆盖的风险。有了此版本,即可直接构建带静态成员的Employee类:
- Hide code
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var e = new Employee('jason', 5000);
console.log(e.toString()); //jason's salary is 5000.
console.log(e.id); //1
console.log(e.constructor === Employee); //true
在getId方法中之所以直接使用this即可访问到构造函数Employee,是因为getId方法是添加至构造函数上的,故当调用Employee.getId()时,getId方法内的this指向即为Employee函数对象。
第二版在第一版基础上,实现继承关系的构建部分:
- Hide code
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用来判断是否为Object的实例
function isObject(o) {
return typeof (o) === 'object';
}
//用来判断是否为Function的实例
function isFunction(f) {
return typeof (f) === 'function';
}
//简单复制
function copy(source) {
var target = {};
for (var i in source) {
if (hasOwn.call(source, i)) {
target[i] = source[i];
}
}
return target;
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) && options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要构建的类的构造函数
function TargetClass() {
if (extend) {
//如果有要继承的父类
//就在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型
//因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型
this.baseProto = extend.prototype;
}
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
//如果有要继承的父类,先把父类的实例方法都复制过来
extend && (TargetClass.prototype = copy(extend.prototype));
//添加实例方法
for (prop in instanceMembers) {
if (hasOwn.call(instanceMembers, prop)) {
TargetClass.prototype[prop] = instanceMembers[prop];
}
}
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();
此版关键部分在于:
this.baseProto主要目的是为了让子类实例拥有一个属性可访问到父类原型,因为后续继承方式为复制方式,会导致原型链断裂。有了此版后,即可加入Manager类来演示效果:
- Hide code
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
//借用父类的init方法,实现属性继承(name, salary)
Employee.prototype.init.apply(this, [name, salary]);
this.percentage = percentage;
},
getSalary: function () {
return this.salary + this.salary * this.percentage;
}
},
extend: Employee
});
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(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2
不过在Manager内部,调用父类方法时仍采用apply借用方式,故在最后一版中,需将其变为预期的this.base方式,原理前文已了解,无非是在方法同名时,对实例方法加一个代理而已,实现如下:
- Hide code
var Class = (function () {
var hasOwn = Object.prototype.hasOwnProperty;
//用来判断是否为Object的实例
function isObject(o) {
return typeof (o) === 'object';
}
//用来判断是否为Function的实例
function isFunction(f) {
return typeof (f) === 'function';
}
//简单复制
function copy(source) {
var target = {};
for (var i in source) {
if (hasOwn.call(source, i)) {
target[i] = source[i];
}
}
return target;
}
function ClassBuilder(options) {
if (!isObject(options)) {
throw new Error('Class options must be an valid object instance!');
}
var instanceMembers = isObject(options) && options.instanceMembers || {},
staticMembers = isObject(options) && options.staticMembers || {},
extend = isObject(options) && isFunction(options.extend) && options.extend,
prop;
//表示要构建的类的构造函数
function TargetClass() {
if (extend) {
//如果有要继承的父类
//就在每个实例中添加baseProto属性,以便实例内部可以通过这个属性访问到父类的原型
//因为copy函数导致原型链断裂,无法通过原型链访问到父类的原型
this.baseProto = extend.prototype;
}
if (isFunction(this.init)) {
this.init.apply(this, arguments);
}
}
//添加静态成员,这段代码需在原型设置的前面执行,避免staticMembers中包含prototype属性,覆盖类的原型
for (prop in staticMembers) {
if (hasOwn.call(staticMembers, prop)) {
TargetClass[prop] = staticMembers[prop];
}
}
//如果有要继承的父类,先把父类的实例方法都复制过来
extend && (TargetClass.prototype = copy(extend.prototype));
//添加实例方法
for (prop in instanceMembers) {
if (hasOwn.call(instanceMembers, prop)) {
//如果有要继承的父类,且在父类的原型上存在当前实例方法同名的方法
if (extend && isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) {
TargetClass.prototype[prop] = (function (name, func) {
return function () {
//记录实例原有的this.base的值
var old = this.base;
//将实例的this.base指向父类的原型的同名方法
this.base = this.baseProto[name];
//调用子类自身定义的实例方法,也就是func参数传递进来的函数
var ret = func.apply(this, arguments);
//还原实例原有的this.base的值
this.base = old;
return ret;
}
})(prop, instanceMembers[prop]);
} else {
TargetClass.prototype[prop] = instanceMembers[prop];
}
}
}
TargetClass.prototype.constructor = TargetClass;
return TargetClass;
}
return ClassBuilder
})();
核心部分是:
仅当需要继承父类,且父类原型中有方法与当前实例方法同名时,才去对当前实例方法添加代理。更详细原理可回到文章第1部分回顾相关内容。至此,我们在Manager类内部调用父类方法时,便十分简单,只需通过this.base即可:
- Hide code
var Employee = Class({
instanceMembers: {
init: function (name, salary) {
this.name = name;
this.salary = salary;
//调用静态方法
this.id = Employee.getId();
},
getName: function () {
return this.name;
},
getSalary: function () {
return this.salary;
},
toString: function () {
return this.name + '\'s salary is ' + this.getSalary() + '.';
}
},
staticMembers: {
idCounter: 1,
getId: function () {
return this.idCounter++;
}
}
});
var Manager = Class({
instanceMembers: {
init: function (name, salary, percentage) {
//通过this.base调用父类的构造方法
this.base(name, salary);
this.percentage = percentage;
},
getSalary: function () {
return this.base() + this.salary * this.percentage;
}
},
extend: Employee
});
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(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2
注意这两处调用:
以上即为本文要实现的继承库的全部细节,其实它所做的事就是把本文第1部分提到的那些问题的解决方式与第二部分模拟的调用场景结合起来,封装到一个模块内部而已,各个细节的原理只要理解了第1部分总结的那些解决方式便已掌握。在最后一版的演示中,亦能看到,本文实现的此继承库,已完全满足模拟场景中的需求,今后有任何需用到继承的场景,完全可以拿最后一版的实现去开发。
总结与展望
4. 结论
本文在三生石上关于javascript继承系列博客的思路指引下,实现了一个易用的继承库,使用它可以更像Java语言构建面向对象的类和类之间的继承关系。我可预见在将来的工作中,此库对我的代码质量和功能实现将起到极其重要的作用,因为在开发中,继承的编码思想应用甚广,尤其是当我们做项目做得多时,一方面肯定想把一些公共的东西写成可重用的组件,另一方面又必须得满足各个项目的个性要求,所以在写组件时不能写得过于死板,多写接口,等到具体项目时再通过继承等方式来扩展该项目独有的功能,如此写出的组件才会更灵活稳定。总之有了此继承库,感觉以后写的代码都会开心好多~所以希望本文的内容也能对你有同样的一些帮助。如果确实有帮助,求点推荐:)
有关javascript继承的实现小编就给大家介绍这么多,希望对大家有所帮助!