javascript
js map 箭头_JS异常函数之箭头函数
來源:logrocket作者:Maciej Cie?lar 譯者:前端小智
為了保證的可讀性,本文采用意譯而非直譯。
在JS中,箭頭函數可以像普通函數一樣以多種方式使用。但是,它們一般用于需要匿名函數表達式,例如回調函數。
下面示例顯示舉例箭頭函數作為回調函數,尤其是對于map(), filter(), reduce(), sort()等數組方法。
const?scores?=?[?1,?28,?66,?666];const?maxScore?=?Math.max(...scores);
scores.map(score?=>?+(score?/?maxScore).toFixed(2));?
乍一看,箭頭函數似乎可以按常規函數來定義與使用,但事實并非如此。出于箭頭函數的簡潔性,它與常規函數有所不同,換一種看法,箭頭函數也許可以把箭頭函數看作是異常的 JS 函數。
雖然箭頭函數的語法非常簡單,但這不是本文的重點。本文主要講講箭頭函數與常規函數行為的差異,以及咱們如果利用這些差異來更好使用箭頭函數。
無論在嚴格模式還是非嚴格模式下,箭頭函數都不能具有重復的命名參數。
箭頭函數沒有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?=>?10console.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
this 是啥
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
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
交流
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
延伸閱讀
深入 JS 對象屬性
36 個JS 面試題為你助力金九銀十(面試必讀)
Vue 和 React 的優點分別是什么?(知乎大佬解答)
總結
以上是生活随笔為你收集整理的js map 箭头_JS异常函数之箭头函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sap 用户权限表_干货丨SAP系统的R
- 下一篇: 柱状图添加数字标签_Python之利用P