簡體   English   中英

什么時候應該在 ECMAScript 6 中使用箭頭函數?

[英]When should I use arrow functions in ECMAScript 6?

使用() => {}function () {}我們得到了兩種非常相似的方法來編寫 ES6 中的函數。 在其他語言中,lambda 函數通常通過匿名來區分自己,但在 ECMAScript 中,任何 function 都可以是匿名的。 這兩種類型中的每一種都有獨特的使用域(即當this需要顯式綁定或顯式不綁定時)。 在這些領域之間,有大量的情況可以使用任何一種表示法。

ES6 中的箭頭函數至少有兩個限制:

  • 不使用new並且不能在創建prototype時使用
  • 在初始化時修復this綁定到 scope

除了這兩個限制,箭頭函數理論上幾乎可以在任何地方取代常規函數。 在實踐中使用它們的正確方法是什么? 是否應該使用箭頭函數,例如:

  • “他們工作的任何地方”,即 function 不必對this變量不可知,我們也不會創建 object。
  • 只有“他們需要的任何地方”,即事件監聽器,超時,需要綁定到某個 scope
  • 具有“短”功能,但不具有“長”功能
  • 僅適用於不包含另一個箭頭的功能 function

我正在尋找在未來版本的 ECMAScript 中選擇適當的 function 符號的指南。 指南需要清晰,以便可以將其傳授給團隊中的開發人員,並且需要保持一致,這樣就不需要不斷地從一個 function 符號來回重構到另一個符號。

這個問題是針對那些在即將到來的 ECMAScript 6 (Harmony) 的上下文中考慮過代碼風格並且已經使用過該語言的人。

不久前,我們的團隊將其所有代碼(一個中型 AngularJS 應用程序)遷移到使用 Traceur Babel編譯的 JavaScript。 我現在對 ES6 及更高版本中的函數使用以下經驗法則:

  • 在全局范圍內和Object.prototype屬性中使用function
  • class用於對象構造函數。
  • 在其他地方使用=>

為什么幾乎到處都使用箭頭函數?

  1. 范圍安全:當一致地使用箭頭函數時,保證一切都使用與根相同的thisObject 如果即使是單個標准函數回調與一堆箭頭函數混合在一起,范圍也有可能變得混亂。
  2. 緊湊性:箭頭函數更易於讀寫。 (這可能看起來很自以為是,所以我將進一步舉幾個例子。)
  3. 清晰:當幾乎所有東西都是箭頭函數時,任何常規function立即突出來定義作用域。 開發人員總是可以查找下一個更高的function語句來查看thisObject是什么。

為什么總是在全局作用域或模塊作用域上使用常規函數?

  1. 指示不應訪問thisObject的函數。
  2. window對象(全局作用域)最好是明確尋址的。
  3. 許多Object.prototype定義都存在於全局范圍內(想想String.prototype.truncate等),而且那些通常必須是function類型。 在全局范圍內始終如一地使用function有助於避免錯誤。
  4. 全局作用域中的許多函數是舊式類定義的對象構造函數。
  5. 函數可以命名為1 這有兩個好處:(1) 編寫function foo(){}const foo = () => {}更不笨拙——尤其是在其他函數調用之外。 (2) 函數名稱顯示在堆棧跟蹤中。 雖然命名每個內部回調會很乏味,但命名所有公共函數可能是一個好主意。
  6. 函數聲明被提升(意味着它們可以在聲明之前被訪問),這是靜態實用函數中的一個有用屬性。

對象構造器

嘗試實例化箭頭函數會引發異常:

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.forEachArray.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關鍵字。


筆記

  1. ES6 規范中推遲命名箭頭函數。 它們可能仍會在未來版本中添加。
  2. 根據規范草案, “類聲明/表達式創建一個與函數聲明完全一樣的構造函數/原型對” ,只要類不使用extend關鍵字。 一個細微的區別是類聲明是常量,而函數聲明不是。
  3. 關於單語句箭頭函數中的塊的注意事項:我喜歡在單獨調用箭頭函數用於副作用(例如,賦值)的任何地方使用塊。 這樣很明顯可以丟棄返回值。

根據提案,箭頭旨在“解決和解決傳統函數表達式的幾個常見痛點”。 他們打算通過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:

  • Mocha(昨天下載量約為 12 萬次)通過this公開其測試方法。
  • Grunt(昨天的下載量約為 63k)通過this公開了構建任務的方法。
  • Backbone(昨天約 22k 次下載)定義了訪問this方法。
  • 事件 API(如 DOM)引用具有thisEventTarget
  • 修補或擴展的原型API 引用具有this .

(通過http://trends.builtwith.com/javascript/jQuery和 https://www.npmjs.com 進行統計。)

您可能已經需要動態this綁定。

詞匯this有時是預期的,但有時不是; 就像動態一樣, this有時是預期的,但有時不是。 值得慶幸的是,有一種更好的方法,它總是產生和傳達預期的綁定。

關於簡潔的語法

箭頭函數成功地為函數提供了“更短的語法形式”。 但是這些較短的函數會讓你更成功嗎?

x => x * xfunction (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

關於指南

您要求一個需要“清晰”和“一致”的指南。 使用箭頭函數最終將導致語法有效、邏輯無效的代碼,兩種函數形式交織在一起,有意義且任意。 因此,我提供以下內容:

ES6 中的函數符號指南:

  • 始終創建具有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 箭頭函數:

箭功能不應使用:

  1. 當我們想要函數提升時
    • 因為箭頭函數是匿名的。
  2. 當我們想在函數中使用this / arguments
    • 由於箭頭函數沒有自己的this / arguments ,它們依賴於它們的外部上下文。
  3. 當我們想使用命名函數時
    • 因為箭頭函數是匿名的。
  4. 當我們想使用函數作為constructor
    • 因為箭頭函數沒有自己的this
  5. 當我們想在對象字面量中添加函數作為屬性並在其中使用對象時
    • 因為我們無法訪問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 函數,它有自己的thisuse-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屬性。
  • 當通過applycall調用箭頭函數時,我們沒有綁定this

我仍然支持在這個線程的第一個答案中寫的所有內容。 然而,我對代碼風格的看法從那時起就發展了,所以我對這個問題有了一個新的答案,這個答案建立在我上一個的基礎上。

關於詞法this

在我的最后一個回答中,我故意回避了我對這種語言持有的潛在信念,因為它與我提出的論點沒有直接關系。 盡管如此,如果沒有明確說明這一點,我可以理解為什么許多人在發現箭頭如此有用時,只是拒絕我不使用箭頭的建議。

我的信念是:我們不應該首先使用this 因此,如果一個人故意避免在他的代碼中使用this ,那么箭頭的“詞法this ”特性就幾乎沒有價值。 此外,前提下, this是一件壞事,箭頭的治療this是更小的“好事”; 相反,它更像是另一種糟糕的語言功能的損害控制形式。

我想,這要么不發生的一些人,但即使是那些誰這樣做,他們必須總是發現自己的代碼庫,其中內工作this似乎每個文件一百倍,並損害控制的一點點(或很多)是一個理性的人所能期望的一切。 因此,在某種程度上,當箭頭使糟糕的情況變得更好時,它們可能是好的。

即使是更容易編寫代碼與this用箭頭比沒有他們,使用箭頭規則仍然很復雜(參見:當前線程)。 因此,正如您所要求的那樣,指南既不“清晰”也不“一致”。 即使程序員知道箭含糊不清,我認為他們聳聳肩,反正接受他們,因為詞匯值this黯然失色他們。

所有這些都是以下認識的序言:如果不使用this ,那么箭頭通常引起的關於this的歧義就變得無關緊要了。 在這種情況下,箭頭變得更加中性。

關於簡潔的語法

當我寫下我的第一個答案時,我認為即使是對最佳實踐的盲目遵守也是值得付出的代價,如果這意味着我可以生成更完美的代碼。 但我最終意識到,簡潔也可以作為一種抽象形式,可以提高代碼質量——足以證明有時偏離最佳實踐是合理的。

換句話說:該死的,我也想要單行函數!

關於指南

由於this中性箭頭函數的可能性,以及值得追求的簡潔性,我提供以下更寬松的指導方針:

ES6 中的函數符號指南:

  • 不要用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 (全局/窗口)。

在所有這些示例中,我們使用不同的對象( obj1obj2 )一個接一個地調用相同的函數,這兩個對象都是在聲明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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM