ES6學習筆記之Function

函數的擴展

rest 參數

ES6 引入 rest 參數(形式爲...變量名),用於獲取函數的多餘參數,這樣就不須要使用arguments對象了。rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。編程

const foo = (...values) = {
  console.log(values)
}
foo(1,2,3)  // [1,2,3]
複製代碼

arguments 對象是使用function聲明函數時自動生成的對象, 包含了函數的參數,但結構複雜。在箭頭函數中被rest代替,不可以使用,不然報錯。數組

// arguments變量的寫法
function f1() {
  console.log(arguments)
}
const f11 = () => {
  console.log(arguments)
}

// rest參數的寫法
const f2 = (...numbers) => {
  console.log(numbers)
};
複製代碼

注意:rest 參數只能是最後一個參數,不然會報錯。bash

const foo = (a, ...rest, b) => {}
//  Uncaught SyntaxError: Rest parameter must be last formal parameter
複製代碼

函數參數的默認值

特色

ES6 容許爲函數的參數設置默認值,即直接寫在參數定義的後面。app

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', undefined) // Hello World

log('Hello', '') // Hello
log('Hello', null) // Hello null
log('Hello', 'China') // Hello China

複製代碼

這種寫法有兩個好處:函數式編程

  1. 就算不用閱讀文檔也能夠直觀的看出哪些參數能夠忽略;
  2. 有利於未來的代碼優化,即便將來的版本在對外接口中,完全拿掉這個參數,也不會致使之前的代碼沒法運行。
  • 只有在這個參數沒有設置或者設置爲undefined的時候,默認值纔會生效,跟解構賦值很相近。因此定義默認值的參數,最好是函數的尾參數。否則會出現一下狀況:
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]
複製代碼

另外,一個容易忽略的地方是,參數默認值不是傳值的,而是每次都從新計算默認值表達式的值。也就是說,參數默認值是惰性求值的。函數

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101
複製代碼

與解構賦值默認值結合使用

當參數是一個對象時:優化

function foo({x, y = 5}) {
  console.log(x, y)
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
複製代碼

若是調用函數時沒有給參數就會報錯.
經過提供函數參數的默認值,就能夠避免這種狀況。ui

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5
複製代碼

要注意函數解構設置默認值的寫。
如下有兩種寫法:this

// 寫法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 寫法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
複製代碼

兩個都是給了默認值,可是是有區別的:spa

  • 寫法一函數參數的默認值是空對象,可是設置了對象解構賦值的默認值;
  • 寫法二函數參數的默認值是一個有具體屬性的對象,可是沒有設置對象解構賦值的默認值。
// 函數沒有參數的狀況
m1() // [0, 0]
m2() // [0, 0]

// x 和 y 都有值的狀況
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x 有值,y 無值的狀況
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]

// x 和 y 都無值的狀況
m1({}) // [0, 0];
m2({}) // [undefined, undefined]

m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]

複製代碼

做用域

一旦設置了參數的默認值,函數進行聲明初始化時,參數會造成一個單獨的做用域(context)。等到初始化結束,這個做用域就會消失。

let x = 1;
let y = 3;
function f1(x, y = x) {
  console.log(x, 'x')
  x = 3
  console.log(y, 'y');
}

f1(2) // 2
複製代碼
  1. 函數頭內 xy 的值不受外界影響。函數體內的值也優先爲頭部的值
  2. y 在函數頭就已經賦值,因此在運行時即使x 改變,也不會受影響。

嚴格模式

從 ES5 開始,函數內部能夠設定爲嚴格模式。

function foo(a, b) {
  'use strict';
  // code
}
複製代碼

然而 ES2016 作了一點修改,規定只要函數參數使用了默認值、解構賦值、或者擴展運算符(rest),那麼函數內部就不能顯式設定爲嚴格模式,不然會報錯。

// 報錯
function foo(a, b = a) {
  'use strict';
  // code
}

// 報錯
const foo = function ({a, b}) {
  'use strict';
  // code
};

// 報錯
const foo = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 報錯
  foo({a, b}) {
    'use strict';
    // code
  }
};
複製代碼

兩種方法能夠規避這種限制:
1.設定全局性的嚴格模式

'use strict';

function foo(a, b = a) {
  // code
}
複製代碼

2.把函數包在一個無參數的當即執行函數裏面。

const foo = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());
複製代碼

箭頭函數

基本用法

特色:

  1. 使用=>來定義函數
  2. 參數數量有且只有一個的時候,能夠不使用括號
  3. 若是函數體內直接返回某個值的時候能夠不用寫大括號和return,就表明返回了這個值。(返回對象時須要用圓括號括起來,不然會被識別成代碼塊。)
const f1 = (x) => {
  return x + 1
}

const f2 = x => x + 1

const f3 = x => ({ x })

function f4 (x) {
  return  { x }
}
複製代碼

嵌套的箭頭函數

箭頭函數內部,還能夠再使用箭頭函數。下面是一個 ES5 語法的多重嵌套函數。

function insert(value) {
  return {into: function (array) {
    return {after: function (afterValue) {
      array.splice(array.indexOf(afterValue) + 1, 0, value);
      return array;
    }};
  }};
}

insert(2).into([1, 3]).after(1); //[1, 2, 3]
複製代碼

使用箭頭函數改寫,明顯少了不少代碼:

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]
複製代碼

箭頭函數有幾個使用注意點

  1. 函數體內的this對象,就是定義時所在的對象(他的外層對象),而不是使用時所在的對象。
  2. 不能夠看成構造函數,因我 他沒有本身的this對象。
  3. 不可使用arguments對象,該對象在函數體內不存在。若是要用,能夠用 rest 參數代替。
  4. 不可使用yield命令,所以箭頭函數不能用做 Generator 函數。
var a=11
function f1(){
  this.a=22;
  let b=function(){
    console.log(this.a);
  };
  b();
}
function f2(){
  this.a=22;
  let b=()=>{console.log(this.a)}
  b();
}
var x=new f1();   // 11
var y=new f2();   // 22
複製代碼

this指向的固定化,並非由於箭頭函數內部有綁定this的機制,實際緣由是箭頭函數根本沒有本身的this,致使內部的this就是外層代碼塊的this。正是由於它沒有this,因此也就不能用做構造函數。
因此上面代碼至關於這樣:

function f2(){
  this.a=22;
  let _this = this
  let b=()=>{console.log(_this.a)}
  b();
}

複製代碼

除了this,如下三個變量在箭頭函數之中也是不存在的,指向外層函數的對應變量:argumentssupernew.target

const foo () =>{
  console.log(arguments)
}
foo() // Uncaught ReferenceError: arguments is not defined


function foo () {
  return () => {
    console.log(arguments)
  }
}

foo()()
複製代碼

另外,因爲箭頭函數沒有本身的this,因此固然也就不能用call()、apply()、bind()這些方法去改變this的指向。

(function() {
  return [
    (function () { return this.x }).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['inner']

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']
複製代碼

函數的 length 屬性 和 name 屬性

函數的length 屬性將返回該函數預期傳入的參數個數。

某個參數指定默認值之後,預期傳入的參數個數就不包括這個參數了。同理,rest 參數也不會計入length屬性。

若是 默認參數不是尾參數,那麼默認參數後面的參數也不計入 length

const f1 = (a,b) => {}
f1.length // 2

const f2 = (a, ...rest) => {}
f2.length // 1

const f3 = (a, b=1, c) => {}
f3.length // 1
複製代碼

函數的name屬性,返回該函數的函數名。

function foo() {}
foo.name // "foo"
複製代碼

1.若是將一個匿名函數賦值給一個變量.

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"
複製代碼

2.若是將一個具名函數賦值給一個變量. 都返回函數本來的名字。

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"
複製代碼

3.Function構造函數返回的函數實例,name屬性的值爲anonymous

const foo = new Function
foo.name  //   "anonymous"
複製代碼

4.bind返回的函數,name屬性值會加上bound前綴。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "
複製代碼

尾調用

簡介

尾調用(Tail Call)是函數式編程的一個重要概念,是指某個函數的最後一步是調用另外一個函數。

function f(x){
  return g(x);
}
複製代碼

如下三種狀況,都不屬於尾調用:

const g = (x) => {}

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

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

// 狀況三
function f(x){
  g(x);
}
//  等同於
function f(x){
  g(x);
  return  undefined
}
複製代碼

尾調用優化

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

調用棧: 若是在函數A的內部調用函數B,那麼在A的調用幀上方,還會造成一個B的調用幀。
等到B運行結束,將結果返回到A,B的調用幀纔會消失。
若是函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。
全部的調用幀,就造成一個「調用棧」(call stack)。

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

function g (x) {
  console.log(x)
}

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

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

// 等同於
g(3);
複製代碼

「尾調用優化」的意義: 若是全部函數都是尾調用,那麼就能夠作到每次執行時,調用幀只有一項,這將大大節省內存.

注意:只有再也不用到外層函數的內部變量,內層函數的調用幀纔會取代外層函數的調用幀,不然就沒法進行「尾調用優化」。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;   // 這裏還要使用 外層函數的 one
  }
  return inner(a);
}
複製代碼

尾遞歸

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

遞歸很是耗費內存,由於須要同時保存成千上百個調用幀,很容易發生「棧溢出」錯誤(stack overflow)。可是若是使用尾調用優化,使每次只存在一個調用幀,就不會發生「棧溢出」錯誤。

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

f1(5) // 120

// 尾遞歸
function f2(n, total) {
  if (n === 1) return total;
  console.log(n - 1, n * total)
  return f2(n - 1, n * total);
}
f2(5,1) // 120
複製代碼

這個函數更具備數學描述性:

若是輸入值是1 => 當前計算數1 * 上一次計算的積total 若是輸入值是x => 當前計算數x * 上一次計算的積total 計算f2(5, 1)的時候,其過程是這樣的:

  • f2(5, 1)
  • f2(4, 5)
  • f2(3, 20)
  • f2(2, 60)
  • f2(1, 120)
  • 120

整個計算過程是線性的,調用一次sum(x, total)後,會進入下一個棧,相關的數據信息和跟隨進入,再也不放在堆棧上保存。當計算完最後的值以後,直接返回到最上層的sum(5,0)。

這能有效的防止堆棧溢出。

普通遞歸改寫須要在最後一步調用自身。作到這一點的方法,就是把全部用到的內部變量改寫成函數的參數。這樣作的缺點就是不太直觀。很難看出這些參數是幹什麼的。 有兩個方法能夠解決這問題: 1. 內部要調的參數給個默認值

function f2(n, total=1) {
  if (n === 1) return total;
  return f2(n - 1, n * total);
}
f2(5) // 120
複製代碼

2. 用另一個函數來返回這個函數。

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

function f3 (n) {
  return  f2(n, 1)
}
f3(5) // 120
複製代碼

嚴格模式

以上只是尾調用優化的寫法,可是並無實現真正的優化。

ES6 的尾調用優化只在嚴格模式下開啓,正常模式是無效的。

這是由於在正常模式下,函數內部有兩個變量,能夠跟蹤函數的調用棧。

  • func.arguments:返回調用時函數的參數。
  • func.caller:返回調用當前函數的那個函數。

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

尾遞歸優化的實現

尾遞歸優化只在嚴格模式下生效,那麼正常模式下,或者那些不支持該功能的環境中,有沒有辦法也使用尾遞歸優化呢。
那就只有本身是實現遞歸優化了。

原理:尾遞歸之因此須要優化,緣由是調用棧太多,形成溢出,那麼只要減小調用棧,就不會溢出。怎麼作能夠減小調用棧呢?就是採用「循環」換掉「遞歸」。

下面是一個直接寫的尾遞歸:

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}
sum(1, 1000)  // 1001
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)
複製代碼

一旦指定sum遞歸 100000 次,就會報錯,提示超出調用棧的最大次數。

有兩種方法避免: 1.使用蹦牀函數(trampoline) 將遞歸執行轉爲循環執行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
複製代碼

上面就是蹦牀函數的一個實現,它接受一個函數f做爲參數。只要f執行後返回一個函數,就繼續執行。 注意,這裏是返回一個函數,而後執行該函數,而不是函數裏面調用函數,這樣就避免了遞歸執行,從而就消除了調用棧過大的問題。

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}
//  sum函數的每次執行,都會返回自身的另外一個版本。

trampoline(sum(1, 100000))
// 100001
複製代碼

2.蹦牀函數並非真正的尾遞歸優化,下面的實現纔是

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];
  // console.log(1)
  return function accumulator() {
      // console.log(2, arguments)
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        // console.log(3)
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 10000)
// 100001
複製代碼
相關文章
相關標籤/搜索