从一道面试题认识函数柯里化

最近在整理面试资源的时候,发现一道有意思的题目,因此就记录下来。javascript

题目

如何实现 multi(2)(3)(4)=24?前端

首先来分析下这道题,实现一个 multi 函数并依次传入参数执行,获得最终的结果。经过题目很容易获得的结论是,把传入的参数相乘就可以获得须要的结果,也就是 2X3X4 = 24。java

简单的实现

那么如何实现 multi 函数去计算出结果值呢?脑海中首先浮现的解决方案是,闭包。git

function multi(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        }
    }
}
复制代码

利用闭包的原则,multi 函数执行的时候,返回 multi 函数中的内部函数,再次执行的时候其实执行的是这个内部函数,这个内部函数中接着又嵌套了一个内部函数,用于计算最终结果并返回。github

闭包实现

单纯从题面来讲,彷佛是已经实现了想要的结果,但仔细一想就会发现存在问题。web

上面的实现方案存在的缺陷:面试

  • 代码不够优雅,实现步骤须要一层一层的嵌套函数。
  • 可扩展性差,假如是要实现 multi(2)(3)(4)...(n) 这样的功能,那就得嵌套 n 层函数。

那么有没有更好的解决方案,答案是,使用函数式编程中的函数柯里化实现。编程

函数柯里化

在函数式编程中,函数是一等公民。那么函数柯里化是怎样的呢?segmentfault

函数柯里化指的是将可以接收多个参数的函数转化为接收单一参数的函数,而且返回接收余下参数且返回结果的新函数的技术。浏览器

函数柯里化的主要做用和特色就是参数复用、提早返回和延迟执行。

例如:封装兼容现代浏览器和 IE 浏览器的事件监听的方法,正常状况下封装是这样的。

var addEvent = function(el, type, fn, capture) {
    if(window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    }else {
        el.attachEvent('on' + type, function(e) {
            fn.call(el, e);
        })
    }
}
复制代码

该封装的方法存在的不足是,每次写监听事件的时候调用 addEvent 函数,都会进行 if else 的兼容性判断。事实上在代码中只须要执行一次兼容性判断就能够了,后续的事件监听就不须要再去判断兼容性了。那么怎么用函数柯里化优化这个封装函数。

var addEvent = (function() {
    if(window.addEventListener) {
        return function(el, type, fn, capture) {
            el.addEventListener(type, function(e) {
                fn.call(el, e);
            }, capture);
        }
    }else {
        return function(ele, type, fn) {
            el.attachEvent('on' + type, function(e) {
                fn.call(el, e);
            })
        }
    }
})()
复制代码

js 引擎在执行该段代码的时候就会进行兼容性判断,而且返回须要使用的事件监听封装函数。这里使用了函数柯里化的两个特色:提早返回和延迟执行。

柯里化另外一个典型的应用场景就是 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)));
	};
};
复制代码

柯里化的实现

那么如何经过函数柯里化实现面试题的功能呢?

通用版

function curry(fn) {
    var args = Array.prototype.slice.call(arguments, 1);
	return function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return fn.apply(this, newArgs);
    }
}
复制代码

curry 函数的第一个参数是要动态建立柯里化的函数,余下的参数存储在 args 变量中。

执行 curry 函数返回的函数接收新的参数与 args 变量存储的参数合并,并把合并的参数传入给柯里化了的函数。

function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);
multi(2,3,4);
复制代码

结果:

image

虽然获得的结果是同样的,可是很容易发现存在问题,就是代码相对于以前的闭包实现方式较复杂,并且执行方式也不是题目要求的那样 multi(2)(3)(4)。那么下面就来改进这版代码。

改进版

就题目而言,是须要执行三次函数调用,那么针对柯里化后的函数,若是传入的参数没有 3 个的话,就继续执行 curry 函数接收参数,若是参数达到 3 个,就执行柯里化了的函数。

function curry(fn, args) {
    var length = fn.length;
    var args = args || [];
    return function(){
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if(newArgs.length < length){
            return curry.call(this,fn,newArgs);
        }else{
            return fn.apply(this,newArgs);
        }
    }
}
function multiFn(a, b, c) {
    return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);
复制代码

image

能够看到,经过改进版的柯里化函数,已经将题目定的实现方式扩展到好几种了。这种实现方案的代码扩展性就比较强了,可是仍是有点不足,就是必须事先知道求值的参数个数,那能不能让代码更灵活点,达到随意传参的效果,例如: multi(2)(3)(4),multi(5)(6)(7)(8)(9) 这样的。

优化版

function multi() {
    var args = Array.prototype.slice.call(arguments);
	var fn = function() {
		var newArgs = args.concat(Array.prototype.slice.call(arguments));
        return multi.apply(this, newArgs);
    }
    fn.toString = function() {
        return args.reduce(function(a, b) {
            return a * b;
        })
    }
    return fn;
}
复制代码

image

这样的解决方案就能够灵活的使用了。不足的是返回值是 Function 类型。

image

总结

  • 就题目自己而言,是存在多种实现方式的,只要理解并充分利用闭包的强大。
  • 可能在实际应用场景中,不多使用函数柯里化的解决方案,可是了解认识函数柯里化对自身的提高仍是有帮助的。
  • 理解闭包和函数柯里化以后,若是在面试中遇到相似的题型,应该就能够迎刃而解了。

后记

本着学习和总结的态度写的技术输出,文中有任何错误和问题,请你们指出。更多的技术输出能够查看个人 github博客

整理了一些前端的学习资源,但愿可以帮助到有须要的人,地址: 学习资源汇总

参考

相关文章
相关标签/搜索