摘要:執(zhí)行完了,銷毀調(diào)用棧中自己的記錄,依次銷毀和的調(diào)用幀,最后完成整個流程。尾遞歸定義先來看一下遞歸,當一個函數(shù)調(diào)用自身,就叫做遞歸。
尾調(diào)用 1. 定義
尾調(diào)用是函數(shù)式編程中一個很重要的概念,當一個函數(shù)執(zhí)行時的最后一個步驟是返回另一個函數(shù)的調(diào)用,這就叫做尾調(diào)用。
注意這里函數(shù)的調(diào)用方式是無所謂的,以下方式均可:
函數(shù)調(diào)用: func(···) 方法調(diào)用: obj.method(···) call調(diào)用: func.call(···) apply調(diào)用: func.apply(···)
并且只有下列表達式會包含尾調(diào)用:
條件操作符: ? : 邏輯或: || 邏輯與: && 逗號: ,
依次舉例:
const a = x => x ? f() : g(); // f() 和 g() 都在尾部。
const a = () => f() || g(); // g()有可能是尾調(diào)用,f()不是 // 因為上述寫法和下面的寫法等效: const a = () => { const fResult = f(); // not a tail call if (fResult) { return fResult; } else { return g(); // tail call } } // 只有當f()的結(jié)果為falsey的時候,g()才是尾調(diào)用
const a = () => f() && g(); // g()有可能是尾調(diào)用,f()不是 // 因為上述寫法和下面的寫法等效: const a = () => { const fResult = f(); // not a tail call if (fResult) { return g(); // tail call } else { return fResult; } } // 只有當f()的結(jié)果為truthy的時候,g()才是尾調(diào)用
const a = () => (f() , g()); // g()是尾調(diào)用 // 因為上述寫法和下面的寫法等效: const a = () => { f(); return g(); }2. 尾調(diào)用優(yōu)化
函數(shù)在調(diào)用的時候會在調(diào)用棧(call stack)中存有記錄,每一條記錄叫做一個調(diào)用幀(call frame),每調(diào)用一個函數(shù),就向棧中push一條記錄,函數(shù)執(zhí)行結(jié)束后依次向外彈出,直到清空調(diào)用棧,參考下圖:
function foo () { console.log(111); } function bar () { foo(); } function baz () { bar(); } baz();
造成這種結(jié)果是因為每個函數(shù)在調(diào)用另一個函數(shù)的時候,并沒有 return 該調(diào)用,所以JS引擎會認為你還沒有執(zhí)行完,會保留你的調(diào)用幀。
baz() 里面調(diào)用了 bar() 函數(shù),并沒有 return 該調(diào)用,所以在調(diào)用棧中保持自己的調(diào)用幀,同時 bar() 函數(shù)的調(diào)用幀在調(diào)用棧中生成,同理,bar() 函數(shù)又調(diào)用了 foo() 函數(shù),最后執(zhí)行到 foo() 函數(shù)的時候,沒有再調(diào)用其他函數(shù),這里沒有顯示聲明 return,所以這里默認 return undefined。
foo() 執(zhí)行完了,銷毀調(diào)用棧中自己的記錄,依次銷毀 bar() 和 baz() 的調(diào)用幀,最后完成整個流程。
如果對上面的例子做如下修改:
function foo () { console.log(111); } function bar () { return foo(); } function baz () { return bar(); } baz();
這里要注意:尾調(diào)用優(yōu)化只在嚴格模式下有效。
在非嚴格模式下,大多數(shù)引擎會包含下面兩個屬性,以便開發(fā)者檢查調(diào)用棧:
func.arguments: 表示對 func最近一次調(diào)用所包含的參數(shù)
func.caller: 引用對 func最近一次調(diào)用的那個函數(shù)
在尾調(diào)用優(yōu)化中,這些屬性不再有用,因為相關(guān)的信息可能以及被移除了。因此,嚴格模式(strict mode)禁止這些屬性,并且尾調(diào)用優(yōu)化只在嚴格模式下有效。
如果尾調(diào)用優(yōu)化生效,流程圖就會變成這樣:
我們可以很清楚的看到,尾調(diào)用由于是函數(shù)的最后一步操作,所以不需要保留外層函數(shù)的調(diào)用記錄,只要直接用內(nèi)層函數(shù)的調(diào)用記錄取代外層函數(shù)的調(diào)用記錄就可以了,調(diào)用棧中始終只保持了一條調(diào)用幀。
這就叫做尾調(diào)用優(yōu)化,如果所有的函數(shù)都是尾調(diào)用的話,那么在調(diào)用棧中的調(diào)用幀始終只有一條,這樣會節(jié)省很大一部分的內(nèi)存,這也是尾調(diào)用優(yōu)化的意義。
尾遞歸 1. 定義先來看一下遞歸,當一個函數(shù)調(diào)用自身,就叫做遞歸。
function foo () { foo(); }
上面這個操作就叫做遞歸,但是注意了,這里沒有結(jié)束條件,是死遞歸,所以會報棧溢出錯誤的,寫代碼時千萬注意給遞歸添加結(jié)束條件。
那么什么是尾遞歸?
前面我們知道了尾調(diào)用的概念,當一個函數(shù)尾調(diào)用自身,就叫做尾遞歸。
function foo () { return foo(); }2. 作用
那么尾遞歸相比遞歸而言,有哪些不同呢?
我們通過下面這個求階乘的例子來看一下:
function factorial (num) { if (num === 1) return 1; return num * factorial(num - 1); } factorial(5); // 120 factorial(10); // 3628800 factorial(500000); // Uncaught RangeError: Maximum call stack size exceeded
上面是使用遞歸來計算階乘的例子,操作系統(tǒng)為JS引擎調(diào)用棧分配的內(nèi)存是有大小限制的,如果計算的數(shù)字足夠大,超出了內(nèi)存最大范圍,就會出現(xiàn)棧溢出錯誤。
這里500000并不是臨界值,只是我用了一個足夠造成棧溢出的數(shù)。
如果用尾遞歸來計算階乘呢?
"use strict"; function factorial (num, total) { if (num === 1) return total; return factorial(num - 1, num * total); } factorial(5, 1); // 120 factorial(10, 1); // 3628800 factorial(500000, 1); // 分情況 // 注意,雖然說這里啟用了嚴格模式,但是經(jīng)測試,在Chrome和Firefox下,還是會報棧溢出錯誤,并沒有進行尾調(diào)用優(yōu)化 // Safari瀏覽器進行了尾調(diào)用優(yōu)化,factorial(500000, 1)結(jié)果為Infinity,因為結(jié)果超出了JS可表示的數(shù)字范圍 // 如果在node v6版本下執(zhí)行,需要加--harmony_tailcalls參數(shù),node --harmony_tailcalls test.js // node最新版本已經(jīng)移除了--harmony_tailcalls功能
通過尾遞歸,我們把復(fù)雜度從O(n)降低到了O(1),如果數(shù)據(jù)足夠大的話,會節(jié)省很多的計算時間。
由此可見,尾調(diào)用優(yōu)化對遞歸操作意義重大,所以一些函數(shù)式編程語言將其寫入了語言規(guī)格。
尾遞歸的實現(xiàn),往往需要改寫遞歸函數(shù),確保最后一步只調(diào)用自身。
要做到這一點,需要把函數(shù)內(nèi)部所有用到的中間變量改寫為函數(shù)的參數(shù),就像上面的factorial()函數(shù)改寫一樣。
這樣做的缺點就是語義不明顯,要計算階乘的函數(shù),為什么還要另外傳入一個參數(shù)叫total?
解決這個問題的辦法有兩個:
"use strict"; function factorial (num, total = 1) { if (num === 1) return total; return factorial(num - 1, num * total); } factorial(5); // 120 factorial(10); // 36288002. 用一個符合語義的函數(shù)去調(diào)用改寫后的尾遞歸函數(shù)
function tailFactorial (num, total) { if (num === 1) return total; return tailFactorial(num - 1, num * total); } function factorial (num) { return tailFactorial(num, 1); } factorial(5); // 120 factorial(10); // 3628800
上面這種寫法其實有點類似于做了一個函數(shù)柯里化,但不完全符合柯里化的概念。
函數(shù)柯里化是指把接受多個參數(shù)的函數(shù)轉(zhuǎn)換為接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下參數(shù)且返回結(jié)果的新函數(shù)。
概念看著很繞口,我們來個例子感受一下:
// 普通加法函數(shù) function add (x, y, z) { return x + y + z; } add(1, 2, 3); // 6 // 改寫為柯里化加法函數(shù) function add (x) { return function (y) { return function (z) { return x + y + z; } } } add(1)(2)(3); // 6
可以看到,柯里化函數(shù)通過閉包找到父作用域里的變量,最后依次相加輸出結(jié)果。
通過這個例子,可能看不出為什么要用柯里化,有什么好處,這個我們以后再談,這里先引出一個概念。
是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。
如果用柯里化改寫求階乘的例子:
// 柯里化函數(shù) function curry (fn) { var _fnArgLength = fn.length; function wrap (...args) { var _args = args; var _argLength = _args.length; // 如果傳的是所有參數(shù),直接返回fn調(diào)用 if (_fnArgLength === _argLength) { return fn.apply(null, args); } function act (...args) { _args = _args.concat(args); if (_args.length === _fnArgLength) { return fn.apply(null, _args); } return act; } return act; } return wrap; } // 尾遞歸函數(shù) function tailFactorial (num, total) { if (num === 1) return total; return tailFactorial(num - 1, num * total); } // 改寫 var factorial = curry(tailFactorial); factorial(5)(1); // 120 factorial(10)(1); // 3628800
這是符合柯里化概念的寫法,在阮一峰老師的文章中是這樣寫的:
function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n === 1) return total; return tailFactorial(n - 1, n * total); } const factorial = currying(tailFactorial, 1); factorial(5) // 120
我個人認為,這種寫法其實不是柯里化,因為并沒有將多參數(shù)的tailFacrotial改寫為接受單參數(shù)的形式,只是換了一種寫法,和下面這樣寫意義是一樣的:
function factorial (num) { return tailFactorial(num, 1); } function tailFactorial (num, total) { if (num === 1) return total; return tailFactorial(num - 1, num * total); } factorial(5); // 120 factorial(10); // 3628800結(jié)束
這篇文章我們主要討論了尾調(diào)用優(yōu)化和柯里化。
要注意的是,經(jīng)過測試,Chrome和Firefox并沒有對尾調(diào)用進行優(yōu)化,Safari對尾調(diào)用進行了優(yōu)化。
Node高版本也已經(jīng)去除了通過--harmony_tailcalls參數(shù)啟用尾調(diào)用優(yōu)化。
有任何問題,歡迎大家留言討論,另附我的博客網(wǎng)站,快來呀~~
歡迎關(guān)注我的公眾號 參考鏈接http://www.ruanyifeng.com/blo...
https://juejin.im/post/5a4d89...
https://github.com/lamdu/lamd...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/94093.html
摘要:專題系列第十八篇,講解遞歸和尾遞歸定義程序調(diào)用自身的編程技巧稱為遞歸。然而非尾調(diào)用函數(shù),就會創(chuàng)建多個執(zhí)行上下文壓入執(zhí)行上下文棧。所以我們只用把階乘函數(shù)改造成一個尾遞歸形式,就可以避免創(chuàng)建那么多的執(zhí)行上下文。 JavaScript 專題系列第十八篇,講解遞歸和尾遞歸 定義 程序調(diào)用自身的編程技巧稱為遞歸(recursion)。 階乘 以階乘為例: function factorial(n...
摘要:如果三個數(shù)據(jù)相加等于了,就存儲該三個值且更新和指針。邊界條件判斷數(shù)組內(nèi)元素是否都為整數(shù)或負數(shù),直接返回。判斷和指針的大小關(guān)系。在原來數(shù)組上進行排序,不生成副本。 Time:2019/4/3Title:3SumDifficulty: mediumAuthor:小鹿 題目三:ADD Two Numbers Given an array?nums?of?n?integers, are the...
摘要:其實是定義了一個入口文件,這個就不細說了,參考官方文檔這個是英文的,大家可以自行百度其他文檔。 最近在某網(wǎng)站看到了handlebars.js,出于好奇就百度了下這是神馬玩意,結(jié)果讓我很是歡喜,于是就開始自學(xué)下,handlebars就幾個方法,蠻簡單,言歸正傳! 以下是基本教學(xué)邏輯演示,會附完整代碼 測試案例就分為3大塊,頭、主體、尾: 先來講講id=contact主體有些什么內(nèi)...
摘要:步驟如下代碼如下思路二循環(huán)上面的思路同樣可以通過循環(huán)的方式來解決?;静襟E如下代碼如下思路減少遍歷次數(shù)之前的兩種思路,都會出現(xiàn)大量的重復(fù)遍歷,重復(fù)遍歷和葉子節(jié)點的深度成正相關(guān),可以想方法將重復(fù)遍歷的次數(shù)減少。 題目要求 You are given a doubly linked list which in addition to the next and previous pointe...
閱讀 1495·2021-11-24 09:39
閱讀 1807·2021-11-22 15:25
閱讀 3758·2021-11-19 09:40
閱讀 3317·2021-09-22 15:31
閱讀 1328·2021-07-29 13:49
閱讀 1240·2019-08-26 11:59
閱讀 1340·2019-08-26 11:39
閱讀 954·2019-08-26 11:00