帶你了解並掌握JavaScript函數的柯里化
Haskell和scala都支援函數的柯里化,JavaScript函數的柯里化也與JavaScript的函數程式設計有很大的連結,如果你有興趣的話,可以在這些方面多下功夫了解,相信收穫一定很多.
看本篇文章需要知道的一些知識點
- 函數部分的
call
/apply
/arguments
- 閉包
- 高階函數
- #不完全函數
文章後面有對這些知識的簡單解釋,大家可以看看.
什麼是柯里化?
我們先來看看維基百科中是如何定義的:在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且傳回結果的新函數的技術。
我們可以舉個簡單的例子,如下函數add
是一般的一個函數,就是將傳進來的參數a
和b
相加;函數curryingAdd
就是對函數add
進行柯里化的函數;
這樣一來,原來我們需要直接傳進去兩個參數來運算的函數,現在需要分別傳入參數a
和b
,函數如下:
function add(a, b) { return a + b; } function curryingAdd(a) { return function(b) { return a + b; } } add(1, 2); // 3 curryingAdd(1)(2); // 3
看到這裡你可能會想,這樣做有什麼用?為什麼要這樣做?這樣做能夠為我們的應用帶來什麼樣的好處?先別著急,我們接著往下看.
為什麼要對函數進行柯里化?
- 可以使用一些小技巧(見下文)
- 提前綁定好函數裡面的某些參數,達到參數復用的效果,提高了適用性.
- 固定易變因素
- 延遲計算
總之,函數的柯里化能夠讓你重新組合你的應用,把你的複雜功能拆分成一個一個的小部分,每一個小的部分都是簡單的,便於理解的,而且是容易測試的;
如何對函數進行柯里化?
在這一部分裡,我們由淺入深的一步步來告訴大家如何對一個多參數的函數進行柯里化.其中用到的知識有閉包
,高階函數
,不完全函數
等等.
-
I 開胃菜
假如我們要實現一個功能,就是輸出語句
name
喜歡song
,其中name
和song
都是可變參數;那麼一般情況下我們會這樣寫:function printInfo(name, song) { console.log(name + '喜欢的歌曲是: ' + song); } printInfo('Tom', '七里香'); printInfo('Jerry', '雅俗共赏');
登入後複製對上面的函數進行柯里化之後,我們可以這樣寫:
function curryingPrintInfo(name) { return function(song) { console.log(name + '喜欢的歌曲是: ' + song); } } var tomLike = curryingPrintInfo('Tom'); tomLike('七里香'); var jerryLike = curryingPrintInfo('Jerry'); jerryLike('雅俗共赏');
登入後複製 -
II 小雞燉蘑菇
#上面我們雖然對對函數
printInfo
進行了柯里化,但是我們可不想在需要柯里化的時候,都像上面那樣不斷地進行函數的嵌套,那簡直是噩夢;
所以我們要創造一些幫助其它函數進行柯里化的函數,我們暫且叫它為curryingHelper
吧,一個簡單的curryingHelper
函數如下:function curryingHelper(fn) { var _args = Array.prototype.slice.call(arguments, 1); return function() { var _newArgs = Array.prototype.slice.call(arguments); var _totalArgs = _args.concat(_newArgs); return fn.apply(this, _totalArgs); } }
登入後複製這裡解釋一點東西,首先函數的
arguments
表示的是傳遞到函數中的參數物件,它不是一個陣列,它是一個類別數組物件;
所以我們可以使用函數的Array.prototype.slice
方法,然後使用.call
方法來取得arguments
裡面的內容.
我們使用fn.apply(this, _totalArgs)
來給函數fn
#傳遞正確的參數.接下來我們來寫一個簡單的函數驗證上面的輔助柯里化函數的正確性, 程式碼部分如下:
function showMsg(name, age, fruit) { console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit); } var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple'); curryingShowMsg1(22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20); curryingShowMsg2('watermelon'); // My name is dreamapple, I'm 20 years old, and I like eat watermelon
登入後複製上面的結果表示,我們的這個柯裡化的函數是正確的.上面的
curryingHelper
就是一個高階函數,關於高階函數的解釋可以參考下文. -
III 牛肉火鍋
上面的柯里化幫助函數確實已經能夠達到我們的一般性需求了,但是它還不夠好,我們希望那些經過柯里化後的函數可以每次只傳遞進去一個參數,
然後可以進行多次參數的傳遞,那麼應該怎麼做呢?我們可以再花費一些腦筋,寫出一個betterCurryingHelper
函數,實現我們上面說的那些
功能.代碼如下:function betterCurryingHelper(fn, len) { var length = len || fn.length; return function () { var allArgsFulfilled = (arguments.length >= length); // 如果参数全部满足,就可以终止递归调用 if (allArgsFulfilled) { return fn.apply(this, arguments); } else { var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments)); return betterCurryingHelper(curryingHelper.apply(this, argsNeedFulfilled), length - arguments.length); } }; }
登入後複製其中
curryingHelper
就是上面II 小雞燉蘑菇中提及的那個函數.需要注意的是fn.length
表示的是這個函數的參數長度.
接下來我們來檢定這個函數的正確性:var betterShowMsg = betterCurryingHelper(showMsg); betterShowMsg('dreamapple', 22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple betterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple betterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple betterShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
登入後複製其中
showMsg
就是II 小雞燉蘑菇部分提及的那個函數.
我們可以看出來,這個betterCurryingHelper
確實實現了我們想要的那個功能.並且我們也可以像使用原來的那個函數一樣使用柯里化後的函數. -
IV 泡椒鳳爪
我们已经能够写出很好的柯里化辅助函数了,但是这还不算是最刺激的,如果我们在传递参数的时候可以不按照顺来那一定很酷;当然我们也可以写出这样的函数来,
这个crazyCurryingHelper
函数如下所示:var _ = {}; function crazyCurryingHelper(fn, length, args, holes) { length = length || fn.length; args = args || []; holes = holes || []; return function() { var _args = args.slice(), _holes = holes.slice(); // 存储接收到的args和holes的长度 var argLength = _args.length, holeLength = _holes.length; var allArgumentsSpecified = false; // 循环 var arg = null, i = 0, aLength = arguments.length; for(; i < aLength; i++) { arg = arguments[i]; if(arg === _ && holeLength) { // 循环holes的位置 holeLength--; _holes.push(_holes.shift()); } else if (arg === _) { // 存储hole就是_的位置 _holes.push(argLength + i); } else if (holeLength) { // 是否还有没有填补的hole // 在参数列表指定hole的地方插入当前参数 holeLength--; _args.splice(_holes.shift(), 0, arg); } else { // 不需要填补hole,直接添加到参数列表里面 _args.push(arg); } } // 判断是否所有的参数都已满足 allArgumentsSpecified = (_args.length >= length); if(allArgumentsSpecified) { return fn.apply(this, _args); } // 递归的进行柯里化 return crazyCurryingHelper.call(this, fn, length, _args, _holes); }; }
登入後複製一些解释,我们使用
_
来表示参数中的那些缺失的参数,如果你使用了lodash的话,会有冲突的;那么你可以使用别的符号替代.
按照一贯的尿性,我们还是要验证一下这个crazyCurryingHelper
是不是实现了我们所说的哪些功能,代码如下:var crazyShowMsg = crazyCurryingHelper(showMsg); crazyShowMsg(_, 22)('dreamapple')('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple crazyShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat apple
登入後複製结果显示,我们这个函数也实现了我们所说的那些功能.
柯里化的一些应用场景
说了那么多,其实这部分才是最重要的部分;学习某个知识要一定可以用得到,不然学习它干嘛.
-
关于函数柯里化的一些小技巧
-
给
setTimeout
传递地进来的函数添加参数一般情况下,我们如果想给一个
setTimeout
传递进来的函数添加参数的话,一般会使用之种方法:function hello(name) { console.log('Hello, ' + name); } setTimeout(hello('dreamapple'), 3600); //立即执行,不会在3.6s后执行 setTimeout(function() { hello('dreamapple'); }, 3600); // 3.6s 后执行
登入後複製我们使用了一个新的匿名函数包裹我们要执行的函数,然后在函数体里面给那个函数传递参数值.
当然,在ES5里面,我们也可以使用函数的
bind
方法,如下所示:setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后执行函数
登入後複製这样也是非常的方便快捷,并且可以绑定函数执行的上下文.
我们本篇文章是讨论函数的柯里化,当然我们这里也可以使用函数的柯里化来达到这个效果:
setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已经提及过的
登入後複製这样也是可以的,是不是很酷.其实函数的
bind
方法也是使用函数的柯里化来完成的,详情可以看这里Function.prototype.bind(). -
写出这样一个函数
multiply(1)(2)(3) == 6
结果为true
,multiply(1)(2)(3)(...)(n) == (1)*(2)*(3)*(...)*(n)
结果为true
这个题目不知道大家碰到过没有,不过通过函数的柯里化,也是有办法解决的,看下面的代码:
function multiply(x) { var y = function(x) { return multiply(x * y); }; y.toString = y.valueOf = function() { return x; }; return y; } console.log(multiply(1)(2)(3) == 6); // true console.log(multiply(1)(2)(3)(4)(5) == 120); // true
登入後複製因为
multiply(1)(2)(3)
的直接结果并不是6,而是一个函数对象{ [Number: 6] valueOf: [Function], toString: [Function] }
,我们
之后使用了==
会将左边这个函数对象转换成为一个数字,所以就达到了我们想要的结果.还有关于为什么使用toString
和valueOf
方法
可以看看这里的解释Number.prototype.valueOf(),Function.prototype.toString(). -
上面的那个函数不够纯粹,我们也可以实现一个更纯粹的函数,但是可以会不太符合题目的要求.
我们可以这样做,先把函数的参数存储,然后再对这些参数做处理,一旦有了这个思路,我们就不难写出些面的代码:function add() { var args = Array.prototype.slice.call(arguments); var _that = this; return function() { var newArgs = Array.prototype.slice.call(arguments); var total = args.concat(newArgs); if(!arguments.length) { var result = 1; for(var i = 0; i < total.length; i++) { result *= total[i]; } return result; } else { return add.apply(_that, total); } } } add(1)(2)(3)(); // 6 add(1, 2, 3)(); // 6
登入後複製 -
当我们的需要兼容IE9之前版本的IE浏览器的话,我们可能需要写出一些兼容的方案 ,比如事件监听;一般情况下我们应该会这样写:
var addEvent = function (el, type, fn, capture) { if (window.addEventListener) { el.addEventListener(type, fn, capture); } else { el.attachEvent('on' + type, fn); } };
登入後複製这也写也是可以的,但是性能上会差一点,因为如果是在低版本的IE浏览器上每一次都会运行
if()
语句,产生了不必要的性能开销.
我们也可以这样写:var addEvent = (function () { if (window.addEventListener) { return function (el, type, fn, capture) { el.addEventListener(type, fn, capture); } } else { return function (el, type, fn) { var IEtype = 'on' + type; el.attachEvent(IEtype, fn); } } })();
登入後複製这样就减少了不必要的开支,整个函数运行一次就可以了.
-
-
延迟计算
上面的那两个函数
multiply()
和add()
实际上就是延迟计算的例子. -
提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性.
我们的
I 开胃菜
部分的tomLike
和jerryLike
其实就是属于这种的,绑定好函数里面的第一个参数,然后后面根据情况分别使用不同的函数. -
固定易变因素
我们经常使用的函数的
bind
方法就是一个固定易变因素的很好的例子.
关于柯里化的性能
当然,使用柯里化意味着有一些额外的开销;这些开销一般涉及到这些方面,首先是关于函数参数的调用,操作arguments
对象通常会比操作命名的参数要慢一点;
还有,在一些老的版本的浏览器中arguments.length
的实现是很慢的;直接调用函数fn
要比使用fn.apply()
或者fn.call()
要快一点;产生大量的嵌套
作用域还有闭包会带来一些性能还有速度的降低.但是,大多数的web应用的性能瓶颈时发生在操作DOM上的,所以上面的那些开销比起DOM操作的开销还是比较小的.
关于本章一些知识点的解释
-
琐碎的知识点
fn.length
: 表示的是这个函数中参数的个数.arguments.callee
: 指向的是当前运行的函数.callee
是arguments
对象的属性。
在该函数的函数体内,它可以指向当前正在执行的函数.当函数是匿名函数时,这是很有用的,比如没有名字的函数表达式(也被叫做"匿名函数").
详细解释可以看这里arguments.callee.我们可以看一下下面的例子:function hello() { return function() { console.log('hello'); if(!arguments.length) { console.log('from a anonymous function.'); return arguments.callee; } } } hello()(1); // hello /* * hello * from a anonymous function. * hello * from a anonymous function. */ hello()()();
登入後複製fn.caller
: 返回调用指定函数的函数.详细的解释可以看这里Function.caller,下面是示例代码:function hello() { console.log('hello'); console.log(hello.caller); } function callHello(fn) { return fn(); } callHello(hello); // hello [Function: callHello]
登入後複製
-
高阶函数(high-order function)
高阶函数就是操作函数的函数,它接受一个或多个函数作为参数,并返回一个新的函数.
我们来看一个例子,来帮助我们理解这个概念.就举一个我们高中经常遇到的场景,如下:f1(x, y) = x + y; f2(x) = x * x; f3 = f2(f3(x, y));
登入後複製我们来实现
f3
函数,看看应该如何实现,具体的代码如下所示:function f1(x, y) { return x + y; } function f2(x) { return x * x; } function func3(func1, func2) { return function() { return func2.call(this, func1.apply(this, arguments)); } } var f3 = func3(f1, f2); console.log(f3(2, 3)); // 25
登入後複製我们通过函数
func3
将函数f1
,f2
结合到了一起,然后返回了一个新的函数f3
;这个函数就是我们期望的那个函数. -
不完全函数(partial function)
什么是不完全函数呢?所谓的不完全函数和我们上面所说的柯里化基本差不多;所谓的不完全函数,就是给你想要运行的那个函数绑定一个固定的参数值;
然后后面的运行或者说传递参数都是在前面的基础上进行运行的.看下面的例子:// 一个将函数的arguments对象变成一个数组的方法 function array(a, n) { return Array.prototype.slice.call(a, n || 0); } // 我们要运行的函数 function showMsg(a, b, c){ return a * (b - c); } function partialLeft(f) { var args = arguments; return function() { var a = array(args, 1); a = a.concat(array(arguments)); console.log(a); // 打印实际传递到函数中的参数列表 return f.apply(this, a); } } function partialRight(f) { var args = arguments; return function() { var a = array(arguments); a = a.concat(array(args, 1)); console.log(a); // 打印实际传递到函数中的参数列表 return f.apply(this, a); } } function partial(f) { var args = arguments; return function() { var a = array(args, 1); var i = 0; j = 0; for(; i < a.length; i++) { if(a[i] === undefined) { a[i] = arguments[j++]; } } a = a.concat(array(arguments, j)); console.log(a); // 打印实际传递到函数中的参数列表 return f.apply(this, a); } } partialLeft(showMsg, 1)(2, 3); // 实际参数列表: [1, 2, 3] 所以结果是 1 * (2 - 3) = -1 partialRight(showMsg, 1)(2, 3); // 实际参数列表: [2, 3, 1] 所以结果是 2 * (3 - 1) = 4 partial(showMsg, undefined, 1)(2, 3); // 实际参数列表: [2, 1, 3] 所以结果是 2 * (1 - 3) = -4
登入後複製
一些你可能会喜欢的JS库
JavaScript的柯里化与JavaScript的函数式编程密不可分,下面列举了一些关于JavaScript函数式编程的库,大家可以看一下:
- underscore
- lodash
- ramda
- bacon.js
- fn.js
- functional-js
推荐教程:《JS教程》
以上是帶你了解並掌握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)

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

JavaScript是一種廣泛應用於Web開發的程式語言,而WebSocket則是一種用於即時通訊的網路協定。結合二者的強大功能,我們可以打造一個高效率的即時影像處理系統。本文將介紹如何利用JavaScript和WebSocket來實作這個系統,並提供具體的程式碼範例。首先,我們需要明確指出即時影像處理系統的需求和目標。假設我們有一個攝影機設備,可以擷取即時的影像數
