摘要:專題系列第十八篇,講解遞歸和尾遞歸定義程序調(diào)用自身的編程技巧稱為遞歸。然而非尾調(diào)用函數(shù),就會創(chuàng)建多個(gè)執(zhí)行上下文壓入執(zhí)行上下文棧。所以我們只用把階乘函數(shù)改造成一個(gè)尾遞歸形式,就可以避免創(chuàng)建那么多的執(zhí)行上下文。
定義JavaScript 專題系列第十八篇,講解遞歸和尾遞歸
程序調(diào)用自身的編程技巧稱為遞歸(recursion)。
階乘以階乘為例:
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):
斐波那契數(shù)列在《JavaScript專題之函數(shù)記憶》中講到過的斐波那契數(shù)列也使用了遞歸:
function fibonacci(n){ return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(5)) // 1 1 2 3 5遞歸條件
從這兩個(gè)例子中,我們可以看出:
構(gòu)成遞歸需具備邊界條件、遞歸前進(jìn)段和遞歸返回段,當(dāng)邊界條件不滿足時(shí),遞歸前進(jìn),當(dāng)邊界條件滿足時(shí),遞歸返回。階乘中的 n == 1 和 斐波那契數(shù)列中的 n < 2 都是邊界條件。
總結(jié)一下遞歸的特點(diǎn):
子問題須與原始問題為同樣的事,且更為簡單;
不能無限制地調(diào)用本身,須有個(gè)出口,化簡為非遞歸狀況處理。
了解這些特點(diǎn)可以幫助我們更好的編寫遞歸函數(shù)。
執(zhí)行上下文棧在《JavaScript深入之執(zhí)行上下文棧》中,我們知道:
當(dāng)執(zhí)行一個(gè)函數(shù)的時(shí)候,就會創(chuàng)建一個(gè)執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時(shí)候,就會將函數(shù)的執(zhí)行上下文從棧中彈出。
試著對階乘函數(shù)分析執(zhí)行的過程,我們會發(fā)現(xiàn),JavaScript 會不停的創(chuàng)建執(zhí)行上下文壓入執(zhí)行上下文棧,對于內(nèi)存而言,維護(hù)這么多的執(zhí)行上下文也是一筆不小的開銷吶!那么,我們該如何優(yōu)化呢?
答案就是尾調(diào)用。
尾調(diào)用尾調(diào)用,是指函數(shù)內(nèi)部的最后一個(gè)動作是函數(shù)調(diào)用。該調(diào)用的返回值,直接返回給函數(shù)。
舉個(gè)例子:
// 尾調(diào)用 function f(x){ return g(x); }
然而
// 非尾調(diào)用 function f(x){ return g(x) + 1; }
并不是尾調(diào)用,因?yàn)?g(x) 的返回值還需要跟 1 進(jìn)行計(jì)算后,f(x)才會返回值。
兩者又有什么區(qū)別呢?答案就是執(zhí)行上下文棧的變化不一樣。
為了模擬執(zhí)行上下文棧的行為,讓我們定義執(zhí)行上下文棧是一個(gè)數(shù)組:
ECStack = [];
我們模擬下第一個(gè)尾調(diào)用函數(shù)執(zhí)行時(shí)的執(zhí)行上下文棧變化:
// 偽代碼 ECStack.push(functionContext); ECStack.pop(); ECStack.push( functionContext); ECStack.pop();
我們再來模擬一下第二個(gè)非尾調(diào)用函數(shù)執(zhí)行時(shí)的執(zhí)行上下文棧變化:
ECStack.push(functionContext); ECStack.push( functionContext); ECStack.pop(); ECStack.pop();
也就說尾調(diào)用函數(shù)執(zhí)行時(shí),雖然也調(diào)用了一個(gè)函數(shù),但是因?yàn)樵瓉淼牡暮瘮?shù)執(zhí)行完畢,執(zhí)行上下文會被彈出,執(zhí)行上下文棧中相當(dāng)于只多壓入了一個(gè)執(zhí)行上下文。然而非尾調(diào)用函數(shù),就會創(chuàng)建多個(gè)執(zhí)行上下文壓入執(zhí)行上下文棧。
函數(shù)調(diào)用自身,稱為遞歸。如果尾調(diào)用自身,就稱為尾遞歸。
所以我們只用把階乘函數(shù)改造成一個(gè)尾遞歸形式,就可以避免創(chuàng)建那么多的執(zhí)行上下文。但是我們該怎么做呢?
階乘函數(shù)優(yōu)化我們需要做的就是把所有用到的內(nèi)部變量改寫成函數(shù)的參數(shù),以階乘函數(shù)為例:
function factorial(n, res) { if (n == 1) return res; return factorial2(n - 1, n * res) } console.log(factorial(4, 1)) // 24
然而這個(gè)很奇怪吶……我們計(jì)算 4 的階乘,結(jié)果函數(shù)要傳入 4 和 1,我就不能只傳入一個(gè) 4 嗎?
這個(gè)時(shí)候就要用到我們在《JavaScript專題之柯里化》中編寫的 curry 函數(shù)了:
var newFactorial = curry(factorial, _, 1) newFactorial(5) // 24應(yīng)用
如果你看過 JavaScript 專題系列的文章,你會發(fā)現(xiàn)遞歸有著很多的應(yīng)用。
作為專題系列的第十八篇,我們來盤點(diǎn)下之前的文章中都有哪些涉及到了遞歸:
1.《JavaScript 專題之?dāng)?shù)組扁平化》:
function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) }
2.《JavaScript 專題之深淺拷貝》:
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 專題之從零實(shí)現(xiàn) jQuery 的 extend:
// 非完整版本,完整版本請點(diǎn)擊查看具體的文章 function extend() { ... // 循環(huán)遍歷要復(fù)制的對象們 for (; i < length; i++) { // 獲取當(dāng)前對象 options = arguments[i]; // 要求不能為空 避免extend(a,,b)這種情況 if (options != null) { for (name in options) { // 目標(biāo)屬性值 src = target[name]; // 要復(fù)制的對象的屬性值 copy = options[name]; if (deep && copy && typeof copy == "object") { // 遞歸調(diào)用 target[name] = extend(deep, src, copy); } else if (copy !== undefined){ target[name] = copy; } } } } ... };
4.《JavaScript 專題之如何判斷兩個(gè)對象相等》:
// 非完整版本,完整版本請點(diǎn)擊查看具體的文章 // 屬于間接調(diào)用 function eq(a, b, aStack, bStack) { ... // 更復(fù)雜的對象使用 deepEq 函數(shù)進(jìn)行深度比較 return deepEq(a, b, aStack, bStack); }; function deepEq(a, b, aStack, bStack) { ... // 數(shù)組判斷 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; } } }
5.《JavaScript 專題之函數(shù)柯里化》:
// 非完整版本,完整版本請點(diǎn)擊查看具體的文章 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); } } }寫在最后
遞歸的內(nèi)容遠(yuǎn)不止這些,比如還有漢諾塔、二叉樹遍歷等遞歸場景,本篇就不過多展開,真希望未來能寫個(gè)算法系列。
專題系列JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript專題系列預(yù)計(jì)寫二十篇左右,主要研究日常開發(fā)中一些功能點(diǎn)的實(shí)現(xiàn),比如防抖、節(jié)流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點(diǎn)是研(chao)究(xi) underscore 和 jQuery 的實(shí)現(xiàn)方式。
如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤?,請?wù)必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對作者也是一種鼓勵(lì)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/91753.html
摘要:專題系列共計(jì)篇,主要研究日常開發(fā)中一些功能點(diǎn)的實(shí)現(xiàn),比如防抖節(jié)流去重類型判斷拷貝最值扁平柯里遞歸亂序排序等,特點(diǎn)是研究專題之函數(shù)組合專題系列第十六篇,講解函數(shù)組合,并且使用柯里化和函數(shù)組合實(shí)現(xiàn)模式需求我們需要寫一個(gè)函數(shù),輸入,返回。 JavaScript 專題之從零實(shí)現(xiàn) jQuery 的 extend JavaScritp 專題系列第七篇,講解如何從零實(shí)現(xiàn)一個(gè) jQuery 的 ext...
摘要:寫在前面專題系列是我寫的第二個(gè)系列,第一個(gè)系列是深入系列。專題系列自月日發(fā)布第一篇文章,到月日發(fā)布最后一篇,感謝各位朋友的收藏點(diǎn)贊,鼓勵(lì)指正。 寫在前面 JavaScript 專題系列是我寫的第二個(gè)系列,第一個(gè)系列是 JavaScript 深入系列。 JavaScript 專題系列共計(jì) 20 篇,主要研究日常開發(fā)中一些功能點(diǎn)的實(shí)現(xiàn),比如防抖、節(jié)流、去重、類型判斷、拷貝、最值、扁平、柯里...
摘要:專題系列第九篇,講解如何實(shí)現(xiàn)數(shù)組的扁平化,并解析的源碼扁平化數(shù)組的扁平化,就是將一個(gè)嵌套多層的數(shù)組嵌套可以是任何層數(shù)轉(zhuǎn)換為只有一層的數(shù)組。 JavaScript 專題系列第九篇,講解如何實(shí)現(xiàn)數(shù)組的扁平化,并解析 underscore 的 _.flatten 源碼 扁平化 數(shù)組的扁平化,就是將一個(gè)嵌套多層的數(shù)組 array (嵌套可以是任何層數(shù))轉(zhuǎn)換為只有一層的數(shù)組。 舉個(gè)例子,假設(shè)有個(gè)...
摘要:不過的實(shí)現(xiàn)中,多了很多細(xì)節(jié)上的判斷,比如第一個(gè)參數(shù)是否是布爾值,是否是一個(gè)對象,不傳參數(shù)時(shí)的默認(rèn)值等。 JavaScritp 專題系列第七篇,講解如何從零實(shí)現(xiàn)一個(gè) jQuery 的 extend 函數(shù) 前言 jQuery 的 extend 是 jQuery 中應(yīng)用非常多的一個(gè)函數(shù),今天我們一邊看 jQuery 的 extend 的特性,一邊實(shí)現(xiàn)一個(gè) extend! extend 基本用...
摘要:專題系列第六篇,講解深淺拷貝的技巧和以及實(shí)現(xiàn)深淺拷貝的思路前言拷貝也是面試經(jīng)典吶數(shù)組的淺拷貝如果是數(shù)組,我們可以利用數(shù)組的一些方法比如返回一個(gè)新數(shù)組的特性來實(shí)現(xiàn)拷貝。所以我們可以看出使用和是一種淺拷貝。 JavaScript 專題系列第六篇,講解深淺拷貝的技巧和以及實(shí)現(xiàn)深淺拷貝的思路 前言 拷貝也是面試經(jīng)典吶! 數(shù)組的淺拷貝 如果是數(shù)組,我們可以利用數(shù)組的一些方法比如:slice、co...
閱讀 3129·2021-11-15 18:14
閱讀 1781·2021-09-22 10:51
閱讀 3293·2021-09-09 09:34
閱讀 3510·2021-09-06 15:02
閱讀 1026·2021-09-01 11:40
閱讀 3194·2019-08-30 13:58
閱讀 2531·2019-08-30 11:04
閱讀 1088·2019-08-28 18:31