深入详解函数的柯里化

深入详解函数的柯里化

一、补充知识点之函数的隐式转换

JavaScript作为一种弱类型语言,它的隐式转换是非常灵活有趣的。当我们没有深入了解隐式转换的时候可能会对一些运算的结果会感动困惑,比如4 + true = 5。当然,如果对隐式转换了解足够深刻,肯定是能够很大程度上提高对js的使用能力。只是我没有打算将所有的隐式转换规则分享给大家,这里暂时只分享一下,函数在隐式转换中的一些规则。

来一个简单的思考题。

1

2

3

4

5

function fn() {

return 20;

}

console.log(fn + 10); // 输出结果是多少?

稍微修改一下,再想想输出结果会是什么?

1

2

3

4

5

6

7

8

9

function fn() {

return 20;

}

fn.toString = function() {

return 10;

}

console.log(fn + 10);  // 输出结果是多少?

还可以继续修改一下。

1

2

3

4

5

6

7

8

9

10

11

12

13

function fn() {

return 20;

}

fn.toString = function() {

return 10;

}

fn.valueOf = function() {

return 5;

}

console.log(fn + 10); // 输出结果是多少?

1

2

3

4

5

6

7

8

// 输出结果分别为

function fn() {

return 20;

}10

20

15

当使用console.log,或者进行运算时,隐式转换就可能会发生。从上面三个例子中我们可以得出一些关于函数隐式转换的结论。

当我们没有重新定义toString与valueOf时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。而当我们主动定义了toString/vauleOf方法时,那么隐式转换的返回结果则由我们自己控制了。其中valueOf的优先级会toString高一点。

因此上面例子的结论就很容易理解了。建议大家动手尝试一下。

二、补充知识点之利用call/apply封数组的map方法

map(): 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。

通俗来说,就是遍历数组的每一项元素,并且在map的第一个参数(回调函数)中进行运算处理后返回计算结果。返回一个由所有计算结果组成的新数组。

1

2

3

4

5

6

7

8

9

10

// 回调函数中有三个参数

// 第一个参数表示newArr的每一项,第二个参数表示该项在数组中的索引值

// 第三个表示数组本身

// 除此之外,回调函数中的this,当map不存在第二参数时,this指向丢失,当存在第二个参数时,指向改参数所设定的对象

var newArr = [1, 2, 3, 4].map(function(item, i, arr) {

console.log(item, i, arr, this);  // 可运行试试看

return item + 1;  // 每一项加1

}, { a: 1 })

console.log(newArr); // [2, 3, 4, 5]

在上面例子的注释中详细阐述了map方法的细节。现在要面临一个难题,就是如何封装map。

可以先想想for循环。我们可以使用for循环来实现一个map,但是在封装的时候,我们会考虑一些问题。我们在使用for循环的时候,一个循环过程确实很好封装,但是我们在for循环里面要对每一项做的事情却很难用一个固定的东西去把它封装起来。因为每一个场景,for循环里对数据的处理肯定都是不一样的。

于是大家就想了一个很好的办法,将这些不一样的操作单独用一个函数来处理,让这个函数成为map方法的第一个参数,具体这个回调函数中会是什么样的操作,则由我们自己在使用时决定。因此,根据这个思路的封装实现如下。

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

Array.prototype._map = function(fn, context) {

var temp = [];

if(typeof fn == ‘function‘) {

var k = 0;

var len = this.length;

// 封装for循环过程

for(; k < len; k++) {

// 将每一项的运算操作丢进fn里,利用call方法指定fn的this指向与具体参数

temp.push(fn.call(context, this[k], k, this))

}

} else {

console.error(‘TypeError: ‘+ fn +‘ is not a function.‘);

}

// 返回每一项运算结果组成的新数组

return temp;

}

var newArr = [1, 2, 3, 4]._map(function(item) {

return item + 1;

})

// [2, 3, 4, 5]

在上面的封装中,我首先定义了一个空的temp数组,该数组用来存储最终的返回结果。在for循环中,每循环一次,就执行一次参数fn函数,fn的参数则使用call方法传入。

在理解了map的封装过程之后,我们就能够明白为什么我们在使用map时,总是期望能够在第一个回调函数中有一个返回值了。在eslint的规则中,如果我们在使用map时没有设置一个返回值,就会被判定为错误。

ok,明白了函数的隐式转换规则与call/apply在这种场景的使用方式,我们就可以尝试通过简单的例子来了解一下柯里化了。

三、由浅入深的柯里化

在前端面试中有一个关于柯里化的面试题,流传甚广。

实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15

很明显,计算结果正是所有参数的和,add方法每运行一次,肯定返回了一个同样的函数,继续计算剩下的参数。

我们可以从最简单的例子一步一步寻找解决方案。

当我们只调用两次时,可以这样封装。

1

2

3

4

5

6

7

function add(a) {

return function(b) {

return a + b;

}

}

console.log(add(1)(2));  // 3

如果只调用三次:

1

2

3

4

5

6

7

8

9

function add(a) {

return function(b) {

return function (c) {

return a + b + c;

}

}

}

console.log(add(1)(2)(3)); // 6

上面的封装看上去跟我们想要的结果有点类似,但是参数的使用被限制得很死,因此并不是我们想要的最终结果,我们需要通用的封装。应该怎么办?总结一下上面2个例子,其实我们是利用闭包的特性,将所有的参数,集中到最后返回的函数里进行计算并返回结果。因此我们在封装时,主要的目的,就是将参数集中起来计算。

来看看具体实现。

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

27

function add() {

// 第一次执行时,定义一个数组专门用来存储所有的参数

var _args = [].slice.call(arguments);

// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值

var adder = function () {

var _adder = function() {

[].push.apply(_args, [].slice.call(arguments));

return _adder;

};

// 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回

_adder.toString = function () {

return _args.reduce(function (a, b) {

return a + b;

});

}

return _adder;

}

return adder.apply(null, [].slice.call(arguments));

}

// 输出结果,可自由组合的参数

console.log(add(1, 2, 3, 4, 5));  // 15

console.log(add(1, 2, 3, 4)(5));  // 15

console.log(add(1)(2)(3)(4)(5));  // 15

上面的实现,利用闭包的特性,主要目的是想通过一些巧妙的方法将所有的参数收集在一个数组里,并在最终隐式转换时将数组里的所有项加起来。因此我们在调用add方法的时候,参数就显得非常灵活。当然,也就很轻松的满足了我们的需求。

那么读懂了上面的demo,然后我们再来看看柯里化的定义,相信大家就会更加容易理解了。

柯里化(英语:Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。

  • 接收单一参数,因为要携带不少信息,因此常常以回调函数的理由来解决。
  • 将部分参数通过回调函数等方式传入函数中
  • 返回一个新函数,用于处理所有的想要传入的参数

在上面的例子中,我们可以将add(1, 2, 3, 4)转换为add(1)(2)(3)(4)。这就是部分求值。每次传入的参数都只是我们想要传入的所有参数中的一部分。当然实际应用中,并不会常常这么复杂的去处理参数,很多时候也仅仅只是分成两部分而已。

咱们再来一起思考一个与柯里化相关的问题。

假如有一个计算要求,需要我们将数组里面的每一项用我们自己想要的字符给连起来。我们应该怎么做?想到使用join方法,就很简单。

1

2

3

4

5

6

7

8

9

10

11

var arr = [1, 2, 3, 4, 5];

// 实际开发中并不建议直接给Array扩展新的方法

// 只是用这种方式演示能够更加清晰一点

Array.prototype.merge = function(chars) {

return this.join(chars);

}

var string = arr.merge(‘-‘)

console.log(string);  // 1-2-3-4-5

增加难度,将每一项加一个数后再连起来。那么这里就需要map来帮助我们对每一项进行特殊的运算处理,生成新的数组然后用字符连接起来了。实现如下:

1

2

3

4

5

6

7

8

9

10

11

var arr = [1, 2, 3, 4, 5];

Array.prototype.merge = function(chars, number) {

return this.map(function(item) {

return item + number;

}).join(chars);

}

var string = arr.merge(‘-‘, 1);

console.log(string); // 2-3-4-5-6

但是如果我们又想要让数组每一项都减去一个数组之后再连起来呢?当然和上面的加法操作一样的实现。

1

2

3

4

5

6

7

8

9

10

11

var arr = [1, 2, 3, 4, 5];

Array.prototype.merge = function(chars, number) {

return this.map(function(item) {

return item - number;

}).join(chars);

}

var string = arr.merge(‘~‘, 1);

console.log(string); // 0~1~2~3~4

机智的小伙伴肯定发现困惑所在了。我们期望封装一个函数,能同时处理不同的运算过程,但是我们并不能使用一个固定的套路将对每一项的操作都封装起来。于是问题就变成了和封装map的时候所面临的问题一样了。我们可以借助柯里化来搞定。

与map封装同样的道理,既然我们事先并不确定我们将要对每一项数据进行怎么样的处理,我只是知道我们需要将他们处理之后然后用字符连起来,所以不妨将处理内容保存在一个函数里。而仅仅固定封装连起来的这一部分需求。

于是我们就有了以下的封装。

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

27

28

29

30

31

32

33

34

35

36

// 封装很简单,一句话搞定

Array.prototype.merge = function(fn, chars) {

return this.map(fn).join(chars);

}

var arr = [1, 2, 3, 4];

// 难点在于,在实际使用的时候,操作怎么来定义,利用闭包保存于传递num参数

var add = function(num) {

return function(item) {

return item + num;

}

}

var red = function(num) {

return function(item) {

return item - num;

}

}

// 每一项加2后合并

var res1 = arr.merge(add(2), ‘-‘);

// 每一项减2后合并

var res2 = arr.merge(red(1), ‘-‘);

// 也可以直接使用回调函数,每一项乘2后合并

var res3 = arr.merge((function(num) {

return function(item) {

return item * num

}

})(2), ‘-‘)

console.log(res1); // 3-4-5-6

console.log(res2); // 0-1-2-3

console.log(res3); // 2-4-6-8

大家能从上面的例子,发现柯里化的特征吗?

四、柯里化通用式

通用的柯里化写法其实比我们上边封装的add方法要简单许多。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

var currying = function(fn) {

var args = [].slice.call(arguments, 1);

return function() {

// 主要还是收集所有需要的参数到一个数组中,便于统一计算

var _args = args.concat([].slice.call(arguments));

return fn.apply(null, _args);

}

}

var sum = currying(function() {

var args = [].slice.call(arguments);

return args.reduce(function(a, b) {

return a + b;

})

}, 10)

console.log(sum(20, 10));  // 40

console.log(sum(10, 5));   // 25

五、柯里化与bind

1

2

3

4

5

6

7

8

Object.prototype.bind = function(context) {

var _this = this;

var args = [].prototype.slice.call(arguments, 1);

return function() {

return _this.apply(context, args)

}

}

这个例子利用call与apply的灵活运用,实现了bind的功能。

在前面的几个例子中,我们可以总结一下柯里化的特点:

  • 接收单一参数,将更多的参数通过回调函数来搞定?
  • 返回一个新函数,用于处理所有的想要传入的参数;
  • 需要利用call/apply与arguments对象收集参数;
  • 返回的这个函数正是用来处理收集起来的参数。

希望大家读完之后都能够大概明白柯里化的概念,如果想要熟练使用它,就需要我们掌握更多的实际经验才行。

时间: 2024-10-08 21:12:12

深入详解函数的柯里化的相关文章

【Scala】高阶函数和柯里化

高阶函数 在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数: - 接受一个或多个函数作为输入 - 输出一个函数 在数学中它们也叫做算子(运算符)或泛函.微积分中的导数就是常见的例子,因为它映射一个函数到另一个函数. 高阶函数的例子 假设有一个函数对给定两个数区间中的所有整数求和: def sumInts(a: Int, b: Int): Int = if(a > b) 0 else a + sumInts(a + 1, b) 如果现在要求连续整数的平方和: def square(x:

浅析 JavaScript 中的 函数 currying 柯里化

原文:浅析 JavaScript 中的 函数 currying 柯里化 何为Curry化/柯里化? curry化来源与数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名). 柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果.因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程. 柯里化一个求和函数 按照分步求值,我们看一个

js高阶函数--判断数据类型、函数胡柯里化;

一.判断数据类型: 常见的判断有typeof.instanceof. constructor. prototype,先来看typeof: var a = "hello world"; var b = 10; var c = [1, 3, 5]; var d = new Date(); var e = function() {}; console.log(typeof a); // string console.log(typeof b); // number console.log(t

Effective JavaScript Item 26 使用bind来进行函数的柯里化(Curry)

本系列作为Effective JavaScript的读书笔记. 在上一个Item中介绍了bind的一种用法:用来绑定this对象.但是实际上,bind含有另一种用法,就是帮助函数进行柯里化.关于柯里化,这里有一份百科可以参考: http://zh.wikipedia.org/wiki/%E6%9F%AF%E9%87%8C%E5%8C%96 但是实际上,关于柯里化只需要记住一点就够了:柯里化是把接受多个参数的函数变换成接受一个单一参数(通常是最初函数的第一个参数,但是并无限制)的函数,并且返回这个

关于arguments对象以及函数的柯里化;

1.arguments对象 Arguments是个类似数组但不是数组的对象,说他类似数组是因为其具备数组相同的访问性质及方式,能够由arguments[n]来访问对应的单个参数的值,并拥有数组长度属性length.还有就是arguments对象存储的是实际 传递给函数的参数,而不局限于函数声明所定义的参数列表,而且不能显式创建 arguments 对象.在JavaScript中函数不介意传递进来多少参数,也不会因为参数不统一而错误.实际上,函数体内可以通过 arguments 对象来接收传递进来

[Effective JavaScript 笔记]第26条:使用bind方法实现函数的柯里化

bind方法的作用,除了有绑定函数到对象外,我们来看看bind方法的一些其它应用. 简单示例 例子:假设有一个装配URL字符串的简单函数.代码如下 function simpleURL(protocol,domain,path){ return protocol+'://'+domain+'/'+path; } 要将特定站点的路径字符串构建为绝对路径URL.可以使用ES5中数组的map方法来实现.如下 var paths=['wengxuesong/','wengxuesong/p/556048

理解运用JS的闭包、高阶函数、柯里化

一.闭包 1. 闭包的概念 闭包与执行上下文.环境.作用域息息相关 执行上下文 执行上下文是用于跟踪运行时代码求值的一个规范设备,从逻辑上讲,执行上下文是用执行上下文栈(栈.调用栈)来维护的. 代码有几种类型:全局代码.函数代码.eval代码和模块代码:每种代码都是在其执行上下文中求值. 当函数被调用时,就创建了一个新的执行上下文,并被压到栈中 - 此时,它变成一个活动的执行上下文.当函数返回时,此上下文被从栈中弹出 function recursive(flag) { // Exit cond

Scala 系列(十)—— 函数 &amp; 闭包 &amp; 柯里化

一.函数 1.1 函数与方法 Scala 中函数与方法的区别非常小,如果函数作为某个对象的成员,这样的函数被称为方法,否则就是一个正常的函数. // 定义方法 def multi1(x:Int) = {x * x} // 定义函数 val multi2 = (x: Int) => {x * x} println(multi1(3)) //输出 9 println(multi2(3)) //输出 9 也可以使用 def 定义函数: def multi3 = (x: Int) => {x * x}

javascript中的函数currying(柯里化) 的理解

首先简要说一下什么是curry化 :它是一种通过把多个参数填充到函数体中,实现将函数转换成一个新的经过简化的(使之接受的参数更少)函数技术.(摘自:精通JavaScrtpt图灵计算机科学丛书-21页) //写一个简单点的 function curry(fn/*function*/ , scope/*object?*/){ //取到除必须参数的其他参数,也就是第二个以后的所有参数,做一个缓存 var args = [].slice.call(arguments , 2); console.log(