百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

JavaScript 分步实现柯里化函数

toyiye 2024-06-21 11:58 10 浏览 0 评论


简介

首先,柯里化(Currying)是什么呢?

简单说,假如有一个函数,接受多个参数,那么一般来说就是一次性传入所有参数并执行。而对其执行柯里化后,就变成了可以分多次接收参数


实现

阶段1

现在有一个加法函数:

function add(x, y, z) {  
  return x + y + z
}

调用方式是 add(1, 2, 3)。

如果执行柯里化,变成了 curriedAdd(),从效果来说,大致就是变成 curriedAdd(1)(2)(3) 这样子。

现在先不看怎么对原函数执行柯里化,而是根据这个调用方式重新写一个函数。代码可能是这样的:

function curriedAdd1(x) {  
  return function (y) {    
    return function (z) {      
      return x + y + z   
    }  
  }
}

阶段2

假如现在想要升级一下,不止可以接受三个参数。可以使用 arguments,或者使用展开运算符来处理传入的参数。

但是有一个衍生的问题。因为之前每次只能传递一个,总共只能传递三个,才保证了调用三次之后参数个数刚好足够,函数才能执行。

既然我们打算修改为可以接受任意个数的参数,那么就要规定一个终点。比如说,可以规定为当不再传入参数的时候,就执行函数。

下面是使用 arguments 的实现。

function getCurriedAdd() {  
  // 在外部维护一个数组保存传递的变量  
  let args_arr = []  
  // 返回一个闭包  
  let closure = function () {   
    // 本次调用传入的参数    
    let args = Array.prototype.slice.call(arguments)    
    // 如果传进了新的参数   
    if (args.length > 0) {      
      // 保存参数     
      args_arr = args_arr.concat(args)      
      // 再次返回闭包,等待下次调用      
      // 也可以 return arguments.callee      
      return closure  
    
    }    // 没有传递参数,执行累加    return args_arr.reduce((total, current) => total + current)  }  return closure}curriedAdd = getCurriedAdd()curriedAdd(1)(2)(3)(4)()复制代码

阶段3

这时可以发现,上面的整个函数里,与函数具体功能(在这里就是执行加法)有关的,就只是当没有传递参数时的部分,其他部分都是在实现怎样多次接收参数。

那么,只要让 getCurriedAdd 接受一个函数作为参数,把没有传递参数时的那一行代码替换一下,就可以实现一个通用的柯里化函数了。

把上面的修改一下,实现一个通用柯里化函数,并把一个阶乘函数柯里化:

function currying(fn) {  
  let args_arr = []  
  let closure =  function (...args) {    
    if (args.length > 0) {      
      args_arr = args_arr.concat(args)      
      return closure   
    }    
    // 没有新的参数,执行函数    
    return fn(...args_arr) 
  }  
  return closure
}

function multiply(...args) { 
  return args.reduce((total, current) => total * current)
}

curriedMultiply = currying(multiply)
console.log(curriedMultiply(2)(3, 4)()

阶段4

上面的代码里,对于函数执行时机的判断,是根据是否有参数传入。但是更多时候,更合理的依据是原函数可以接受的参数的总数。

函数名的 length 属性就是该函数接受的参数个数。比如:

function test1(a, b) {}
function test2(...args){}
console.log(test1.length) // 2
console.log(test2.length) // 0

改写一下:

function currying(fn) {  
  let args_arr = [], 
      max_length = fn.length  let closure = function (...args) {    // 先把参数加进去    args_arr = args_arr.concat(args)    // 如果参数没满,返回闭包等待下一次调用    if (args_arr.length < max_length) return closure    // 传递完成,执行    return fn(...args_arr)  }  return closure}function add(x, y, z) {  return x + y + z}curriedAdd = currying(add)console.log(curriedAdd(1, 2)(3))复制代码

Lodash 中的柯里化

让我们先看一下 lodash.js 的文档,看看一个真正的 curry 方法到底是做什么的。

var abc = function(a, b, c) { return [a, b, c];};
var curried = _.curry(abc);
curried(1)(2)(3); // => [1, 2, 3]
curried(1, 2)(3); // => [1, 2, 3]
curried(1, 2, 3); // => [1, 2, 3]
// Curried with placeholders.
curried(1)(_, 3)(2); // => [1, 2, 3]

在我理解看来,curry 能够让我们:

  1. 在多个函数调用中逐步收集参数,不用在一个函数调用中一次收集。
  2. 当收集到足够的参数时,返回函数执行结果。

为了更好的理解它,我在网上找了多个实现示例。然而,我希望是有一个非常简单的教程从一个基本的例子开始,就像下面这个一样,而不是直接从最终的实现开始。

var fn = function() {
  console.log(arguments);
  return fn.bind(null, ...arguments);
  // 如果没有es6的话我们可以这样写:
  // return Function.prototype.bind.apply(fn, [null].concat(
  //   Array.prototype.slice.call(arguments)
  // ));
}

fb = fn(1); //[1]
fb = fb(2); //[1, 2]
fb = fb(3); //[1, 2, 3]
fb = fb(4); //[1, 2, 3, 4]

理解 fn 函数是所有的起点。基本上,这个函数的作用就是一个“参数收集器”。每次调用该函数时,它都会返回一个自身的绑定函数(fb),并且将该函数提供的“参数”绑定到返回函数上。该“参数”将位于之后调用返回的绑定函数时提供的任何参数之前。因此,每个调用中传的参数将被逐渐收集到一个数组当中。

当然,就像 curry 函数一样,我们不必一直收集下去。现在我们可以先写死一个终止点。

var numOfRequiredArguments = 5;
var fn = function() {
  if (arguments.length < numOfRequiredArguments) {
    return fn.bind(null, ...arguments);
  } else {
    console.log('we already collect 5 arguments: ', [...arguments]);
    return null;
  }
}

为了让它表现得和 curry 方法一样,需要解决两个问题:

  1. 我们希望将收集到的参数传递给需要它们的目标函数,而不是通过将它们传递给 console.log 在最后打印出来。
  2. 变量 numOfRequiredArguments 不应该是写死的,它应该是目标函数所期望的参数个数。

幸运的是,JavaScript函数确实带有一个名为 “length” 的属性,它指定了函数所期望的参数个数。因此,我们就可以使用这个属性来确定所需要的参数个数,而不用再写死了。那么第二个问题就解决了。

那第一个问题呢:保持对目标函数的引用?

网上有几个例子可以解决这个问题。它们之间虽然略有不同,但是有着相同的思路:除去存储参数以外,我们还需要在某处存储对于目标函数的引用。

这里我把它们分为两种不同的方法,它们之间或多或少都有相似之处,理解它们能够帮助我们更好地理解背后的逻辑。顺便说一句,这里我将这个函数叫做 magician,以代替 curry。

方法1

function magician(targetfn) {
  var numOfArgs = targetfn.length;
  return function fn() {
    if (arguments.length < numOfArgs) {
      return fn.bind(null, ...arguments);
    } else {
      return targetfn.apply(null, arguments);
    }
  }
}

magician 函数的作用是:它接收目标函数作为参数,然后返回‘参数收集器’函数,与上例中 fn 函数作用相同。唯一的不同点在于,当收集的参数数量与目标函数所必需的参数数量相等时,它将把收集到的参数通过 apply 方法给到该目标函数,并返回计算的结果。这个方法通过将其存储在 magician 创建的闭包当中来解决第一个问题(引用目标函数)。

方法2

这个方法更进一步,由于参数收集器函数只是一个普通函数,那为什么不使用 magician 函数本身作为参数收集器呢?

function magician (targetfn) {
  var numOfArgs = targetfn.length;
  if (arguments.length - 1 < numOfArgs) {
    return magician.bind(null, ...arguments);
  } else {
    return targetfn.apply(null, Array.prototype.slice.call(arguments, 1));
  }
}

注意方法2中的一个不同。因为 magician 接收目标函数作为它的第一个参数,因此收集到的参数将始终包含该函数作为 arguments[0]。这就导致,我们在检查有效参数的总数时,需要减去第一个参数。

顺便说一句,因为目标函数是递归地传递给 magician 函数的,所以我们可以通过传入第一个参数显式地引用目标函数,以代替使用闭包来存储目标函数的引用。

正如你所见,Eric Elliott 上面使用到的 “curry” 函数和方法1功能相似,但实际上它是一个偏函数(这又是另外一说了)。

const curry = fn => (…args) => fn.bind(null, …args);

上面是一个 curry 函数,它返回“参数收集器”,该收集器只收集一次参数,并返回绑定的目标函数。

更进一步

上面的‘magician’函数仍然没有lodash.js中的‘curry’函数那样神奇。lodash的curry允许使用‘_’作为输入参数的占位符。

curried(1)(_, 3)(2); // => [1, 2, 3], 注意占位符 '_'

为了实现占位符功能,有一个隐含的需求:我们需要知道哪些参数被预设给了绑定函数,以及哪些是在调用函数时显示提供的附加参数(这里我们称之为added参数)。

这个功能可以通过创建另外一个闭包来完成:

function fn2() {
  var preset = Array.prototype.slice.call(arguments);
  /*
    原先是这样:
    return fn.bind(null, ...arguments);
  */
  return function helper() {
    var added = Array.prototype.slice.call(arguments);
    return fn2.apply(null, [...preset, ...added]); //简单起见,使用es6
  }
}

上面的 fn2 几乎和 fn 一样,功能就像‘参数收集器’一样。然而,fn2 不是直接返回绑定函数,而是返回一个中间辅助函数 helper。helper 函数是未绑定的,因此它可以用来分离预设的参数和后来提供的参数。

当然,我们需要在组合时进行一些修改,而不是通过 [...preset, ...added] 将预设的参数和后来提供的参数合并起来。我们需要在preset参数中找到占位符的位置,并用有效的added参数替换它。我没有看lodash是如何实现它的,但下面是一个完成类似功能的简单实现。

// 定义占位符
var _ = '_';

function magician3 (targetfn, ...preset) {
  var numOfArgs = targetfn.length;
  var nextPos = 0; // 下一个有效输入位置的索引,可以是'_',也可以是preset的结尾

  // 查看是否有足够的有效参数
  if (preset.filter(arg=> arg !== _).length === numOfArgs) {
    return targetfn.apply(null, preset);
  } else {
    // 返回'helper'函数
    return function (...added) {
      // 循环并将added参数添加到preset参数
      while(added.length > 0) {
        var a = added.shift();
        // 获取下一个占位符的位置,可以是'_'也可以是preset的末尾
        while (preset[nextPos] !== _ && nextPos < preset.length) {
          nextPos++
        }
        // 更新preset
        preset[nextPos] = a;
        nextPos++;
      }
      // 绑定更新后的preset
      return magician3.call(null, targetfn, ...preset);
    }
  }
}

第15到24行是用于将added参数放入preset数组中正确位置的逻辑:无论是占位符或是preset的结尾。该位置被标记为 nextPos 并初始化为索引0。

现在,函数 magician3 几乎已经和lodash的curry函数功能相当了。


链接文章:

https://blog.csdn.net/weixin_34329187/article/details/91396617

https://www.jianshu.com/p/822c4bfeb8a9

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码