- PVSM.RU - https://www.pvsm.ru -
Одной из самых заметных новшеств современного JavaScript стало появление стрелочных функций (arrow function), которые иногда называют «толстыми» стрелочными функциями (fat arrow function). При объявлении таких функций используют особую комбинацию символов — =>.
У стрелочных функций есть два основных преимущества перед традиционными функциями. Первое — это очень удобный и компактный синтаксис. Второе заключается в том, что подход к работе со значением this в стрелочных функциях выглядит интуитивно понятнее, чем в обычных функциях.

Иногда эти и другие преимущества ведут к тому, что стрелочному синтаксису отдают безусловное предпочтение перед другими способами объявления функций. Например, популярная конфигурации eslint [1] от Airbnb принуждает к тому, чтобы всегда, когда создают анонимную функцию, такая функция была бы стрелочной.
Однако, как и у других концепций и механизмов, используемых в программировании, у стрелочных функций есть свои плюсы и минусы. Их применение способно вызывать негативные побочные эффекты. Для того чтобы пользоваться стрелочными функциями правильно, необходимо знать о возможных проблемах, связанных с ними.
В материале, перевод которого мы сегодня публикуем, речь пойдёт о том, как работают стрелочные функции. Здесь будут рассмотрены ситуации, в которых их использование способно улучшить код, и ситуации, в которых их применять не стоит.
Стрелочные функции в JavaScript — это нечто вроде лямбда-функций [2] в Python и блоков [3] в Ruby.
Это — анонимные функции с особым синтаксисом, которые принимают фиксированное число аргументов и работают в контексте включающей их области видимости, то есть — в контексте функции или другого кода, в котором они объявлены.
Поговорим об этом подробнее.
Стрелочные функции строятся по единой схеме, при этом структура функций может быть, в особых случаях, упрощена. Базовая структура стрелочной функции выглядит так:
(argument1, argument2, ... argumentN) => {
// тело функции
}
Список аргументов функции находится в круглых скобках, после него следует стрелка, составленная из символов = и >, а дальше идёт тело функции в фигурных скобках.
Это очень похоже на то, как устроены обычные функции, главные отличия заключаются в том, что здесь опущено ключевое слово function и добавлена стрелка после списка аргументов.
В определённых случаях, однако, простые стрелочные функции можно объявлять, используя гораздо более компактные конструкции.
Рассмотрим вариант синтаксиса, который используется в том случае, если тело функции представлено единственным выражением. Он позволяет обойтись без фигурных скобок, обрамляющих тело функции, и избавляет от необходимости явного возврата результатов вычисления выражения, так как этот результат будет возвращён автоматически. Например, это может выглядеть так:
const add = (a, b) => a + b;
Вот ещё один вариант сокращённой записи функции, применяемый в том случае, когда функция имеет лишь один аргумент.
const getFirst = array => array[0];
Как видите, тут опущены круглые скобки, обрамляющие список аргументов. Кроме того, тело функции, которое и в этом примере представлено одной командой, так же записано без скобок. Позже мы ещё поговорим о преимуществах подобных конструкций.
При работе со стрелочными функциями используются и некоторые более сложные синтаксические конструкции, о которых полезно знать.
Например, попытаемся использовать однострочное выражение для возврата из функции объектного литерала. Может показаться, учитывая то, что мы уже знаем о стрелочных функциях, что объявление функции будет выглядеть так:
(name, description) => {name: name, description: description};
Проблема этого кода заключается в его неоднозначности. А именно, фигурные скобки, которые мы хотим использовать для описания объектного литерала, выглядят так, будто мы пытаемся заключить в них тело функции.
Для того чтобы указать системе на то, что мы имеем в виду именно объектный литерал, нужно заключить его в круглые скобки:
(name, description) => ({name: name, description: description});
В отличие от других функций, стрелочные функции не имеют собственного контекста выполнения [4].
На практике это означает, что они наследуют сущности this и arguments от родительской функции.
Например, сравним две функции, представленные в следующем коде. Одна и них обычная, вторая — стрелочная.
const test = {
name: 'test object',
createAnonFunction: function() {
return function() {
console.log(this.name);
console.log(arguments);
};
},
createArrowFunction: function() {
return () => {
console.log(this.name);
console.log(arguments);
};
}
};
Тут имеется объект test с двумя методами. Каждый из них представляет собой функцию, которая создаёт и возвращает анонимную функцию. Разница между этими методами заключается лишь в том, что в первом из них используется традиционное функциональное выражение, а во втором — стрелочная функция.
Если поэкспериментировать с этим кодом в консоли, передавая методам объекта одни и те же аргументы, то, хотя методы и выглядят очень похожими, мы получим разные результаты:
> const anon = test.createAnonFunction('hello', 'world');
> const arrow = test.createArrowFunction('hello', 'world');
> anon();
undefined
{}
> arrow();
test object
{ '0': 'hello', '1': 'world' }
У анонимной функции есть собственный контекст, поэтому, когда её вызывают, при обращении к test.name не будет выдано значение свойства name объекта, а при обращении к arguments не будет выведен список аргументов функции, которая использовалась для создания и возврата исследуемой функции.
В случае же со стрелочной функцией оказывается, что её контекст совпадает с контекстом создавшей её функции, что даёт ей доступ и к списку аргументов, переданных этой функцией, и к свойству name объекта, методом которого эта функция является.
Традиционными лямбда-функциями, а также — стрелочными функциями, после появления их в JavaScript, обычно пользуются в ситуации, когда некая функция применяется к каждому элементу некоего списка.
Например, если имеется массив значений, который нужно преобразовать с использованием метода массивов map, для описания такого преобразования идеально подходит стрелочная функция:
const words = ['hello', 'WORLD', 'Whatever'];
const downcasedWords = words.map(word => word.toLowerCase());
Вот чрезвычайно распространённый пример подобного использования стрелочных функций, который заключается в работе со свойствами объектов:
const names = objects.map(object => object.name);
Аналогично, если вместо традиционных циклов for используют современные циклы forEach, основанные на итераторах, то, что стрелочные функции используют this родительской сущности, делает их использование понятным на интуитивном уровне:
this.examples.forEach(example => {
this.runExample(example);
});
Ещё одна ситуация, где стрелочные функции позволяют писать более чистый и понятный код, представлена асинхронными программными конструкциями.
Так, промисы [5] значительно упрощают работу с асинхронным кодом. При этом, даже если вы предпочитаете использовать конструкцию async/await, то без понимания промисов [6] вам не обойтись, так как эта конструкция основана на них.
Однако при использовании промисов нужно объявлять функции, которые вызываются после завершения работы асинхронного кода или завершения асинхронного обращения к некоему API.
Это — идеальное место для использования стрелочных функций, в особенности в том случае, если результирующая функция обладает неким состоянием, ссылается на что-либо в объекте. Например, это может выглядеть так:
this.doSomethingAsync().then((result) => {
this.storeResult(result);
});
Ещё один распространённый пример использования стрелочных функций заключается в инкапсуляции трансформаций объектов.
Например, в Vue.js существует общепринятый паттерн [7] включения фрагментов хранилища Vuex напрямую в компонент Vue с использованием mapState.
Эта операция включает в себя объявления набора «преобразователей», которые выбирают из исходного полного состояния именно то, что нужно для конкретного компонента.
Такие вот простые преобразования — идеальное место для использования стрелочных функций. Например:
export default {
computed: {
...mapState({
results: state => state.results,
users: state => state.users,
});
}
}
Существует целый ряд ситуаций, в которых использование стрелочных функций — не лучшая идея. Стрелочные функции, если применять их необдуманно, не только не помогают программистам, но и становятся источником проблем.
Первая такая ситуация заключается в использовании стрелочных функций в качестве методов объектов. Здесь важны контекст выполнения и ключевое слово this, характерные для традиционных функций.
Одно время популярным было применение комбинации свойств классов и стрелочных функций для создания методов с «автоматической привязкой», то есть таких, которые могут быть использованы обработчиками событий, но остаются привязанными к классу. Выглядело это примерно так:
class Counter {
counter = 0;
handleClick = () => {
this.counter++;
}
}
При использовании подобной конструкции, даже если функция handleClick вызывалась обработчиком событий, а не в контексте экземпляра класса Counter, у этой функции был доступ к данным этого экземпляра.
Однако у такого подхода масса минусов, которым посвящён этот материал [8].
Хотя применение стрелочной функции здесь, безусловно, представляет собой удобно выглядящий способ привязки функции, поведение этой функции во многих аспектах оказывается далёким от интуитивной понятности, мешая тестированию и создавая проблемы в ситуациях, когда, например, соответствующий объект пытаются использовать как прототип.
В подобных случаях, вместо стрелочных функций, используйте обычные функции, и, если нужно, привязывайте к ним экземпляр объекта в конструкторе:
class Counter {
counter = 0;
handleClick() {
this.counter++;
}
constructor() {
this.handleClick = this.handleClick.bind(this);
}
}
Стрелочные функции могут стать источником проблем в том случае, если их планируется использовать во множестве различных комбинаций, в частности — в длинных цепочках вызовов функций.
Основная причина подобных проблем, как и при использовании анонимных функций, заключается в том, что они дают крайне неинформативные результаты трассировки стека вызовов [9].
Это не так уж и плохо, если, например, имеется лишь один уровень вложенности вызовов функций, скажем, если речь идёт о функции, используемой в итераторе. Однако если все используемые функции объявлены как стрелочные, и эти функции друг друга вызывают, то, при возникновении ошибки, разобраться в происходящем будет нелегко. Сообщения об ошибках будут выглядеть примерно так:
{anonymous}()
{anonymous}()
{anonymous}()
{anonymous}()
{anonymous}()
Последняя из обсуждаемых нами ситуаций, в которых стрелочные функции могут стать источником неприятностей, заключается в использовании их там, где нужна динамическая привязка this.
Если в подобных ситуациях применяются стрелочные функции, то динамическая привязка this работать не будет. Эта неприятная неожиданность может заставить поломать голову над причинами происходящего тех, кому придётся работать кодом, в котором стрелочными функциями пользуются неправильно.
Вот некоторые вещи, о которых нужно помнить, рассматривая возможность использования стрелочных функций:
this, привязанным к атрибуту события currentTarget.this к выбранному элементу DOM.this к компоненту Vue.Конечно, стрелочные функции можно использовать намеренно, для того, чтобы изменить стандартное поведение программных механизмов. Но, особенно в случаях с jQuery и Vue, подобное часто вступает в конфликт с обычным функционированием системы, что приводит к тому, что программист не может понять, почему некий код, выглядящий вполне нормальным, вдруг отказывается работать.
Стрелочные функции — это замечательная свежая возможность JavaScript. Они позволяют, во многих ситуациях, писать более удобный код, чем раньше. Но, как и в случае с другими возможностями, у них есть и преимущества, и недостатки. Поэтому использовать стрелочные функции надо там, где они могут принести пользу, не рассматривая их в виде полной замены для обычных функций.
Уважаемые читатели! Сталкивались ли вы с ситуациями, в которых использование стрелочных функций приводит к ошибкам, неудобствам или к неожиданному поведению программ?
Автор: ru_vds
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/298315
Ссылки в тексте:
[1] конфигурации eslint: https://github.com/airbnb/javascript#arrow-functions
[2] лямбда-функций: https://www.programiz.com/python-programming/anonymous-function
[3] блоков: http://ruby-for-beginners.rubymonstas.org/blocks.html
[4] контекста выполнения: https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0
[5] промисы: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
[6] понимания промисов: https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8
[7] паттерн: https://vuex.vuejs.org/guide/state.html#the-mapstate-helper
[8] этот материал: https://medium.com/@charpeni/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think-3b3551c440b1
[9] стека вызовов: https://hackernoon.com/three-reasons-i-avoid-anonymous-js-functions-like-the-plague-7f985c27a006
[10] Image: https://ruvds.com/ru-rub/#order
[11] Источник: https://habr.com/post/428566/?utm_campaign=428566
Нажмите здесь для печати.