ES6 之 函數的擴展 尾調用以及尾遞歸

函數參數的默認值

    function log(x, y) {
      y = y || 'world'
      console.log(x + ' ' + y);
    }
    log('hello') // hello world
    log('hello','China') // hello China
    log('hello', '') // hello world
    /*
    * 若是 y 沒有賦值,指定默認值爲world // log('hello') // hello world
    * 若是 y 有賦值,指定賦值  log('hello','China') // hello China
    * 若是 y 有賦值,可是爲 boolean 值 false,則該賦值就不起做用了  log('hello', '') // hello world
    * */

函數的length屬性

console.log((function (a) {}).length) // 1
console.log((function (a = 1) {}).length) // 0 
console.log((function (a, b = 1, c) {}).length) //1
console.log((function (a, b , c = 2) {}).length) // 2
// 返回沒有賦值形參的個數
// 若是設置了默認值的參數不是尾參數,那麼length屬性也再也不計入後面的參數

做用域

賦值的形參造成單獨的做用域編程

rest參數

ES6 引入rest參數,(形式爲「...變量名」),用於獲取函數的多餘參數,這樣就不須要arguments對象了。rest參數搭配的變量是一個數組,該變量將多餘的參數放入其中
    function add(...value) {
      let sum = 0
      for(let val of value) {
        sum += val
      }
      return sum
    }
    const NUM = add(2, 5, 8, 90)
    console.log(NUM)

 

箭頭函數

var f = v => v;
// 若是見箭頭函數不須要參數 或者 須要對個參數,就是用圓括號表明參數部分
var sum = (num1, num2) => num1 + num2
// 若是箭頭函數的代碼塊部分多餘一條語句,就要使用大括號將其闊起來,
// 並使用return語句返回
// 若是箭頭函數直接返回一個對象,必須在對象外面加上括號
var getTempItem = id => ({ id: id, name: "Temp"})

// 箭頭函數 注意事項
/*
* 函數體內的this對象就是定義時所在的對象,而不是使用時所在的隊形
* 不能夠當作構造函數
* 不能夠使用arguments對象
* 不能夠使用yield命令,所以箭頭函數態用做Generator函數
* 箭頭函數可讓this指向固定化,這種形式很是有利於封裝回調函數
* 箭頭函數能夠綁定this,大大減小了顯示綁定this對象的寫法(call,apply,bind)*/

《ES6標準入門》 第三版數組

箭頭函數有幾個使用注意點。
(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不能夠看成構造函數,也就是說,不能夠使用new命令,不然會拋出一個錯誤。
(3)不能夠使用arguments對象,該對象在函數體內不存在。若是要用,能夠用 rest 參數代替。
(4)不能夠使用yield命令,所以箭頭函數不能用做 Generator 函數。
上面四點中,第一點尤爲值得注意。this對象的指向是可變的,可是在箭頭函數中,它是固定的。app

箭頭函數可讓setTimeout裏面的this,綁定定義時所在的做用域,而不是指向運行時所在的做用域。函數式編程

箭頭函數可讓this指向固定化,這種特性頗有利於封裝回調函數。函數

綁定this

箭頭函數能夠綁定this對象,大大減小了顯式綁定this對象的寫法(call、apply、bind)。優化

可是,箭頭函數並不適用於全部場合,因此如今有一個 提案,提出了「函數綁定」(function bind)運算符,用來取代call、apply、bind調用。this

函數綁定運算符是並排的兩個冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,做爲上下文環境(即this對 象),綁定到右邊的函數上面。spa

若是雙冒號左邊爲空,右邊是一個對象的方法,則等於將該方法綁定在該對象上面。rest

尾調用及優化

就是指某個函數的後一步是調用另外一個函數。code

函數調用會在內存造成一個「調用記錄」,又稱「調用幀」(call frame),保存調用位置和內部變量等信息。

若是在函數A的內部調用函數B,那 麼在A的調用幀上方,還會造成一個B的調用幀。

等到B運行結束,將結果返回到A,B的調用幀纔會消失。若是函數B內部還調用函數C,那就還有一 個C的調用幀,

以此類推。全部的調用幀,就造成一個「調用棧」(call stack)。


尾調用因爲是函數的後一步操做,因此不須要保留外層函數的調用幀,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用 幀,取代外層函數的調用幀就能夠了。

 

只保留內層函數的調用幀。若是全部函數都是尾調用,那麼徹底能夠作到每次執行時,調用幀只有 一項,這將大大節省內存。這就是「尾調用優化」的意義。
注意,只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」。

尾遞歸及優化

函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。

遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。但對於尾遞歸來講,因爲只存在一個調用幀, 因此永遠不會發生「棧溢出」錯誤。

/* ----------- 普通遞歸 ------------------ */
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

let num1 = factorial(5)
console.time(1); //設置時間起點
console.log(num1); // 120
console.timeEnd(1); // 3.450ms
/* 上面代碼是一個階乘函數,計算n的階乘,多須要保存n個調用記錄,複雜度 O(n) 。 */


/* ----------- 尾遞歸 ------------------ */
/* 若是改寫成尾遞歸,只保留一個調用記錄,複雜度 O(1) 。 */
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

let num2 = factorial(5, 1) // 120
console.time(1); //設置時間起點
console.log(num2); // 120
console.timeEnd(1); // 0.275ms

ES6 中只要使用尾遞歸,就不會發生棧溢出,相對節省內存。

/* 非尾遞歸的 費氏數列 Fibonacci */
function Fibonacci(n) {
  if (n <= 1) {
    return 1
  };
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

// let num21 = Fibonacci(10) // 573147844013817200000
// console.time(1); //設置時間起點
// console.log(num21); // 89
// console.timeEnd(1); // 4.030ms


// let num22 = Fibonacci(100) // 573147844013817200000
// console.time(2); //設置時間起點
// console.log(num22); // 堆棧溢出 
// console.timeEnd(2); //  4.堆棧溢出 

// let num23 = Fibonacci2(1000) // 7.0330367711422765e+208
// console.time(3); //設置時間起點
// console.log(num23); // 堆棧溢出 
// console.timeEnd(3); //  堆棧溢出 


/* 尾遞歸優化過的 Fibonacci 數列實現以下 */
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  };
  return Fibonacci2(n - 1, ac2, ac1 + ac2);
}

// let num21 = Fibonacci2(10) // 573147844013817200000
// console.time(1); //設置時間起點
// console.log(num21); // 89
// console.timeEnd(1); // 3.469ms


// let num22 = Fibonacci2(100) // 573147844013817200000
// console.time(2); //設置時間起點
// console.log(num22); // 573147844013817200000
// console.timeEnd(2); //  4.511ms

// let num23 = Fibonacci2(1000) // 7.0330367711422765e+208
// console.time(3); //設置時間起點
// console.log(num23); // 7.0330367711422765e+208
// console.timeEnd(3); //  4.053ms

/* ES6 是如此,第一次明確規定,全部 ECMAScript 的實 現,都必須部署「尾調用優化」。這就是說,ES6 中只要使用尾遞歸,就不會發生棧溢出,相對節省內存。 */

 

/* 尾遞歸的實現,每每須要改寫遞歸函數,確保後一步只調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。 */

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
相關文章
相關標籤/搜索