前端進階(八):深入函數的柯里化
配圖與本文無關
柯里化是函數的一個比較高級的應用,想要理解它並不簡單。因此我一直在思考應該如何更加表達才能讓大家理解起來更容易。想了很久,決定先拋開柯里化這個概念不管,補充兩個重要、但是容易被忽略的知識點。
一、補充知識點之函數的隱含轉換
JavaScript作為一種弱型別語言,它的隱含轉換是非常靈活有趣的。當我們沒有深入了解隱式轉換的時候可能會對一些運算的結果會感動困惑,例如4 + true = 5
。當然,如果對隱式轉換了解夠深刻,肯定是能夠大幅提高對js的使用能力。只是我沒有打算將所有的隱式轉換規則分享給大家,這裡暫時只分享一下,函數在隱式轉換中的一些規則。
來一個簡單的思考題。
function fn() { return 20; } console.log(fn + 10); // 输出结果是多少?
稍微修改一下,再想想輸出結果會是什麼?
function fn() { return 20; } fn.toString = function() { return 10; } console.log(fn + 10); // 输出结果是多少?
還可以繼續修改一下。
function fn() { return 20; } fn.toString = function() { return 10; } fn.valueOf = function() { return 5; } console.log(fn + 10); // 输出结果是多少?
// 输出结果分别为 function fn() { return 20; }10 20 15
當使用console.log,或是進行運算時,隱式轉換就可能會發生。從上面三個例子我們可以得到一些關於函數隱式轉換的結論。
當我們沒有重新定義toString與valueOf時,函數的隱含轉換會呼叫預設的toString方法,它會將函數的定義內容作為字串#傳回。而當我們主動定義了toString/vauleOf方法時,那麼隱式轉換的回傳結果就由我們自己控制了。其中valueOf會比toString後執行
因此上面例子的結論就很容易理解了。建議大家動手試試。
二、補充知識點之利用call/apply封陣列的map方法
map( ): 對數組中的每一項運行給定函數,傳回每次函數呼叫的結果組成的數組。
通俗來說,就是遍歷陣列的每一項元素,並且在map的第一個參數(回呼函數)中進行運算處理後回傳計算結果。傳回一個由所有計算結果組成的新數組。
// 回调函数中有三个参数 // 第一个参数表示newArr的每一项,第二个参数表示该项在数组中的索引值 // 第三个表示数组本身 // 除此之外,回调函数中的this,当map不存在第二参数时,this指向丢失,当存在第二个参数时,指向改参数所设定的对象 var newArr = [1, 2, 3, 4].map(function(item, i, arr) { console.log(item, i, arr, this); // 可运行试试看 return item + 1; // 每一项加1 }, { a: 1 }) console.log(newArr); // [2, 3, 4, 5]
在上面例子的註解中詳細闡述了map方法的細節。現在要面臨一個難題,就是如何封裝map。
可以先想想for迴圈。我們可以使用for迴圈來實作一個map,但是在封裝的時候,我們會考慮一些問題。我們在使用for迴圈的時候,一個循環過程確實很好封裝,但是我們在for循環裡面要對每一項做的事情卻很難用一個固定的東西去把它封裝起來。因為每一個場景,for迴圈裡對資料的處理一定都是不一樣的。
於是大家就想了一個很好的辦法,將這些不一樣的運算單獨用一個函數來處理,讓這個函數成為map方法的第一個參數,具體這個回呼函數中會是什麼樣的操作,則由我們自己在使用時決定。因此,根據這個思路的封裝實作如下。
Array.prototype._map = function(fn, context) { var temp = []; if(typeof fn == 'function') { var k = 0; var len = this.length; // 封装for循环过程 for(; k < len; k++) { // 将每一项的运算操作丢进fn里,利用call方法指定fn的this指向与具体参数 temp.push(fn.call(context, this[k], k, this)) } } else { console.error('TypeError: '+ fn +' is not a function.'); } // 返回每一项运算结果组成的新数组 return temp; } var newArr = [1, 2, 3, 4]._map(function(item) { return item + 1; }) // [2, 3, 4, 5]
在上面的封裝中,我首先定義了一個空的temp數組,該數組用來儲存最終的回傳結果。在for迴圈中,每迴圈一次,就執行一次參數fn函數,fn的參數則使用call方法傳入。
在理解了map的封裝過程之後,我們就能夠明白為什麼我們在使用map時,總是期望能夠在第一個回呼函數中有一個回傳值了。在eslint的規則中,如果我們在使用map時沒有設定一個回傳值,就會被判定為錯誤。
ok,明白了函數的隱式轉換規則與call/apply在這種場景的使用方式,我們就可以嘗試透過簡單的例子來了解一下柯里化了。
三、由淺入深的柯里化
在前端面試中有一個關於柯里化的面試題,流傳甚廣。
實作一個add方法,讓計算結果能夠滿足如下預期:
add(1)(2)(3) = 6
add (1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
很明顯,計算結果正是所有參數的和,add方法每運行一次,肯定回傳了一個同樣的函數,繼續計算剩下的參數。
我們可以從最簡單的例子一步一步尋找解決方案。
当我们只调用两次时,可以这样封装。
function add(a) { return function(b) { return a + b; } } console.log(add(1)(2)); // 3
如果只调用三次:
function add(a) { return function(b) { return function (c) { return a + b + c; } } } console.log(add(1)(2)(3)); // 6
上面的封装看上去跟我们想要的结果有点类似,但是参数的使用被限制得很死,因此并不是我们想要的最终结果,我们需要通用的封装。应该怎么办?总结一下上面2个例子,其实我们是利用闭包的特性,将所有的参数,集中到最后返回的函数里进行计算并返回结果。因此我们在封装时,主要的目的,就是将参数集中起来计算。
来看看具体实现。
function add() { // 第一次执行时,定义一个数组专门用来存储所有的参数 var _args = [].slice.call(arguments); // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值 var adder = function () { var _adder = function() { [].push.apply(_args, [].slice.call(arguments)); return _adder; }; // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } return adder.apply(null, [].slice.call(arguments)); } // 输出结果,可自由组合的参数 console.log(add(1, 2, 3, 4, 5)); // 15 console.log(add(1, 2, 3, 4)(5)); // 15 console.log(add(1)(2)(3)(4)(5)); // 15
上面的实现,利用闭包的特性,主要目的是想通过一些巧妙的方法将所有的参数收集在一个数组里,并在最终隐式转换时将数组里的所有项加起来。因此我们在调用add方法的时候,参数就显得非常灵活。当然,也就很轻松的满足了我们的需求。
那么读懂了上面的demo,然后我们再来看看柯里化的定义,相信大家就会更加容易理解了。
柯里化(英语:Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。
接收单一参数,因为要携带不少信息,因此常常以回调函数的理由来解决。
将部分参数通过回调函数等方式传入函数中
返回一个新函数,用于处理所有的想要传入的参数
在上面的例子中,我们可以将add(1, 2, 3, 4)
转换为add(1)(2)(3)(4)
。这就是部分求值。每次传入的参数都只是我们想要传入的所有参数中的一部分。当然实际应用中,并不会常常这么复杂的去处理参数,很多时候也仅仅只是分成两部分而已。
咱们再来一起思考一个与柯里化相关的问题。
假如有一个计算要求,需要我们将数组里面的每一项用我们自己想要的字符给连起来。我们应该怎么做?想到使用join方法,就很简单。
var arr = [1, 2, 3, 4, 5]; // 实际开发中并不建议直接给Array扩展新的方法 // 只是用这种方式演示能够更加清晰一点 Array.prototype.merge = function(chars) { return this.join(chars); } var string = arr.merge('-') console.log(string); // 1-2-3-4-5
增加难度,将每一项加一个数后再连起来。那么这里就需要map来帮助我们对每一项进行特殊的运算处理,生成新的数组然后用字符连接起来了。实现如下:
var arr = [1, 2, 3, 4, 5]; Array.prototype.merge = function(chars, number) { return this.map(function(item) { return item + number; }).join(chars); } var string = arr.merge('-', 1); console.log(string); // 2-3-4-5-6
但是如果我们又想要让数组每一项都减去一个数之后再连起来呢?当然和上面的加法操作一样的实现。
var arr = [1, 2, 3, 4, 5]; Array.prototype.merge = function(chars, number) { return this.map(function(item) { return item - number; }).join(chars); } var string = arr.merge('~', 1); console.log(string); // 0~1~2~3~4
机智的小伙伴肯定发现困惑所在了。我们期望封装一个函数,能同时处理不同的运算过程,但是我们并不能使用一个固定的套路将对每一项的操作都封装起来。于是问题就变成了和封装map的时候所面临的问题一样了。我们可以借助柯里化来搞定。
与map封装同样的道理,既然我们事先并不确定我们将要对每一项数据进行怎么样的处理,我只是知道我们需要将他们处理之后然后用字符连起来,所以不妨将处理内容保存在一个函数里。而仅仅固定封装连起来的这一部分需求。
于是我们就有了以下的封装。
// 封装很简单,一句话搞定 Array.prototype.merge = function(fn, chars) { return this.map(fn).join(chars); } var arr = [1, 2, 3, 4]; // 难点在于,在实际使用的时候,操作怎么来定义,利用闭包保存于传递num参数 var add = function(num) { return function(item) { return item + num; } } var red = function(num) { return function(item) { return item - num; } } // 每一项加2后合并 var res1 = arr.merge(add(2), '-'); // 每一项减2后合并 var res2 = arr.merge(red(1), '-'); // 也可以直接使用回调函数,每一项乘2后合并 var res3 = arr.merge((function(num) { return function(item) { return item * num } })(2), '-') console.log(res1); // 3-4-5-6 console.log(res2); // 0-1-2-3 console.log(res3); // 2-4-6-8
大家能从上面的例子,发现柯里化的特征吗?
四、柯里化通用式
通用的柯里化写法其实比我们上边封装的add方法要简单许多。
var currying = function(fn) { var args = [].slice.call(arguments, 1); return function() { // 主要还是收集所有需要的参数到一个数组中,便于统一计算 var _args = args.concat([].slice.call(arguments)); return fn.apply(null, _args); } } var sum = currying(function() { var args = [].slice.call(arguments); return args.reduce(function(a, b) { return a + b; }) }, 10) console.log(sum(20, 10)); // 40 console.log(sum(10, 5)); // 25
五、柯里化与bind
Object.prototype.bind = function(context) { var _this = this; var args = [].slice.call(arguments, 1); return function() { return _this.apply(context, args) } }
这个例子利用call与apply的灵活运用,实现了bind的功能。
在前面的几个例子中,我们可以总结一下柯里化的特点:
接收单一参数,将更多的参数通过回调函数来搞定?
返回一个新函数,用于处理所有的想要传入的参数;
需要利用call/apply与arguments对象收集参数;
返回的这个函数正是用来处理收集起来的参数。
希望大家读完之后都能够大概明白柯里化的概念,如果想要熟练使用它,就需要我们掌握更多的实际经验才行。
以上是前端進階(八):深入函數的柯里化的詳細內容。更多資訊請關注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)

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。
