>  기사  >  웹 프론트엔드  >  JavaScript의 고차 함수, 커링 및 결합 함수에 대한 심층 분석

JavaScript의 고차 함수, 커링 및 결합 함수에 대한 심층 분석

青灯夜游
青灯夜游앞으로
2021-09-22 20:10:451967검색

이 글에서는 JavaScript의 함수형 프로그래밍을 소개하고, 고차 함수, 커링 및 조합 함수는 물론 일반적인 함수 함수도 소개하겠습니다.

JavaScript의 고차 함수, 커링 및 결합 함수에 대한 심층 분석

객체 지향 프로그래밍함수 프로그래밍은 고유한 규칙, 장단점이 있는 매우 다른 두 가지 프로그래밍 패러다임입니다.

그러나 JavaScript는 항상 하나의 규칙을 따르지 않고 이 두 규칙의 중간에 클래스, 객체, 상속 등 일반 OOP 언어의 일부 측면을 제공합니다. 그러나 동시에 고차 함수 및 이를 구성하는 능력과 같은 함수형 프로그래밍의 일부 개념도 제공합니다.

고차 함수

세 가지 개념 중 가장 중요한 고차 함수부터 시작해 보겠습니다.

고차 함수는 함수가 코드에서 정의하고 호출할 수 있는 함수가 아니라 실제로 할당 가능한 엔터티로 사용할 수 있음을 의미합니다. JavaScript를 사용해 본 적이 있다면 이는 놀라운 일이 아닙니다. 익명 함수를 상수에 할당하는 것은 매우 일반적입니다.

const adder = (a, b) => {
  return a + b
}

위 논리는 다른 많은 언어에서는 유효하지 않습니다. 정수와 같은 함수를 할당할 수 있는 것은 매우 유용한 도구입니다. 실제로 이 기사에서 다루는 대부분의 주제는 이 함수의 부산물입니다.

고차 함수의 이점: 캡슐화된 동작

고차 함수를 사용하면 위와 같은 함수를 할당할 수 있을 뿐만 아니라 함수 호출 시 매개 변수로 전달할 수도 있습니다. 이를 통해 복잡한 동작을 매개변수로 직접 전달하여 재사용할 수 있는 지속적으로 동적인 코드 베이스를 생성할 수 있는 기회가 열립니다.

순수한 객체 지향 환경에서 작업하고 작업을 완료하기 위해 클래스 기능을 확장하고 싶다고 상상해 보세요. 이 경우 해당 구현 논리를 추상 클래스에 캡슐화한 다음 이를 구현 클래스 집합으로 확장하여 상속을 사용할 수 있습니다. 이것은 완벽한 OOP 동작이며 작동합니다.

  • 재사용 가능한 논리를 캡슐화하기 위해 추상 구조를 만듭니다.
  • 보조 구성을 만듭니다.
  • 원본 클래스를 재사용하고 확장합니다. 그게 전부입니다

이제 우리가 원하는 것은 로직을 재사용하려면 재사용 가능한 로직을 함수로 추출한 다음 해당 함수를 다른 함수에 매개변수로 전달하면 됩니다. 이렇게 하면 함수를 생성하는 것이므로 "상용구" 프로세스를 많이 줄일 수 있습니다. .

아래 코드는 OOP에서 프로그램 로직을 재사용하는 방법을 보여줍니다.

//Encapsulated behavior封装行为stract class LogFormatter {
  
  format(msg) {
    return Date.now() + "::" + msg
  } 
}

//重用行为
class ConsoleLogger extends LogFormatter {
  
  log(msg) {
    console.log(this.format(msg))
  }  
}

class FileLogger extends LogFormatter {

  log(msg) {
    writeToFileSync(this.logFile, this.format(msg))
  }
}

두 번째 그림은 논리를 함수로 추출하는 것입니다. 혼합하고 일치시켜 필요한 것을 쉽게 만들 수 있습니다. 더 많은 서식 지정 및 작성 기능을 계속 추가한 다음 한 줄의 코드로 함께 혼합할 수 있습니다.

// 泛型行为抽象
function format(msg) {
  return Date.now() + "::" + msg
}

function consoleWriter(msg) {
  console.log(msg)
}

function fileWriter(msg) {
  let logFile = "logfile.log"
  writeToFileSync(logFile, msg)
}

function logger(output, format) {
  return msg => {
    output(format(msg))
  }
}
// 通过组合函数来使用它
const consoleLogger = logger(consoleWriter, format)
const fileLogger = logger(fileWriter, format)

두 접근 방식 모두 장점이 있고 둘 다 매우 효과적이므로 최고는 없습니다. 이 접근 방식의 유연성을 보여주기 위해 동작(예: 함수)을 기본 유형(예: 정수 또는 문자열)인 것처럼 인수로 전달할 수 있습니다.

고차 함수의 이점: 간결한 코드

이 이점을 위한 좋은 예는 forEach, mapArray 메서드입니다. / code>, 줄이기 등등. C와 같은 비기능적 프로그래밍 언어에서 배열 요소를 반복하고 변환하려면 for 루프나 다른 루프 구성을 사용해야 합니다. 이를 위해서는 지정된 방식으로 코드를 작성해야 합니다. 즉, 요구 사항은 주기가 발생하는 프로세스를 설명합니다. Array方法,例如forEachmapreduce等等。 在非函数式编程语言(例如C)中,对数组元素进行迭代并对其进行转换需要使用for循环或某些其他循环结构。 这就要求我们以指定方式编写代码,就是需求描述循环发生的过程。

let myArray = [1,2,3,4]
let transformedArray = []

for(let i = 0; i < myArray.length; i++) {
  transformedArray.push(myArray[i] * 2) 
}

上面的代码主要做了:

  • 声明一个新变量i,该变量将用作myArray的索引,其值的范围为0myArray的长度
  • 对于i的每个值,将myArray的值在i的位置相乘,并将其添加到transformedArray数组中。

这种方法很有效,而且相对容易理解,然而,这种逻辑的复杂性会随着项目的复杂程度上升而上升,认知负荷也会随之增加。但是,像下面这种方式就更容易阅读:

const double = x => x * 2;

let myArray = [1,2,3,4];
let transformedArray = myArray.map(double);

与第一种方式相比,这种方式更容易阅读,而且由于逻辑隐藏在两个函数(mapdouble)中,因此你不必担心了解它们的工作原理。 你也可以在第一个示例中将乘法逻辑隐藏在函数内部,但是遍历逻辑必须存在,这就增加了一些不必要的阅读阻碍。

柯里化

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。我们来看个例子:

function adder(a, b) {
  return a + b
}

// 变成
const add10 = x => adder(a, 10)

现在,如果你要做的就是将10添加到一系列值中,则可以调用add10而不是每次都使用相同的第二个参数调用adder。 这个事例看起来比较蠢,但它是体现了 柯里化

function log(msg, msgPrefix, output) {
  output(msgPrefix + msg)
} 

function consoleOutput(msg) {
  console.log(msg)
}

function fileOutput(msg) {
  let filename = "mylogs.log"
  writeFileSync(msg, filename)
}

const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);

위 코드의 주요 기능은 다음과 같습니다. 🎜🎜🎜myArray의 인덱스로 사용될 새 변수 i를 선언하고 해당 값 범위는 입니다. 0myArray 길이로 늘립니다. 🎜🎜i의 각 값에 대해 myArray의 값을 i의 위치를 ​​곱하여 <code>transformedArray 배열에 추가합니다. 🎜🎜🎜이 접근 방식은 효과적이고 비교적 이해하기 쉽지만, 프로젝트의 복잡성이 증가함에 따라 이 논리의 복잡성도 증가하고 인지 부하도 증가합니다. 하지만 다음과 같이 읽기가 더 쉽습니다: 🎜
var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};
🎜 첫 번째 방법에 비해 이 방법이 읽기 더 쉽고 논리가 두 함수(mapdouble), 따라서 작동 방식을 이해하는 데 대해 걱정할 필요가 없습니다. 첫 번째 예에서는 함수 내부에 곱셈 논리를 숨길 수도 있지만 순회 논리가 있어야 하므로 불필요한 읽기 방해가 추가됩니다. 🎜<h2 id="item-2">🎜Currying🎜🎜🎜🎜Function currying🎜은 여러 매개변수를 허용하는 함수를 단일 매개변수(원래 함수의 첫 번째 매개변수)를 허용하고 반환하는 함수로 변환하는 것입니다. 나머지 매개변수를 받아들이고 결과를 반환하는 새로운 함수입니다. 예를 살펴보겠습니다. 🎜<pre class="brush:js;toolbar:false;">var add10 = value =&gt; value + 10; var mult5 = value =&gt; value * 5;</pre>🎜이제 원하는 값이 <code>10을 값 범위에 추가하는 것이라면 매번 사용하는 대신 add10을 호출하면 됩니다. 동일한 두 번째 인수는 adder를 호출합니다. 이 예는 어리석게 보일 수도 있지만 커링의 이상을 구현합니다. 🎜

你可以将柯里化视为函数式编程的继承,然后按照这种思路再回到logger的示例,可以得到以下内容:

function log(msg, msgPrefix, output) {
  output(msgPrefix + msg)
} 

function consoleOutput(msg) {
  console.log(msg)
}

function fileOutput(msg) {
  let filename = "mylogs.log"
  writeFileSync(msg, filename)
}

const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);

log的函数需要三个参数,而我们将其引入仅需要一个参数的专用版本中,因为其他两个参数已由我们选择。

注意,这里将log函数视为抽象类,只是因为在我的示例中,不想直接使用它,但是这样做是没有限制的,因为这只是一个普通的函数。 如果我们使用的是类,则将无法直接实例化它。

组合函数

函数组合就是组合两到多个函数来生成一个新函数的过程。将函数组合在一起,就像将一连串管道扣合在一起,让数据流过一样。

在计算机科学中,函数组合是将简单函数组合成更复杂函数的一种行为或机制。就像数学中通常的函数组成一样,每个函数的结果作为下一个函数的参数传递,而最后一个函数的结果是整个函数的结果

这是来自维基百科的函数组合的定义,粗体部分是比较关键的部分。使用柯里化时,就没有该限制,我们可以轻松使用预设的函数参数。

代码重用听起来很棒,但是实现起来很难。如果代码业务性过于具体,就很难重用它。如时代码太过通用简单,又很少人使用。所以我们需要平衡两者,一种制作更小的、可重用的部件的方法,我们可以将其作为构建块来构建更复杂的功能。

在函数式编程中,函数是我们的构建块。每个函数都有各自的功能,然后我们把需要的功能(函数)组合起来完成我们的需求,这种方式有点像乐高的积木,在编程中我们称为 组合函数。

看下以下两个函数:

var add10 = function(value) {
    return value + 10;
};
var mult5 = function(value) {
    return value * 5;
};

上面写法有点冗长了,我们用箭头函数改写一下:

var add10 = value => value + 10;
var mult5 = value => value * 5;

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:

var mult5AfterAdd10 = value => 5 * (value + 10)

尽管这是一个非常简单的例子,但仍然不想从头编写这个函数。首先,这里可能会犯一个错误,比如忘记括号。第二,我们已经有了一个加 10 的函数  add10 和一个乘以 5 的函数 mult5 ,所以这里我们就在写已经重复的代码了。

使用函数 add10mult5 来重构 mult5AfterAdd10

var mult5AfterAdd10 = value => mult5(add10(value));

我们只是使用现有的函数来创建 mult5AfterAdd10,但是还有更好的方法。

在数学中, f ∘ g 是函数组合,叫作“f 由 g 组合”,或者更常见的是 “f after g”。 因此 (f ∘ g)(x) 等效于f(g(x)) 表示调用 g 之后调用 f

在我们的例子中,我们有 mult5 ∘ add10 或 “add10 after mult5”,因此我们的函数的名称叫做 mult5AfterAdd10。由于Javascript本身不做函数组合,看看 Elm 是怎么写的:

add10 value =
    value + 10
mult5 value =
    value * 5
mult5AfterAdd10 value =
    (mult5 << add10) value

Elm 中 << 表示使用组合函数,在上例中 value 传给函数 add10 然后将其结果传递给 mult5。还可以这样组合任意多个函数:

f x =
   (g << h << s << r << t) x

这里 x 传递给函数 t,函数 t 的结果传递给 r,函数 t 的结果传递给 s,以此类推。在Javascript中做类似的事情,它看起来会像 g(h(s(r(t(x))))),一个括号噩梦。

常见的函数式函数(Functional Function)

函数式语言中3个常见的函数:Map,Filter,Reduce

如下JavaScript代码:

 for (var i = 0; i < something.length; ++i) {
    // do stuff
 }

这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。如果你用命令式语言来编程,比如Java,C#,JavaScript,PHP,Python等等,你会发现这样的代码你写地最多。这就是问题所在

现在让我们一步一步的解决问题,最后封装成一个看不见 for 语法函数:

先用名为 things 的数组来修改上述代码:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // 警告:值被改变!
}
console.log(things); // [10, 20, 30, 40]

这样做法很不对,数值被改变了!

在重新修改一次:

var things = [1, 2, 3, 4];
var newThings = [];
for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

这里没有修改things数值,但却却修改了newThings。暂时先不管这个,毕竟我们现在用的是 JavaScript。一旦使用函数式语言,任何东西都是不可变的。

现在将代码封装成一个函数,我们将其命名为 map,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。

var map = (f, array) => {
    var newArray = [];
    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

函数 f 作为参数传入,那么函数 map 可以对 array 数组的每项进行任意的操作。

现在使用 map 重写之前的代码:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

这里没有 for 循环!而且代码更具可读性,也更易分析。

现在让我们写另一个常见的函数来过滤数组中的元素:

var filter = (pred, array) => {
    var newArray = [];
for (var i = 0; i < array.length; ++i) {
        if (pred(array[i]))
            newArray[newArray.length] = array[i];
    }
    return newArray;
};

当某些项需要被保留的时候,断言函数 pred 返回TRUE,否则返回FALSE。

使用过滤器过滤奇数:

var isOdd = x => x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

比起用 for 循环的手动编程,filter 函数简单多了。最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。

在函数式语言中,这个函数称为 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() 有2个参数
    return acc;
});

reduce函数接受一个归约函数 f,一个初始值 start,以及一个数组 array

这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。

英文原文地址:https://blog.bitsrc.io/functional-programming-in-functions-composition-and-currying-3c765a50152e

作者:Fernando Doglio

更多编程相关知识,请访问:编程视频!!

위 내용은 JavaScript의 고차 함수, 커링 및 결합 함수에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제