詳細介紹JavaScript函數柯里化的一些思考
1. 高階函數的坑
在學習柯里化之前,我們先來看看下面一段程式碼:
var f1 = function(x){ return f(x); }; f1(x);
很多同學都能看出來,這些寫是非常愚蠢的,因為函數f1
和f
是等效的,我們直接令var f1 = f;
就行了,完全沒有必要包裹那麼一層。
但是,下面一段程式碼就未必能夠看得出問題來了:
var getServerStuff = function(callback){ return ajaxCall(function(json){ return callback(json); }); };
這是我摘自《JS函數式程式設計指南》中的一段程式碼,實際上,利用上面的規則,我們可以得到callback
與函數
function(json){return callback(json);};
是等價的,所以函數可以化簡為:
var getServerStuff = function(callback){ return ajaxCall(callback); };
繼續化簡:
var getServerStuff = ajaxCall;
如此一來,我們發現那麼長一段程式都白寫了。
函數既可以當參數,又可以當回傳值,是高階函數的重要特性,但是稍不留神就容易踩到坑裡。
2. 函數柯里化(curry)
言歸正傳,什麼是函數柯里化?函數柯里化(curry)就是只傳遞給函數一部分參數來呼叫它,讓它回傳一個函數去處理剩下的參數。聽得很繞口,其實很簡單,其實就是將函數的變數分開來呼叫:f(x,y,z) -> f(x)(y)(z)
。
對於最開始的例子,按照如下實現,要傳入兩個參數,f1
呼叫方式是f1(f,x)
。
var f1 = function(f,x){ return f(x); };
注意,由於f
是作為一個函數變數傳入,所以f1
變成了一個新的函數。
我們將f1
改變一下,利用閉包可以寫成如下形式,則f1
呼叫方式變成了f1( f)(x)
,而且得到的結果完全一樣。這就完成了f1
的柯里化。
var f1 = function(f){ return function(x){ return f(x); } }; var f2 = f1(f); f2(x);
其實這個例子舉得不恰當,細心的同學可能會發現,f1
雖然是一個新函數,但是f2
和f
#是完全等效的,繞了半天,還是繞回來了。
這裡有一個很經典的例子:
['11', '11', '11'].map(parseInt) //[ 11, NaN, 3 ] ['11', '11', '11'].map(f1(parseInt)) //[ 11, 11, 11 ]
由於parseInt
接受兩個參數,所以直接呼叫會有進位轉換的問題,參考「不願相離」的文章。
var f2 = f1(parseInt)
,f2
讓parseInt
由原來的接受兩個參數變成了只接受一個參數的新函數,從而解決這個進制轉換問題。透過我們的f1
包裹以後就能夠運行出正確的結果了。
有同學覺得這個不算柯里化的應用,我覺得還是算吧,各位同學可以一起來討論下。
3. 函數柯里化進一步思考
如果說上一節的例子中,我們不是直接運行f(x)
,而是把函數f
當做一個參數,結果會怎麼樣呢?我們來看下面這個範例:
假設f1
傳回函數g
,g
的作用域指向xs
,函數f
作為g
的參數。最後我們可以寫成如下形式:
var f1 = function(f,xs){ return g.call(xs,f); };
實際上,用f1
來取代g.call(xxx)
的做法叫反柯里化。例如:
var forEach = function(xs,f){ return Array.prototype.forEach.call(xs,f); }; var f = function(x){console.log(x);}; var xs = {0:'peng',1:'chen',length:2}; forEach(xs,f);
反curring就是把原來已經固定的參數或this上下文等當作參數延遲到未來傳遞。
它能夠在很大程度上簡化函數,前提是你得習慣它。
拋開反柯里化,如果我們要柯里化f1
怎麼辦?
使用閉包,我們可以寫成如下形式:
var f1 = function(f){ return function(xs){ return g.call(xs,f); } }; var f2 = f1(f); f2(xs);
把f
傳入f1
中,我們就可以得到f2
這個新函數。
只傳給函數一部分參數通常也叫做局部呼叫(partial application),能夠大量減少樣板檔案程式碼(boilerplate code)。
當然,函數f1
傳入的兩個參數不一定要包含函數+非函數,可能兩個都是函數,也可能兩個都是非函數。
我個人覺得柯里化並非是必須的,而且不熟悉的同學閱讀起來可能會遇到麻煩,但是它能幫助我們理解JS中的函數式編程,更重要的是,我們以後在閱讀類似的程式碼時,不會感到陌生。知乎上羅納同學講的挺好:
並非「柯里化」對函數式程式設計有意義。而是,函數式程式設計在把函數當作一等公民的同時,就不可避免的會產生「柯里化」這種用法。所以它並不是因為「有什麼意義」才出現的。當然既然存在了,我們自然可以探討怎麼利用這個現象。
練習:
// 通过局部调用(partial apply)移除所有参数 var filterQs = function(xs) { return filter(function(x){ return match(/q/i, x); }, xs); }; //这两个函数原题没有,是我自己加的 var filter = function(f,xs){ return xs.filter(f); }; var match = function(what,x){ return x.match(what); };
分析:函數filterQs
的作用是:傳入一個字串數組,過濾出包含'q'的字串,並組成一個新的數組回傳。
我們可以透過以下步驟得到函數filterQs
:
a. filter
传入的两个参数,第一个是回调函数,第二个是数组,filter
主要功能是根据回调函数过滤数组。我们首先将filter
函数柯里化:
var filter = function(f){ return function (xs) { return xs.filter(f); } };
b. 其次,filter
函数传入的回调函数是match
,match
的主要功能是判断每个字符串是否匹配what
这个正则表达式。这里我们将match
也柯里化:
var match = function(what){ return function(x){ return x.match(what); } }; var match2 = match(/q/i);
创建匹配函数match2
,检查字符串中是否包含字母q。
c. 把match2
传入filter
中,组合在一起,就形成了一个新的函数:
var filterQs = filter(match2); var xs = ['q','test1','test2']; filterQs(xs);
从这个示例中我们也可以体会到函数柯里化的强大。所以,柯里化还有一个重要的功能:封装不同功能的函数,利用已有的函数组成新的函数。
4. 函数柯里化的递归调用
函数柯里化还有一种有趣的形式,就是函数可以在闭包中调用自己,类似于函数递归调用。如下所示:
function add( seed ) { function retVal( later ) { return add( seed + later ); } retVal.toString = function() { return seed; }; return retVal; } console.log(add(1)(2)(3).toString()); // 6
add
函数返回闭包retVal
,在retVal
中又继续调用add
,最终我们可以写成add(1)(2)(3)(...)
这样柯里化的形式。
关于这段代码的解答,知乎上的李宏训同学回答地很好:
每调用一次add函数,都会返回retValue函数;调用retValue函数会调用add函数,然后还是返回retValue函数,所以调用add的结果一定是返回一个retValue函数。add函数的存在意义只是为了提供闭包,这个类似的递归调用每次调用add都会生成一个新的闭包。
5. 函数组合(compose)
函数组合是在柯里化基础上完成的:
var compose = function(f,g) { return function(x) { return f(g(x)); }; }; var f1 = compose(f,g); f1(x);
将传入的函数变成两个,通过组合的方式返回一个新的函数,让代码从右向左运行,而不是从内向外运行。
函数组合和柯里化有一个好处就是pointfree。
pointfree 模式指的是,永远不必说出你的数据。它的意思是说,函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。
// 非 pointfree,因为提到了数据:name var initials = function (name) { return name.split(' ').map(compose(toUpperCase, head)).join('. '); }; // pointfree var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' ')); initials("hunter stockton thompson"); // 'H. S. T'
以上是詳細介紹JavaScript函數柯里化的一些思考的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Go語言提供了兩種動態函數創建技術:closures和反射。 closures允許存取閉包作用域內的變量,而反射可使用FuncOf函數建立新函數。這些技術在自訂HTTP路由器、實現高度可自訂的系統和建置可插拔的元件方面非常有用。

在C++函數命名中,考慮參數順序至關重要,可提高可讀性、減少錯誤並促進重構。常見的參數順序約定包括:動作-物件、物件-動作、語意意義和遵循標準函式庫。最佳順序取決於函數目的、參數類型、潛在混淆和語言慣例。

1. SUM函數,用於對一列或一組單元格中的數字進行求和,例如:=SUM(A1:J10)。 2、AVERAGE函數,用於計算一列或一組儲存格中的數字的平均值,例如:=AVERAGE(A1:A10)。 3.COUNT函數,用於計算一列或一組單元格中的數字或文字的數量,例如:=COUNT(A1:A10)4、IF函數,用於根據指定的條件進行邏輯判斷,並返回相應的結果。

C++函數中預設參數的優點包括簡化呼叫、增強可讀性、避免錯誤。缺點是限制靈活性、命名限制。可變參數的優點包括無限彈性、動態綁定。缺點包括複雜性更高、隱式型別轉換、除錯困難。

C++中的函數傳回參考類型的好處包括:效能提升:引用傳遞避免了物件複製,從而節省了記憶體和時間。直接修改:呼叫方可以直接修改傳回的參考對象,而無需重新賦值。程式碼簡潔:引用傳遞簡化了程式碼,無需額外的賦值操作。

自訂PHP函數與預定義函數的差異在於:作用域:自訂函數僅限於其定義範圍,而預定義函數可在整個腳本中存取。定義方式:自訂函數使用function關鍵字定義,而預先定義函數則由PHP核心定義。參數傳遞:自訂函數接收參數,而預先定義函數可能不需要參數。擴充性:自訂函數可以根據需要創建,而預定義函數是內建的且無法修改。

C++中的異常處理可透過自訂異常類別增強,提供特定錯誤訊息、上下文資訊以及根據錯誤類型執行自訂操作。定義繼承自std::exception的異常類,提供特定的錯誤訊息。使用throw關鍵字拋出自訂異常。在try-catch區塊中使用dynamic_cast將捕獲到的異常轉換為自訂異常類型。在實戰案例中,open_file函數會拋出FileNotFoundException異常,捕捉並處理該異常可提供更具體的錯誤訊息。
