高级函数技巧-函数柯里化

维基百科上说道:柯里化,英语:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化的实用性

参数复用

【通用函数】解决了兼容性问题,但同时也会再来,使用的不便利性,不同的应用场景往,要传递很多参数,以达到解决特定问题的目的。有时候应用中,同一种规则可能会反复使用,这就可能会造成代码的重复性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
return reg.test(txt);
}

check(/\d+/g, "test"); //false
check(/[a-z]+/g, "test"); //true

// Currying后
function curryingCheck(reg) {
return function (txt) {
return reg.test(txt);
};
}

var hasNumber = curryingCheck(/\d+/g);
var hasLetter = curryingCheck(/[a-z]+/g);

hasNumber("test1"); // true
hasNumber("testtest"); // false
hasLetter("21212"); // false

上面的示例是一个正则的校验,正常来说直接调用 check 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber,hasLetter 等函数,让参数能够复用,调用起来也更方便。

提高适用性

柯里化不仅仅是提高了代码的合理性,更重的它突出一种思想—降低适用范围,提高适用性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Ajax() {
this.xhr = new XMLHttpRequest();
}

Ajax.prototype.open = function (type, url, data, callback) {
this.onload = function () {
callback(this.xhr.responseText, this.xhr.status, this.xhr);
};

this.xhr.open(type, url, data.async);
this.xhr.send(data.paras);
};

"get post".split(" ").forEach(function (mt) {
Ajax.prototype[mt] = currying(Ajax.prototype.open, mt);
});

var xhr = new Ajax();
xhr.get("/articles/list.php", {}, function (datas) {
// done(datas)
});

var xhr1 = new Ajax();
xhr1.post("/articles/add.php", {}, function (datas) {
// done(datas)
});

延迟执行

1
2
3
4
5
6
7
8
Function.prototype.bind = function (context) {
var _this = this;
var args = Array.prototype.slice.call(arguments, 1);

return function () {
return _this.apply(context, args);
};
};

固定易变因素

柯里化特性决定了它这应用场景。提前把易变因素,传参固定下来,生成一个更明确的应用函数。

bind 函数用以固定 this 这个易变对象

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.bind = function (context) {
var _this = this,
_args = Array.prototype.slice.call(arguments, 1);

return function () {
return _this.apply(
context,
_args.concat(Array.prototype.slice.call(arguments))
);
};
};

封装一些 dom 操作

1
2
3
4
5
6
7
8
var on = function (isSupport, element, event, handler) {
isSupport = isSupport || document.addEventListener;
if (isSupport) {
return element.addEventListener(event, handler, false);
} else {
return element.attachEvent("on" + event, handler);
}
};

通用的封装方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function curry(fn, args) {
var _this = this;
var len = fn.length;
var args = args || [];

return function () {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);

// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return curry.call(_this, fn, _args);
}

// 参数收集完毕,则执行fn
return fn.apply(this, _args);
};
}

性能

在大部分应用中,主要的性能瓶颈是在操作 DOM 节点上,这 js 的性能损耗基本是可以忽略不计的,所以 curry 是可以直接放心的使用。

  • 存取 arguments 对象通常要比存取命名参数要慢一点
  • 一些老版本的浏览器在 arguments.length 的实现上是相当慢的
  • 使用 fn.apply( … )fn.call( … ) 通常比直接调用 fn( … ) 稍微慢点
  • 创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

额外开销

函数柯里化可以用来构建复杂的算法 和 功能, 但是滥用也会带来额外的开销。

从上面实现部分的代码中,可以看到,使用柯里化函数,离不开闭包, arguments, 递归。

  • 闭包,函数中的变量都保存在内存中,内存消耗大,有可能导致内存泄漏。
  • 递归,效率非常差,
  • arguments, 变量存取慢,访问性很差

经典面试题

  1. 实现add(1)(2)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 普通的add函数
    function add(x, y) {
    return x + y;
    }
    add(1, 2); // 3

    // Currying后
    function curryingAdd(x) {
    return function (y) {
    return x + y;
    };
    }

    curryingAdd(1)(2); // 3
  2. 实现一个 add 方法,使计算结果能够满足如下预期:

    1
    2
    3
    add(1)(2)(3) = 6;
    add(1, 2, 3)(4) = 10;
    add(1)(2)(3)(4)(5) = 15;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function () {
    _args.push(...arguments);
    return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
    return _args.reduce(function (a, b) {
    return a + b;
    });
    };
    return _adder;
    }

    add(1)(2)(3); // 6
    add(1, 2, 3)(4); // 10
    add(1)(2)(3)(4)(5); // 15
    add(2, 6)(1); // 9