尾調用和尾遞歸

尾調用

1. 定義

尾調用是函數式編程中一個很重要的概念,當一個函數執行時的最後一個步驟是返回另外一個函數的調用,這就叫作尾調用。html

注意這裏函數的調用方式是無所謂的,如下方式都可:node

函數調用:     func(···)
方法調用:     obj.method(···)
call調用:     func.call(···)
apply調用:    func.apply(···)
複製代碼

而且只有下列表達式會包含尾調用:git

條件操做符:      ? :
邏輯或:         ||
邏輯與:         &&
逗號:           ,
複製代碼

依次舉例:github

const a = x => x ? f() : g();

// f() 和 g() 都在尾部。
複製代碼
const a = () => f() || g();

// g()有多是尾調用,f()不是

// 由於上述寫法和下面的寫法等效:

const a = () => {
    const fResult = f(); // not a tail call
    if (fResult) {
        return fResult;
    } else {
        return g(); // tail call
    }
}

// 只有當f()的結果爲falsey的時候,g()纔是尾調用
複製代碼
const a = () => f() && g();

// g()有多是尾調用,f()不是

// 由於上述寫法和下面的寫法等效:

const a = () => {
    const fResult = f(); // not a tail call
    if (fResult) {
        return g(); // tail call
    } else {
        return fResult;
    }
}

// 只有當f()的結果爲truthy的時候,g()纔是尾調用
複製代碼
const a = () => (f() , g());

// g()是尾調用

// 由於上述寫法和下面的寫法等效:

const a = () => {
    f();
    return g();
}
複製代碼

2. 尾調用優化

函數在調用的時候會在調用棧(call stack)中存有記錄,每一條記錄叫作一個調用幀(call frame),每調用一個函數,就向棧中push一條記錄,函數執行結束後依次向外彈出,直到清空調用棧,參考下圖:編程

function foo () { console.log(111); }
function bar () { foo(); }
function baz () { bar(); }

baz();
複製代碼

call stack

形成這種結果是由於每一個函數在調用另外一個函數的時候,並無 return 該調用,因此JS引擎會認爲你尚未執行完,會保留你的調用幀。瀏覽器

baz() 裏面調用了 bar() 函數,並無 return 該調用,因此在調用棧中保持本身的調用幀,同時 bar() 函數的調用幀在調用棧中生成,同理,bar() 函數又調用了 foo() 函數,最後執行到 foo() 函數的時候,沒有再調用其餘函數,這裏沒有顯示聲明 return,因此這裏默認 return undefinedbash

foo() 執行完了,銷燬調用棧中本身的記錄,依次銷燬 bar()baz() 的調用幀,最後完成整個流程。微信

若是對上面的例子作以下修改:閉包

function foo () { console.log(111); }
function bar () { return foo(); }
function baz () { return bar(); }

baz();
複製代碼

這裏要注意:尾調用優化只在嚴格模式下有效。app

在非嚴格模式下,大多數引擎會包含下面兩個屬性,以便開發者檢查調用棧:

  • func.arguments: 表示對 func最近一次調用所包含的參數
  • func.caller: 引用對 func最近一次調用的那個函數

在尾調用優化中,這些屬性再也不有用,由於相關的信息可能以及被移除了。所以,嚴格模式(strict mode)禁止這些屬性,而且尾調用優化只在嚴格模式下有效。

若是尾調用優化生效,流程圖就會變成這樣:

call stack

咱們能夠很清楚的看到,尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用記錄,只要直接用內層函數的調用記錄取代外層函數的調用記錄就能夠了,調用棧中始終只保持了一條調用幀。

這就叫作尾調用優化,若是全部的函數都是尾調用的話,那麼在調用棧中的調用幀始終只有一條,這樣會節省很大一部分的內存,這也是尾調用優化的意義

尾遞歸

1. 定義

先來看一下遞歸,當一個函數調用自身,就叫作遞歸。

function foo () {
    foo();
}
複製代碼

上面這個操做就叫作遞歸,可是注意了,這裏沒有結束條件,是死遞歸,因此會報棧溢出錯誤的,寫代碼時千萬注意給遞歸添加結束條件。

那麼什麼是尾遞歸? 前面咱們知道了尾調用的概念,當一個函數尾調用自身,就叫作尾遞歸

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
複製代碼

上面是使用遞歸來計算階乘的例子,操做系統爲JS引擎調用棧分配的內存是有大小限制的,若是計算的數字足夠大,超出了內存最大範圍,就會出現棧溢出錯誤。

這裏500000並非臨界值,只是我用了一個足夠形成棧溢出的數。

若是用尾遞歸來計算階乘呢?

'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);           // 分狀況

// 注意,雖說這裏啓用了嚴格模式,可是經測試,在Chrome和Firefox下,仍是會報棧溢出錯誤,並無進行尾調用優化
// Safari瀏覽器進行了尾調用優化,factorial(500000, 1)結果爲Infinity,由於結果超出了JS可表示的數字範圍
// 若是在node v6版本下執行,須要加--harmony_tailcalls參數,node --harmony_tailcalls test.js
// node最新版本已經移除了--harmony_tailcalls功能
複製代碼

經過尾遞歸,咱們把複雜度從O(n)下降到了O(1),若是數據足夠大的話,會節省不少的計算時間。 因而可知,尾調用優化對遞歸操做意義重大,因此一些函數式編程語言將其寫入了語言規格。

避免改寫遞歸函數

尾遞歸的實現,每每須要改寫遞歸函數,確保最後一步只調用自身。 要作到這一點,須要把函數內部全部用到的中間變量改寫爲函數的參數,就像上面的factorial()函數改寫同樣。

這樣作的缺點就是語義不明顯,要計算階乘的函數,爲何還要另外傳入一個參數叫total? 解決這個問題的辦法有兩個:

1. ES6參數默認值

'use strict';

function factorial (num, total = 1) {
    if (num === 1) return total;
    return factorial(num - 1, num * total);
}

factorial(5);                // 120
factorial(10);               // 3628800
複製代碼

2. 用一個符合語義的函數去調用改寫後的尾遞歸函數

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
複製代碼

上面這種寫法其實有點相似於作了一個函數柯里化,但不徹底符合柯里化的概念。 函數柯里化是指把接受多個參數的函數轉換爲接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下參數且返回結果的新函數。

概念看着很繞口,咱們來個例子感覺一下:

// 普通加法函數
function add (x, y, z) {
    return x + y + z;
}

add(1, 2, 3);        // 6

// 改寫爲柯里化加法函數
function add (x) {
    return function (y) {
        return function (z) {
            return x + y + z;
        }
    }
}

add(1)(2)(3);        // 6
複製代碼

能夠看到,柯里化函數經過閉包找到父做用域裏的變量,最後依次相加輸出結果。 經過這個例子,可能看不出爲何要用柯里化,有什麼好處,這個咱們之後再談,這裏先引出一個概念。

是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。

若是用柯里化改寫求階乘的例子:

// 柯里化函數
function curry (fn) {
    var _fnArgLength = fn.length;

    function wrap (...args) {
        var _args = args;
        var _argLength = _args.length;
        // 若是傳的是全部參數,直接返回fn調用
        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;
}

// 尾遞歸函數
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
複製代碼

我我的認爲,這種寫法其實不是柯里化,由於並無將多參數的tailFacrotial改寫爲接受單參數的形式,只是換了一種寫法,和下面這樣寫意義是同樣的:

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
複製代碼

結束

這篇文章咱們主要討論了尾調用優化和柯里化。 要注意的是,通過測試,Chrome和Firefox並無對尾調用進行優化,Safari對尾調用進行了優化。 Node高版本也已經去除了經過--harmony_tailcalls參數啓用尾調用優化。

有任何問題,歡迎你們留言討論,另附個人博客網站,快來呀~~

歡迎關注個人公衆號

微信公衆號

參考連接

www.ruanyifeng.com/blog/2015/0… juejin.im/post/5a4d89… github.com/lamdu/lamdu…

相關文章
相關標籤/搜索