1. 程式人生 > 其它 >18-2 尾呼叫/尾遞迴/尾遞迴優化/柯里化函式/蹦床函式

18-2 尾呼叫/尾遞迴/尾遞迴優化/柯里化函式/蹦床函式

1.尾呼叫:就是指某個函式的最後一步是呼叫另一個函式的,並且呼叫的這個函式必須被return出去的 尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。 情況一:
  function func1(x) {
    let y = g(x); //g函式呼叫後還有賦值的操作,不符合
    return y;
  }

  // 情況二
  function func2(x) {
    return g(x) + 1; //g執行也進行了+1操作,不符合
  }

  // 情況三
  function func3(x) {
    g(x); // 返回了unfined,不符合
  }
2.尾呼叫優化:
尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置 我們知道,函式呼叫時候會在記憶體形成一個“呼叫記錄”,又稱“呼叫幀”,儲存呼叫位置和內部變數等資訊。 如果函式A的內部呼叫函式B,那麼在A的呼叫呼叫幀上方,還會形成一個B的呼叫幀。等到B執行結束,將結果返回到A, B的呼叫幀才會消失。如果B的內部還有函式C,那就還有一個C的呼叫幀,以次類推,所有的呼叫幀就形成了“呼叫棧”。   尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫幀了,因為呼叫位置,內部變數等資訊都不會再用到了, 只要直接用內層函式的呼叫幀,取代外層函式的呼叫幀就可以了。
  function fun() {
    let m 
= 1; let n = 2; return g(m + n); } fun(); // 等同於 function fun2() { return g(3); } fun2(); // 等同於 g(3);
 3.尾遞迴 : 函式呼叫自身成為遞迴。如果尾呼叫自身,就稱為尾遞迴。
// 複雜度O/n
function fn(n) {
    if (!n) return 1;
    return fn(n - 1);
  }
  let res = fn(1000);
  console.log(res);  // 1

尾遞迴改寫:

  尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只調用自身。做到這一點的方法,就是把所有用到的內部變數改寫成函式的引數。
  如下面這個例子:
  // 複雜度 O/1
  function fn1(n, m) {
    if (!n) return m;
    return fn1(n - 1, m);
  }
  let res1 = fn1(100, 1);  //如果傳入fn1(1000000,1) 輸出:Maximum call stack size exceeded,原因分析請看下面[蹦床函式的由來:是解決什麼問題的?]
  console.log(res1); // 1
所以問題就又來了,上面雖然做了把內部變數變為了函式的引數,但是這樣的缺點就是不太直觀,第一眼很難看出來,憑什麼要多傳一個1? 於是我們有兩個方式可以解決這個問題:  (1)第一種:用一箇中間函式包裝一下,執行這個中間函式傳入一個值
  function fn1(n, m) {
    if (!n) return m;
    return fn1(n - 1, m);
  }
  function tempFn(n) {
    return fn1(n, 1)
  }
  tempFn(100)
(2)第二種:運用4.函式的柯里化思想
 function curryFn(fn) {
    let outerArgs = Array.prototype.slice.call(arguments, 1)
    return (...args) => {
      return fn.call(this, ...args, ...outerArgs)
    }
  }
  function fn1(n, m) {
    if (!n) return m;
    return fn1(n - 1, m);
  }
  var cFn = curryFn(fn1, 1)  //這個也可以說是偏函式,把固定要傳的引數預置好
  var res = cFn(100)
  console.log(res);  // 1
第三種:通過函式引數的預設值
  function fn1(n, m = 1) {
    if (!n) return m;
    return fn1(n - 1, m)
  }
  let res = fn1(100)
  console.log(res); //1

注意:

ES5中不存在尾遞迴,並且ES6的尾呼叫優化只在嚴格模式下開啟,正常模式是無效的。   - 這是因為在正常模式下,函式內部有兩個變數,可以跟蹤函式的呼叫棧   func.arguments:返回呼叫時函式的引數。   func.caller:返回呼叫當前函式的那個函式。   - 尾呼叫優化發生時,函式的呼叫棧會改寫,因此上面這兩個變數就會失真。嚴格模式禁用了這倆變數,所以尾呼叫模式只在嚴格模式下生效
  function restricted() {
    "use strict";
    restricted.caller; // 報錯
    restricted.arguments; // 報錯
  }
  restricted();
既然尾遞迴優化只在嚴格模式下生效,那麼正常模式下,或者那些不支援該功能的環境中,我們要想使用遞迴尾優化,需要自己實現尾遞迴優化: 那我們想了,尾遞迴之所以需要優化,原因無非就是呼叫棧太多,造成溢位,那麼只要減少呼叫棧,就不會溢位。怎麼做可以減少呼叫棧呢? 就是採用“迴圈”替換掉“遞迴”
// 先看一個正常遞迴函式
  function sum(x, y) {
    if (y > 0) {
      return sum(x + 1, y - 1)
    } else {
      return x
    }
  }
  let r = sum(1, 10000)
  console.log(r);  // Maximum call stack size exceeded

// ----------------------------------------------------------------------------------------------------------------------- // 蹦床函式可以將遞迴執行轉為迴圈執行:本質就是將原來的遞迴函式,通過fn.bind()改寫為每一步返回另一個函式 function trampoline(f) { while (f && f instanceof Function) { f = f() } return f } // 通過bind返回一個新函式,而不是在sum中又調了sum自身,這樣就避免了遞迴執行。從而就消除呼叫棧過大的問題 function sum(x, y) { if (y > 0) { return sum.bind(null, x + 1, y - 1) } else { return x } } var result = trampoline(sum(1, 10000)) console.log(result); //就不會發生棧溢位,輸出:10001
 5.蹦床函式的由來:是解決什麼問題的?
// 看下面的例子:
  function testFn(n) {
    if (!n) return n;
    return testFn(n - 1)
  }
  var res = testFn(123456)
  console.log(res);  // Maximum call stack size exceeded
分析:為什麼會報"Maximum call stack size exceeded"的錯誤?   - 我覺得原因是在每次遞迴的時候,會把當前作用域裡面的基本型別的值推進棧中,所以一旦遞迴層數過多,桟就會溢位,所以會報錯   注意:     1. js中的桟只會儲存基本型別的值:number,string,undefined,null,boolean     2. 為什麼在呼叫下一層遞迴函式的時候沒有釋放上一層遞迴函式的作用域?因為在回來的時候還需要用到裡面的變數。
 // 改寫:
  function testFn(n) {
    if (!n) return n;
    return function () {
      return testFn(n - 1)
    }
  }
  var res = testFn(2)()()
  console.log(res); // 0
分析:這個改寫就是建立一個閉包來封裝遞迴函式,他的好處是由於不是直接的呼叫,所以不會把上一次的遞迴作用域           推進棧中,而是把封裝函式儲存在堆記憶體中,利用這個容量更大但讀取時間更慢的儲存形式來替代棧這個容量小           但讀取速度快的儲存形式,用時間來換取空間。     結論: 通過上面的改寫示例,如果引數不是2(需要執行2次,才能走到if語句中),我們就需要寫多個括號來呼叫很多次。   如果testFn(12345678) 你要想執行到最後的!n-> n為0,那麼後面需要執行12345678次  testFn(12345678)()()()....() 12345678次   為了簡便,我們可以把這種呼叫方式,寫成函式 ->蹦床函式:
function bengchuangFn(f) {
    while (f && f instanceof Function) {
      f = f()
    }
    return f
  }
  function testFn(n) {
    if (!n) return n;
    return function () {
      return testFn(n - 1)
    }
  }

  console.log(bengchuangFn(testFn(12345678)));   // 由於儲存在堆中,所以耗時較長,過了一會才會輸出 0,但是並不會報桟記憶體溢位的錯誤。 
另外一種寫法:
 function bengchuangFn2(f, num) {
    let result = f.call(f, num);
    while (result && typeof result === 'function') {
      result = result()
    }
    return result
  }
  function testFn(n) {
    if (!n) return n;
    return function () {
      return testFn(n - 1)
    }
  }
  bengchuangFn2(testFn, 123456)  //過一會輸出0