1. 程式人生 > >自學-ES6篇-函式的擴充套件

自學-ES6篇-函式的擴充套件

const pipeline = (...funcs) =>
  val => funcs.reduce((a, b) => b(a), val);

const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = pipeline(plus1, mult2);

addThenMult(5)
// 12

如果覺得上面的寫法可讀性比較差,也可以採用下面的寫法。

const plus1 = a => a + 1;
const mult2 = a => a * 2;

mult2(plus1(5))
// 12

箭頭函式還有一個功能,就是可以很方便地改寫λ演算。

// λ演算的寫法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// ES6的寫法
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));
上面兩種寫法,幾乎是一一對應的。由於λ演算對於電腦科學非常重要,這使得我們可以用ES6作為替代工具,探索電腦科學。
6、尾呼叫優化
什麼是尾呼叫?

尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另一個函式。


function f(x){
  return g(x);
}

上面程式碼中,函式f的最後一步是呼叫函式g,這就叫尾呼叫。

以下三種情況,都不屬於尾呼叫。

// 情況一
function f(x){
  let y = g(x);
  return y;
}

// 情況二
function f(x){
  return g(x) + 1;
}

// 情況三
function f(x){
  g(x);
}
上面程式碼中,情況一是呼叫函式g之後,還有賦值操作,所以不屬於尾呼叫,即使語義完全一樣。情況二也屬於呼叫後還有操作,即使寫在一行內。情況三等同於下面的程式碼。
function f(x){
  g(x);
  return undefined;
}

尾呼叫不一定出現在函式尾部,只要是最後一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

上面程式碼中,函式m和n都屬於尾呼叫,因為它們都是函式f的最後一步操作。

尾呼叫優化

尾呼叫之所以與其他呼叫不同,就在於它的特殊的呼叫位置。

我們知道,函式呼叫會在記憶體形成一個“呼叫記錄”,又稱“呼叫幀”(call frame),儲存呼叫位置和內部變數等資訊。如果在函式A的內部呼叫函式B,那麼在A的呼叫幀上方,還會形成一個B的呼叫幀。等到B執行結束,將結果返回到A,B的呼叫幀才會消失。如果函式B內部還呼叫函式C,那就還有一個C的呼叫幀,以此類推。所有的呼叫幀,就形成一個“呼叫棧”(call stack)。

尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫幀,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫幀,取代外層函式的呼叫幀就可以了。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同於
function f() {
  return g(3);
}
f();

// 等同於
g(3);

上面程式碼中,如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除 f(x) 的呼叫幀,只保留 g(3) 的呼叫幀。

這就叫做“尾呼叫優化”(Tail call optimization),即只保留內層函式的呼叫幀。如果所有函式都是尾呼叫,那麼完全可以做到每次執行時,呼叫幀只有一項,這將大大節省記憶體。這就是“尾呼叫優化”的意義。

注意,只有不再用到外層函式的內部變數,內層函式的呼叫幀才會取代外層函式的呼叫幀,否則就無法進行“尾呼叫優化”。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}
上面的函式不會進行尾呼叫優化,因為內層函式inner用到了外層函式addOne的內部變數one

尾遞迴

函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。

遞迴非常耗費記憶體,因為需要同時儲存成千上百個呼叫幀,很容易發生“棧溢位”錯誤(stack overflow)。但對於尾遞迴來說,由於只存在一個呼叫幀,所以永遠不會發生“棧溢位”錯誤。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面程式碼是一個階乘函式,計算n的階乘,最多需要儲存n個呼叫記錄,複雜度 O(n) 。

如果改寫成尾遞迴,只保留一個呼叫記錄,複雜度 O(1)

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

factorial(5, 1) // 120

還有一個比較著名的例子,就是計算fibonacci 數列,也能充分說明尾遞迴優化的重要性

如果是非尾遞迴的fibonacci 遞迴方法

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};

  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10); // 89
// Fibonacci(100)
// Fibonacci(500)
// 堆疊溢位了

如果我們使用尾遞迴優化過的fibonacci 遞迴演算法

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

由此可見,“尾呼叫優化”對遞迴操作意義重大,所以一些函數語言程式設計語言將其寫入了語言規格。ES6也是如此,第一次明確規定,所有ECMAScript的實現,都必須部署“尾呼叫優化”。這就是說,在ES6中,只要使用尾遞迴,就不會發生棧溢位,相對節省記憶體。

遞迴函式的改寫

尾遞迴的實現,往往需要改寫遞迴函式,確保最後一步只調用自身。做到這一點的方法,就是把所有用到的內部變數改寫成函式的引數。比如上面的例子,階乘函式 factorial 需要用到一箇中間變數 total ,那就把這個中間變數改寫成函式的引數。這樣做的缺點就是不太直觀,第一眼很難看出來,為什麼計算5的階乘,需要傳入兩個引數5和1?

兩個方法可以解決這個問題。方法一是在尾遞迴函式之外,再提供一個正常形式的函式。

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

上面程式碼通過一個正常形式的階乘函式 factorial ,呼叫尾遞迴函式 tailFactorial ,看起來就正常多了。

函數語言程式設計有一個概念,叫做柯里化(currying),意思是將多引數的函式轉換成單引數的形式。這裡也可以使用柯里化。

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

上面程式碼通過柯里化,將尾遞迴函式 tailFactorial 變為只接受1個引數的 factorial 。

第二種方法就簡單多了,就是採用ES6的函式預設值。

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

factorial(5) // 120

上面程式碼中,引數 total 有預設值1,所以呼叫時不用提供這個值。

總結一下,遞迴本質上是一種迴圈操作。純粹的函數語言程式設計語言沒有迴圈操作命令,所有的迴圈都用遞迴實現,這就是為什麼尾遞迴對這些語言極其重要。對於其他支援“尾呼叫優化”的語言(比如Lua,ES6),只需要知道迴圈可以用遞迴代替,而一旦使用遞迴,就最好使用尾遞迴。


嚴格模式

ES6的尾呼叫優化只在嚴格模式下開啟,正常模式是無效的。

這是因為在正常模式下,函式內部有兩個變數,可以跟蹤函式的呼叫棧。

  • func.arguments:返回呼叫時函式的引數。
  • func.caller:返回呼叫當前函式的那個函式。

尾呼叫優化發生時,函式的呼叫棧會改寫,因此上面兩個變數就會失真。嚴格模式禁用這兩個變數,所以尾呼叫模式僅在嚴格模式下生效。

function restricted() {
  "use strict";
  restricted.caller;    // 報錯
  restricted.arguments; // 報錯
}
restricted();

尾遞迴優化的實現

相關推薦

自學-ES6-函式擴充套件

const pipeline = (...funcs) => val => funcs.reduce((a, b) => b(a), val); const plus1 = a => a + 1; const mult2 = a => a * 2; const addThe

自學-ES6-非同步操作和Async函式

非同步程式設計對JavaScript語言太重要。Javascript語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。 ES6誕生以前,非同步程式設計的方法,大概有下面四種。 回撥函式事件監聽釋出/訂閱Promise 物件回撥函式 ES6

自學-ES6-陣列的擴充套件

Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1這個方法的主要目的,是彌補陣列建構函式Array()的不足。因為引數個數的不同,會導致Array()的行為有差異。Array() // [] Array(3) //

自學-ES6-let和conts命令

// ES6嚴格模式 'use strict'; if (true) { function f() {} } // 不報錯並且ES6規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。function f() { console.log('I am outside!'); }

ES6正則擴充套件(建構函式的變化)

1、ES5中正則表示式的寫法 //第一個引數表示要匹配規則字串,第二個引數是修飾符(i表示不區分大小寫進行匹配) let reg1 = new RegExp('abc','i'); let reg2 = new RegExp(/abc/i); //這樣的寫法只能有一個引數 let reg

es6 入坑筆記(二)---函式擴充套件,箭頭函式擴充套件運算子...

函式擴充套件 1.函式可以有預設值 function demo( a = 10,b ){} 2.函式可以使用解構  function demo( { a = 0,b = 0 } = {} ){ } 3.函式引數最後可以多一個逗號 function demo(a,b,

es6——函式擴充套件

1.形參設定預設值 es5 { function sum1(num1, num2) { num1 = num1 || 10;

es6 函式擴充套件,引數作用域和箭頭函式

函式的擴充套件 函式引數的預設值 基本用法 ES6 之前,不能直接為函式的引數指定預設值,只能採用變通的方法。 function log(x, y = 'World') { console.log(x, y); } log('Hello

ES6函式擴充套件

函式引數的預設值 function log(x, y) { y = y || 'world' console.log(x + ' ' + y); } log('hello') // hello world log('hello','China')

ECMAScript6(ES6)標準之函式擴充套件特性箭頭函式、Rest引數及展開操作符

ES6擴充套件了很多語法糖語法 其中對於函式我們又可以使用一種叫做“箭頭函式”的寫法 同時引入了Rest引數 利用“…”可以獲取多餘引數 這樣就我們就不要使用arguments物件了 下面我來詳細地談一談 函式預設引數 ES6沒有出現之前 面

ES6 函式擴充套件

函式預設值 ES6 與ES5 的區別 在ES6之前,不能直接為函式的指定預設值 ES5預設值方法 //方法一 function log(x, y) { y = y || 'value'; console.log(x, y); } func

ES6函式擴充套件(1)

1.函式引數的預設值 在ES6之前,不能直接為函式的引數指定預設值,只能採用變通的方法。 function log(x, y) { y = y || 'World'; console.log(x, y); } log('Hello') // Hello Worl

5. es6函式擴充套件

1.1函式引數的預設值 es6之前,不能直接為函式引數提供預設值,只能採用變通的方法 //es5寫法 function log(x,y){ if(typeof y === 'undefined'){ y = 'world';

es6可變引數-擴充套件運算子

es5中引數不確定個數的情況下: //求引數和 function f(){ var a = Array.prototype.slice.call(arguments); var sum = 0; a.forEach(function(item){ sum += item*1;

Python基礎--函式簡介

python函式     python中函式有兩種,一種是內建函式,一種是自定義函式。這裡不曉得有沒有大牛知道為什麼內建函式可以直接呼叫,沒見在哪裡定義了。另外一種是自定義函式,函式的定義方式如下,關鍵字def後面跟空格,再跟函式名,引數,函式體,函式名的命名可以使用posix

ES6的陣列擴充套件( entries(),keys(),values() )

ES6 提供三個新的方法 —— entries(),keys()和values() —— 用於**遍歷陣列**,它們都返回一個遍歷器物件(Array Iterator),可以用for…of迴圈進行遍歷,唯一的區別是keys()是對鍵名的遍歷、values()是對鍵值的遍歷,entries()

ES6的陣列擴充套件( fill()方法 )

fill()函式,使用指定的元素替換原陣列內容,會改變原來的陣列。 該函式有三個引數: fill(value, start, end) value:替換值。 start:替換起始位置(陣列的下標),可以省略。 end:替換結束位置(陣列的下標),如果省略不寫就預設為陣列結束。有引數時為結

ES6的陣列擴充套件( Array.from()方法 )

Array.from()方法用於將類陣列和可遍歷的集合物件轉為真正的陣列,這樣他們就能夠使用陣列中的方法來處理資料了。 一、把獲取到元素的類陣列集合轉化為真正的陣列 let aP = document.querySelectorAll("p"); var arrP = Array.f

ES6的陣列擴充套件( Array.of()方法 )

Array.of() 方法建立一個具有可變數量引數的新陣列例項,而不考慮引數的數量或型別。 Array.of() 和 Array 建構函式之間的區別在於處理**整數引數**。 let arr1 = Array.of(3); let arr11 = Array.of("a"); let a

ES6正則擴充套件(新增修飾符y、u)

1、y修飾符 y :也是全域性匹配,首次匹配和g修飾符效果一樣,但是第二次往後就不一樣了, g修飾符不一定要求匹配下標緊接著上一次開始匹配的去找,只要找到了就行; 而y修飾符是規定要求匹配下標緊接著上一次匹配的開始 去匹配,不合適條件就為匹配失敗為null。 從上圖程式碼第二段列印two