Коли я повинен використовувати функції стрілок у ECMAScript 6?


406

Питання спрямоване на людей, які задумалися про стиль коду в контексті майбутньої ECMAScript 6 (Гармонія) і які вже працювали з мовою.

З () => {}і function () {}ми отримуємо два дуже схожих способи запису функцій в ES6. В інших мовах функції лямбда часто розрізняють себе за анонімністю, але в ECMAScript будь-яка функція може бути анонімною. Кожен з двох типів має унікальні домени використання (а саме тоді, коли їх thisпотрібно явно або явно не зв’язувати). Між цими доменами існує велика кількість випадків, коли буде здійснено будь-яке позначення.

Функції стрілки в ES6 мають щонайменше два обмеження:

  • Не працюйте з ним newі його не можна використовувати під час створенняprototype
  • Фіксований thisобмежений обсяг при ініціалізації

Ці два обмеження в сторону, функції стрілок теоретично могли замінити звичайні функції майже де завгодно. Який правильний підхід використовує їх на практиці? Якщо слід використовувати функції стрілок, наприклад:

  • "скрізь, де вони працюють", тобто скрізь, коли функція не повинна бути агностичною щодо thisзмінної, і ми не створюємо об'єкт.
  • лише "всюди, де вони потрібні", тобто слухачі подій, тайм-аути, які потрібно прив’язати до певної межі
  • з "короткими" функціями, але не з "довгими"
  • лише з функціями, які не містять іншої функції зі стрілкою

Що я шукаю - це настанова щодо вибору відповідної позначення функції у майбутній версії ECMAScript. Посібник повинен бути чітким, щоб його можна було навчити розробникам в команді та бути послідовним, щоб воно не вимагало постійного рефакторингу вперед-назад від однієї нотації функції до іншої.


33
Ви вважаєте Fixed this bound to scope at initialisationобмеженням?
thefourtheye

12
Це є перевагою, але також може бути обмеженням, якщо ви плануєте повторно використовувати функцію поза вихідним контекстом. Наприклад, динамічно додаючи функцію до класу через Object.prototype. Що я маю на увазі під «обмеженням», це те, що зміна значення - thisце те, що ви можете робити зі звичайними функціями, а не зі стрілковими функціями.
lyschoening

1
Чесно кажучи, я вважаю, що вказівки щодо стилю кодування досить сумнівні. Не зрозумійте мене неправильно, я вважаю, що вони важливі, але немає жодної настанови, яка підходить для всіх.
Фелікс Клінг

Я не думаю Fixed this bound to scope at initialisation, що це обмеження. :) Подивіться на цю статтю: exploringjs.com/es6/ch_arrow-functions.html
NgaNguyenDuy

3
@thefourtheye, тут "обмеження" означає "обмеження, тому що німий автоматичний перекладач коду не міг наосліп замінити один іншим і припустити, що все буде працювати так, як очікувалося".
Pacerier

Відповіді:


322

Нещодавно наша команда перенесла весь код (середній розмір програми AngularJS) на JavaScript, складений за допомогою Traceur Babel . Зараз я використовую таке правило: для функцій в ES6 та поза ним:

  • Використання functionв глобальному масштабі та для Object.prototypeвластивостей.
  • Використовувати 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 ES Harmony майже настільки ж компактне:

class Person {
    constructor(name) {
        this.name = name;
    }
}

Я сподіваюся, що використання колишнього позначення врешті-решт буде відмовлено. Позначення конструктора об'єктів все ще можуть використовуватися деякими для простих фабрик анонімних об'єктів, де об'єкти програмно генеруються, але не для багатьох інших.

Там, де потрібен конструктор об'єктів, слід розглянути можливість перетворення функції в а, classяк показано вище. Синтаксис працює і з анонімними функціями / класами.


Читання функцій стрілок

Напевно, найкращим аргументом для дотримання звичайних функцій - безпека сфери дії буде проклята - буде те, що функції стрілок менш читабельні, ніж звичайні функції. Якщо ваш код в першу чергу не функціонує, то функції стрілок можуть здатися не потрібними, а коли функції стрілок не використовуються послідовно, вони виглядають некрасиво.

ECMAScript змінився досить сильно, оскільки ECMAScript 5.1 дав нам функціонал Array.forEach, Array.mapі всі ці функції функціонального програмування, які дозволяють нам використовувати функції, де раніше використовувались цикли. Асинхронний 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. Примітка щодо блоків у функціях стрілок з одним висловом: Мені подобається використовувати блок там, де функція стрілки викликається лише для побічного ефекту (наприклад, призначення). Таким чином зрозуміло, що повернене значення можна відкинути.

4
Інший раз, коли ви хочете скористатися, functionце коли ви не хочете thisбути пов'язаними, правда? Мій найпоширеніший сценарій для цього - події, де ви, можливо, захочете thisпосилатися на об’єкт (зазвичай вузол DOM), який ініціював подію.
Бретт

13
Я фактично думаю, що в прикладі 3 регулярні функції є більш читабельними. Навіть непрограмісти могли божествувати, що відбувається. За допомогою стрілок вам потрібно точно знати, як вони працюють, щоб зрозуміти цей приклад. Можливо, більше нових рядків допоможе прикладом стрілки, але я не знаю. Тільки мої 2 копійки, але стрілки змушують мене скуйовджуватись (але я їх ще не використовував, тому я можу скоро перетворитись)
Спенсер,

3
@Spencer - це справедлива точка. З мого власного досвіду, =>з часом виглядаю краще. Сумніваюсь, що непрограмісти почувались би по-різному щодо двох прикладів. Якщо ви пишете код ES2016, зазвичай ви також не будете користуватися цією кількістю функцій стрілок. У цьому прикладі, використовуючи async / await та розуміння масиву, ви отримаєте лише одну функцію зі стрілкою у reduce()виклику.
lyschoening

3
Я повністю погоджуюся зі Спенсером, що регулярні функції в цьому прикладі набагато легше читати.
jonschlinkert

2
Гарна відповідь, thx! особисто я також максимально використовую стрілки в глобальному масштабі. Це не залишає у мене майже жодної функції. Для мене "функція" в коді означає особливий випадок, який повинен бути вичерпаним і ретельно продуманим.
кофіфус

80

Згідно з пропозицією , стрілки спрямовані на "вирішення та усунення декількох загальних больових точок традиційних Function Expression". Вони мали намір покращити питання, 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 використовується на понад 50 мільйонах веб-сайтів (станом на цей текст у лютому 2016 року). Ось інші API, що thisдинамічно прив’язуються :

  • Mocha (~ 120 к завантажень вчора) розкриває методи своїх тестів через this.
  • Grunt (~ 63 к завантаження вчора) розкриває методи складання завдань через this.
  • Хребет (~ 22 к. Завантажень вчора) визначає методи доступу this.
  • API - інтерфейси подій (як і DOM - х) відносяться до EventTargetз this.
  • Прототипні API, які виправлені або розширені, посилаються на екземпляри з this.

(Статистика через http://trends.builtwith.com/javascript/jQuery та https://www.npmjs.com .)

Вам, швидше за все, потрібні динамічні thisприв’язки.

Лексика thisіноді очікується, але іноді ні; так само, як thisіноді очікується динаміка , але іноді ні. На щастя, є кращий спосіб, який завжди виробляє та повідомляє очікуване прив'язування.

Щодо короткого синтаксису

Функції стрілок досягли «коротшої синтаксичної форми» для функцій. Але чи зроблять вас ці коротші функції більш успішними?

Чи x => 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 parens

Блоки мають вигляд об’єктів:

(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змінну. Не використовуйте () => {}.

5
Цікаво написати на функціональному перегляді програміста на JavaScript. Я не впевнений, що згоден з аргументом приватних змінних. ІМО мало хто їх справді потребує; тим, хто це зробив, ймовірно, також знадобляться інші функції контракту та все-таки піти на розширення мови, наприклад TypeScript. Я, безумовно, бачу заклик selfзамість цього. Ваші заявлені підводні функції стрілок також є дійсними, і тут також безумовно застосовуються ті самі стандарти, що і для інших тверджень, які можуть пройти без дужок; в іншому випадку, я думаю, що з вашого аргументу можна було б так само захищати функції стрілок скрізь.
лишчогінг

7
"Наявність кількох способів робити речі створює непотрібні вектори для аргументів та розбіжностей на робочому місці та мовній спільноті. Було б краще, якби граматика мови не дозволяла нам робити поганий вибір". Погодьтеся так багато. Гарний запис! Я думаю, що функції стрілок - це насправді крок назад. Щодо іншої теми, я хотів би, щоб мої колеги перестали намагатися перетворити JavaScript на C # із серією визначень .prototype. Це огидно. Я повинен анонімно пов’язати ваше повідомлення :)
Спенсер,

11
Дуже добре написано! Хоча я не згоден з більшістю ваших точок, важливо враховувати протилежну точку зору.
minexew

4
Не функції стрілок, а дивна поведінка this- це проблема Javascript. Замість того, щоб бути неявно пов'язаними, thisслід передавати як явний аргумент.
боб

5
" Завжди використовуйте функцію (тому це завжди можна динамічно обмежувати), і завжди посилайтеся на це через змінну. " Я більше не могла погодитися!

48

Функції стрілок були створені для спрощення функціонування 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?

Тут це означатиме, window objectніж те inner function, що випало scope. Тому що thisзавжди посилається на власника функції, в якій він перебуває, для цього випадку - оскільки він зараз поза сферою дії - вікно / глобальний об'єкт.

Коли він знаходиться всередині objectметоду function's, власник об'єкта є власником. Таким чином, це ключове слово пов'язане з об'єктом. Але коли він знаходиться всередині функції, або окремо, або в рамках іншого методу, він завжди буде посилатися на 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функцію стрілки, ми можемо вирішити lexical scopingпроблему більш простим способом.

var Actor = {
  name: 'RajiniKanth',
  movies: ['Kabali', 'Sivaji', 'Baba'],
  showMovies: function() {
   this.movies.forEach((movie) => {
     alert(this.name + " has acted in " + movie);
   });
  }
};

Actor.showMovies();

Arrow functionsбільше схожі на функції функцій, за винятком того, що вони bindце parent scope. Якщо arrow function is in top scope, thisаргумент буде посилатися window/global scope, в той час як стрілка функція всередині звичайної функції буде мати цей аргумент так само , як його зовнішня функція.

З arrowфункціями thisпов'язано з огороджувальних scopeпід час створення і не може бути змінений. Новий оператор, прив’язати, викликати та застосувати не впливає на це.

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або використовуючи bind. З ES6 стає легше керувати тим this, що його пов'язано lexical scoping.

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визначається за допомогою функції стрілки, але при виклику вона попереджає не визначено, оскільки this.nameце undefinedконтекст залишається window.

Це відбувається тому, що функція стрілки лексично пов'язує контекст із window object... тобто зовнішньою областю. Виконання this.nameеквівалентне window.name, що не визначено.

Об'єктний прототип

Це ж правило застосовується при визначенні методів на a 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у будівельній виклику - новостворений об’єкт. При виконанні нового 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 є загальним завданням у програмуванні на стороні клієнта. Подія запускає функцію обробника з цим як цільовим елементом.

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.innerHTML = 'Clicked button'правильно змінюється текст кнопки, щоб відображати статус натиснутого.

Посилання: https://dmitripavlutin.com/when-not-to-use-arrow-functions-in-javascript/


Ну, я мушу визнати, що "найкраще лежить посередині" . Запропонований для твердження, що функції стрілок не охоплюють жодних можливих випадків використання функцій. Вони справді розроблені для вирішення лише частини загальних проблем. Просто переключитися на них повністю буде зайвим.
BlitZ

@DmitriPavlutin: Перевірте мій оновлений пост, його колекцію багато речей ... можливо, я повинен розмістити посилання.
Талайвар

2
Ваш код після рядка "використовуючи прив'язувати для приєднання цього ключового слова, яке відноситься до методу до внутрішньої функції методу". в ньому є помилки. Ви перевірили решту ваших прикладів?
Ісаак Пак

У них using bind to attach the this keyword that refers to the method to the method’s inner function.є синтаксичні помилки.
Coda Chang

Має бути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();
Coda Chang

14

Функції стрілки - найпоширеніша функція ES6 до цих пір ...

Використання: Усі функції ES5 слід замінити на функції стрілок ES6, за винятком наступних сценаріїв:

Функції стрілки НЕ слід використовувати:

  1. Коли ми хочемо підняти функцію
    • оскільки функції стрілок анонімні.
  2. Коли ми хочемо використовувати this/ argumentsв функції
    • оскільки функції стрілок не мають this/ argumentsє власними, вони залежать від їх зовнішнього контексту.
  3. Коли ми хочемо використовувати названу функцію
    • оскільки функції стрілок анонімні.
  4. Коли ми хочемо використовувати функцію як a 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, з argumentsмасивом , як об'єкт і аргументом , переданим це 2так arguments[0]для foo2.

abcє ES6 стрілки функції , так як вона не має його власного , argumentsотже , вона виводить arguments[0]з fooцього зовнішнього контексту замість цього.

Варіант 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властивості. У нас НЕ є прив'язка, thisколи функція стрілки викликається через applyабо call.


6

Окрім чудових відповідей на даний момент, я хотів би представити зовсім іншу причину, чому функції стрілок у певному сенсі принципово кращі, ніж "звичайні" функції 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 не зустрічається у списку аргументів f2, тому потрібний тип для "цього" неможливо додати як зауваження до f2.

Чи впливає ця проблема також на людей, які не використовують контрольні типи? Я так думаю, бо навіть коли ми не маємо статичних типів, ми думаємо так, ніби вони є. ("Першими параметрами повинно бути число, другим - рядок" тощо). Прихований "цей" -аргумент, який може бути або не може використовуватися в тілі функції, ускладнює наш ментальний облік.

Ось запущена нетипізована версія, яку випустив 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!



3

Я досі стою біля всього, що написав у своїй першій відповіді в цій темі. Однак моя думка про стиль коду склалася з тих пір, тож у мене є нова відповідь на це питання, яке базується на моєму останньому.

Щодо лексичного this

В останній своїй відповіді я навмисно ухилявся від основної віри, яку я притримуюсь щодо цієї мови, оскільки це не було безпосередньо пов'язане з аргументом, який я висловлював. Тим не менш, без цього прямо не сказано, я можу зрозуміти, чому багато людей просто наголошують на моїй рекомендації не використовувати стрілки, якщо вони вважають стрілки настільки корисними.

Я вважаю, що ми не повинні користуватися thisв першу чергу. Тому, якщо людина навмисно уникає використання thisу своєму коді, тоді "лексична this" особливість стрілок має мало значення. Крім того, за умови, що thisце погана річ, поводження зі стрілкою thisменше "доброї речі". натомість це скоріше форма контролю пошкоджень для іншої функції поганої мови.

Я вважаю, що це або не трапляється з деякими людьми, але навіть тим, кому це потрібно, вони повинні незмінно опинятися в кодових базах, де this сто разів з'являється за файл, і трохи (або багато) контролю за пошкодженнями - це все. розумна людина може сподіватися. Тож стрілки можуть бути хорошими, певним чином, коли вони покращують погану ситуацію.

Навіть якщо простіше писати код за thisдопомогою стрілок, ніж без них, правила використання стрілок залишаються дуже складними (див. Поточний потік). Таким чином, вказівки не є ні "чіткими", ні "послідовними", як ви просили. Навіть якщо програмісти знають про двозначності стрілок, я думаю, що вони все-таки знизують плечима і приймають їх, бо значення лексичного thisзатьмарює їх.

Все це є передмовою до наступного усвідомлення: якщо хтось не використовує this, то неоднозначність щодо того, thisщо стрілки зазвичай викликають, стає неактуальною. У цьому контексті стрілки стають більш нейтральними.

Щодо короткого синтаксису

Коли я писав свою першу відповідь, я вважав, що навіть рабське дотримання найкращих практик було ціною, яку варто заплатити, якщо це означало, що я можу створити більш досконалий код. Але я врешті-решт зрозумів, що лаконічність може слугувати формою абстракції, яка також може покращити якість коду - достатньо, щоб іноді виправдати відмову від найкращих практик.

Іншими словами: чорт, я теж хочу однолінійних функцій!

Щодо настанови

З можливістю this-нейтральних функцій стрілок і лагідності варто переслідувати, я пропоную наступне більш м'яке керівництво:

Керівництво для функцій нотації в ES6:

  • Не використовуйте this.
  • Використовуйте декларації функцій для функцій, які ви б дзвонили по імені (тому що вони піднімаються).
  • Використовуйте стрілочні функції для зворотних викликів (адже вони, як правило, стисліші).

Погодьтесь на 100% із розділом "Настанови щодо функціональних позначень у ES6" внизу, особливо з функціями підйому та вбудованого виклику. приємна відповідь!
Джефф

1

По-простому,

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буде виконаний, JS створить новий стек і виконає innerфункцію aglobal context хоча є локальний контекст.

Отже, якщо ми хочемо, щоб innerфункція мала локальний контекст, який є, exтоді нам потрібно прив’язати контекст до внутрішньої функції.

Стрілки вирішують цю проблему, замість того, щоб Global contextприймати те, local contextякщо вони є. У given example,ньому буде прийнято new ex()як this.

Отже, у всіх випадках, коли прив'язка явна, стрілки вирішують проблему за замовчуванням.


1

У 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()коли print()метод визначений за допомогою регулярного function , він працював, thisправильно вирішивши objAдля виклику методу, але не вдався, коли визначено як функцію стрілки =>. Це тому, що thisв звичайній функції, коли викликається як метод на об'єкті ( objA), знаходиться сам об'єкт. Однак, у випадку функції стрілки, вона thisлексично прив’язана до області thisдодаючої області, де вона була визначена (global / Window в нашому випадку), і залишається вона залишається такою ж під час виклику, як метод на 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 , thisвирішується правильно, objBколи функція стрілки використовувалася як зворотній дзвінок, але не вдалася коли зворотний дзвінок був визначений як звичайна функція. Це тому, що =>функція стрілки переходить до setTimeout(()=>..)закритої thisлексично від її батьківського, тобто. виклик objB.print()якого визначив це. Іншими словами, =>функція стрілки переходить до setTimeout(()==>...прив’язаного до objBяк її, thisоскільки виклик objB.print() thisбувobjB самим собою.

Ми можемо легко використовувати Function.prototype.bind(), щоб зробити зворотний виклик визначеним як звичайна функція, прив’язавши його до правильного this.

const objB = {
 id: 20,
 name: "Singh",
 print () { // same as print: function() 
  setTimeout( (function() {
    console.log(`[${this.id} -> ${this.name}]`);
  }).bind(this), 1)
 }
}
objB.print() // logs: [20 -> Singh]

Однак функції стрілок корисні і менш схильні до помилок у випадку зворотного виклику асинхронізації, де нам відомо this на момент визначення функцій, до яких вони потрапляють і повинні бути прив’язані.

Обмеження стрілок-функцій, коли це потрібно змінювати через виклики

У будь-який час нам потрібна функція, thisяку можна змінити під час виклику, ми не можемо використовувати функції стрілок.

/* this = global | Window (enclosing scope) */

function print() { 
   console.log(`[${this.id} -> {this.name}]`);
}
const obj1 = {
 id: 10,
 name: "Simar",
 print // 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тієї прикладної області, де вона була визначена (global / Window). У всіх цих прикладах ми викликали одну і ту ж функцію з різними об'єктами ( obj1і obj2) один за одним, обидва вони були створені післяprint() функції.

Це були надумані приклади, але давайте подумаємо про ще кілька прикладів із реального життя. Якщо нам довелося написати наш reduce()метод, подібний до того, який працює arrays , ми знову не можемо визначити його як лямбда, оскільки він повинен випливати thisз контексту виклику, тобто. масив, на який він викликався

З цієї причини constructorфункції ніколи не можна визначити як функції стрілки, так як 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');
});

Це також причина того, що в таких структурах, як Angular 2+ і Vue.js очікують, що методи прив'язки компонентів шаблону будуть регулярними функціями / методами, оскільки thisїх виклик управляється рамками для функцій зв'язування. (Angular використовує Zone.js для управління контекстом асинхронізації для викликів функцій прив'язки шаблона представлення).

З іншого боку, в React , коли ми хочемо передати метод компонента як обробник подій, наприклад, <input onChange={this.handleOnchange} />ми повинні визначити handleOnchanage = (event)=> {this.props.onInputChange(event.target.value);}функцію стрілки, як і для кожного виклику, ми хочемо, щоб це був той самий екземпляр компонента, який видав JSX для візуалізації Елемент DOM.


Ця стаття також прихильна до моєї середньої публікації. Якщо вам подобається артиль чи є якісь коментарі та пропозиції, будь ласка, плескайте або залишайте коментарі на Medium .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.