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); });