JavaScript 專題系列第十八篇,講解遞歸和尾遞歸git
程序調用自身的編程技巧稱爲遞歸(recursion)。github
以階乘爲例:算法
function factorial(n) { if (n == 1) return n; return n * factorial(n - 1) } console.log(factorial(5)) // 5 * 4 * 3 * 2 * 1 = 120
示意圖(圖片來自 wwww.penjee.com):編程
在《JavaScript專題之函數記憶》中講到過的斐波那契數列也使用了遞歸:數組
function fibonacci(n){ return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(5)) // 1 1 2 3 5
從這兩個例子中,咱們能夠看出:app
構成遞歸需具有邊界條件、遞歸前進段和遞歸返回段,當邊界條件不知足時,遞歸前進,當邊界條件知足時,遞歸返回。階乘中的 n == 1
和 斐波那契數列中的 n < 2
都是邊界條件。函數
總結一下遞歸的特色:優化
子問題須與原始問題爲一樣的事,且更爲簡單;this
不能無限制地調用自己,須有個出口,化簡爲非遞歸情況處理。spa
瞭解這些特色能夠幫助咱們更好的編寫遞歸函數。
在《JavaScript深刻之執行上下文棧》中,咱們知道:
當執行一個函數的時候,就會建立一個執行上下文,而且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。
試着對階乘函數分析執行的過程,咱們會發現,JavaScript 會不停的建立執行上下文壓入執行上下文棧,對於內存而言,維護這麼多的執行上下文也是一筆不小的開銷吶!那麼,咱們該如何優化呢?
答案就是尾調用。
尾調用,是指函數內部的最後一個動做是函數調用。該調用的返回值,直接返回給函數。
舉個例子:
// 尾調用 function f(x){ return g(x); }
然而
// 非尾調用 function f(x){ return g(x) + 1; }
並非尾調用,由於 g(x) 的返回值還須要跟 1 進行計算後,f(x)纔會返回值。
二者又有什麼區別呢?答案就是執行上下文棧的變化不同。
爲了模擬執行上下文棧的行爲,讓咱們定義執行上下文棧是一個數組:
ECStack = [];
咱們模擬下第一個尾調用函數執行時的執行上下文棧變化:
// 僞代碼 ECStack.push(<f> functionContext); ECStack.pop(); ECStack.push(<g> functionContext); ECStack.pop();
咱們再來模擬一下第二個非尾調用函數執行時的執行上下文棧變化:
ECStack.push(<f> functionContext); ECStack.push(<g> functionContext); ECStack.pop(); ECStack.pop();
也就說尾調用函數執行時,雖然也調用了一個函數,可是由於原來的的函數執行完畢,執行上下文會被彈出,執行上下文棧中至關於只多壓入了一個執行上下文。然而非尾調用函數,就會建立多個執行上下文壓入執行上下文棧。
函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。
因此咱們只用把階乘函數改形成一個尾遞歸形式,就能夠避免建立那麼多的執行上下文。可是咱們該怎麼作呢?
咱們須要作的就是把全部用到的內部變量改寫成函數的參數,以階乘函數爲例:
function factorial(n, res) { if (n == 1) return res; return factorial2(n - 1, n * res) } console.log(factorial(4, 1)) // 24
然而這個很奇怪吶……咱們計算 4 的階乘,結果函數要傳入 4 和 1,我就不能只傳入一個 4 嗎?
這個時候就要用到咱們在《JavaScript專題之柯里化》中編寫的 curry 函數了:
var newFactorial = curry(factorial, _, 1) newFactorial(5) // 24
若是你看過 JavaScript 專題系列的文章,你會發現遞歸有着不少的應用。
做爲專題系列的第十八篇,咱們來盤點下以前的文章中都有哪些涉及到了遞歸:
function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) }
var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; }
3.JavaScript 專題之從零實現 jQuery 的 extend:
// 非完整版本,完整版本請點擊查看具體的文章 function extend() { ... // 循環遍歷要複製的對象們 for (; i < length; i++) { // 獲取當前對象 options = arguments[i]; // 要求不能爲空 避免extend(a,,b)這種狀況 if (options != null) { for (name in options) { // 目標屬性值 src = target[name]; // 要複製的對象的屬性值 copy = options[name]; if (deep && copy && typeof copy == 'object') { // 遞歸調用 target[name] = extend(deep, src, copy); } else if (copy !== undefined){ target[name] = copy; } } } } ... };
// 非完整版本,完整版本請點擊查看具體的文章 // 屬於間接調用 function eq(a, b, aStack, bStack) { ... // 更復雜的對象使用 deepEq 函數進行深度比較 return deepEq(a, b, aStack, bStack); }; function deepEq(a, b, aStack, bStack) { ... // 數組判斷 if (areArrays) { length = a.length; if (length !== b.length) return false; while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } // 對象判斷 else { var keys = Object.keys(a), key; length = keys.length; if (Object.keys(b).length !== length) return false; while (length--) { key = keys[length]; if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false; } } }
// 非完整版本,完整版本請點擊查看具體的文章 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); } } }
遞歸的內容遠不止這些,好比還有漢諾塔、二叉樹遍歷等遞歸場景,本篇就不過多展開,真但願將來能寫個算法系列。
JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript專題系列預計寫二十篇左右,主要研究平常開發中一些功能點的實現,好比防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的實現方式。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。