JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

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的那部分就行了,只是少了一些智能化。