[英]When should I use arrow functions in ECMAScript 6?
使用() => {}
和function () {}
我們得到了兩種非常相似的方法來編寫 ES6 中的函數。 在其他語言中,lambda 函數通常通過匿名來區分自己,但在 ECMAScript 中,任何 function 都可以是匿名的。 這兩種類型中的每一種都有獨特的使用域(即當this
需要顯式綁定或顯式不綁定時)。 在這些領域之間,有大量的情況可以使用任何一種表示法。
ES6 中的箭頭函數至少有兩個限制:
new
並且不能在創建prototype
時使用this
綁定到 scope除了這兩個限制,箭頭函數理論上幾乎可以在任何地方取代常規函數。 在實踐中使用它們的正確方法是什么? 是否應該使用箭頭函數,例如:
this
變量不可知,我們也不會創建 object。我正在尋找在未來版本的 ECMAScript 中選擇適當的 function 符號的指南。 指南需要清晰,以便可以將其傳授給團隊中的開發人員,並且需要保持一致,這樣就不需要不斷地從一個 function 符號來回重構到另一個符號。
這個問題是針對那些在即將到來的 ECMAScript 6 (Harmony) 的上下文中考慮過代碼風格並且已經使用過該語言的人。
不久前,我們的團隊將其所有代碼(一個中型 AngularJS 應用程序)遷移到使用
Traceur
Babel編譯的 JavaScript。 我現在對 ES6 及更高版本中的函數使用以下經驗法則:
Object.prototype
屬性中使用function
。class
用於對象構造函數。=>
。為什么幾乎到處都使用箭頭函數?
thisObject
。 如果即使是單個標准函數回調與一堆箭頭函數混合在一起,范圍也有可能變得混亂。function
立即突出來定義作用域。 開發人員總是可以查找下一個更高的function
語句來查看thisObject
是什么。為什么總是在全局作用域或模塊作用域上使用常規函數?
thisObject
的函數。window
對象(全局作用域)最好是明確尋址的。Object.prototype
定義都存在於全局范圍內(想想String.prototype.truncate
等),而且那些通常必須是function
類型。 在全局范圍內始終如一地使用function
有助於避免錯誤。function foo(){}
比const foo = () => {}
更不笨拙——尤其是在其他函數調用之外。 (2) 函數名稱顯示在堆棧跟蹤中。 雖然命名每個內部回調會很乏味,但命名所有公共函數可能是一個好主意。嘗試實例化箭頭函數會引發異常:
var x = () => {};
new x(); // TypeError: x is not a constructor
因此,函數相對於箭頭函數的一個關鍵優勢是函數兼作對象構造函數:
function Person(name) {
this.name = name;
}
然而,功能相同的2 ECMAScript Harmony 草案類定義幾乎同樣緊湊:
class Person {
constructor(name) {
this.name = name;
}
}
我希望最終不鼓勵使用前一種符號。 對象構造函數符號可能仍然被一些人用於簡單的匿名對象工廠,其中對象是通過編程生成的,但不是其他很多。
在需要對象構造函數的地方,應該考慮將函數轉換為class
如上所示。 該語法也適用於匿名函數/類。
堅持使用常規函數的最佳論據——該死的范圍安全——可能是箭頭函數的可讀性不如常規函數。 如果您的代碼一開始就不起作用,那么箭頭函數可能看起來沒有必要,而且當箭頭函數沒有持續使用時,它們看起來很丑陋。
自從 ECMAScript 5.1 為我們提供函數式Array.forEach
、 Array.map
和所有這些函數式編程特性以來,ECMAScript 已經發生了很大變化,這些特性讓我們使用了以前會使用for循環的函數。 異步 JavaScript 已經起飛了很多。 ES6 還將提供一個Promise
對象,這意味着更多的匿名函數。 函數式編程沒有回頭路可走。 在函數式 JavaScript 中,箭頭函數優於常規函數。
以這段(特別令人困惑的)代碼3為例:
function CommentController(articles) {
this.comments = [];
articles.getList()
.then(articles => Promise.all(articles.map(article => article.comments.getList())))
.then(commentLists => commentLists.reduce((a, b) => a.concat(b)));
.then(comments => {
this.comments = comments;
})
}
具有常規功能的同一段代碼:
function CommentController(articles) {
this.comments = [];
articles.getList()
.then(function (articles) {
return Promise.all(articles.map(function (article) {
return article.comments.getList();
}));
})
.then(function (commentLists) {
return commentLists.reduce(function (a, b) {
return a.concat(b);
});
})
.then(function (comments) {
this.comments = comments;
}.bind(this));
}
雖然任何一個箭頭函數都可以用標准函數代替,但這樣做幾乎沒有什么好處。 哪個版本更易讀? 我會說第一個。
我認為是使用箭頭函數還是常規函數的問題會隨着時間的推移變得不那么重要。 大多數函數要么成為類方法,而無需使用function
關鍵字,要么成為類。 函數將繼續用於通過Object.prototype
修補類。 同時,我建議為任何真正應該是類方法或類的東西保留function
關鍵字。
根據提案,箭頭旨在“解決和解決傳統函數表達式的幾個常見痛點”。 他們打算通過this
詞法上綁定this
並提供簡潔的語法來改進問題。
然而,
this
因此,箭頭函數創造了混淆和錯誤的機會,應該從 JavaScript 程序員的詞匯表中排除,只用function
代替。
this
this
是有問題的:
function Book(settings) {
this.settings = settings;
this.pages = this.createPages();
}
Book.prototype.render = function () {
this.pages.forEach(function (page) {
page.draw(this.settings);
}, this);
};
箭頭函數旨在解決我們需要在回調中訪問this
屬性的問題。 目前已經有幾種方法可以做到這一點:人們可以指定this
一個變量,使用bind
,或使用可用的第三個參數Array
聚集方法。 然而箭頭似乎是最簡單的解決方法,因此該方法可以像這樣重構:
this.pages.forEach(page => page.draw(this.settings));
但是,請考慮代碼是否使用了像 jQuery 這樣的庫,其方法專門綁定了this
。 現在,有兩個this
值需要處理:
Book.prototype.render = function () {
var book = this;
this.$pages.each(function (index) {
var $page = $(this);
book.draw(book.currentPage + index, $page);
});
};
我們必須使用function
才能讓each
動態綁定this
。 我們不能在這里使用箭頭函數。
具有多個處理this
值也可能會造成混淆,因為它很難知道哪些this
作者說的是:
function Reader() {
this.book.on('change', function () {
this.reformat();
});
}
作者是否真的打算調用Book.prototype.reformat
? 還是他忘了綁定this
,打算調用Reader.prototype.reformat
? 如果我們將處理程序更改為箭頭函數,我們同樣會懷疑作者是否想要動態this
,但選擇了一個箭頭,因為它適合一行:
function Reader() {
this.book.on('change', () => this.reformat());
}
有人可能會提出:“箭頭有時可能是錯誤的函數使用是不是很特殊?也許如果我們很少需要動態this
值,那么大多數時間使用箭頭仍然可以。”
但問問自己:“調試代碼並發現錯誤的結果是由‘邊緣情況’引起的,這是否‘值得’?”我寧願不僅在大多數時候避免麻煩,而且100% 的時間。
有一個更好的方法:始終使用function
(因此this
始終可以動態綁定),並且始終通過變量引用this
。 變量是詞法的,有很多名字。 this
分配給變量將使您的意圖明確:
function Reader() {
var reader = this;
reader.book.on('change', function () {
var book = this;
book.reformat();
reader.reformat();
});
}
此外,始終將this
分配給一個變量(即使只有一個this
或沒有其他函數)確保即使在更改代碼后仍然保持清晰的意圖。
此外,動態this
幾乎不是例外。 jQuery 用於超過 5000 萬個網站(截至 2016 年 2 月撰寫本文時)。 以下是動態綁定this
其他 API:
this
公開其測試方法。this
公開了構建任務的方法。this
方法。this
的EventTarget
。this
.(通過http://trends.builtwith.com/javascript/jQuery和 https://www.npmjs.com 進行統計。)
您可能已經需要動態this
綁定。
詞匯this
有時是預期的,但有時不是; 就像動態一樣, this
有時是預期的,但有時不是。 值得慶幸的是,有一種更好的方法,它總是產生和傳達預期的綁定。
箭頭函數成功地為函數提供了“更短的語法形式”。 但是這些較短的函數會讓你更成功嗎?
x => x * x
比function (x) { return x * x; }
更容易閱讀嗎function (x) { return x * x; }
function (x) { return x * x; }
? 可能是這樣,因為它更有可能產生一行簡短的代碼。 根據戴森的《閱讀速度和行長對屏幕閱讀效率的影響》 ,
中等行長(每行 55 個字符)似乎支持以正常和快速速度進行有效閱讀。 這產生了最高層次的修真。 . .
條件(三元)運算符和單行if
語句也有類似的理由。
但是,您真的在編寫提案中宣傳的簡單數學函數嗎? 我的領域不是數學的,所以我的子程序很少如此優雅。 相反,我經常看到箭頭函數打破了列限制,並由於編輯器或樣式指南而換行到另一行,這使戴森定義的“可讀性”無效。
有人可能會提出,“如果可能,只使用簡短版本的簡短功能怎么樣?”。 但是現在一個文體規則與語言約束相矛盾:“盡量使用最短的函數符號,記住有時只有最長的符號才能按預期綁定this
。” 這種混淆使得箭頭特別容易被誤用。
箭頭函數語法有很多問題:
const a = x =>
doSomething(x);
const b = x =>
doSomething(x);
doSomethingElse(x);
這兩個函數在語法上都是有效的。 但是doSomethingElse(x);
不在b
的主體中。 它只是一個縮進很差的頂級語句。
當擴展到塊形式時,不再有隱式return
,人們可能會忘記恢復。 但是該表達式可能只是為了產生副作用,所以誰知道未來是否需要顯式return
?
const create = () => User.create();
const create = () => {
let user;
User.create().then(result => {
user = result;
return sendEmail();
}).then(() => user);
};
const create = () => {
let user;
return User.create().then(result => {
user = result;
return sendEmail();
}).then(() => user);
};
可以將用作剩余參數的內容解析為擴展運算符:
processData(data, ...results => {}) // Spread
processData(data, (...results) => {}) // Rest
賦值可能與默認參數混淆:
const a = 1;
let x;
const b = x => {}; // No default
const b = x = a => {}; // "Adding a default" instead creates a double assignment
const b = (x = a) => {}; // Remember to add parentheses
塊看起來像對象:
(id) => id // Returns `id`
(id) => {name: id} // Returns `undefined` (it's a labeled statement)
(id) => ({name: id}) // Returns an object
這是什么意思?
() => {}
作者是打算創建一個空操作,還是一個返回空對象的函數? (考慮到這一點,我們是否應該將{
放在=>
?我們是否應該僅限於表達式語法?這將進一步降低箭頭的頻率。)
=>
看起來像<=
和>=
:
x => 1 ? 2 : 3
x <= 1 ? 2 : 3
if (x => 1) {}
if (x >= 1) {}
要立即調用箭頭函數表達式,必須將()
放在外面,而將()
放在里面是有效的並且可能是有意的。
(() => doSomething()()) // Creates function calling value of `doSomething()`
(() => doSomething())() // Calls the arrow function
雖然,如果有人寫(() => doSomething()());
為了編寫一個立即調用的函數表達式,什么都不會發生。
考慮到上述所有情況,很難說箭頭函數“更容易理解”。 可以學習使用這種語法所需的所有特殊規則。 是不是真的值得嗎?
function
的語法非常普遍。 專門使用function
意味着語言本身可以防止編寫令人困惑的代碼。 為了編寫在所有情況下都應該在語法上理解的過程,我選擇了function
。
您要求一個需要“清晰”和“一致”的指南。 使用箭頭函數最終將導致語法有效、邏輯無效的代碼,兩種函數形式交織在一起,有意義且任意。 因此,我提供以下內容:
function
過程。this
一個變量。 不要使用() => {}
。創建箭頭函數是為了簡化函數scope
並通過使其更簡單來解決this
關鍵字。 他們使用=>
語法,看起來像一個箭頭。
注意:它不會取代現有功能。 如果你用箭頭函數替換每個函數語法,它不會在所有情況下都有效。
讓我們看看現有的 ES5 語法。 如果this
關鍵字在對象的方法(屬於對象的函數)中,它指的是什么?
var Actor = {
name: 'RajiniKanth',
getName: function() {
console.log(this.name);
}
};
Actor.getName();
上面的代碼片段將引用一個object
並打印出名稱"RajiniKanth"
。 讓我們探索下面的代碼片段,看看這會指出什么。
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
this.movies.forEach(function(movie) {
alert(this.name + " has acted in " + movie);
});
}
};
Actor.showMovies();
現在如果this
關鍵字在method's function
內部呢?
這里 this 指的是window object
不是inner function
因為它超出了scope
。 因為this
總是引用它所在函數的所有者,在這種情況下——因為它現在超出了范圍——窗口/全局對象。
當它在object
的方法內部時—— function
的所有者就是對象。 因此this關鍵字綁定到對象。 然而,當它在一個函數內部時,無論是獨立的還是在另一個方法中,它總是指向window/global
對象。
var fn = function(){
alert(this);
}
fn(); // [object Window]
我們的 ES5 本身有辦法解決這個問題。 在深入研究 ES6 箭頭函數之前,讓我們先研究一下如何解決它。
通常,您會在方法的內部函數之外創建一個變量。 現在, 'forEach'
方法可以訪問this
,從而訪問object's
屬性及其值。
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
var _this = this;
this.movies.forEach(function(movie) {
alert(_this.name + " has acted in " + movie);
});
}
};
Actor.showMovies();
使用bind
將引用方法的this
關鍵字附加到method's inner function
。
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
this.movies.forEach(function(movie) {
alert(this.name + " has acted in " + movie);
}.bind(this));
}
};
Actor.showMovies();
現在有了 ES6 箭頭函數,我們可以用更簡單的方式處理詞法范圍問題。
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
showMovies: function() {
this.movies.forEach((movie) => {
alert(this.name + " has acted in " + movie);
});
}
};
Actor.showMovies();
箭頭函數更像是函數語句,只不過它們將this綁定到父作用域。 如果箭頭函數在頂部作用域中,則this
參數將引用窗口/全局作用域,而常規函數內的箭頭函數的 this 參數將與其外部函數相同。
使用箭頭函數, this
在創建時綁定到封閉范圍並且不能更改。 new 運算符、bind、call 和 apply 對此沒有影響。
var asyncFunction = (param, callback) => {
window.setTimeout(() => {
callback(param);
}, 1);
};
// With a traditional function if we don't control
// the context then can we lose control of `this`.
var o = {
doSomething: function () {
// Here we pass `o` into the async function,
// expecting it back as `param`
asyncFunction(o, function (param) {
// We made a mistake of thinking `this` is
// the instance of `o`.
console.log('param === this?', param === this);
});
}
};
o.doSomething(); // param === this? false
在上面的例子中,我們失去了對 this 的控制。 我們可以通過使用this
的變量引用或使用bind
來解決上面的例子。 使用 ES6,管理this
作為其綁定到詞法范圍變得更容易。
var asyncFunction = (param, callback) => {
window.setTimeout(() => {
callback(param);
}, 1);
};
var o = {
doSomething: function () {
// Here we pass `o` into the async function,
// expecting it back as `param`.
//
// Because this arrow function is created within
// the scope of `doSomething` it is bound to this
// lexical scope.
asyncFunction(o, (param) => {
console.log('param === this?', param === this);
});
}
};
o.doSomething(); // param === this? true
在對象字面量內。
var Actor = {
name: 'RajiniKanth',
movies: ['Kabali', 'Sivaji', 'Baba'],
getName: () => {
alert(this.name);
}
};
Actor.getName();
Actor.getName
是用箭頭函數定義的,但在調用時它會警告 undefined 因為this.name
undefined
因為上下文仍然是window
。
發生這種情況是因為箭頭函數在詞法上將上下文與window object
綁定在一起……即外部作用域。 執行this.name
等價於window.name
,它是未定義的。
在prototype object
上定義方法時,同樣的規則適用。 而不是使用箭頭函數來定義 sayCatName 方法,這會帶來不正確的context window
:
function Actor(name) {
this.name = name;
}
Actor.prototype.getName = () => {
console.log(this === window); // => true
return this.name;
};
var act = new Actor('RajiniKanth');
act.getName(); // => undefined
this
在構造調用中是新創建的對象。 執行 new Fn() 時, constructor Fn
的上下文是一個新對象: this instanceof Fn === true
。
this
是從封閉上下文中設置的,即外部作用域使其不被分配給新創建的對象。
var Message = (text) => {
this.text = text;
};
// Throws "TypeError: Message is not a constructor"
var helloMessage = new Message('Hello World!');
箭頭函數在聲明時靜態綁定context
,不可能使其動態化。 將事件偵聽器附加到 DOM 元素是客戶端編程中的一項常見任務。 一個事件以 this 作為目標元素觸發處理函數。
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
this
是在全局上下文中定義的箭頭函數中的窗口。 當點擊事件發生時,瀏覽器會嘗試調用帶有按鈕上下文的處理函數,但箭頭函數不會改變其預定義的上下文。 this.innerHTML
相當於window.innerHTML
沒有意義。
您必須應用一個函數表達式,它允許根據目標元素進行更改:
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});
當用戶單擊按鈕時,處理函數中的this
就是按鈕。 因此this.innerHTML = 'Clicked button'
正確修改按鈕文本以反映點擊狀態。
箭頭函數 - 迄今為止使用最廣泛的 ES6 特性......
用法:除以下場景外,所有 ES5 函數都應替換為 ES6 箭頭函數:
箭功能不應使用:
this
/ arguments
時
this
/ arguments
,它們依賴於它們的外部上下文。constructor
this
。this
(它應該是對象本身)。讓我們了解一些箭頭函數的變體以更好地理解:
變體 1 :當我們想要將多個參數傳遞給函數並從中返回一些值時。
ES5 版本:
var multiply = function (a, b) {
return a*b;
};
console.log(multiply(5, 6)); // 30
ES6 版本:
var multiplyArrow = (a, b) => a*b;
console.log(multiplyArrow(5, 6)); // 30
筆記:
該function
不是必需的關鍵字。 =>
是必需的。 {}
是可選的,當我們不提供{}
return
是由 JavaScript 隱式添加的,當我們提供{}
我們需要在需要時添加return
。
變體2:當我們想通過只使用一個參數的函數,並從它返回一定的價值。
ES5 版本:
var double = function(a) {
return a*2;
};
console.log(double(2)); // 4
ES6 版本:
var doubleArrow = a => a*2;
console.log(doubleArrow(2)); // 4
筆記:
當只傳遞一個參數時,我們可以省略括號()
。
變體3:當我們不想傳遞任何參數的函數,不想返回任何值。
ES5 版本:
var sayHello = function() {
console.log("Hello");
};
sayHello(); // Hello
ES6 版本:
var sayHelloArrow = () => {console.log("sayHelloArrow");}
sayHelloArrow(); // sayHelloArrow
變體 4 :當我們想要明確地從箭頭函數返回時。
ES6 版本:
var increment = x => {
return x + 1;
};
console.log(increment(1)); // 2
變體 5 :當我們想從箭頭函數返回一個對象時。
ES6 版本:
var returnObject = () => ({a:5});
console.log(returnObject());
筆記:
我們需要將對象括在括號()
。 否則,JavaScript 無法區分塊和對象。
變體 6 :箭頭函數沒有自己的arguments
(類似於對象的數組)。 它們依賴於arguments
外部上下文。
ES6 版本:
function foo() {
var abc = i => arguments[0];
console.log(abc(1));
};
foo(2); // 2
筆記:
foo
是一個 ES5 函數,有一個像 object 一樣的arguments
數組,傳遞給它的參數是2
所以foo
arguments[0]
是 2。
abc
是一個ES6箭頭功能,因為它沒有自己的arguments
。 因此,它會打印foo
arguments[0]
其外部上下文。
變體7:箭頭的功能沒有this
屬於自己的,他們依靠的外部背景下this
ES5 版本:
var obj5 = {
greet: "Hi, Welcome ",
greetUser : function(user) {
setTimeout(function(){
console.log(this.greet + ": " + user); // "this" here is undefined.
});
}
};
obj5.greetUser("Katty"); //undefined: Katty
筆記:
傳遞給 setTimeout 的回調是一個 ES5 函數,它有自己的this
在use-strict
環境中未定義。 因此我們得到輸出:
undefined: Katty
ES6 版本:
var obj6 = {
greet: "Hi, Welcome ",
greetUser : function(user) {
setTimeout(() => console.log(this.greet + ": " + user));
// This here refers to outer context
}
};
obj6.greetUser("Katty"); // Hi, Welcome: Katty
筆記:
傳遞給回調setTimeout
是一個ES6箭頭功能,它沒有自己的this
,所以它需要從它的外部環境正在greetUser
它有this
。 那是obj6
,因此我們得到輸出:
Hi, Welcome: Katty
各種各樣的:
new
與箭頭函數一起使用。prototype
屬性。apply
或call
調用箭頭函數時,我們沒有綁定this
。我仍然支持我在這個線程的第一個答案中寫的所有內容。 然而,我對代碼風格的看法從那時起就發展了,所以我對這個問題有了一個新的答案,這個答案建立在我上一個的基礎上。
關於詞法this
在我的最后一個回答中,我故意回避了我對這種語言持有的潛在信念,因為它與我提出的論點沒有直接關系。 盡管如此,如果沒有明確說明這一點,我可以理解為什么許多人在發現箭頭如此有用時,只是拒絕我不使用箭頭的建議。
我的信念是:我們不應該首先使用this
。 因此,如果一個人故意避免在他的代碼中使用this
,那么箭頭的“詞法this
”特性就幾乎沒有價值。 此外,前提下, this
是一件壞事,箭頭的治療this
是更小的“好事”; 相反,它更像是另一種糟糕的語言功能的損害控制形式。
我想,這要么不發生的一些人,但即使是那些誰這樣做,他們必須總是發現自己的代碼庫,其中內工作this
似乎每個文件一百倍,並損害控制的一點點(或很多)是一個理性的人所能期望的一切。 因此,在某種程度上,當箭頭使糟糕的情況變得更好時,它們可能是好的。
即使是更容易編寫代碼與this
用箭頭比沒有他們,使用箭頭規則仍然很復雜(參見:當前線程)。 因此,正如您所要求的那樣,指南既不“清晰”也不“一致”。 即使程序員知道箭含糊不清,我認為他們聳聳肩,反正接受他們,因為詞匯值this
黯然失色他們。
所有這些都是以下認識的序言:如果不使用this
,那么箭頭通常引起的關於this
的歧義就變得無關緊要了。 在這種情況下,箭頭變得更加中性。
關於簡潔的語法
當我寫下我的第一個答案時,我認為即使是對最佳實踐的盲目遵守也是值得付出的代價,如果這意味着我可以生成更完美的代碼。 但我最終意識到,簡潔也可以作為一種抽象形式,可以提高代碼質量——足以證明有時偏離最佳實踐是合理的。
換句話說:該死的,我也想要單行函數!
關於指南
由於this
中性箭頭函數的可能性,以及值得追求的簡潔性,我提供以下更寬松的指導方針:
this
。除了到目前為止的出色答案之外,我想提出一個非常不同的原因,為什么箭頭函數在某種意義上從根本上優於“普通”JavaScript 函數。
為了便於討論,讓我們暫時假設我們使用類型檢查器,如TypeScript或 Facebook 的“Flow”。 考慮以下玩具模塊,它是有效的 ECMAScript 6 代碼加上 Flow 類型注釋(我將在本答案的末尾包含實際上由 Babel 產生的無類型代碼,因此它實際上可以運行):
export class C { n : number; f1: number => number; f2: number => number; constructor(){ this.n = 42; this.f1 = (x:number) => x + this.n; this.f2 = function (x:number) { return x + this.n;}; } }
現在看看當我們使用來自不同模塊的類 C 時會發生什么,如下所示:
let o = { f1: new C().f1, f2: new C().f2, n: "foo" }; let n1: number = o.f1(1); // n1 = 43 console.log(n1 === 43); // true let n2: number = o.f2(1); // n2 = "1foo" console.log(n2 === "1foo"); // true, not a string!
如您所見,類型檢查器在這里失敗了:f2 應該返回一個數字,但它返回了一個字符串!
更糟糕的是,似乎沒有任何可以想象的類型檢查器可以處理普通的(非箭頭)JavaScript 函數,因為 f2 的“this”沒有出現在 f2 的參數列表中,因此不可能添加“this”所需的類型作為 f2 的注釋。
這個問題是否也會影響不使用類型檢查器的人? 我認為是這樣,因為即使我們沒有靜態類型,我們也會認為它們就在那里。 (“第一個參數必須是一個數字,第二個參數必須是一個字符串”等等)一個隱藏的“this”參數可能會或可能不會在函數體中使用,這讓我們的心理簿記更加困難。
這是可運行的無類型版本,它將由 Babel 生成:
class C { constructor() { this.n = 42; this.f1 = x => x + this.n; this.f2 = function (x) { return x + this.n; }; } } let o = { f1: new C().f1, f2: new C().f2, n: "foo" }; let n1 = o.f1(1); // n1 = 43 console.log(n1 === 43); // true let n2 = o.f2(1); // n2 = "1foo" console.log(n2 === "1foo"); // true, not a string!
我更喜歡在不需要訪問本地this
任何時候使用箭頭函數,因為箭頭函數不綁定自己的this 、參數、 super 或 new.target 。
箭頭函數或lambdas是在 ES 6 中引入的。 除了它在最小語法中的優雅之外,最顯着的功能差異是箭頭函數內部的this
作用域
在正則函數表達式中,
this
關鍵字根據調用它的上下文綁定到不同的值。在箭頭函數中,
this
是詞法綁定的,這意味着它從定義箭頭函數的范圍(父范圍)關閉this
,並且無論在何處以及如何調用/調用它都不會改變。
// this = global Window
let objA = {
id: 10,
name: "Simar",
print () { // same as print: function()
console.log(`[${this.id} -> ${this.name}]`);
}
}
objA.print(); // logs: [10 -> Simar]
objA = {
id: 10,
name: "Simar",
print: () => {
// Closes over this lexically (global Window)
console.log(`[${this.id} -> ${this.name}]`);
}
};
objA.print(); // logs: [undefined -> undefined]
在objA.print()
的情況下,當使用常規function
定義print()
方法時,它通過將this
正確解析為objA
進行方法調用來工作,但在定義為箭頭=>
函數時失敗。 這是因為this
在常規函數中作為對象 ( objA
) 上的方法調用時,是對象本身。
然而,在箭頭函數的情況下, this
在詞法上綁定到它被定義的封閉范圍的this
(在我們的例子中是全局/窗口),並在它作為objA
上的方法調用期間保持不變。
有超過常規功能的箭頭功能優點在方法的對象的(一個或多個),但僅當this
預期是固定的,並在定義時的約束。
/* this = global | Window (enclosing scope) */
let objB = {
id: 20,
name: "Paul",
print () { // Same as print: function()
setTimeout( function() {
// Invoked async, not bound to objB
console.log(`[${this.id} -> ${this.name}]`);
}, 1)
}
};
objB.print(); // Logs: [undefined -> undefined]'
objB = {
id: 20,
name: "Paul",
print () { // Same as print: function()
setTimeout( () => {
// Closes over bind to this from objB.print()
console.log(`[${this.id} -> ${this.name}]`);
}, 1)
}
};
objB.print(); // Logs: [20 -> Paul]
在objB.print()
的情況下, print()
方法被定義為調用console.log(
[${this.id} -> {this.name}] )
函數作為setTimeout
上的回調,當箭頭函數用作objB
時, this
正確解析為objB
但當回調定義為常規函數時失敗。
這是因為傳遞給setTimeout(()=>..)
的箭頭=>
函數在詞法上從其父級關閉了this
,即調用定義它的objB.print()
。 換句話說,箭頭=>
函數傳入到setTimeout(()==>...
綁定到objB
作為它的this
因為objB.print()
this
的調用是objB
本身。
我們可以很容易地使用Function.prototype.bind()
使定義為常規函數的回調工作,通過將其綁定到正確的this
。
const objB = {
id: 20,
name: "Singh",
print () { // The same as print: function()
setTimeout( (function() {
console.log(`[${this.id} -> ${this.name}]`);
}).bind(this), 1)
}
}
objB.print() // logs: [20 -> Singh]
然而,箭頭函數派上用場,並且在異步回調的情況下不太容易出錯,在這種情況下,我們知道this
在它獲取並應該綁定到的函數定義時。
this
需要跨調用更改任何時候,我們都需要一個可以在調用時更改this
的函數,我們不能使用箭頭函數。
/* this = global | Window (enclosing scope) */
function print() {
console.log(`[${this.id} -> {this.name}]`);
}
const obj1 = {
id: 10,
name: "Simar",
print // The same as print: print
};
obj.print(); // Logs: [10 -> Simar]
const obj2 = {
id: 20,
name: "Paul",
};
printObj2 = obj2.bind(obj2);
printObj2(); // Logs: [20 -> Paul]
print.call(obj2); // logs: [20 -> Paul]
以上都不適用於箭頭函數const print = () => { console.log(
[${this.id} -> {this.name}] );}
因為this
無法更改並且將保持綁定到定義它的封閉范圍的this
(全局/窗口)。
在所有這些示例中,我們使用不同的對象( obj1
和obj2
)一個接一個地調用相同的函數,這兩個對象都是在聲明print()
函數之后創建的。
這些都是人為的例子,但讓我們考慮一些更真實的例子。 如果我們必須編寫類似於處理arrays
reduce()
方法,我們又不能將其定義為 lambda,因為它需要從調用上下文(即調用它的數組)中推斷出this
。
出於這個原因,構造函數永遠不能定義為箭頭函數,因為不能在聲明時設置構造函數的this
。 每次使用new
關鍵字調用構造函數時,都會創建一個新對象,然后將其綁定到該特定調用。
此外,當框架或系統接受稍后使用動態上下文this
調用的回調函數時,我們不能再次使用箭頭函數,因為this
可能需要隨每次調用而更改。 這種情況通常出現在 DOM 事件處理程序中。
'use strict'
var button = document.getElementById('button');
button.addEventListener('click', function {
// web-api invokes with this bound to current-target in DOM
this.classList.toggle('on');
});
var button = document.getElementById('button');
button.addEventListener('click', () => {
// TypeError; 'use strict' -> no global this
this.classList.toggle('on');
});
這也是為什么像角2+和Vue.js框架期望為模板組件綁定的方法是定時功能/方法的原因this
為他們的調用由為綁定功能的框架進行管理。 (Angular 使用 Zone.js 來管理視圖模板綁定函數調用的異步上下文。)
另一方面,在React 中,當我們想要將組件的方法作為事件處理程序傳遞時,例如<input onChange={this.handleOnchange} />
,我們應該定義handleOnchanage = (event)=> {this.props.onInputChange(event.target.value);}
作為每次調用的箭頭函數。 我們希望它與為渲染的 DOM 元素生成 JSX 的組件實例相同。
這篇文章也可以在我的 Medium出版物中找到。 如果你喜歡這篇文章,或者有什么意見和建議,請在Medium上鼓掌或留言。
簡單來說,
var a = 20; function a() {this.a = 10; console.log(a);}
//20, since the context here is window.
另一個例子:
var a = 20;
function ex(){
this.a = 10;
function inner(){
console.log(this.a); // Can you guess the output of this line?
}
inner();
}
var test = new ex();
答:控制台會打印 20。
原因是每當一個函數被執行時,它自己的堆棧就會被創建,在這個例子中, ex
函數是用new
操作符執行的,因此將創建一個上下文,當執行inner
時,JavaScript 將創建一個新堆棧並執行inner
函數盡管存在局部上下文,但在global context
中。
因此,如果我們希望inner
函數具有局部上下文,即ex
,那么我們需要將上下文綁定到內部函數。
箭頭解決了這個問題。 它們不采用Global context
,而是采用local context
如果存在)。 在*給定的示例中,它將采用new ex()
作為this
。
因此,在所有綁定是顯式的情況下,默認情況下箭頭可以解決問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.