JavaScript 中匿名函式的遞迴呼叫
不管是什麼程式語言,相信稍微寫過幾行程式碼的同學,對遞迴都不會陌生。 以一個簡單的階乘計算為例:
function factorial(n) { if (n <= 1) { return 1; } else { return n * factorial(n-1); } }
我們可以看出,遞迴就是在函式內部呼叫對自身的呼叫。 那麼問題來了,我們知道在Javascript中,有一類函式叫做匿名函式,沒有名稱,怎麼呼叫呢?當然你可以說,可以把匿名函式賦值給一個常量:
const factorial = function(n){ if (n <= 1) { return 1; } else { return n * factorial(n-1); } }
這當然是可以的。但是對於一些像,函式編寫時並不知道自己將要賦值給一個明確的變數的情況時,就會遇到麻煩了。如:
(function(f){ f(10); })(function(n){ if (n <= 1) { return 1; } else { return n * factorial(n-1);//太依賴於上下文變數名 } }) //Uncaught ReferenceError: factorial is not defined(…)
那麼存不存在一種完全不需要這種給予準確函式名(函式引用變數名)的方式呢?
arguments.callee
我們知道在任何一個function
內部,都可以訪問到一個叫做arguments
的變數。
(function(){console.dir(arguments)})(1,2)
列印出這個arguments
變數的細節,可以看出他是Arguments
的一個例項,而且從資料結構上來講,他是一個類陣列。他除了類陣列的元素成員和length
屬性外,還有一個callee
方法。 那麼這個callee
方法是做什麼的呢?我們來看下MDN
callee
是arguments
物件的屬性。在該函式的函式體內,它可以指向當前正在執行的函式。當函式是匿名函式時,這是很有用的, 比如沒有名字的函式表示式 (也被叫做”匿名函式”)。
哈哈,很明顯這就是我們想要的。接下來就是:
(function(f){ console.log(f(10)); })(function(n){ if (n <= 1) { return 1; } else { return n * arguments.callee(n-1); } }) //output: 3628800
但是還有一個問題,MDN的文件裡明確指出
警告
:在 ECMAScript 第五版 (ES5) 的 嚴格模式 中禁止使用 arguments.callee()。
哎呀,原來在ES5的use strict;
中不給用啊,那麼在ES6中,我們換個ES6的arrow function
寫寫看:
((f) => console.log(f(10)))( (n) => n <= 1? 1: arguments.callee(n-1)) //Uncaught ReferenceError: arguments is not defined(…)
有一定ES6基礎的同學,估計老早就想說了,箭頭函式就是個簡寫形式的函式表示式,並且它擁有詞法作用域的this
值(即不會新產生自己作用域下的this
, arguments
, super
和 new.target
等物件),且都是匿名的。
那怎麼辦呢?嘿嘿,我們需要藉助一點FP的思想了。
Y組合子
關於Y Combinator
的文章可謂數不勝數,這個由師從希爾伯特的著名邏輯學家Haskell B.Curry(Haskell語言就是以他命名的,而函數語言程式設計語言裡面的Curry手法也是以他命名)“發明”出來的組合運算元(Haskell是研究組合邏輯(combinatory logic)的)彷彿有種神奇的魔力,它能夠算出給定lambda表示式(函式)的不動點。從而使得遞迴成為可能。
這裡需要告知一個概念不動點組合子
:
不動點組合子(英語:Fixed-point combinator,或不動點運算元)是計算其他函式的一個不動點的高階函式。
函式f的不動點是一個值x使得
f(x) = x
。例如,0和1是函式 f(x) = x^2 的不動點,因為 0^2 = 0而 1^2 = 1。鑑於一階函式(在簡單值比如整數上的函式)的不動點是個一階值,高階函式f的不動點是另一個函式g使得f(g) = g
。那麼,不動點運算元是任何函式fix使得對於任何函式f都有
f(fix(f)) = fix(f)
. 不動點組合子允許定義匿名的遞迴函式。它們可以用非遞迴的lambda抽象來定義.
在無型別lambda演算中眾所周知的(可能是最簡單的)不動點組合子叫做Y組合子。
接下來,我們通過一定的演算推到下這個Y組合子。
// 首先我們定義這樣一個可以用作求階乘的遞迴函式 const fact = (n) => n<=1?1:n*fact(n-1) console.log(fact(5)) //120 // 既然不讓這個函式有名字,我們就先給這個遞迴方法一個叫做self的代號 // 首先是一個接受這個遞迴函式作為引數的一個高階函式 const fact_gen = (self) => (n) => n<=1?1:n*self(n-1) console.log(fact_gen(fact)(5)) //120 // 我們是將遞迴方法和引數n,都傳入遞迴方法,得到這樣一個函式 const fact1 = (self, n) => n<=1?1:n*self(self, n-1) console.log(fact1(fact1, 5)) //120 // 我們將fact1 柯理化,得到fact2 const fact2 = (self) => (n) => n<=1?1:n*self(self)(n-1) console.log(fact2(fact2)(5)) //120 // 驚喜的事發生了,如果我們將self(self)看做一個整體 // 作為引數傳入一個新的函式: (g)=> n<= 1? 1: n*g(n-1) const fact3 = (self) => (n) => ((g)=>n <= 1?1:n*g(n-1))(self(self)) console.log(fact3(fact3)(5)) //120 // fact3 還有一個問題是這個新抽離出來的函式,是上下文有關的 // 他依賴於上文的n, 所以我們將n作為新的引數 // 重新構造出這麼一個函式: (g) => (m) => m<=1?1:m*g(m-1) const fact4 = (self) => (n) => ((g) => (m) => m<=1?1:m*g(m-1))(self(self))(n) console.log(fact4(fact4)(5)) // 很明顯fact4中的(g) => (m) => m<=1?1:m*g(m-1) 就是 fact_gen // 這就很有意思啦,這個fact_gen上下文無關了, 可以作為引數傳入了 const weirdFunc = (func_gen) => (self) => (n) => func_gen(self(self))(n) console.log(weirdFunc(fact_gen)(weirdFunc(fact_gen))(5)) //120 // 此時我們就得到了一種Y組合子的形式了 const Y_ = (gen) => (f) => (n)=> gen(f(f))(n) // 構造一個階乘遞迴也很easy了 const factorial = Y_(fact_gen) console.log(factorial(factorial)(5)) //120 // 但上面這個factorial並不是我們想要的 // 只是一種fact2,fact3,fact4的形式 // 我們肯定希望這個函式的呼叫是factorial(5) // 沒問題,我們只需要把定義一個 f' = f(f) = (f)=>f(f) // eg. const factorial = fact2(fact2) const Y = gen => n => (f=>f(f))(gen)(n) console.log(Y(fact2)(5)) //120 console.log(Y(fact3)(5)) //120 console.log(Y(fact4)(5)) //120
推導到這裡,是不是已經感覺到脊背嗖涼了一下,反正筆者我第一次接觸在康托爾、哥德爾、圖靈——永恆的金色對角線這篇文章裡接觸到的時候,整個人瞬間被這種以數學語言去表示程式的方式所折服。
來,我們回憶下,我們最終是不是得到了一個不定點運算元,這個運算元可以找出一個高階函式的不動點f(Y(f)) = Y(f)
。 將一個函式傳入一個運算元(函式),得到一個跟自己功能一樣,但又並不是自己的函式,這個說法有些拗口,但又味道十足。
好了,我們回到最初的問題,怎麼完成匿名函式的遞迴呢?有了Y組合子就很簡單了:
/*求不動點*/ (f => f(f)) /*以不動點為引數的遞迴函式*/ (fact => n => n <= 1 ? 1 : n * fact(fact)(n - 1)) /*遞迴函式引數*/ (5) // 120
曾經看到過一些說法是”最讓人沮喪是,當你推匯出它(Y組合子)後,完全沒法兒通過只看它一眼就說出它到底是想幹嘛”,而我恰恰認為這就是函數語言程式設計的魅力,也是數學的魅力所在,精簡優雅的公式,背後隱藏著複雜有趣的推導過程。
總結
務實點兒講,匿名函式的遞迴呼叫,在日常的js開發中,用到的真的很少。把這個問題拿出來講,主要是想引出對arguments
的一些講解和對Y組合子
這個概念的一個普及。
但既然講都講了,我們真的用到的話,該怎麼選擇呢?來,我們喜聞樂見的benchmark下: 分別測試:
// fact fact(10) // Y (f => f(f))(fact => n => n <= 1 ? 1 : n * fact(fact)(n - 1))(10) // Y' const fix = (f) => f(f) const ygen = fix(fact2) ygen(10) // callee (function(n) {n<=1?1:n*arguments.callee(n-1)})(10)
環境:Macbook pro(2.5 GHz Intel Core i7), node-5.0.0(V8:4.6.85.28) 結果:
fact x 18,604,101 ops/sec ±2.22% (88 runs sampled)
Y x 2,799,791 ops/sec ±1.03% (87 runs sampled)
Y’ x 3,678,654 ops/sec ±1.57% (77 runs sampled)
callee x 2,632,864 ops/sec ±0.99% (81 runs sampled)
可見Y和callee的效能相差不多,因為需要臨時構建函式,所以跟直接的fact遞迴呼叫有差不多一個數量級的差異,將不定點函式算出後儲存下來,大概會有一倍左右的效能提升。
相關文章
- 第 8 節:函式-匿名函式、遞迴函式函式遞迴
- JavaScript 函式遞迴JavaScript函式遞迴
- 好程式設計師Python教程系列遞迴函式與匿名函式呼叫程式設計師Python遞迴函式
- javascript匿名函式常用呼叫方式介紹JavaScript函式
- 好程式設計師Python培訓分享Python的遞迴函式與匿名函式呼叫程式設計師Python遞迴函式
- 遞迴、三元表示式、生成式(列表,字典)、匿名函式遞迴函式
- JavaScript 匿名函式JavaScript函式
- JavaScript匿名函式JavaScript函式
- 函式的遞迴函式遞迴
- 遞迴匿名函式手動實現 http_build_query 系統函式遞迴函式HTTPUI
- 遞迴函式遞迴函式
- (011)我們一起學Python;匿名函式,遞迴函式Python函式遞迴
- JavaScript中的遞迴JavaScript遞迴
- 遞迴函式的理解遞迴函式
- JavaScript 匿名函式 閉包JavaScript函式
- 函式表示式–遞迴函式遞迴
- php遞迴函式PHP遞迴函式
- 函式之遞迴函式遞迴
- js中的匿名函式JS函式
- 函式遞迴與生成式函式遞迴
- [20180531]函式呼叫與遞迴.txt函式遞迴
- javascript中關於匿名函式高階介紹JavaScript函式
- Javascript的函式呼叫與thisJavaScript函式
- 初學 PHP 函式的遞迴PHP函式遞迴
- Python 函式進階-遞迴函式Python函式遞迴
- JS函式表示式——函式遞迴、閉包JS函式遞迴
- JavaScript 匿名立即自執行函式JavaScript函式
- javascript匿名函式簡單介紹JavaScript函式
- 學習javaScript必知必會(1)~js介紹、函式、匿名函式、自呼叫函式、不定長引數JavaScriptJS函式
- lambda匿名函式使用中的坑函式
- Python中的匿名函式-lambdaPython函式
- javascript匿名函式的使用簡單介紹JavaScript函式
- 1.5.6 python遞迴函式Python遞迴函式
- 13.0、python遞迴函式Python遞迴函式
- day 17 – 1 遞迴函式遞迴函式
- 遞迴函式例項大全遞迴函式
- 遞迴函式-樹形列表遞迴函式
- 遞迴函式、演算法之二分法、三元表示式、各種生成式、匿名函式遞迴函式演算法