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

3.4 函数的扩展与修复

ecma262v5对函数唯一的扩展就是bind函数,众所周知,这是来自Prototype.js,此外,其他重要的函数都来自Prototype.js。

Prototype.js的函数扩展如下。

argumentNames:取得函数的形参,以字符串数组形式返回,这只要用于其类工厂的方法链设计。

bind:不用多言,劫持作用域,并预先添加更多参数。

bindAsEventListener:如bind相似,但强制返回函数的第一个参数为事件对象,这是用于修复IE的多投事件API与标准API的差异。

curry:函数柯里化,用于一个操作分成多步进行,并可以改变原函数的行为。

wrap:AOP的实现。

delay:setTimeout的偷懒写法。

defer:强制延迟0.01秒才执行原函数。

methodize:将一个函数变成其调用对象的方法,这也是为其类工厂的方法链服务。

我们先看bind方法,它用到了著名的闭包。所谓闭包,就是一个引用着外部变量的内部函数。比如下面这个函数。

      var observable = function(val) {
          var cur = val;//一个内部变量
          function field(neo) {
              if (arguments.length) {//setter
                  if (cur !== neo) {
                      cur = neo;
                  }
              } else {//getter
                  return cur;
              }
          }
          field();
          return field;
      }

它里面的field函数将与外部的cur构成一个闭包。Prototype.js中的bind方法只要依仗原函数与经过切片化的args构成闭包,而让这方法名符其实的是那个curry,用户最初的那个传参,劫持到返回函数修正this的指向。

      Function.prototype.bind = function(context) {
          if (arguments.length < 2 && context == void 0)
              return this;
          var __method = this, args = [].slice.call(arguments, 1);
          return function() {
              return __method.apply(context, args.concat.apply(args, arguments));
          }
      }

正因为有这东西,我们才方便修复IE多投事件API attachEvent回调中的this问题,它总是指向window对象,而标准浏览器的addEventListener中的this则为其调用对象。

      var addEvent = document.addEventListener ?
              function(el, type, fn, capture) {
                  el.addEventListener(type, fn, capture)
              } :
              function(el, type, fn) {
                  el.attachEvent("on" + type, fn.bind(el, event))
              }

ECMA62 v5对其认证后,唯一的增强是对调用者进行检测,确保它是一个函数。顺便总结一下这3样东西。

call是obj.method()到method(obj)的变换。

apply是obj.method(a,b,c)到method(obj, [a,b,c])的变幻,它要求第二个参数必须存在,一定是数组或 Arguments 这样的类数组,NodeList 这样具有争议性的东西就不要乱传进去了。因此jQuery对两个数组或类数组的合并是使用jQuery.merge,放弃使用Array.prototype.push.apply。

bind就是apply的变种,保证返回值是一个函数。

这3个方法是非常有用,我们可以设法将它们“偷”出来。

      var bind = function(bind) {
          return{
              bind: bind.bind(bind),
              call: bind.bind(bind.call),
              apply: bind.bind(bind.apply)
          }
      }(Function.prototype.bind)

那怎么用它们呢?比如我们想合并两个数组,直接调用concat方法如下。

      var concat = bind.apply([].concat);
      var a = [1, [2, 3], 4];
      var b = [1, 3];

使用bind.bind方法可以将它们的结果进一步平坦化。

      var concat = bind.apply([].concat);
      console.log(concat(b, a))//[1,3,1,2,3,4]

又如切片化操作,它经常用于转换类数组对象为纯数组的。

      var slice = bind([].slice)
      var array = slice({
          0: "aaa",
          1: "bbb",
          2: "ccc",
          length: 3
      });
      console.log(array)//[ "aaa", "bbb", "ccc"]

更常用的操作是转换arguments对象,目的是为了使用数组的一系列方法。

      function test() {
          var args = slice(arguments)
          console.log(args)//[1,2,3,4,5]
      }
      test(1, 2, 3, 4, 5)

我们可以将hasOwnProperty提取出来,判定对象是否在本地就拥有某属性。

      var hasOwn = bind.call(Object.prototype.hasOwnProperty);
      hasOwn([],"xx") // false
      //使用bind.bind就需要多执行一次
      var hasOwn2 = bind.bind(Object.prototype.hasOwnProperty);
      hasOwn2([],"xx")() // false

上面bind.bind的行为其实就是一种curry,它给了你再一次传参的机会,这样你就可以在内部判定参数的个数,决定是否继续返回函数还是结果。这在设计计算器的连续运算上非常有用。从此角度来看,我们可以得到一信息,bind着重在于作用域的劫持,curry在于参数的不断补充。

我们可以编写如下一个 curry,当所有步骤输入的参数个数等于最初定义时的函数的形参个数时,就执行它。

      function curry(fn) {
          function inner(len, arg) {
              if (len == 0)
                  return fn.apply(null, arg);
              return function(x) {
                  return inner(len - 1, arg.concat(x));
              };
          }
          return inner(fn.length, []);
      }
      function sum(x, y, z, w) {
          return x + y + z + w;
      }
      curry(sum)('a')('b')('c')('d'); // => 'abcd'

不过这里我们假定了用户每次都只传入一个参数,我们可以改进一下。

      function curry2(fn) {
          function inner(len, arg) {
              if (len <= 0)
                  return fn.apply(null, arg);
              return function() {
                  return inner(len - arguments.length,
                          arg.concat(Array.apply([], arguments)));
              };
          }
          return inner(fn.length, []);
      }

这样就可以在中途传递多个参数,或不传递参数。

      curry2(sum)('a')('b', 'c')('d'); // => 'abcd'
      curry2(sum)('a')()('b', 'c')()('d'); // => 'abcd'

不过,上面的函数形式有个更帅气的名称,叫self-curry或recurry。它强调的是递归调用自身来补全参数。

与curry相似的partial。curry的不足的参数总是通过push的方式来补全,而partial则是在定义时所有参数已经都有了,但某些位置上的参数只是个占位符,我们在接着下来的传参只是替换掉它们。博客上有专文《Partial Application in JavaScript》介绍这个内容。

      Function.prototype.partial = function() {
          var fn = this, args = Array.prototype.slice.call(arguments);
          return function() {
              var arg = 0;
              for (var i = 0; i < args.length && arg < arguments.length; i++)
                  if (args[i] === undefined)
                      args[i] = arguments[arg++];
              return fn.apply(this, args);
          };
      }

它是使用undefined作为占位符的。

      var delay = setTimeout.partial(undefined, 10);
      //接下来的工作就是代替掉第一个参数
      delay(function() {
          alert("A call to this function will be temporarily delayed.");
      })

有关这个占位符,该博客的评论列表中也有大量的讨论,最后确定下来是使用_作为变量名,内部还是指向 undefined。我认为这样做还是比较危险的,框架应该提供一个特殊的对象,如Prototype在内部使用$break = {}作为断点的标识。我们可以用一个纯空对象作为partial的占位符。

      var _ = Object.create(null)

纯空对象没有原型,没有toString, valueOf等继承自Object的方法,是一个很特别的东西。在IE下我们可以这样模拟它:

      var _ = (function() {
          var doc = new ActiveXObject('htmlfile')
          doc.write('<script><\/script>')
          doc.close()
          var Obj = doc.parentWindow.Object
          if (!Obj || Obj === Object)
              return
          var name, names =
                  ['constructor', 'hasOwnProperty', 'isPrototypeOf'
                              , 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf']
          while (name = names.pop())
              delete Obj.prototype[name]
          return Obj
      }())
      function partial(fn) {
          var A = [].slice.call(arguments, 1);
          return A.length < 1 ? fn : function() {
              var a = Array.apply([], arguments);
              var c = A.concat();//复制一份
              for (var i = 0; i < c.length; i++) {
                  if (c[i] === _) {//替换占位符
                      c[i] = a.shift();
                  }
              }
              return fn.apply(this, c.concat(a));
          }
      }
      function test(a, b, c, d) {
          return "a = " + a + " b = " + b + " c = " + c + " d = " + d
      }
      var fn = partail(test, 1, _, 2, _);
      fn(44, 55)// "a = 1 b = 44 c = 2 d = 55"

curry、partial的应用场景在前端世界真心不多,前端讲究的是即时显示,许多API都是同步的,后端由于IO操作等耗时够长,像node.js提供了大量的异步函数来提高性能,防止堵塞。但是过多异步函数也必然带来回调嵌套的问题,因此我们需要通过curry等函数变换,将套嵌减少到可以接受的程度。这个我会在Ajax的章节讲述它们的使用方法的。

函数的修复。这涉及两个方法apply与call,这两个方法的本质就是生成一个新的函数,将原函数与用户传参放到里面执行而已。在JavaScript创建一个函数有很多办法,常见的有函数声明和函数表达式,次之是函数构造器,再次是eval, setTimeout……

      Function.prototype.apply || (Function.prototype.apply = function (x, y) {
          x = x || window;
          y = y ||[];
          x.__apply = this;
          if (!x.__apply)
              x.constructor.prototype.__apply = this;
          var r, j = y.length;
          switch (j) {
              case 0: r = x.__apply(); break;
              case 1: r = x.__apply(y[0]); break;
              case 2: r = x.__apply(y[0], y[1]); break;
              case 3: r = x.__apply(y[0], y[1], y[2]); break;
              case 4: r = x.__apply(y[0], y[1], y[2], y[3]); break;
              default:
                  var a = [];
                  for (var i = 0; i < j; ++i)
                      a[i] = "y[" + i + "]";
                  r = eval("x.__apply(" + a.join(",") + ")");
                  break;
          }
          try {
              delete x.__apply ? x.__apply : x.constructor.prototype.__apply;
          }
          catch (e) {}
          return r;
      });
      Function.prototype.call || (Function.prototype.call = function () {
          var a = arguments, x = a[0], y = [];
          for (var i = 1, j = a.length; i < j; ++i)
              y[i - 1] = a[i]
          return this.apply(x, y);
      });