在本章中,我们将分析Prototypejs中关于JavaScript继承的实现。
Prototypejs是最早的JavaScript类库,可以说是JavaScript类库的鼻祖。 我在几年前接触的第一个JavaScript类库就是这位,因此Prototypejs有着广泛的群众基础。
不过当年Prototypejs中的关于继承的实现相当的简单,源代码就寥寥几行,我们来看下。
早期Prototypejs中继承的实现
源码:
var Class = { // Class.create仅仅返回另外一个函数,此函数执行时将调用原型方法initialize create: function() { return function() { this.initialize.apply(this, arguments); } } }; // 对象的扩展 Object.extend = function(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; };
调用方式:
var Person = Class.create(); Person.prototype = { initialize: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }; var Employee = Class.create(); Employee.prototype = Object.extend(new Person(), { initialize: function(name, employeeID) { this.name = name; this.employeeID = employeeID; }, getName: function() { return "Employee name: " + this.name; } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
很原始的感觉对吧,在子类函数中没有提供调用父类函数的途径。
Prototypejs 1.6以后的继承实现
首先来看下调用方式:
// 通过Class.create创建一个新类 var Person = Class.create({ // initialize是构造函数 initialize: function(name) { this.name = name; }, getName: function(prefix) { return prefix + this.name; } }); // Class.create的第一个参数是要继承的父类 var Employee = Class.create(Person, { // 通过将子类函数的第一个参数设为$super来引用父类的同名函数 // 比较有创意,不过内部实现应该比较复杂,至少要用一个闭包来设置$super的上下文this指向当前对象 initialize: function($super, name, employeeID) { $super(name); this.employeeID = employeeID; }, getName: function($super) { return $super("Employee name: "); } }); var zhang = new Employee("ZhangSan", "1234"); console.log(zhang.getName()); // "Employee name: ZhangSan"
这里我们将Prototypejs 1.6.0.3中继承实现单独取出来, 那些不想引用整个prototype库而只想使用prototype式继承的朋友, 可以直接把下面代码拷贝出来保存为JS文件就行了。
var Prototype = { emptyFunction: function() { } }; var Class = { create: function() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); function klass() { this.initialize.apply(this, arguments); } Object.extend(klass, Class.Methods); klass.superclass = parent; klass.subclasses = []; if (parent) { var subclass = function() { }; subclass.prototype = parent.prototype; klass.prototype = new subclass; parent.subclasses.push(klass); } for (var i = 0; i < properties.length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) klass.prototype.initialize = Prototype.emptyFunction; klass.prototype.constructor = klass; return klass; } }; Class.Methods = { addMethods: function(source) { var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); if (!Object.keys({ toString: true }).length) properties.push("toString", "valueOf"); for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments) }; })(property).wrap(method); value.valueOf = method.valueOf.bind(method); value.toString = method.toString.bind(method); } this.prototype[property] = value; } return this; } }; Object.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); var length = iterable.length || 0, results = new Array(length); while (length--) results[length] = iterable[length]; return results; } Object.extend(Object, { keys: function(object) { var keys = []; for (var property in object) keys.push(property); return keys; }, isFunction: function(object) { return typeof object == "function"; }, isUndefined: function(object) { return typeof object == "undefined"; } }); Object.extend(Function.prototype, { argumentNames: function() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; }, bind: function() { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); } }, wrap: function(wrapper) { var __method = this; return function() { return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); } } }); Object.extend(Array.prototype, { first: function() { return this[0]; } });
首先,我们需要先解释下Prototypejs中一些方法的定义。
argumentNames: 获取函数的参数数组
function init($super, name, employeeID) { console.log(init.argumentNames().join(",")); // "$super,name,employeeID" }
bind: 绑定函数的上下文this到一个新的对象(一般是函数的第一个参数)
var name = "window"; var p = { name: "Lisi", getName: function() { return this.name; } }; console.log(p.getName()); // "Lisi" console.log(p.getName.bind(window)()); // "window"
wrap: 把当前调用函数作为包裹器wrapper函数的第一个参数