尾調(diào)用师坎,是指函數(shù)內(nèi)部的最后一個動作是函數(shù)調(diào)用涩惑。該調(diào)用的返回值射窒,直接返回給函數(shù)渴频。
Example:
function sum(x) {
return sum(x + 1);
}
這里的sum()內(nèi)部的sum就是屬于尾調(diào)用芽丹,ta所返回的值直接返回給調(diào)用ta的上層sum()函數(shù)。
function sum(x) {
return 1 + sum(x + 1);
}
這里的sum()內(nèi)部的sum就不屬于尾調(diào)用卜朗,ta所返回的值在返回給調(diào)用ta的上層sum()函數(shù)之前拔第,需要先跟1計算和,然后再返回场钉。
尾調(diào)用和非尾調(diào)用有什么差異蚊俺?
編寫一個求和函數(shù),有大致3種方式:
-
循環(huán)
function sum(x) { var result = x; while (x >= 1) { result += --x; } return result; }
循環(huán)自然是速度和性能最好的逛万,但是在編寫復(fù)雜的代碼時泳猬,循環(huán)往往模塊化很差,可讀性很差宇植,而且無法體現(xiàn)數(shù)學(xué)上的描述得封。
-
普通遞歸
function sum(x) { if (x === 1) { return 1; } return x + sum(--x); }
普通遞歸時,內(nèi)存需要記錄調(diào)用的堆棧所出的深度和位置信息指郁。在最底層計算返回值忙上,再根據(jù)記錄的信息,跳回上一層級計算闲坎,然后再跳回更高一層疫粥,依次運行洋腮,直到最外層的調(diào)用函數(shù)。在cpu計算和內(nèi)存會消耗很多手形,而且當(dāng)深度過大時啥供,會出現(xiàn)堆棧溢出。
比如库糠,計算sum(5)的時候伙狐,其計算過程是這樣的:
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15在計算的過程中,堆棧一直不停的記錄每一層次的詳細信息瞬欧,以確保該層次的操作完成贷屎,能返回到上一層次。
通過尾遞歸艘虎,可以取消過多的堆棧記錄唉侄,而只記錄最外層和當(dāng)前層的信息,完成計算后野建,立刻返回到最上層属划。從而減少cpu計算和內(nèi)存消耗。
-
尾遞歸
function sum(x, total) { if (x === 1) { return x + total; } return sum(x - 1, x + total); }
這個函數(shù)更具有數(shù)學(xué)描述性:
- 如果輸入值是1 => 當(dāng)前計算數(shù)1 + 上一次計算的和total
- 如果輸入值是x => 當(dāng)前計算數(shù)x + 上一次計算的和total
計算sum(5, 0)的時候候生,其過程是這樣的:
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15整個計算過程是線性的同眯,調(diào)用一次sum(x, total)后,會進入下一個棧唯鸭,相關(guān)的數(shù)據(jù)信息和跟隨進入须蜗,不再放在堆棧上保存。當(dāng)計算完最后的值之后目溉,直接返回到最上層的sum(5,0)明肮。
這能有效的防止堆棧溢出。
而且最happy的是缭付,在ECMAScript 6柿估,我們將迎來尾遞歸優(yōu)化,享受只有LISP HASKELL這些古典函數(shù)式語言才擁有的能力蛉腌。通過尾遞歸優(yōu)化官份,javascript代碼在解釋成機器碼的時候,將會向while看起烙丛,也就是說舅巷,同時擁有數(shù)學(xué)表達能力和while的效能。
使用尾遞歸
這里有一個使用尾遞歸提取絕對文件名的例子河咽,可以來展示下尾遞歸的魅力钠右。這個函數(shù)需要輸入一個(或幾個)目錄名和當(dāng)前所在的文件位置,然后會遞歸提取目錄下的所有文件名忘蟹,放入一個棧中飒房。
var FS = require('fs');
var PATH = require('path');
function readSync(paths, i) {
if (i >= 0 && i < paths.length) {
var stats = FS.statSync(paths[i]);
if (stats.isFile()) {
return readSync(paths, ++i);
} else if (stats.isDirectory()) {
var newPaths = FS.readdirSync(paths[i]).map(function (path) {
return PATH.join(paths[i],path);
});
return readSync(paths.slice(0, i).concat(newPaths,
paths.slice(i + 1)),
i);
} else {
return readSync(paths.slice(0, i).concat(paths.slice(i + 1)),
i);
}
} else {
return paths;
}
}
測試一下搁凸,這個函數(shù)可以在服務(wù)器啟動時,提取某一個(或者幾個)目錄內(nèi)的所有文件狠毯,然后用于編譯或者是其他的操作护糖。
readSync([PATH.join(__dirname, './tpls')], 0).forEach(function (path) {
console.log(path);
});
readSync([PATH.join(__dirname, './tpls'), PATH.join(__dirname, './pages')], 0).forEach(function (path) {
console.log(path);
});