JavaScript 專題之函數柯里化

JavaScript 專題系列第十三篇,講解函數柯里化以及如何實現一個 curry 函數

定義

維基百科中對柯里化 (Currying) 的定義為:

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

翻譯成中文:

在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。

舉個例子:

function add(a, b) {return a + b;
}// 執行 add 函數,一次傳入兩個參數即可
add(1, 2) // 3// 假設有一個 curry 函數可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3復制代碼

用途

我們會講到如何寫出這個 curry 函數,并且會將這個 curry 函數寫的很強大,但是在編寫之前,我們需要知道柯里化到底有什么用?

舉個例子:

// 示意而已
function ajax(type, url, data) {var xhr = new XMLHttpRequest();xhr.open(type, url, true);xhr.send(data);
}// 雖然 ajax 這個函數非常通用,但在重復調用的時候參數冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")// 利用 curry
var ajaxCurry = curry(ajax);// 以 POST 類型請求數據
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");// 以 POST 類型請求來自于 www.test.com 的數據
var postFromTest = post('www.test.com');
postFromTest("name=kevin");復制代碼

想想 jQuery 雖然有 $.ajax 這樣通用的方法,但是也有 $.get 和 $.post 的語法糖。(當然 jQuery 底層是否是這樣做的,我就沒有研究了)。

curry 的這種用途可以理解為:參數復用。本質上是降低通用性,提高適用性。

可是即便如此,是不是依然感覺沒什么用呢?

如果我們僅僅是把參數一個一個傳進去,意義可能不大,但是如果我們是把柯里化后的函數傳給其他函數比如 map 呢?

舉個例子:

比如我們有這樣一段數據:

var person = [{name: 'kevin'}, {name: 'daisy'}]復制代碼

如果我們要獲取所有的 name 值,我們可以這樣做:

var name = person.map(function (item) {return item.name;
})復制代碼

不過如果我們有 curry 函數:

var prop = curry(function (key, obj) {return obj[key]
});var name = person.map(prop('name'))復制代碼

我們為了獲取 name 屬性還要再編寫一個 prop 函數,是不是又麻煩了些?

但是要注意,prop 函數編寫一次后,以后可以多次使用,實際上代碼從原本的三行精簡成了一行,而且你看代碼是不是更加易懂了?

person.map(prop('name')) 就好像直白的告訴你:person 對象遍歷(map)獲取(prop) name 屬性。

是不是感覺有點意思了呢?

第一版

未來我們會接觸到更多有關柯里化的應用,不過那是未來的事情了,現在我們該編寫這個 curry 函數了。

一個經常會看到的 curry 函數的實現為:

// 第一版
var curry = function (fn) {var args = [].slice.call(arguments, 1);return function() {var newArgs = args.concat([].slice.call(arguments));return fn.apply(this, newArgs);};
};復制代碼

我們可以這樣使用:

function add(a, b) {return a + b;
}var addCurry = curry(add, 1, 2);
addCurry() // 3
//或者
var addCurry = curry(add, 1);
addCurry(2) // 3
//或者
var addCurry = curry(add);
addCurry(1, 2) // 3復制代碼

已經有柯里化的感覺了,但是還沒有達到要求,不過我們可以把這個函數用作輔助函數,幫助我們寫真正的 curry 函數。

第二版

// 第二版
function sub_curry(fn) {var args = [].slice.call(arguments, 1);return function() {return fn.apply(this, args.concat([].slice.call(arguments)));};
}function curry(fn, length) {length = length || fn.length;var slice = Array.prototype.slice;return function() {if (arguments.length < length) {var combined = [fn].concat(slice.call(arguments));return curry(sub_curry.apply(this, combined), length - arguments.length);} else {return fn.apply(this, arguments);}};
}復制代碼

我們驗證下這個函數:

var fn = curry(function(a, b, c) {return [a, b, c];
});fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]復制代碼

效果已經達到我們的預期,然而這個 curry 函數的實現好難理解吶……

為了讓大家更好的理解這個 curry 函數,我給大家寫個極簡版的代碼:

function sub_curry(fn){return function(){return fn()}
}function curry(fn, length){length = length || 4;return function(){if (length > 1) {return curry(sub_curry(fn), --length)}else {return fn()}}
}var fn0 = function(){console.log(1)
}var fn1 = curry(fn0)fn1()()()() // 1復制代碼

大家先從理解這個 curry 函數開始。

當執行 fn1() 時,函數返回:

curry(sub_curry(fn0))
// 相當于
curry(function(){return fn0()
})復制代碼

當執行 fn1()() 時,函數返回:

curry(sub_curry(function(){return fn0()
}))
// 相當于
curry(function(){return (function(){return fn0()})()
})
// 相當于
curry(function(){return fn0()
})復制代碼

當執行 fn1()()() 時,函數返回:

// 跟 fn1()() 的分析過程一樣
curry(function(){return fn0()
})復制代碼

當執行 fn1()()()() 時,因為此時 length > 2 為 false,所以執行 fn():

fn()
// 相當于
(function(){return fn0()
})()
// 相當于
fn0()
// 執行 fn0 函數,打印 1復制代碼

再回到真正的 curry 函數,我們以下面的例子為例:

var fn0 = function(a, b, c, d) {return [a, b, c, d];
}var fn1 = curry(fn0);fn1("a", "b")("c")("d")復制代碼

當執行 fn1("a", "b") 時:

fn1("a", "b")
// 相當于
curry(fn0)("a", "b")
// 相當于
curry(sub_curry(fn0, "a", "b"))
// 相當于
// 注意 ... 只是一個示意,表示該函數執行時傳入的參數會作為 fn0 后面的參數傳入
curry(function(...){return fn0("a", "b", ...)
})復制代碼

當執行 fn1("a", "b")("c") 時,函數返回:

curry(sub_curry(function(...){return fn0("a", "b", ...)
}), "c")
// 相當于
curry(function(...){return (function(...) {return fn0("a", "b", ...)})("c")
})
// 相當于
curry(function(...){return fn0("a", "b", "c", ...)
})復制代碼

當執行 fn1("a", "b")("c")("d") 時,此時 arguments.length < length 為 false ,執行 fn(arguments),相當于:

(function(...){return fn0("a", "b", "c", ...)
})("d")
// 相當于
fn0("a", "b", "c", "d")復制代碼

函數執行結束。

所以,其實整段代碼又很好理解:

sub_curry 的作用就是用函數包裹原函數,然后給原函數傳入之前的參數,當執行 fn0(...)(...) 的時候,執行包裹函數,返回原函數,然后再調用 sub_curry 再包裹原函數,然后將新的參數混合舊的參數再傳入原函數,直到函數參數的數目達到要求為止。

如果要明白 curry 函數的運行原理,大家還是要動手寫一遍,嘗試著分析執行步驟。

更易懂的實現

當然了,如果你覺得還是無法理解,你可以選擇下面這種實現方式,可以實現同樣的效果:

function curry(fn, args) {length = fn.length;args = args || [];return function() {var _args = args.slice(0),arg, i;for (i = 0; i < arguments.length; i++) {arg = arguments[i];_args.push(arg);}if (_args.length < length) {return curry.call(this, fn, _args);}else {return fn.apply(this, _args);}}
}var fn = curry(function(a, b, c) {console.log([a, b, c]);
});fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]復制代碼

或許大家覺得這種方式更好理解,又能實現一樣的效果,為什么不直接就講這種呢?

因為想給大家介紹各種實現的方法嘛,不能因為難以理解就不給大家介紹吶~

第三版

curry 函數寫到這里其實已經很完善了,但是注意這個函數的傳參順序必須是從左到右,根據形參的順序依次傳入,如果我不想根據這個順序傳呢?

我們可以創建一個占位符,比如這樣:

var fn = curry(function(a, b, c) {console.log([a, b, c]);
});fn("a", _, "c")("b") // ["a", "b", "c"]復制代碼

我們直接看第三版的代碼:

// 第三版
function curry(fn, args, holes) {length = fn.length;args = args || [];holes = holes || [];return function() {var _args = args.slice(0),_holes = holes.slice(0),argsLen = args.length,holesLen = holes.length,arg, i, index = 0;for (i = 0; i < arguments.length; i++) {arg = arguments[i];// 處理類似 fn(1, _, _, 4)(_, 3) 這種情況,index 需要指向 holes 正確的下標if (arg === _ && holesLen) {index++if (index > holesLen) {_args.push(arg);_holes.push(argsLen - 1 + index - holesLen)}}// 處理類似 fn(1)(_) 這種情況else if (arg === _) {_args.push(arg);_holes.push(argsLen + i);}// 處理類似 fn(_, 2)(1) 這種情況else if (holesLen) {// fn(_, 2)(_, 3)if (index >= holesLen) {_args.push(arg);}// fn(_, 2)(1) 用參數 1 替換占位符else {_args.splice(_holes[index], 1, arg);_holes.splice(index, 1)}}else {_args.push(arg);}}if (_holes.length || _args.length < length) {return curry.call(this, fn, _args, _holes);}else {return fn.apply(this, _args);}}
}var _ = {};var fn = curry(function(a, b, c, d, e) {console.log([a, b, c, d, e]);
});// 驗證 輸出全部都是 [1, 2, 3, 4, 5]
fn(1, 2, 3, 4, 5);
fn(_, 2, 3, 4, 5)(1);
fn(1, _, 3, 4, 5)(2);
fn(1, _, 3)(_, 4)(2)(5);
fn(1, _, _, 4)(_, 3)(2)(5);
fn(_, 2)(_, _, 4)(1)(3)(5)復制代碼

寫在最后

至此,我們已經實現了一個強大的 curry 函數,可是這個 curry 函數符合柯里化的定義嗎?柯里化可是將一個多參數的函數轉換成多個單參數的函數,但是現在我們不僅可以傳入一個參數,還可以一次傳入兩個參數,甚至更多參數……這看起來更像一個柯里化 (curry) 和偏函數 (partial application) 的綜合應用,可是什么又是偏函數呢?下篇文章會講到。

專題系列

JavaScript專題系列目錄地址:github.com/mqyqingfeng…。

JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/455415.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/455415.shtml
英文地址,請注明出處:http://en.pswp.cn/news/455415.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

機器學習模板

根據心情補充&#xff0c;語言都是Python hash&#xff0c;把所有的文本轉化成數字 from sklearn.preprocessing import LabelEncoder for c in train.columns:if train[c].dtype object:lbl LabelEncoder()lbl.fit(list(train[c].values) list(test[c].values))train[c] l…

漂亮特殊字體可復制_12個創意字體免費下載網站

今天為大家介紹12個創意字體的網站&#xff0c;這些網站都有提供免費下載的字體哦&#xff0c;希望對大家在創作上面有所幫助。FontSpace在Fontspace上有超過42000種免費字體。在這里字體被整齊的分門歸類&#xff0c;幫助你找到想要的字體。除了典型的“serif” “script”等&…

使用postman測試接口

Postman是一款功能強大的網頁調試與發送網頁HTTP請求的Chrome插件。在java web開發中使用非常多&#xff0c;經常用來測試接口。 使用postman模擬json數據的發送 第一步:在header里邊設置發送數據的類型 Paste_Image.png設置發送數據類型為json&#xff0c;也就是key為Content-…

刪除github上的commit歷史記錄

刪除github上的commit歷史記錄 起步 今天小編發現了git克隆下來的遠程庫特別大: 經過查詢之后發現是每次推送之后都會留下記錄緩存&#xff0c;這樣很多沒用的記錄就會占用多余的空間&#xff0c;別人克隆的時候也會多耗費時間&#xff0c;今天我查到了一個清除無用記錄的方…

DirectShow組件原理分析及應用

1 DirectX簡介  DirectX是Microsoft公司為游戲和其他高性能多媒體應用所提供的一套底層應用程序編程接口。這些接口包括對二維和三維圖形&#xff0c;聲效和音樂&#xff0c;輸入設備以及多玩家網絡游戲等的支持。目前DirectX的最高版本是DirectX 9.0。  1.1 DirectX的組成…

接口安全

老大發了篇文章&#xff0c;讓看如何寫出安全的接口。 如何寫出安全的API接口&#xff1f;接口參數加密簽名設計思路轉載于:https://www.cnblogs.com/Tpf386/p/7053795.html

python中xml模塊_python學習第十五天-2(XML模塊)

也是一種文本轉換形式。importxxxxxxxxxxxxxxxxxxx asxx,可以用xx代替xxxxxxxxxxxxxxxxxxx模塊xml文件的新增&#xff0c;修改&#xff0c;刪除&#xff0c;查詢。新增&#xff1a;?import xml.etree.ElementTreeas ET?new_xmlET.Element(nameList)#創建xml的根節點相當于na…

ubuntu系統下Java環境JDK的安裝

Debian Linux下安裝jdk 下載壓縮包 官網下載對應的.gz包 點擊下載 解壓文件 創建一個目錄用于存放解壓后的文件&#xff0c;并解壓縮到該目錄下 sudo mkdir /opt/java8 sudo tar -zxvf jdk-8u221-linux-x64.tar.gz -C /opt/java8修改環境變量 sudo vim ~/.bashrc 進入…

棧溢出筆記1.3 準備Shellcode

經過1.1和1.2節的講述&#xff0c;我們已經知道了怎樣更改EIP的值。程序運行函數之后將跳轉到我們設定的位置開始運行&#xff0c;因此&#xff0c;我們須要準備一個自己的程序&#xff0c;接手后面的工作。這是一個什么樣的程序&#xff1f;是一個C語言編寫的代碼&#xff1f;…

DirectShow開發快速入門之慨述

文章來源&#xff1a;http://tech.163.com/school 2005-08-18 10:21:32 來源: 天極網摘要&#xff1a;本篇文檔概括性的介紹了DirectShow的主要組成部分&#xff0c;以及一些Directshow的基本概念。熟悉這些基本的知識對于Directshow的應用開發或者過濾器的開發者都會有所幫助…

Android selector中的item的順序

在selector中&#xff0c;要將默認狀態的item放在最后面&#xff0c;因為一旦前面的item滿足匹配條件&#xff0c;后面的item就不會去匹配。因此&#xff0c;把默認狀態的item放在前面的話&#xff0c;后面的item沒有執行的機會轉載于:https://www.cnblogs.com/xiaoyuersdch/p/…

權限表使用聯合主鍵嗎_天天寫 order by,你知道Mysql底層執行流程嗎?

前言 在實際的開發中一定會碰到根據某個字段進行排序后來顯示結果的需求&#xff0c;但是你真的理解order by在 Mysql 底層是如何執行的嗎&#xff1f;假設你要查詢城市是蘇州的所有人名字&#xff0c;并且按照姓名進行排序返回前 1000 個人的姓名、年齡&#xff0c;這條 sql 語…

nodejs簡介

nodejs是啥&#xff1f; Node.js是運行在服務端的JavaScript。 Node.js是一個基于Chrome JavaScript運行時建立的一個平臺。 Node.js是一個事件驅動I/O服務端JavaScript環境&#xff0c;基于Google的V8引擎&#xff0c;V8引擎執行Javascript的速度非常快&#xff0c;性能非常…

jumpserver v0.4.0 基于 CenOS7 的安裝詳解

標簽&#xff08;linux&#xff09;&#xff1a; jumpserver 筆者Q:972581034 交流群&#xff1a;605799367。有任何疑問可與筆者或加群交流 首首先使用Jumpserver前要理解清楚這三個用戶關系: 1.用戶&#xff1a; 是指你在web上創建的用戶,會在跳板機上創建這個用戶,作用就是用…

Node.js中事件的循環

Node.js 事件循環 Node.js 是單進程單線程應用程序&#xff0c;但是通過事件和回調支持并發&#xff0c;所以性能非常高。 Node.js 的每一個 API 都是異步的&#xff0c;并作為一個獨立線程運行&#xff0c;使用異步函數調用&#xff0c;并處理并發。 Node.js 基本上所有的事…

python爬boss網站_python之requests爬蟲Boss數據

python之requests爬蟲Boss數據需要用到的庫&#xff1a;reqeusts、lxml沒有的可以用直接下載pip install requestspip install lxm這里以python崗位&#xff0c;地點北京為例爬取的數據就是崗位名稱、薪資、地點 首先導入需要用到的模塊import requestsfrom lxml import etree崗…

live555源代碼簡介

文章出自&#xff1a;http://blog.csdn.net/imliujie/archive/2008/01/30/2072657.aspx live555源代碼簡介liveMedia項目的源代碼包括四個基本的庫&#xff0c;各種測試代碼以及IVE555 Media Server。四個基本的庫分別是UsageEnvironment&TaskScheduler&#xff0c;groups…

并發無鎖隊列學習(單生產者單消費者模型)

1、引言 本文介紹單生產者單消費者模型的隊列。依據寫入隊列的內容是定長還是變長&#xff0c;分為單生產者單消費者定長隊列和單生產者單消費者變長隊列兩種。單生產者單消費者模型的隊列操作過程是不須要進行加鎖的。生產者通過寫索引控制入隊操作&#xff0c;消費者通過讀索…

ecshop 收貨人信息電話必填改為手機必填

首先通過在flow.dwt中&#xff0c;查找flow.php?stepconsignee中的關鍵字 consignee&#xff08;結算中心&#xff09;查找所在模板/Library/consignee.lbi 大概57行 把必填去掉&#xff0c;其次 在js/shopping_flow.js里邊注釋掉 if (Utils.isEmpty(frm.elements[‘tel’].v…

流媒體傳輸協議

1&#xff0e;流媒體( Streaming Media) 1.1流媒體概念 流媒體技術是網絡技術和多媒體技術發展到一定階段的產物。術語流媒體既可以指在網上傳輸連續時基媒體的流式技術,也可以指使用流式技術的連續時基媒體本身。在網上傳輸音頻、視頻等多媒體信息目前主要有兩種方式:下載和流…