PS:
本文参考了司徒正美的《JavaScript框架设计》,以及许多其它的书籍和网络文章,感谢作者们分享了这么好的学习资源!
关于JS的类和继承的原理,见:JavaScript里的类和继承
文中提到的库和测试文件戳这里:https://github.com/hellobugme/jsclass/
Prototype(http://prototypejs.org/)和 jQuery(https://jquery.com/)都是风靡一时的库,功能大而全,并不是纯粹的类工厂,这里只是简单说下里面的继承。
它们的 extend 都是基于拷贝继承,其中 jQuery 支持 深度拷贝(deep copy)。
拷贝继承的缺点在上篇文章中有提过:子类实例无法通过父类的 instanceof 验证。
Prototype 中类和继承相关的代码如下:
var Class = { 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; };
是吧,够简单的… 只不过是使用 Class.create() 创建出来的类,在实例化时会主动帮你执行下初始化函数 initialize 而已。使用方法大致如下:
var Person = Class.create(); Object.extend(Person.prototype, { initialize: function(name) { this.name = name; }, getName: function() { return this.name; } }); var User = Class.create(); User.prototype = Object.extend(new Person(), { initialize: function(name, password) { this.name = name; this.password = password; }, getPassword: function() { return this.password; } });
jQuery 在 extend 的实现中加入递归,对数组和对象等引用类型的属性值进行深度拷贝:
(... 表示一大波的代码,下同)
jQuery.extend = jQuery.fn.extend = function() { //... for (name in options) { src = target[name]; copy = options[name]; //... if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // 通过递归对数组和对象进行深度拷贝 target[name] = jQuery.extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; } } //... };
deep 是可选参数,指定是否进行深度拷贝。
通过 jQuery.isPlainObject() 和 jQuery.isArray() 判断属性值是否为纯粹对象或数组,然后决定是递归操作还是直接赋值。
道爷(Douglas Crockford)在自己的网站上提出了一套简单的方法来模拟类式继承—— Sugar(http://javascript.crockford.com/inheritance.html)。
1 // 辅助函数,可以将新函数绑定到对象的 prototype 上 2 Function.prototype.method = function(name, func) { 3 this.prototype[name] = func; 4 return this; 5 }; 6 7 // 从其它对象继承函数,同时仍然可以调用数据父对象的那些函数 8 Function.method('inherits', function(parent) { 9 // 继承父对象的方法 10 this.prototype = new parent(); 11 this.prototype.constructor = parent; 12 13 var d = {}, 14 p = this.prototype; 15 // 创建一个新的特权函数'uber',调用它时会执行所有在继承时被重写的函数 16 this.method('uber', function uber(name) { 17 if (!(name in d)) { 18 d[name] = 0; 19 } 20 var f, // 要执行的函数 21 r, // 函数的返回值 22 t = d[name], // 记录当前所在的父层次的级数 23 v = parent.prototype; // 父对象的prototype 24 25 // 如果已经在某个'uber'函数之内 26 if (t) { 27 // 上溯必要的t,找到原始的prototype 28 while (t) { 29 v = v.constructor.prototype; 30 t -= 1; 31 } 32 // 从该prototype中获得函数 33 f = v[name]; 34 // 否则这就是'uber'函数的第一次调用 35 } else { 36 // 从prototype获得要执行的函数 37 f = p[name]; 38 // 如果此函数属于当前的prototype 39 if (f == this[name]) { 40 // 则改为调用父对象的prototype 41 f = v[name]; 42 } 43 } 44 45 // 记录在继承堆栈中所在位置的级数 46 d[name] += 1; 47 48 // 使用除第一个以外所有的arguments调用此函数 49 r = f.apply(this, Array.prototype.slice.apply(arguments, [1])); 50 // 恢复继承堆栈 51 d[name] -= 1; 52 53 // 返回执行过的函数的返回值 54 return r; 55 }); 56 return this; 57 }); 58 59 // 只继承父对象特定函数的函数(非使用new parent()继承所有的函数) 60 Function.method('swiss', function(parent) { 61 // 遍历所有要继承的方法 62 for (var i = 1; i < arguments.length; i += 1) { 63 // 需要导入的方法名 64 var name = arguments[i]; 65 // 将此方法导入this对象的prototype中 66 this.prototype[name] = parent.prototype[name]; 67 } 68 return this; 69 });
因为JS中的类本身就是一个函数,所以直接为 Function 的原型扩展了 method、inherits、swiss 三个方法,所有类也能获得这些方法。
重点在于 inherits 中添加的特权方法“uber”(为什么不叫“super”叫“uber”?难道道爷是优步忠实粉?好吧,很冷…),它根据继承堆栈级数回溯到相应的祖先类原型方法,让你在子类中可以非常方便地调用被重写的父类原型方法。
function Person(name) { this.name = name; } Person.method('getName', function() { return name; }); function User(name, password) { this.name = name; this.password = password; } User.inherits(Person); User.method('getPassword', function() { return this.password; }); // 重写父类原型方法 User.method('getName', function() { // 通过 uber() 调用父类的 getName() return "My name is: " + this.uber('getName'); });
Base(http://dean.edwards.name/base/)是 Dean Edwards 开发的一个类工厂库,它提供了一套比较直观的对象继承方法,对后来许多类工厂的实现有不小的影响,如:
JS.Class(http://dkraczkowski.github.io/js.class/)、
simple-inheritance(http://ejohn.org/blog/simple-javascript-inheritance/)
都借鉴了 Base 的继承系统。
Base 的代码较长,忽略具体实现,只留关键步骤后大致如下:
1 var Base = function() { 2 // 并没有什么卵用 3 }; 4 5 //Base.extend 方法功能: 6 //1、创建子类;2、继承父类;3、返回子类 7 Base.extend = function(_instance, _static) { 8 var proto = new this; //原型继承,proto将作为子类的prototype 9 Base.prototype.extend.call(proto, _instance); //给子类添加原型方法 10 //... 11 var klass = function() {}; //子类 12 klass.prototype = proto; 13 Base.prototype.extend.call(klass, _static); //给子类添加特权方法 14 //... 15 return klass; //返回子类 16 }; 17 18 //Base.prototype.extend 方法功能: 19 //复制新的成员到子类 20 Base.prototype = { 21 extend: function(source, value) { 22 if (arguments.length > 1) { 23 //传入的是2个参数,为键值对,直接给this的source赋值为value 24 //该方法通过call来调用,这里的执行环境this为调用时传入的环境 25 //... 26 this[source] = value; 27 } else { 28 //传入的是1个参数,为对象,遍历对象,把对象的所有属性复制给this 29 //... 30 for (var key in source) { 31 Base.prototype.extend.call(this, key, source[key]); 32 } 33 } 34 return this; 35 } 36 }; 37 38 //初始化Base 39 Base = Base.extend({ 40 constructor: function() { /*...*/ } 41 }, { 42 ancestor: Object, 43 version: "1.1" 44 // ... 45 });
Base 通过两个 extend 方法实现了类的创建和继承:
调用 Base.extends() 创建子类的过程如下:
Base 的使用方法如下:
var Person = Base.extend({ constructor: function(name) { this.name = name; }, getName: function() { return this.name; } }); var User = Person.extend({ constructor: function(name, password) { this.base(name); this.password = password; }, getPassword: function() { return this.password; } });
一个代码块就创建了一个类并实现了继承,用起来是不是很爽?
P.js(https://github.com/jneen/pjs) 是一个非常精巧的库,它最大的特点是直接把父类的原型抛出来,在调用父类方法时非常方便。
1 var P = (function(prototype, ownProperty, undefined) { 2 return function P(_superclass /* = Object */ , definition) { 3 // 如果只有一个参数,表示没有父类 4 if (definition === undefined) { 5 definition = _superclass; 6 _superclass = Object; 7 } 8 9 // C为要返回的子类,init为初始化函数 10 function C() { 11 var self = this instanceof C ? this : new Bare; 12 self.init.apply(self, arguments); 13 return self; 14 } 15 16 // Bare让C不用new就能返回实例 17 function Bare() {} 18 C.Bare = Bare; 19 20 // 为了防止改动子类影响到父类,所以将父类的原型赋值给中介者Bare,然后再将Bare的实例作为子类的原型 21 var _super = Bare[prototype] = _superclass[prototype]; 22 var proto = Bare[prototype] = C[prototype] = C.p = new Bare; 23 24 var key; 25 26 // 修正子类的构造器函数,使其指向自身 27 proto.constructor = C; 28 29 C.extend = function(def) { 30 return P(C, def); 31 } 32 33 return (C.open = function(def) { 34 // 如果def是函数,则直接调用,并传入子类原型、父类原型、子类构造器、父类构造器 35 if (typeof def === 'function') { 36 def = def.call(C, proto, _super, C, _superclass); 37 } 38 39 // 如果def是对象,则是子类的扩展包,将其属性添加到子类原型 40 if (typeof def === 'object') { 41 for (key in def) { 42 // ownProperty其实就是传入的Object.hasOwnProperty 43 if (ownProperty.call(def, key)) { 44 proto[key] = def[key]; 45 } 46 } 47 } 48 49 // 确保有初始化函数可以调用 50 if (!('init' in proto)) proto.init = _superclass; 51 52 return C; 53 })(definition); 54 } 55 56 })('prototype', ({}).hasOwnProperty);
里面有几个关键点:
P.js 的使用方法如下:
// 将一个函数传给P,P会先创建一个类,然后后调用该函数,并将创建的类的原型和父类原型传入,所以在该函数中可以通过第一个参数来给类添加原型成员 var Person = P(function(proto, superProto) { // init为初始化函数,P会在完成类的处理后自动调用该方法 proto.init = function(name) { // 添加特权成员 this.name = name; }; proto.getName = function() { return this.name; }; }); var User = P(Person, function(user, person) { user.init = function(name, password) { // 通过P传入的父类原型,可以很方便的访问父类原型成员 person.init.call(this, arguments); this.password = password; }; user.getPassword = function() { return this.password; }; });
妈妈再也不用担心我调不到父类方法了!
如果说有什么库最能体现 JS 的灵活性,def.js(http://badassjs.com/post/811837523/def-js-ruby-style-inheritance-in-javascript) 肯定名列前茅!
它试图在形式上模拟 Ruby 那种继承,让学过 Ruby 的人一眼就看到哪个是父类,哪个是子类。
Ruby 的继承是这样的:
class User < Person #... end
而 def.js 能做到这样:
def("Person")({ init: function(name) { this.name = name; }, getName: function() { return this.name; } }); def("User") < Person({ init: function(name, password) { this._super(); this.password = password; }, getPassword: function() { return this.password; } });
是不是很神奇!!
这里面有几个魔法:
先来说说“<”操作符的运用。
在 JS 中,当两个对象进行算术运算或大小比较时,如果它们不是数值,则会尝试将其转换为数值,即调用其 valueOf 方法。
“<”操作符的目的就是强制两边计算自身,从而调用自己的 valueOf 方法,def.js 就是通过重写父类与子类的 valueOf,在里面偷偷实现了原型继承。
先来看一个简单的例子:
var a = { valueOf: function() { console.log("aaaaa"); } }, b = { valueOf: function() { console.log("bbbbb"); } }; console.log(a < b);
执行结果如下:
可以看到,先是执行了 a 的 valueOf(),然后执行了 b 的 valueOf(),最后返回比较结果false。
再来看看 def.js 的源码,我们在其中几个关键点输出日志(代码中的console.log):
1 (function(global) { 2 // deferred 是整个库中最重要的部件,扮演3个角色 3 // 1、def("SuperClass")时就是返回deferred,此时我们可以直接接括号对原型进行扩展 4 // 2、在继承父类时 < 触发两者调用 valueOf,此时会执行 deferred.valueOf 里面的逻辑 5 // 3、在继承父类时,父类的后面还可以接括号(此时构造器当普通函数使用),当作传送器,保存着父类与扩展包到 _super._props 6 var deferred; 7 8 // 扩展自定义的原型 9 function extend(source) { 10 var prop, target = this.prototype; 11 12 for (var key in source) 13 if (source.hasOwnProperty(key)) { 14 prop = target[key] = source[key]; 15 if ('function' == typeof prop) { 16 // 在每个原型方法上添加2个自定义属性,保存其名字与当前类 17 prop._name = key; 18 prop._class = this; 19 } 20 } 21 22 return this; 23 } 24 25 function base() { 26 // 取得调用 this._super() 这个函数本身,如果是在init内,那么就是当前类 27 var caller = arguments.callee.caller; 28 // 执行父类的同名方法,有2种形式,一是用户自己传,二是只能取当前函数的参数 29 return caller._class._super.prototype[caller._name] 30 .apply(this, arguments.length ? arguments : caller.arguments); 31 } 32 33 function def(context, klassName) { 34 console.log("def called"); 35 klassName || (klassName = context, context = global); 36 // 偷偷在给定的全局作用域或某对象上创建一个类 37 var Klass = context[klassName] = function Klass() { 38 if (context != this) { 39 // 如果不使用 new 操作符,大多数情况下 context 与 this 都为 window 40 return this.init && this.init.apply(this, arguments); 41 } 42 // 实现继承的第二步,复制自身与扩展包到 deferred 43 console.log("Klass constructor called"); 44 deferred._super = Klass; 45 deferred._props = arguments[0] || {}; 46 } 47 48 // 让所有自定义类都共用一个 extend 方法 49 Klass.extend = extend; 50 51 // 实现继承的第一步,重写 deferred,为新创建的自定义类的扩展函数 52 deferred = function(props) { 53 return Klass.extend(props); 54 }; 55 56 // 一个中介者,用于切断子类与父类的原型连接,会被反复读写 57 function Subclass() {} 58 59 // 实现继承的第三步,重写 valueOf,方便在 def("SubClass") < SuperClass({}) 执行它 60 deferred.valueOf = function() { 61 console.log("deferred.valueOf called"); 62 var Superclass = deferred._super; 63 64 if (!Superclass) { 65 return Klass; 66 } 67 // 先将父类的原型赋给中介者,然后再将中介者的实例作为子类的原型 68 Subclass.prototype = Superclass.prototype; 69 var proto = Klass.prototype = new Subclass; 70 // 引用自身与父类 71 Klass._class = Klass; 72 Klass._super = Superclass; 73 // 获取该类名字 74 Klass.toString = function() { 75 return klassName; 76 }; 77 // 修复原型中 constructor 指向自身 78 proto.constructor = Klass; 79 // 让所有自定义类都共用这个 base 方法,它是构成方法链的关系 80 proto._super = base; 81 // 把父类后来传入的扩展包混入子类的原型中 82 deferred(deferred._props); 83 }; 84 85 return deferred; 86 } 87 88 global.def = def; 89 }(this));
结果如下:
第一个“def called”是创建 Person 时输出的,忽略。
从接下来 3 个日志的输出顺序可以推测出,def("User") < Person({/*...*/}) 执行顺序如下:
太妙了!!这需要对 def("User") < Person({/*...*/}) 这一行代码各个部分的执行顺序有充分的了解,才能做出如此巧妙的设计。
接下来说说第 3 点:_super 的原理。
这里其实是对 arguments.callee.caller 的运用,通过 caller 可以获取到当前调用的原型方法的引用。
而 def.js 在 extend() 中为每个原型方法都添加了 _name(方法名)和 _class(指向当前类)两个属性,因此,通过 caller._name 可以获得方法名,caller._class._super 可以获得父类引用,有了这两样东西,自然就能调用父类同名方法了。
不过遗憾的是, caller 是被废弃的属性,在 ES5 的严格模式下不可用。当然也可以修改,只是会少了些智能化。