在JS中,箭頭函數可以像普通函數一樣以多種方式使用。但是,它們一般用於需要匿名函數表達式,例如回呼函數。
下面範例顯示舉例箭頭函數作為回呼函數,特別是對於map()
, filter()
, reduce()
, sort()
等陣列方法。
const scores = [ 1, 28, 66, 666]; const maxScore = Math.max(...scores); scores.map(score => +(score / maxScore).toFixed(2));
乍一看,箭頭函數似乎可以按常規函數來定義與使用,但事實並非如此。出於箭頭函數的簡潔性,它與常規函數有所不同,換一種看法,箭頭函數也許可以把箭頭函數看作是異常的 JS 函數。
【相關課程推薦:JavaScript影片教學】
雖然箭頭函數的語法非常簡單,但這不是本文的重點。本文主要講講箭頭函數與常規函數行為的差異,以及咱們如果利用這些差異來更好使用箭頭函數。
● 無論在嚴格模式或非嚴格模式下,箭頭函數都不能有重複的命名參數。
● 箭頭函數沒有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()
函數由三個命名參數定義: first
、second
和third
。如果命名參數多於傳遞給函數的參數,則其餘參數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
属性。
由于箭头函数的prototype
为 undefined
,尝试使用属性和方法来扩充它,或者访问它上面的属性,都会引发错误。
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
JS函数的每次调用都与调用上下文相关联,这取决于函数是如何调用的,或者在哪里调用的。
函数内部this
值依赖于函数在调用时的调用上下文,这通常会让开发人员不得不问自己一个问题:this
值是啥。
下面是对不同类型的函数调用this
指向一些总结:
● 使用new
关键字调用:this
指向由函数的内部[[Construct]]
方法创建的新实例对象。this
(新创建的实例对象)通常在默认情况下返回,除了在函数定义中显式指定了不同的返回值。
● 不使用new
关键字直接调用:在非严格模式下,this
指向window
对象(浏览器中)。然而,在严格模式下,this
值为undefined;因此,试图访问或设置此属性将引发错误。
● 间接使用绑定对象调用:Function.prototype
对象提供了三种方法,可以在调用函数时将函数绑定到任意对象,即:call()
,apply()
和bind(
)。 使用这些方法调用函数时,this
指向指定的绑定对象。
● 作为对象方法调用:this
指向调用函数(方法)的对象,无论该方法是被定义为对象的自己的属性还是从对象的原型链中解析。
● 作为事件处理程序调用:对于用作DOM事件侦听器的常规函数,this
指向触发事件的目标对象、DOM元素、document
或window
。
再来看个函数,该函数将用作单击事件侦听器,例如,表单提交按钮:
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._sortByFileSize
为 undefined
。
要解决此问题,需要将事件侦听器中的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.seconds
和 this.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
:对函数的调用上下文对象的引用
原文:https://s0dev0to.icopy.site/bnevilleoneill/anomalies-in-javascript-arrow-functions-2afh
为了保证的可读性,本文采用意译而非直译。
本文来自 js教程 栏目,欢迎学习!
以上是詳解JS箭頭函數的詳細內容。更多資訊請關注PHP中文網其他相關文章!