高阶函数

173 阅读6分钟

原文链接

juejin.cn/post/689288…

高阶函数

高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入;
  • 输出一个函数。

组合函数

  • 组合函数就是将两个或两个以上的函数组合生成一个新函数的过程;

组合函数的作用

在项目开发过程中,为了实现函数的复用,我们通常会尽量保证函数的职责单一,在拥有单一功能函数的基础上,我们就可以自由的对函数进行组合,来实现特定的功能:
  1. 实现几个单一函数
function trim(input) {
    return input && typeof input === 'string' ? input.trim() : input;
}

function lowerCase(input) {
    return input && typeof input === "string" ? input.toLowerCase() : input;
}

function split(input, delimiter = ",") {
    return typeof input === "string" ? input.split(delimiter) : input;
}
  1. 实现组合函数
function compose(...funcs) {
    return function(x){
        // 使用Array.prototype.reduce方法实现组合函数的调度,对应得执行顺序是从左到右
        //如果你想从右往左执行的话,可以使用Array.prototype.reduceRight方法来实现
        return funcs.reduce(function(arg, fn){
            return fn(arg)
        }, x)
    }
}
  1. 调用组合函数
const trimLowercaseAndSplit = compose(trim, lowerCase, split);
let result = trimLowercaseAndSplit(" a,B,c")
console.log(result)  // [ 'a', ' b', 'c' ]

柯里化(Currying)

  • 柯里化可以把一个带有多个参数的函数转化为一系列的嵌套函数。
  • 当柯里化后的函数接收到足够的参数后,就会开始执行原函数。而如果接收到的参数不足的话,就返回一个新的函数,用来接收余下新的函数。
  • 在数学和理论计算机科学中的柯里化函数,一次只能传递一个参数,而对于JavaScript语言来说,在实际应用中的柯里化函数,可以传递一个或多个参数。
const add = function(a) {
    return function(b) {
        return a + b
    }
}

const result = add(1)(2)
console.log(result)

柯里化的作用:

  • 参数复用
  1. 实现一个多参数函数
function buildUri(scheme, domain, path){
    return `${scheme}://${domain}/${path}`
}
  1. 调用lodash的curry函数,将多参数函数转化为嵌套函数
const _ = require("lodash")
let buildUriCurry = _.curry(buildUri)
  1. 调用转化后的嵌套函数实现参数复用
let commonUrl = buildUriCurry('http','localhost')
let indexUrl = commonUrl('index.html')
let listUrl = commonUrl('list.html')
  • 延迟计算/运行
  1. 实现一个多参数函数
function add(a, b) {
    return a + b;
}
  1. 调用lodash的curry函数,将多参数函数转化为嵌套函数
const _ = require("lodash")
const addCurry = _.curry(add)
  1. 传递第一个参数
const addNext = addCurry(1)
  1. 延迟传递第2个参数,实现延迟计算
setTimeout(function(){
    let result = addNext(2)
    console.log(result)
}, 1000)

柯里化的实现

// 1. 柯里化函数接收一个函数作为参数,这个参数即是原函数
// 2. 柯里化函数返回一个函数,即柯里化后的嵌套函数
function curry(func) {
    // 3. 转化后的嵌套函数可以接收1个或多个参数
    return function curried(...args) {
        // 4. 判断传递的参数个数是否大于等于原函数的形参个数
        // 通过func.length获取函数形参的个数
        if(args.length >= func.length){
            // 6. 传递的参数个数满额后,执行原函数
            return func.apply(this, args)
        }else{
            return function(...args2){
                // 5. 传递的参数个数不足原函数的形参个数,则返回一个函数继续接受参数,并将新的参数与之前的参数合并
                // 递归调用,直至传递的参数个数大于等于原函数的形参个数
                return curried.apply(this, args.concat(args2))
            }
        }
    }
}

偏函数

  • 元: 函数参数的个数
  • 一元函数: 含有一个参数的函数
  • 在计算机科学中,偏函数应用(Partrial Application)是指固定一个函数的某些参数,然后产生另外一个更小元的函数。

偏函数应用(Partial Application)很容易与函数柯里化混淆,它们之间的区别是:

  • 偏函数应用是固定一个函数的一个或多个参数,并返回一个可以接收剩余参数的函数;
  • 柯里化是将函数转化为可以嵌套的一元函数,也就是每个函数只接受一个参数。

实现一个偏函数

function partial(fn){
    // 1. 获取除了第一个参数外的其他参数
    var args = [].slice.call(arguments, 1)
    console.log(args) //[ 'http', 'localhost' ]
    // 2. 返回一个接收其它参数的函数
    return function(){
        // 3. 合并固定参数和新的参数
        args = [].concat.apply(args, [].slice.apply(arguments))
        console.log(args) //[ 'http', 'localhost', 'index.html' ]
        // 4. 执行原函数, 并返回结果
        return fn.apply(this, args) //http://localhost/index.html
    }
}

var url = partial(buildUri, 'http', 'localhost')('index.html')
console.log(url)

惰性函数

由于不同的浏览器之间存在一些兼容性问题,这导致了我们在使用一些 Web API 时,需要解析判断,比如:

function addHandler(element, type, handler) {
    if(element.addEventListener) {
        element.addEventListener(type, handler, false)
    } else if(element.attachEvent) {
        element.attachEvent('on' + type, handler)
    } else {
        element["on" + type] = handler
    }
}

在上面代码中,我们实现了不同浏览器 添加事件监听 的处理。代码实现起来也很简单,但存在一个问题,即每次调用的时候都需要进行判断,很明显这是不合理的。对于上述这个问题,我们可以通过惰性载入函数来解决。

所谓惰性载入即是当第1次根据条件执行函数后,在第2次调用函数时,就不再检查条件, 直接执行函数。

要实现这个功能,我们可以在第1次条件判断的时候,在满足判断条件的分支中覆盖掉所调用的函数,具体的实现方式如下所示:

function addHandler(element, type, handler) {
    if(element.addEventListener){
        addHandler = function(element, type, handler){
            element.addEventListener(type, handler, false)
        }
    }else if (element.attachEvent){
        addHandler = function(element, type, handler){
            element.attachEvent("on" + type, handler)
        }
    }else{
        addHandler = function(element, type, handler){
            element["on" + type] = handler
        }
    }
    return addHandler(element, type, handler)
}

除了使用以上的方式,我们也可以利用自执行函数来实现惰性载入:

通过自执行函数,在代码加载阶段就会执行一次条件判断,然后在对应得条件分支中返回一个新的函数,来实现对应得处理逻辑。

const addHandler = (function(){
    if(document.addEventListener) {
        return function(element, type, handler){
            element.addEventListener(type, handler, false)
        }
    }else if (document.attachEvent){
        return function(element, type, handler){
            element.attachEvent('on' + type, handler)
        }
    }else{
        return function(element, type, handler){
            element["on" + type] = handler
        }
    }
})()

缓存函数

缓存函数是将函数的计算结果缓存起来,当下次以同样的参数调用该函数时,直接返回已缓存的结果,而无需再次执行函数。这是一种常见的以空间换时间的性能优化手段。

要实现缓存函数的功能,我们可以把经过序列化的参数作为key,在把第一次调用后的结果作为value存储到对象中。在每次执行函数调用前,都先判断缓存中是否存在有对应得key,如果有的话,直接返回该key对应得值。

function memorize(fn) {
    const cache = Object.create(null) //存储缓存数据的对象
    return function(...args) {
        const _args = JSON.stringify(args)
        console.log(cache)
        return cache[_args] || (cache[_args] = fn.apply(fn, args))
    }
}

let add = (a, b) => {
    return a + b
}

const addCache = memorize(add)
console.log(addCache(1, 2))
console.log(addCache(1, 2))