5.2.4 体现JavaScript灵活性的库——def.js
如果有什么库最能体现 JavaScript 的灵活性,此库肯定名列前茅。它试图在形式上模拟 Ruby那种继承,让学过Ruby的人一眼就看到哪个是父类,哪个是子类。
下面就是Ruby的继承示例。
class Child < Father #略 end
def.js能做如下这个程度。
def("Animal")({ init: function(name) { this.name = name; }, speak: function(text) { console.log("this is a " + this.name); } }); var animal = new Animal("Animal"); console.log(animal.name) def("Dog") < Animal({ init: function(name, age) { this._super(); //魔术般地调用父类 this.age = age; }, run: function(s) { console.log(s) } }); var dog = new Dog("wangwang"); console.log(dog.name); //wangwang //在命名空间对象上创建子类 var namespace = {} def(namespace, "Shepherd") < Dog({ init: function() { this._super(); } }); var shepherd = new namespace.Shepherd("Shepherd") console.log(shepherd.name);
由于涉及的魔术比较多,我逐个分解一下。
第一个是 curry 的运用,在 def(“Animal”)之后它还能直接添加括号,说明它是一个函数。而此时真正的类已经创建出来,可以在当前作用域下+[“Animal”]访问到。
第二个是“<”操作符在这里的作用。其实原项目是用“<<”,不过换成“+”、“-”也行,但要保证与Ruby的拟态,我还是推荐用“<”。“<”操作符目的是强制两边计算自身,从而调用自己的valueOf方法。def.js就是通过重写了父类与子类定义的valueOf实现在某个作用域中偷偷地进行原型继承,如图5.1所示。
var a = {valueOf:function(){ console.log("aaaaaaa") }}, b = {valueOf:function(){ console.log("bbbbbbb") }} a < b
▲图5.1
由于操作符两边都是函数,那么我们能做更多的事!
function def(name) { console.log("def(" + name + ") called") var obj = { valueOf: function() { console.log(name + " (valueOf)") } } return obj } def("Dog") < def("Animal");
第三个是arguments.callee.caller的运用。大家看一下Dog的构造函数,里面只有一句this._super(),没有传参,但它依然能调用到它的父类构造器,并把arguments塞进去。arguments.callee就是指_super这个函数,caller就是init这个函数,然后我们访问caller.arguments,就得到"wangwang"这个传参了。因此它这个_super比simple-inheritance的智能多了,就像Java的super关键字那样,摆在那里自行干活。同时_super不但能自动调用父类的构造器,同名超类方法的实现也由它一手打包。
下面是源码解读。
//https://github.com/RubyLouvre/def.js (function(global) { //deferred是整个库中最重要的构件,扮演三个角色 //1 def("Animal")时就是返回deferred,此时我们可以直接接括号对原型进行扩展 //2 在继承父类时 < 触发两者调用valueOf,此时会执行deferred.valueOf里面的逻辑 //3 在继承父类时, 父类的后面还可以接括号(废话,此时构造器当普通函数使用),当作传送器, // 保存着父类与扩展包到_super,_props var deferred; function extend(source) { //扩展自定义类的原型 var prop, target = this.prototype; for(var key in source) if(source.hasOwnProperty(key)) { prop = target[key] = source[key]; if('function' == typeof prop) { //在每个原型方法上添加两个自定义属性,保存其名字与当前类 prop._name = key; prop._class = this; } } return this; } // 一个中介者,用于切断子类与父类的原型连接 //它会像DVD+R光盘那样被反复擦写 function Subclass() {} function base() { // 取得调用this._super()这个函数本身,如果是在init内,那么就是当前类 //http://larryzhao.com/blog/arguments-dot-callee-dot-caller-bug-in-internet-explorer-9/ var caller = base.caller; //执行父类的同名方法,有两种形式,一是用户自己传,二是智能取当前函数的参数 return caller._class._super.prototype[caller._name].apply(this, arguments.length ? arguments : caller.arguments); } function def(context, klassName) { klassName || (klassName = context, context = global); //偷偷在给定的全局作用域或某对象上创建一个类 var Klass = context[klassName] = function Klass() { if(context != this) { //如果不使用new 操作符,大多数情况下context与this都为window return this.init && this.init.apply(this, arguments); } //实现继承的第二步,让渡自身与扩展包到deferred deferred._super = Klass; deferred._props = arguments[0] || {}; } //让所有自定义类都共用同一个extend方法 Klass.extend = extend; //实现继承的第一步,重写deferred,乍一看是刚刚生成的自定义类的扩展函数 deferred = function(props) { return Klass.extend(props); }; // 实现继承的第三步,重写valueOf,方便在def("Dog") < Animal({})执行它 deferred.valueOf = function() { var Superclass = deferred._super; if(!Superclass) { return Klass; } // 先将父类的原型赋给中介者,然后再将中介者的实例作为子类的原型 Subclass.prototype = Superclass.prototype; var proto = Klass.prototype = new Subclass; // 引用自身与父类 Klass._class = Klass; Klass._super = Superclass; //一个小甜点,方便人们知道这个类叫什么名字 Klass.toString = function() { return klassName; }; //强逼原型中的constructor指向自身 proto.constructor = Klass; //让所有自定义类都共用这个base方法,它是构成方法链的关系 proto._super = base; //最后把父类后来传入的扩展包混入子类的原型中 deferred(deferred._props); }; return deferred; } global.def = def; }(this));
它的实现非常巧妙。这要对def(“Dog”) < Animal({})这一行的代码各个部分的执行顺序有充分的了解。无疑左边的def会先执行,重新擦写了deferred与deferred.valueOf,然后是父类Animal作为普通函数接受子类的扩展包,扩展包与父类也在这时偷偷附加到deferred上。最后是中间的操作符触发deferred.valueOf,完成继承!
当然也有美中不足的地方,就是利用了caller这个被废弃的属性。在es5的严格模式下,它是不可用的,导致继承系统瘫痪!这个修改也很简单,直接参考jQuery UI的那部分就行了,只是少了一些智能化。