>웹 프론트엔드 >JS 튜토리얼 >JavaScript 함수 커링

JavaScript 함수 커링

小云云
小云云원래의
2017-12-06 16:11:271450검색

커링이란 무엇인가요?

컴퓨터 과학에서 Currying(영어: Currying)은 Currying 또는 Currying으로도 번역되며 여러 매개변수를 허용하는 함수를 단일 매개변수(원래 함수의 첫 번째 매개변수)를 허용하는 함수로 변환하는 것입니다. 나머지 매개변수를 받아들이고 결과를 반환하는 새 함수를 반환합니다. 이 기술은 Moses Schönfinkel과 Gottlob Frege가 발명했지만 논리학자 Haskell Gary의 이름을 따서 Christopher Strachey가 명명했습니다.

직관적으로 커링은 특정 매개변수를 수정하면 나머지 매개변수를 허용하는 함수를 얻게 된다고 주장합니다.
이론 컴퓨터 과학에서 커링은 단일 매개변수만 허용하는 람다 미적분과 같은 간단한 이론 모델에서 여러 매개변수가 있는 함수를 연구하는 방법을 제공합니다.
함수 커링의 이중성은 익명의 단일 매개변수 함수를 사용하여 다중 매개변수 함수를 구현하는 방법인 Uncurring입니다.

이해하기 쉽습니다

커링의 개념은 실제로 매우 간단합니다. 매개변수 중 일부만 함수에 전달하여 호출하고, 나머지 매개변수를 처리하는 함수를 반환하도록 합니다.

세 숫자의 합을 구하는 함수를 구현해야 하는 경우:

<span style="font-size: 16px;">function add(x, y, z) {<br>  return x + y + z;<br>}<br>console.log(add(1, 2, 3)); // 6<br></span>
<span style="font-size: 16px;">var add = function(x) {<br>  return function(y) {<br>    return function(z) {<br>      return x + y + z;<br>    }<br>  }<br>}<br><br>var addOne = add(1);<br>var addOneAndTwo = addOne(2);<br>var addOneAndTwoAndThree = addOneAndTwo(3);<br><br>console.log(addOneAndTwoAndThree);<br></span>

여기에서는 매개변수를 받아들이고 새 함수를 반환하는 add 함수를 정의합니다. add를 호출한 후 반환된 함수는 클로저를 통해 add의 첫 번째 매개변수를 기억합니다. 한꺼번에 호출하는 것은 약간 지루하지만 다행히도 특별한 카레 도우미 함수를 사용하면 이러한 함수를 더 쉽게 정의하고 호출할 수 있습니다.

ES6의 화살표 기능을 사용하면 위의 추가를 다음과 같이 구현할 수 있습니다.

<span style="font-size: 16px;">const add = x => y => z => x + y + z;<br></span>

화살표 기능을 사용하는 것이 훨씬 더 명확한 것 같습니다.

일부 기능?

이 함수를 살펴보겠습니다.

<span style="font-size: 16px;">function ajax(url, data, callback) {<br>  // ..<br>}<br></span>

여러 인터페이스에 HTTP 요청을 시작해야 하는 시나리오가 있습니다. 이를 수행하는 방법에는 두 가지가 있습니다.

  • ajax를 호출합니다. () 함수 이면 전역 URL 상수를 전달합니다.

  • 미리 설정된 URL 매개변수를 사용하여 함수 참조를 만듭니다.

다음으로 내부적으로 ajax() 요청을 시작하는 새로운 함수를 만듭니다. 또한 다른 두 실제 매개변수를 수신하기를 기다리는 동안 ajax()의 첫 번째 실제 매개변수를 수동으로 설정합니다. API 주소에 신경쓰세요.

첫 번째 접근 방식의 경우 다음 호출 메서드를 생성할 수 있습니다.

<span style="font-size: 16px;">function ajaxTest1(data, callback) {<br>  ajax('http://www.test.com/test1', data, callback);<br>}<br><br>function ajaxTest2(data, callback) {<br>  ajax('http://www.test.com/test2', data, callback);<br>}<br></span>

이 두 가지 유사한 기능의 경우 다음 패턴도 추출할 수 있습니다.

<span style="font-size: 16px;">function beginTest(callback) {<br>  ajaxTest1({<br>    data: GLOBAL_TEST_1,<br>  }, callback);<br>}<br></span>

이 패턴을 본 적이 있을 것 같습니다. 함수 호출 사이트에서 실제 매개변수를 형식 매개변수에 적용합니다. 보시다시피 처음에는 인수 중 일부만 적용하고(특히 이를 URL 매개변수에 적용) 나머지는 나중에 적용합니다.

위 개념은 부분 함수의 정의입니다. 부분 함수는 함수 매개변수 수를 줄이는 프로세스입니다. 여기서 매개변수 수는 전달될 것으로 예상되는 형식 매개변수 수를 나타냅니다. ajaxTest1()을 통해 원래 함수 ajax()의 매개변수 수를 3개에서 2개로 줄였습니다.

partial() 함수를 다음과 같이 정의합니다.

<span style="font-size: 16px;">function partial(fn, ...presetArgs) {<br>  return function partiallyApplied(...laterArgs) {<br>    return fn(...presetArgs, ...laterArgs);<br>  }<br>}<br></span>

partial() 함수는 fn 매개변수를 수신하여 실제 매개변수를 부분적으로 적용하는 함수를 나타냅니다. 그런 다음, fn 형식 매개변수 뒤의 PresetArgs 배열은 나중에 전달된 실제 매개변수를 수집하고 나중에 사용할 수 있도록 저장합니다.

LaterArgs 배열이 모든 실제 매개변수를 수집하는 새 내부 함수(명확성을 위해 부분적으로 Applied(..)로 이름 지정)를 생성하고 반환합니다.

화살표 함수를 사용하면 더 간결해집니다.

<span style="font-size: 16px;">var partial =<br>  (fn, ...presetArgs) =><br>    (...laterArgs) =><br>      fn(...presetArgs, ...laterArgs);<br></span>

이 부분 함수 모드를 사용하면 이전 코드를 재구성합니다.

<span style="font-size: 16px;">function ajax(url, data, callback) {<br>  // ..<br>}<br><br>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br>var ajaxTest2 = partial(ajax, 'http://www.test.com/test1');<br></span>

beginTest() 함수에 대해 다시 생각해 보면, Partial()을 사용합니다. 어떻게 리팩토링해야 할까요?

<span style="font-size: 16px;">function ajax(url, data, callback) {<br>  // ..<br>}<br><br>// 版本1<br>var beginTest = partial(ajax, 'http://www.test.com/test1', {<br>  data: GLOBAL_TEST_1,<br>});<br><br>// 版本2<br>var ajaxTest1 = partial(ajax, 'http://www.test.com/test1');<br>var beginTest = partial(ajaxTest1, {<br>  data: GLOBAL_TEST_1,<br>});<br></span>

한 번에 하나씩 전달하세요

위의 예에서 버전 1에 비해 버전 2의 장점을 확인하셨을 것입니다. 예, 커링은 다음과 같습니다. 여러 매개변수가 있는 함수를 변환하는 것은 하나의 함수를 작동하는 프로세스입니다. 한 번에. 함수가 호출될 때마다 하나의 인수만 허용하고 모든 인수가 전달될 때까지 함수를 반환합니다.

The process of converting a function that takes multiple arguments into a function that takes them one at a time.

Each time the function is called it only accepts one argument and returns a function that takes one argument until all arguments are passed.

假设我们已经创建了一个柯里化版本的ajax()函数curriedAjax():

<span style="font-size: 16px;">curriedAjax('http://www.test.com/test1')<br>  ({<br>    data: GLOBAL_TEST_1,<br>  })<br>  (function callback(data) {<br>    // dosomething<br>  });<br></span>

我们将三次调用分别拆解开来,这也许有助于我们理解整个过程:

<span style="font-size: 16px;">var ajaxTest1 = curriedAjax('http://www.test.com/test1');<br><br>var beginTest = ajaxTest1({<br>  data: GLOBAL_TEST_1,<br>});<br><br>var ajaxCallback = beginTest(function callback(data) {<br>  // dosomething<br>});<br></span>

实现柯里化

那么,我们如何来实现一个自动的柯里化的函数呢?

<span style="font-size: 16px;">var currying = function(fn) {<br>  var args = [];<br><br>  return function() {<br>    if (arguments.length === 0) {<br>      return fn.apply(this, args); // 没传参数时,调用这个函数<br>    } else {<br>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br>      return arguments.callee; // 返回这个函数的引用<br>    }<br>  }<br>}<br></span>

调用上述currying()函数:

<span style="font-size: 16px;">var cost = (function() {<br>  var money = 0;<br>  return function() {<br>    for (var i = 0; i < arguments.length; i++) {<br>      money += arguments[i];<br>    }<br>    return money;<br>  }<br>})();<br><br>var cost = currying(cost);<br><br>cost(100); // 传入了参数,不真正求值<br>cost(200); // 传入了参数,不真正求值<br>cost(300); // 传入了参数,不真正求值<br><br>console.log(cost()); // 求值并且输出600<br></span>

上述函数是我之前的JavaScript设计模式与开发实践读书笔记之闭包与高阶函数所写的currying版本,现在仔细思考后发现仍旧有一些问题。

我们在使用柯里化时,要注意同时为函数预传的参数的情况。

因此把上述柯里化函数更改如下:

<span style="font-size: 16px;">var currying = function(fn) {<br>  var args = Array.prototype.slice.call(arguments, 1);<br><br>  return function() {<br>    if (arguments.length === 0) {<br>      return fn.apply(this, args); // 没传参数时,调用这个函数<br>    } else {<br>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br>      return arguments.callee; // 返回这个函数的引用<br>    }<br>  }<br>}<br></span>

使用实例:

<span style="font-size: 16px;">var cost = (function() {<br>  var money = 0;<br>  return function() {<br>    for (var i = 0; i < arguments.length; i++) {<br>      money += arguments[i];<br>    }<br>    return money;<br>  }<br>})();<br><br>var cost = currying(cost, 100);<br>cost(200); // 传入了参数,不真正求值<br>cost(300); // 传入了参数,不真正求值<br><br>console.log(cost()); // 求值并且输出600<br></span>

你可能会觉得每次都要在最后调用一下不带参数的cost()函数比较麻烦,并且在cost()函数都要使用arguments参数不符合你的预期。我们知道函数都有一个length属性,表明函数期望接受的参数个数。因此我们可以充分利用预传参数的这个特点。

借鉴自mqyqingfeng:

<span style="font-size: 16px;">function sub_curry(fn) {<br>  var args = [].slice.call(arguments, 1);<br>  return function() {<br>    return fn.apply(this, args.concat([].slice.call(arguments)));<br>  };<br>}<br><br>function curry(fn, length) {<br><br>  length = length || fn.length;<br><br>  var slice = Array.prototype.slice;<br><br>  return function() {<br>    if (arguments.length < length) {<br>      var combined = [fn].concat(slice.call(arguments));<br>      return curry(sub_curry.apply(this, combined), length - arguments.length);<br>    } else {<br>      return fn.apply(this, arguments);<br>    }<br>  };<br>}<br></span>

在上述函数中,我们在currying的返回函数中,每次把arguments.length和fn.length作比较,一旦arguments.length达到了fn.length的数量,我们就去调用fn(return fn.apply(this, arguments);)

验证:

<span style="font-size: 16px;">var fn = curry(function(a, b, c) {<br>  return [a, b, c];<br>});<br><br>fn("a", "b", "c") // ["a", "b", "c"]<br>fn("a", "b")("c") // ["a", "b", "c"]<br>fn("a")("b")("c") // ["a", "b", "c"]<br>fn("a")("b", "c") // ["a", "b", "c"]<br></span>

bind方法的实现

使用柯里化,能够很方便地借用call()或者apply()实现bind()方法的polyfill。

<span style="font-size: 16px;">Function.prototype.bind = Function.prototype.bind || function(context) {<br>  var me = this;<br>  var args = Array.prototype.slice.call(arguments, 1);<br>  return function() {<br>    var innerArgs = Array.prototype.slice.call(arguments);<br>    var finalArgs = args.concat(innerArgs);<br>    return me.apply(contenxt, finalArgs);<br>  }<br>}<br></span>

上述函数有的问题在于不能兼容构造函数。我们通过判断this指向的对象的原型属性,来判断这个函数是否通过new作为构造函数调用,来使得上述bind方法兼容构造函数。

Function.prototype.bind() by MDN如下说到:

绑定函数适用于用new操作符 new 去构造一个由目标函数创建的新的实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。然而, 原先提供的那些参数仍然会被前置到构造函数调用的前面。

这是基于MVC的JavaScript Web富应用开发的bind()方法实现:

<span style="font-size: 16px;">Function.prototype.bind = function(oThis) {<br>  if (typeof this !== "function") {<br>    throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");<br>  }<br><br>  var aArgs = Array.prototype.slice.call(arguments, 1),<br>    fToBind = this,<br>    fNOP = function() {},<br>    fBound = function() {<br>      return fToBind.apply(<br>        this instanceof fNOP && oThis ? this : oThis || window,<br>        aArgs.concat(Array.prototype.slice.call(arguments))<br>      );<br>    };<br><br>  fNOP.prototype = this.prototype;<br>  fBound.prototype = new fNOP();<br><br>  return fBound;<br>};<br></span>

反柯里化(uncurrying)

可能遇到这种情况:拿到一个柯里化后的函数,却想要它柯里化之前的版本,这本质上就是想将类似f(1)(2)(3)的函数变回类似g(1,2,3)的函数。

下面是简单的uncurrying的实现方式:

<span style="font-size: 16px;">function uncurrying(fn) {<br>  return function(...args) {<br>    var ret = fn;<br><br>    for (let i = 0; i < args.length; i++) {<br>      ret = ret(args[i]); // 反复调用currying版本的函数<br>    }<br><br>    return ret; // 返回结果<br>  };<br>}<br></span>

注意,不要以为uncurrying后的函数和currying之前的函数一模一样,它们只是行为类似!

<span style="font-size: 16px;">var currying = function(fn) {<br>  var args = Array.prototype.slice.call(arguments, 1);<br><br>  return function() {<br>    if (arguments.length === 0) {<br>      return fn.apply(this, args); // 没传参数时,调用这个函数<br>    } else {<br>      [].push.apply(args, arguments); // 传入了参数,把参数保存下来<br>      return arguments.callee; // 返回这个函数的引用<br>    }<br>  }<br>}<br><br>function uncurrying(fn) {<br>  return function(...args) {<br>    var ret = fn;<br><br>    for (let i = 0; i < args.length; i++) {<br>      ret = ret(args[i]); // 反复调用currying版本的函数<br>    }<br><br>    return ret; // 返回结果<br>  };<br>}<br><br>var cost = (function() {<br>  var money = 0;<br>  return function() {<br>    for (var i = 0; i < arguments.length; i++) {<br>      money += arguments[i];<br>    }<br>    return money;<br>  }<br>})();<br><br>var curryingCost = currying(cost);<br>var uncurryingCost = uncurrying(curryingCost);<br>console.log(uncurryingCost(100, 200, 300)()); // 600<br></span>

柯里化或偏函数有什么用?

无论是柯里化还是偏应用,我们都能进行部分传值,而传统函数调用则需要预先确定所有实参。如果你在代码某一处只获取了部分实参,然后在另一处确定另一部分实参,这个时候柯里化和偏应用就能派上用场。

另一个最能体现柯里化应用的的是,当函数只有一个形参时,我们能够比较容易地组合它们(单一职责原则(Single responsibility principle))。因此,如果一个函数最终需要三个实参,那么它被柯里化以后会变成需要三次调用,每次调用需要一个实参的函数。当我们组合函数时,这种单元函数的形式会让我们处理起来更简单。

归纳下来,主要为以下常见的三个用途:

  • 延迟计算

  • 参数复用

  • 동적 함수 생성

위 내용은 JavaScript 함수 커링에 대한 설명입니다.

관련 추천:

JS 커링에 대한 자세한 예

JS의 커링 방지

자바스크립트 함수 커링에 대한 자세한 설명

위 내용은 JavaScript 함수 커링의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.