首頁 > web前端 > js教程 > 主體

深入淺析JavaScript中的高階函數、柯里化與組合函數

青灯夜游
發布: 2021-09-22 20:10:54
轉載
2004 人瀏覽過

這篇文章帶大家了解一下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)
登入後複製

這兩種方法都有優點,而且都非常有效,沒有誰最優。這裡只是展示這種方法的靈活性,我們有能力透過 行為(即函數)作為參數,就好像它們是基本類型(如整數或字串)一樣。

高階函數的好處:簡潔程式碼

對於這個好處,一個很好的例子就是Array方法,例如forEach mapreduce等等。在非函數式程式語言(例如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。這個例子看起來比較蠢,但它是體現了 柯里化 的理想。

你可以将柯里化视为函数式编程的继承,然后按照这种思路再回到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
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!