es6學習-函數擴展

函數的擴展

函數參數的默認值

基本用法

ES6提供了函數的指定默認值的寫法,例子如下

function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = new Point();
p // { x: 0, y: 0 }

優點:①讀代碼的人容易知道哪些參數可以省略。②以後代碼優化,把這參數去掉也不會導致代碼無法運行。

有幾個注意點

①參數是默認聲明,不能使用letconst再次聲明

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

②函數參數不能有同名參數

// 不報錯
function foo(x, x, y) {
  // ...
}

// 報錯
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

③參數的值是重新計算默認值的表達式的值。也就是說,參數是惰性求值

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

foo() // 100

x = 100;
foo() // 101

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

參數默認值可以與解構賦值結合起來使用,實現參數的自動賦值以及對一些配置項進行省略。

例子如下

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

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

第一種寫法由於第二種,因爲第一種寫法當傳過來的參數爲undefined時,參數是有值的,而第二種參數是無值的,因爲當有參數傳過來的時候,就不會使用默認值。

參數默認值的位置

有默認值的參數一般都是放在函數參數的尾部,因爲這樣比較容易看出來,哪些參數是可以省略的,而如果設置了默認值的參數不再尾部的話,這個參數其實是不能省略的。

例子

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 報錯
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 報錯
f(1, undefined, 2) // [1, 5, 2]

函數的length屬性

指定了默認值之後,函數的length屬性,將放回沒有指定默認值的參數個數,所以它失準了。

作用域

設置了參數默認值後,在函數進行聲明初始化時,參數會形成一個單獨的作用域,初始化結束都,作用域小時。

例子

var x = 1;

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

f(2) // 2

上面例子,f函數會有參數作用域,所以y是賦予傳進來的2,而不是1.

再看一個例子

let x = 1;

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

f() // 1

例子中,參數作用域沒有聲明x,所以它會到外層的全局作用域中找到x=1賦值給y,如果外層作用域找不到x會報錯。

應用

使用參數默認值,我們可以指定一個參數不得省略,省略就報錯,如下例子

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

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

foo()
// Error: Missing parameter

把參數的默認值設定爲一個拋出錯誤的函數,如果調用這個函數的時候沒有傳這個參數,這個參數就會自動報錯,而如果你想忽略某個參數,就把參數默認設置爲undefined

rest參數

es6引入了rest參數(形式爲...變量名),作用是獲取多餘的函數,它與一個數組搭配起來,多餘的變量就是放入這個數組中

例子如下

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

上面是實現一個加法的函數,參數可以傳任意個,都會存進values裏面

另外,可以用rest代替arguments變量,使代碼更加簡潔。

// arguments變量的寫法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();

注意:rest參數之後不能有其他參數

嚴格模式

es6開始,如果函數參數使用了默認值,解構賦值,或者擴展運算符,那麼函數內部就不能設定爲嚴格模式,否則會報錯。

兩種解決辦法:設定全局性的嚴格模式。

'use strict';

function doSomething(a, b = a) {
  // code
}

函數包在一個無參數的立即執行函數中

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

name屬性

函數name屬性會返回函數名

function foo() {}
foo.name // "foo"

需要注意,如果將一個變量賦值爲匿名函數,調用name屬性,es5會返回空字符串,es6會返回變量名

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

(new Function).name // "anonymous"

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

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

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

箭頭函數

基本用法

ES6 允許使用“箭頭”(=>)定義函數。

var f = v => v;

// 等同於
var f = function (v) {
  return v;
};

如果箭頭函數不需要參數或需要多個參數,就使用一個圓括號代表參數部分。

var f = () => 5;
// 等同於
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return語句返回。

var sum = (num1, num2) => { return num1 + num2; }

由於大括號被解釋爲代碼塊,所以如果箭頭函數直接返回一個對象,必須在對象外面加上括號,否則會報錯。

// 報錯
let getTempItem = id => { id: id, name: "Temp" };

// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });

箭頭函數可以與變量解構結合使用。

const full = ({ first, last }) => first + ' ' + last;

// 等同於
function full(person) {
  return person.first + ' ' + person.last;
}

下面是 rest 參數與箭頭函數結合的例子。

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

使用注意點

  1. 函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
  2. 不可以當作構造函數,也就是說,不可以使用new命令,否則會拋出一個錯誤。
  3. 不可以使用arguments對象,該對象在函數體內不存在。如果要用,可以用 rest 參數代替。
  4. 不可以使用yield命令,因此箭頭函數不能用作 Generator 函數。

其中。第一點想說的是,箭頭函數可以讓this固定化

例子

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代碼中,setTimeout的參數是一個箭頭函數,這個箭頭函數的定義生效是在foo函數生成時,而它的真正執行要等到 100 毫秒後。如果是普通函數,執行時this應該指向全局對象window,這時應該輸出21。但是,箭頭函數導致this總是指向函數定義生效時所在的對象(本例是{id: 42}),所以輸出的是42

this指向的固定化,並不是因爲箭頭函數內部有綁定this的機制,實際原因是箭頭函數根本沒有自己的this,導致內部的this就是外層代碼塊的this。正是因爲它沒有this,所以也就不能用作構造函數。

除了this,arguments super new.target在匿名函數中都是不存在的。

箭頭函數不適用的場景

  1. 定義對象的方法,且該方法內部包括this

    const cat = {
      lives: 9,
      jumps: () => {
        this.lives--;
      }
    }
    
    

上面代碼中,cat.jumps()方法是一個箭頭函數,這是錯誤的。調用cat.jumps()時,如果是普通函數,該方法內部的this指向cat;如果寫成上面那樣的箭頭函數,使得this指向全局對象,因此不會得到預期結果。這是因爲對象不構成單獨的作用域,導致jumps箭頭函數定義時的作用域就是全局作用域。

  1. 需要動態this的時候,也不應該使用箭頭函數。

    var button = document.getElementById('press');
    button.addEventListener('click', () => {
      this.classList.toggle('on');
    });
    
    

上面代碼運行時,點擊按鈕會報錯,因爲button的監聽函數是一個箭頭函數,導致裏面的this就是全局對象。如果改成普通函數,this就會動態指向被點擊的按鈕對象。

尾調用優化

什麼是尾調用

尾調用就是指某個函數的最後一步是調用另一個函數。例子如下

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

下面例子不屬於尾調用

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

以爲它等同於

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

尾調用優化

函數調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那麼在A的調用幀上方,還會形成一個B的調用幀。等到B運行結束,將結果返回到AB的調用幀纔會消失。如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。所有的調用幀,就形成一個“調用棧”

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

例子

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

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

// 等同於
g(3);

上面代碼中,如果函數g不是尾調用,函數f就需要保存內部變量mn的值、g的調用位置等信息。但由於調用g之後,函數f就結束了,所以執行到最後一步,完全可以刪除f(x)的調用幀,只保留g(3)的調用幀

所以,“尾調用優化”,即只保留內層函數的調用幀。如果所有函數都是尾調用,那麼完全可以做做到每次執行時,調用幀只有一項,這大大節省內存。

注意,目前只有 Safari 瀏覽器支持尾調用優化,Chrome 和 Firefox 都不支持。

尾遞歸

尾遞歸:就是尾調用函數自身。

遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生“棧溢出”錯誤(stack overflow)。但對於尾遞歸來說,由於只存在一個調用幀,所以永遠不會發生“棧溢出”錯誤。

階乘例子

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

factorial(5) // 120

計算n的階乘,最多需要保存n個調用記錄,複雜度 O(n)

尾遞歸

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

factorial(5, 1) // 120

只保留一個調用記錄,複雜度O(1) 。

遞歸函數的改寫

尾遞歸的實現,往往需要改寫遞歸函數,確保最後一步只調用自身。做到這一點的方法,就是把所有用到的內部變量改寫成函數的參數

但能會造成代碼不直觀

解決辦法

  1. 函數式編程柯里化:意思是將多參數的函數轉換成單參數的形式。
  2. 使用ES6的函數默認值

嚴格模式

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

函數參數的尾逗號

ES2017允許函數的最後一個參數有尾逗號

以前,函數定義或調用時,不允許最後一個參數後面有逗號

function clownsEverywhere(
  param1,
  param2
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar'
);

現在是可以的

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

Function.protorype.toString()

ES2019對函數實例的toString()做了修改。

toString()方法返回函數代碼本身,以前會省略註釋和空格

以前

function /* foo comment */ foo () {}

foo.toString()
// function foo() {}

如今

function /* foo comment */ foo () {}

foo.toString()
// "function /* foo comment */ foo () {}"

catch命令的參數省略

ES2019中,try….catch的catch代碼塊後面可以不接參數

try {
  // ...
} catch {
  // ...
}

參考資料:阮一峯es6入門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章