JS異常函數之-箭頭函數

來源:logrocketjavascript

做者:Maciej Cieślarhtml

譯者:前端小智前端


阿里雲最近在作活動,低至2折,有興趣能夠看看promotion.aliyun.com/ntms/yunpar…java


爲了保證的可讀性,本文采用意譯而非直譯。git

在JS中,箭頭函數能夠像普通函數同樣以多種方式使用。可是,它們通常用於須要匿名函數表達式,例如回調函數es6

下面示例顯示舉例箭頭函數做爲回調函數,尤爲是對於map(), filter(), reduce(), sort()等數組方法。github

const scores = [ 1, 28, 66, 666];
const maxScore = Math.max(...scores);

scores.map(score => +(score / maxScore).toFixed(2)); 
複製代碼

乍一看,箭頭函數彷佛能夠按常規函數來定義與使用,但事實並不是如此。出於箭頭函數的簡潔性,它與常規函數有所不一樣,換一種見解,箭頭函數也許能夠把箭頭函數看做是異常的 JS 函數。數組

雖然箭頭函數的語法很是簡單,但這不是本文的重點。本文主要講講箭頭函數與常規函數行爲的差別,以及我們若是利用這些差別來更好使用箭頭函數。瀏覽器

  • 不管在嚴格模式仍是非嚴格模式下,箭頭函數都不能具備重複的命名參數。app

  • 箭頭函數沒有arguments綁定。可是,它們能夠訪問最接近的非箭頭父函數的arguments對象。

  • 箭頭函數永遠不能用做構造函數,天然的不能使用new關鍵字調用它們,所以,對於箭頭函數不存在prototype屬性。

  • 在函數的整個生命週期中,箭頭函數內部的值保持不變,而且老是與接近的非箭頭父函數中的值綁定。

命名函數參數

JS中的函數一般用命名參數定義。命名參數用於根據位置將參數映射到函數做用域中的局部變量。

來看看下面的函數:

function logParams (first, second, third) {
  console.log(first, second, third);
}

// first => 'Hello'
// second => 'World'
// third => '!!!'
logParams('Hello', 'World', '!!!'); // "Hello"  "World"  "!!!"

// first => { o: 3 }
// second => [ 1, 2, 3 ]
// third => undefined
logParams({ o: 3 }, [ 1, 2, 3 ]); // {o: 3}  [1, 2, 3]
複製代碼

logParams()函數由三個命名參數定義: firstsecondthird。若是命名參數多於傳遞給函數的參數,則其他參數undefined

對於命名參數,JS函數在非嚴格模式下表現出奇怪的行爲。在非嚴格模式下,JS函數容許有重複命名參數,來看看示例:

function logParams (first, second, first) {
  console.log(first, second);
}

// first => 'Hello'
// second => 'World'
// first => '!!!'
logParams('Hello', 'World', '!!!'); // "!!!"  "World"

// first => { o: 3 }
// second => [ 1, 2, 3 ]
// first => undefined
logParams({ o: 3 }, [ 1, 2, 3 ]); // undefined  [1, 2, 3]
複製代碼

我們能夠看到,first參數重複了,所以,它被映射到傳遞給函數調用的第三個參數的值,覆蓋了第一個參數,這不是一個讓人喜歡的行爲。

// 因爲參數重複,嚴格模式會報錯
function logParams (first, second, first) {
  "use strict";
  console.log(first, second);
}
複製代碼

箭頭函數如何處理重複的參數

關於箭頭函數:

與常規函數不一樣,不管在嚴格模式仍是非嚴格模式下,箭頭函數都不容許重複參數,重複的參數將引起語法錯誤。
// 只要你敢寫成重複的參數,我就敢死給你看 const logParams = (first, second, first) => { console.log(first, second); }

函數重載

函數重載是定義函數的能力,這樣就能夠根據不一樣的參數數量來調用對應的函數, JS 中能夠利用綁定方式來實現這一功能。

來看個簡單的重載函數,計算傳入參數的平均值:

function average() {
  const length = arguments.length;

  if (length == 0) return 0;

  // 將參數轉換爲數組
  const numbers = Array.prototype.slice.call(arguments);

  const sumReduceFn = function (a, b) { return a + Number(b) };
  // 返回數組元素的總和除以數組的長度
  return numbers.reduce(sumReduceFn, 0) / length;
}
複製代碼

這樣函數能夠用任意數量的參數調用,從0到函數能夠接受的最大參數數量應該是255
average(); // 0 average('3o', 4, 5); // NaN average('1', 2, '3', 4, '5', 6, 7, 8, 9, 10); // 5.5 average(1.75, 2.25, 3.5, 4.125, 5.875); // 3.5

如今嘗試使用剪頭函數語法複製average()函數,通常我們會以爲,這沒啥難的,沒法就這樣:

const average = () => {
  const length = arguments.length;

  if (length == 0) return 0;

  const numbers = Array.prototype.slice.call(arguments);
  const sumReduceFn = function (a, b) { return a + Number(b) };

  return numbers.reduce(sumReduceFn, 0) / length;
}
複製代碼

如今測試這個函數時,我們會發現它會拋出一個引用錯誤,arguments 未定義。

我們作錯了啥

對於箭頭函數:

與常規函數不一樣,arguments不存在於箭頭函數中。可是,能夠訪問非箭頭父函數的arguments對象。

基於這種理解,能夠將average()函數修改成一個常規函數,該函數將返回當即調用的嵌套箭頭函數執行的結果,該嵌套箭頭函數就可以訪問父函數的arguments

function average() {
  return (() => {
    const length = arguments.length;

    if (length == 0) return 0;

    const numbers = Array.prototype.slice.call(arguments);
    const sumReduceFn = function (a, b) { return a + Number(b) };

    return numbers.reduce(sumReduceFn, 0) / length;
  })();
}
複製代碼

這樣就能夠解決了arguments對象沒有定義的問題,但這種狗屎作法顯然不少餘了。

作點不同的

對於上面問題是否存在替代方法呢,可使用 es6 的 rest 參數。

使用ES6 rest 參數,我們能夠獲得一個數組,該數組保存了傳遞給該函數的全部的參數。rest語法適用於全部類型的函數,不管是常規函數仍是箭頭函數。

const average = (...args) => {
  if (args.length == 0) return 0;
  const sumReduceFn = function (a, b) { return a + Number(b) };

  return args.reduce(sumReduceFn, 0) / args.length;
}
複製代碼

對於使用rest參數須要注意一些事項:

  • rest參數與函數內部的arguments對象不一樣。rest參數是一個實際的函數參數,而arguments對象是一個綁定到函數做用域的內部對象。

  • 一個函數只能有一個rest參數,並且它必須位於最後一個參數。這意味着函數能夠包含命名參數和rest參數的組合。

  • rest 參數與命名參數一塊兒使用時,它不包含全部傳入的參數。可是,當它是唯一的函數參數時,表示函數參數。另外一方面,函數的arguments對象老是捕獲全部函數的參數。

  • rest參數指向包含全部捕獲函數參數的數組對象,而arguments對象指向包含全部函數參數的類數組對象。

接着考慮另外一個簡單的重載函數,該函數將數字根據傳入的進制轉換爲另外一個類的進制數。 可使用一到三個參數調用該函數。 可是,當使用兩個或更少的參數調用它時,它會交換第二個和第三個函數參數。以下所示:

function baseConvert (num, fromRadix = 10, toRadix = 10) {
  if (arguments.length < 3) {
    // swap variables using array destructuring
    [toRadix, fromRadix] = [fromRadix, toRadix];
  }
  return parseInt(num, fromRadix).toString(toRadix);
}
複製代碼

調用 baseConvert 方法:

// num => 123, fromRadix => 10, toRadix => 10
console.log(baseConvert(123)); // "123"

// num => 255, fromRadix => 10, toRadix => 2
console.log(baseConvert(255, 2)); // "11111111"

// num => 'ff', fromRadix => 16, toRadix => 8
console.log(baseConvert('ff', 16, 8)); // "377"
複製代碼

使用箭頭函數來重寫上面的方法:

const baseConvert = (num, ...args) => {
  // 解構`args`數組和
  // 設置`fromRadix`和`toRadix`局部變量
  let [fromRadix = 10, toRadix = 10] = args;

  if (args.length < 2) {
    // 使用數組解構交換變量
    [toRadix, fromRadix] = [fromRadix, toRadix];
  }

  return parseInt(num, fromRadix).toString(toRadix);
}
複製代碼

構造函數

可使用new關鍵字調用常規JS函數,該函數做爲類構造函數用於建立新的實例對象。

function Square (length = 10) {
  this.length = parseInt(length) || 10;

  this.getArea = function() {
    return Math.pow(this.length, 2);
  }

  this.getPerimeter = function() {
    return 4 * this.length;
  }
}

const square = new Square();

console.log(square.length); // 10
console.log(square.getArea()); // 100
console.log(square.getPerimeter()); // 40

console.log(typeof square); // "object"
console.log(square instanceof Square); // true
複製代碼

當使用new關鍵字調用常規JS函數時,將調用函數內部[[Construct]]方法來建立一個新的實例對象並分配內存。以後,函數體將正常執行,並將this映射到新建立的實例對象。最後,函數隱式地返回 this(新建立的實例對象),只是在函數定義中指定了一個不一樣的返回值。

此外,全部常規JS函數都有一個prototype屬性。函數的prototype屬性是一個對象,它包含函數建立的全部實例對象在用做構造函數時共享的屬性和方法。

如下是對前面的Square函數的一個小修改,此次它從函數的原型上的方法,而不是構造函數自己。

function Square (length = 10) {
  this.length = parseInt(length) || 10;
}

Square.prototype.getArea = function() {
  return Math.pow(this.length, 2);
}

Square.prototype.getPerimeter = function() {
  return 4 * this.length;
}

const square = new Square();

console.log(square.length); // 10
console.log(square.getArea()); // 100
console.log(square.getPerimeter()); // 40

console.log(typeof square); // "object"
console.log(square instanceof Square); // true
複製代碼

以下所知,一切仍然按預期工做。 事實上,這裏有一個小祕密:ES6 類在後臺執行相似於上面代碼片斷的操做 - 類(class)只是個語法糖。

那麼箭頭函數呢

它們是否也與常規JS函數共享此行爲?答案是否認的。關於箭頭函數:

與常規函數不一樣,箭頭函數永遠不能使用new關鍵字調用,由於它們沒有[[Construct]]方法。 所以,箭頭函數也不存在prototype屬性。

箭頭函數不能用做構造函數,沒法使用new關鍵字調用它們,若是這樣作了會拋出一個錯誤,代表該函數不是構造函數。

所以,對於箭頭函數,不存在能夠做爲構造函數調用的函數內部的new.target等綁定,相反,它們使用最接近的非箭頭父函數的new.target值。

此外,因爲沒法使用new關鍵字調用箭頭函數,所以實際上不須要它們具備原型。 所以,箭頭函數不存在prototype屬性。

因爲箭頭函數的prototypeundefined,嘗試使用屬性和方法來擴充它,或者訪問它上面的屬性,都會引起錯誤。

const Square = (length = 10) => {
  this.length = parseInt(length) || 10;
}

// throws an error
const square = new Square(5);

// throws an error
Square.prototype.getArea = function() {
  return Math.pow(this.length, 2);
}

console.log(Square.prototype); // undefined
複製代碼

this 是啥

JS函數的每次調用都與調用上下文相關聯,這取決於函數是如何調用的,或者在哪裏調用的。

函數內部this值依賴於函數在調用時的調用上下文,這一般會讓開發人員不得不問本身一個問題:this值是啥。

下面是對不一樣類型的函數調用this指向一些總結:

  • 使用new關鍵字調用:this指向由函數的內部[[Construct]]方法建立的新實例對象。this(新建立的實例對象)一般在默認狀況下返回,除了在函數定義中顯式指定了不一樣的返回值。

  • 不使用new關鍵字直接調用:在非嚴格模式下,this指向window對象(瀏覽器中)。然而,在嚴格模式下,this值爲undefined;所以,試圖訪問或設置此屬性將引起錯誤。

  • 間接使用綁定對象調用Function.prototype對象提供了三種方法,能夠在調用函數時將函數綁定到任意對象,即:call()apply()bind()。 使用這些方法調用函數時,this指向指定的綁定對象。

  • 做爲對象方法調用this指向調用函數(方法)的對象,不管該方法是被定義爲對象的本身的屬性仍是從對象的原型鏈中解析。

  • 做爲事件處理程序調用:對於用做DOM事件偵聽器的常規函數,this指向觸發事件的目標對象、DOM元素、documentwindow

再來看個函數,該函數將用做單擊事件偵聽器,例如,表單提交按鈕:

function processFormData (evt) {
  evt.preventDefault();

  const form = this.closest('form');

  const data = new FormData(form);
  const { action: url, method } = form;
}

button.addEventListener('click', processFormData, false);
複製代碼

與前面看到的同樣,事件偵聽器函數中的 this值是觸發單擊事件的DOM元素,在本例中是button

所以,可使用如下命令指向submit按鈕的父表單

this.closest('form');
複製代碼

若是將函數更改成箭頭函數語法,會發生什麼?

const processFormData = (evt) => {
  evt.preventDefault();

  const form = this.closest('form');
  const data = new FormData(form);
  const { action: url, method } = form;
}

button.addEventListener('click', processFormData, false);
複製代碼

若是如今嘗試此操做,我們就獲得一個錯誤。從表面上看,this 的值並非各位想要的。因爲某種緣由,它再也不指向button元素,而是指向window對象。

如何修復this指向

利用上面提到的 Function.prototype.bind() 強制將this值綁定到button元素:

button.addEventListener('click', processFormData.bind(button), false);
複製代碼

但這彷佛不是各位想要的解決辦法。this仍然指向window對象。這是箭頭函數特有的問題嗎?這是否意味着箭頭函數不能用於依賴於this的事件處理?

爲何會搞錯

關於箭頭函數的最後一件事:

與常規函數不一樣,箭頭函數沒有this的綁定。 this的值將解析爲最接近的非箭頭父函數或全局對象的值。

這解釋了爲何事件偵聽器箭頭函數中的this值指向window 對象(全局對象)。 因爲它沒有嵌套在父函數中,所以它使用來自最近的父做用域的this值,該做用域是全局做用域。

可是,這並不能解釋爲何不能使用bind()將事件偵聽器箭頭函數綁定到button元素。對此有一個解釋:

與常規函數不一樣,內部箭頭函數的this值保持不變,而且不管調用上下文如何,都不能在其整個生命週期中更改。

箭頭函數的這種行爲使得JS引擎能夠優化它們,由於能夠事先肯定函數綁定。

考慮一個稍微不一樣的場景,其中事件處理程序是使用對象方法中的常規函數​​定義的,而且還取決於同一對象的另外一個方法:

({
  _sortByFileSize: function (filelist) {
    const files = Array.from(filelist).sort(function (a, b) {
      return a.size - b.size;
    });

    return files.map(function (file) {
      return file.name;
    });
  },

  init: function (input) {
    input.addEventListener('change', function (evt) {
      const files = evt.target.files;
      console.log(this._sortByFileSize(files));
    }, false);
  }

}).init(document.getElementById('file-input'));
複製代碼

上面是一個一次性的對象,該對象帶有_sortByFileSize()方法和init()方法,並當即調init方法。init()方法接受一個input元素,併爲input元素設置一個更改事件處理程序,該事件處理程序按文件大小對上傳的文件進行排序,並打印在瀏覽器的控制檯。

若是測試這段代碼,會發現,當選擇要上載的文件時,文件列表不會被排序並打印到控制檯;相反,會控制檯上拋出一個錯誤,問題就出在這一行:

console.log(this._sortByFileSize(files));
複製代碼

在事件監聽器函數內部,this 指向 input 元素 所以this._sortByFileSizeundefined

要解決此問題,須要將事件偵聽器中的this綁定到包含方法的外部對象,以即可以調用this._sortByFileSize()。 在這裏,可使用bind(),以下所示:

init: function (input) {
  input.addEventListener('change', (function (evt) {
    const files = evt.target.files;
    console.log(this._sortByFileSize(files));
  }).bind(this), false);
}
複製代碼

如今一切正常。這裏不使用bind(),能夠簡單地用一個箭頭函數替換事件偵聽器函數。箭頭函數將使用父init()方法中的this的值:

init: function (input) {
  input.addEventListener('change', (function (evt) {
    const files = evt.target.files;
    console.log(this._sortByFileSize(files));
  }).bind(this), false);
}
複製代碼

再考慮一個場景,假設有一個簡單的計時器函數,能夠將其做爲構造函數調用來建立以秒爲單位的倒計時計時器。使用setInterval()進行倒計時,直到持續時間過時或間隔被清除爲止,以下所示:

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval(function () {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }, 1000);
}

const timer = new Timer(30);
複製代碼

若是運行這段代碼,會看到倒計時計時器彷佛被打破了,在控制檯上一直打印 NaN

這裏的問題是,在傳遞給setInterval()的回調函數中,this指向全局window對象,而不是Timer()函數做用域內新建立的實例對象。所以,this.secondsthis.interval 都是undefined的。

與以前同樣,要修復這個問題,可使用bind()setInterval()回調函數中的this值綁定到新建立的實例對象,以下所示

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval((function () {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }).bind(this), 1000);
}
複製代碼

或者,更好的方法是,能夠用一個箭頭函數替換setInterval()回調函數,這樣它就可使用最近的非箭頭父函數的this值:

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval(() => {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }, 1000);
}
複製代碼

如今理解了箭頭函數如何處理this關鍵字,還須要注意箭頭函數對於須要保留this值的狀況並不理想 - 例如,在定義須要引用的對象方法時 使用須要引用目標對象的方法來擴展對象或擴充函數的原型。

不存在的綁定

在本文中,已經看到了一些綁定,這些綁定能夠在常規JS函數中使用,可是不存在用於箭頭函數的綁定。相反,箭頭函數從最近的非箭頭父函數派生此類綁定的值。

總之,下面是箭頭函數中不存在綁定的列表:

  • arguments:調用時傳遞給函數的參數列表

  • new.target:使用new關鍵字做爲構造函數調用的函數的引用

  • super:對函數所屬對象原型的引用,前提是該對象被定義爲一個簡潔的對象方法

  • this:對函數的調用上下文對象的引用

原文:s0dev0to.icopy.site/bnevilleone…

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

相關文章
相關標籤/搜索