1. 程式人生 > >ES6之主要知識點(五)函數

ES6之主要知識點(五)函數

call() line [] 簡寫 過大 get color 復雜 obj

函數參數的默認值

作用域

var x = 1;

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

f(2) // 2
let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1
var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1
var x = 1;
function foo(x, y = function() { x = 2
; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1

應用

function throwIfMissing() {
  throw new Error(Missing parameter);
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

上面代碼的foo函數,如果調用的時候沒有參數,就會調用默認值throwIfMissing函數,從而拋出一個錯誤。

2.rest 參數

ES6 引入 rest 參數(形式為...變量名),用於獲取函數的多余參數,這樣就不需要使用arguments對象了。

rest 參數搭配的變量是一個數組,該變量將多余的參數放入數組中。

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];

註意,rest 參數之後不能再有其他參數(即只能是最後一個參數),否則會報錯。

3.箭頭函數

x => x * x

上面的箭頭函數相當於:

function (x) {
    return x * x;
}

如果參數不是一個,就需要用括號()括起來:

// 兩個參數:
(x, y) => x * x + y * y

// 無參數:
() => 3.14

// 可變參數:
(x, y, ...rest) => {
    var i, sum = x + y;
    for (i=0; i<rest.length; i++) {
        sum += rest[i];
    }
    return sum;
}

如果要返回一個對象,就要註意,如果是單表達式,這麽寫的話會報錯:

// SyntaxError:
x => { foo: x }

因為和函數體的{ ... }有語法沖突,所以要改為:

// ok:
x => ({ foo: x })

箭頭函數看上去是匿名函數的一種簡寫,但實際上,箭頭函數和匿名函數有個明顯的區別:箭頭函數內部的this是詞法作用域,由上下文確定。

回顧前面的例子,由於JavaScript函數對this綁定的錯誤處理,下面的例子無法得到預期結果:

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }

現在,箭頭函數完全修復了this的指向,this總是指向詞法作用域,也就是外層調用者obj

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj對象
        return fn();
    }
};
obj.getAge(); // 25

使用註意點

箭頭函數有幾個使用註意點。

(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。

(2)不可以當作構造函數,也就是說,不可以使用new命令,否則會拋出一個錯誤。

(3)不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。

(4)不可以使用yield命令,因此箭頭函數不能用作 Generator 函數。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭頭函數
  setInterval(() => this.s1++, 1000);
  // 普通函數
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log(s1: , timer.s1), 3100);
setTimeout(() => console.log(s2: , timer.s2), 3100);
// s1: 3
// s2: 0

上面代碼中,Timer函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。前者的this綁定定義時所在的作用域(即Timer函數)

,後者的this指向運行時所在的作用域(即全局對象)。所以,3100毫秒之後,timer.s1被更新了3次,而timer.s2一次都沒更新。

另外,由於箭頭函數沒有自己的this,所以當然也就不能用call()apply()bind()這些方法去改變this的指向。

4.綁定 this

函數綁定運算符是並排的兩個冒號(::),雙冒號左邊是一個對象,右邊是一個函數。該運算符會自動將左邊的對象,

作為上下文環境(即this對象),綁定到右邊的函數上面。

foo::bar;
// 等同於
bar.bind(foo);

foo::bar(...arguments);
// 等同於
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

5.尾調用優化

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

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

尾遞歸

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

嚴格模式

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

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

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

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

在非嚴格模式時候我們可以自己模擬

蹦床函數(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函數的每次執行,都會返回自身的另一個版本。

現在,使用蹦床函數執行sum,就不會發生調用棧溢出。

trampoline(sum(1, 100000))
// 100001

蹦床函數並不是真正的尾遞歸優化,下面的實現才是。

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        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, 100000)
// 100001

ES6之主要知識點(五)函數