从一道面试题谈谈函数柯里化 (Currying)

阅读 2058
收藏 108
2017-01-23
原文链接:segmentfault.com

  欢迎大家再一次来到我的文章专栏:从面试题中我们能学到什么,各位同行小伙伴是否已经开始了悠闲的春节假期呢?在这里提前祝大家鸡年大吉吧~哈哈,之前有人说,学面试题不会有什么长进,其实我觉得这个就像是我们英语考试中的阅读理解,带着问题去看文章反而更有利于自己的学习。
  之前的两篇文章:

都在稀土掘金和Segmentfault都获得了非常多的点击量,没有看的小伙伴们可以点击了解一下,今天为大家带来一道关于闭包和函数的柯里化方面的编程题目,各位小伙伴有没有开始跃跃欲试呢?
  编程题目的要求如下,完成plus函数,通过全部的测试用例。

'use strict';
function plus(n){
  
}
module.exports = plus

测试用例如下

'use strict';
var assert = require('assert')

var plus = require('../lib/assign-4')

describe('闭包应用',function(){
  it('plus(0) === 0',function(){
    assert.equal(0,plus(0).toString())
  })
  it('plus(1)(1)(2)(3)(5) === 12',function(){
    assert.equal(12,plus(1)(1)(2)(3)(5).toString())
  })
  it('plus(1)(4)(2)(3) === 10',function(){
    assert.equal(10,plus(1)(4)(2)(3).toString())
  })
  it('方法引用',function(){
    var plus2 = plus(1)(1)
    assert.equal(12,plus2(1)(4)(2)(3).toString())
  })
})

  实话说刚开始拿到这道题的时候我并没有完全的做出来,但是具体的思路是有的,肯定是关于函数的柯里化(Currying)方面的,应该是想考察一下面试者的闭包理解能力.
  那么首先介绍一下什么是函数的柯里化(Currying)。《JavaScript忍者秘籍》一书中,对于柯里化的定义如下:
  

在一个函数中首先填充几个参数(然后再返回一个新函数)的技术称为柯里化(Currying。

维基百科中关于其定义如下:

在计算机科学中,柯里化(Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个技术由克里斯托弗·斯特雷奇以逻辑学家哈斯凯尔·加里命名的。

  首先我们举个例子来具体的解释一下以上的概念。
  例如一个最简单的加法函数:

//函数定义
function add(x,y){
    return x + y;
}
//函数调用
add(3,4);//5

  如果采用柯里化是怎样将接受两个参数的函数变成接受单一参数的函数呢,其实很简单如下:

//函数表达式定义
var add = function(x){
    return function(y){
        return x + y;
    }
};
//函数调用
add(3)(4);

  这样理解起来其实是不是就很简单了,其实实质利用的就是闭包的概念(大家可以在我的另一篇文章浅谈JavaScript闭包看一下)。本质上讲柯里化(Currying)只是一个理论模型,柯里化所要表达是:如果你固定某些参数,你将得到接受余下参数的一个函数,所以对于有两个变量的函数y^x,如果固定了y=2,则得到有一个变量的函数2^x。这就是求值策略中的部分求值策略。
  柯里化(Currying)具有:延迟计算、参数复用、动态生成函数的作用。例如如果我们需要创建一个通用的DOM事件绑定函数,不使用柯里化的写法如下(该示例来自于博客园 Tong Zeng):

//第四个参数用来标识是在冒泡阶段还是在捕获阶段执行函数
var addEvent = function(el,type,fn,capture){
    if (window.addEventListener) {
         el.addEventListener(type, function(e) {
             fn.call(el, e);
         }, capture);
     } else if (window.attachEvent) {
         el.attachEvent("on" + type, function(e) {
             fn.call(el, e);
         });
     } 
}

  但是在使用了柯里化(Currying)的情况下,不再需要每次添加事件处理都要执行一遍if...else...判断,只需要在浏览器中判定一次就可以了,把根据一次判定之后的结果动态生成新的函数,以后就不必重新计算。其实在实际使用中使用最多的一个柯里化的例子就是Function.prototype.bind()函数,我们也一并给出一个较为简单的Function.prototype.bind()函数的实现方式。

Function.prototype.bind = function(){
    var fn = this;
    var args = Array.prototye.slice.call(arguments);
    var context = args.shift();

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

  回到我们的题目本身,其实根据测试用例我们可以发现,plus函数的要求就是接受单一函数,例如:

plus(1)(4)(2)(3).toString()

但是与柯里化不同之处在于,柯里化返回的一个新函数。我们观察其实最后的求值是通过toString函数得到的,那么我们就很容易想到,我们可以给返回的函数增加一个toString属性就可以了。我自己写出的答案如下:

/**
 * Created by lei.wang on 2017/1/22.
 */

'use strict';

function plus(num) {
    var adder = function () {
        var _args = [];
        var _adder = function _adder() {
            [].push.apply(_args, [].slice.call(arguments));
            return _adder;
        };

        _adder.toString = function () {
            return _args.reduce(function (a, b) {
                return a + b;
            });
        }

        return _adder;
    }
    return adder()(num);
}

module.exports = plus;

  运行一下,通过全部的测试用例,需要注意的是由于题目的要求运行在严格模式下,所以我们在_adder函数内部是不能引用arguments.callee,这时我们采用的方法是给函数表达式中函数本身起名_adder,这样就解决的这个问题。
  再次感谢大家阅读本篇文章,希望大家能从中或多或少学到一些东西,入行资历甚浅,不足之处请多指教,欢迎大家在去我的博客MrErHu中留言打赏,愿大家一同进步。
  
  

评论