Стрелочные функции в JavaScript: зачем они нужны, как с ними обращаться, когда ими стоит пользоваться, а когда — нет

в 9:00, , рубрики: javascript, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

Одной из самых заметных новшеств современного JavaScript стало появление стрелочных функций (arrow function), которые иногда называют «толстыми» стрелочными функциями (fat arrow function). При объявлении таких функций используют особую комбинацию символов — =>.

У стрелочных функций есть два основных преимущества перед традиционными функциями. Первое — это очень удобный и компактный синтаксис. Второе заключается в том, что подход к работе со значением this в стрелочных функциях выглядит интуитивно понятнее, чем в обычных функциях.

image


Иногда эти и другие преимущества ведут к тому, что стрелочному синтаксису отдают безусловное предпочтение перед другими способами объявления функций. Например, популярная конфигурации eslint от Airbnb принуждает к тому, чтобы всегда, когда создают анонимную функцию, такая функция была бы стрелочной.

Однако, как и у других концепций и механизмов, используемых в программировании, у стрелочных функций есть свои плюсы и минусы. Их применение способно вызывать негативные побочные эффекты. Для того чтобы пользоваться стрелочными функциями правильно, необходимо знать о возможных проблемах, связанных с ними.

В материале, перевод которого мы сегодня публикуем, речь пойдёт о том, как работают стрелочные функции. Здесь будут рассмотрены ситуации, в которых их использование способно улучшить код, и ситуации, в которых их применять не стоит.

Особенности стрелочных функций в JavaScript

Стрелочные функции в JavaScript — это нечто вроде лямбда-функций в Python и блоков в 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});

▍Стрелочные функции и включающий их контекст выполнения

В отличие от других функций, стрелочные функции не имеют собственного контекста выполнения.

На практике это означает, что они наследуют сущности 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);
});

▍Промисы и цепочки промисов

Ещё одна ситуация, где стрелочные функции позволяют писать более чистый и понятный код, представлена асинхронными программными конструкциями.

Так, промисы значительно упрощают работу с асинхронным кодом. При этом, даже если вы предпочитаете использовать конструкцию async/await, то без понимания промисов вам не обойтись, так как эта конструкция основана на них.

Однако при использовании промисов нужно объявлять функции, которые вызываются после завершения работы асинхронного кода или завершения асинхронного обращения к некоему API.

Это — идеальное место для использования стрелочных функций, в особенности в том случае, если результирующая функция обладает неким состоянием, ссылается на что-либо в объекте. Например, это может выглядеть так:

this.doSomethingAsync().then((result) => {
  this.storeResult(result);
});

▍Трансформация объектов

Ещё один распространённый пример использования стрелочных функций заключается в инкапсуляции трансформаций объектов.

Например, в Vue.js существует общепринятый паттерн включения фрагментов хранилища 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, у этой функции был доступ к данным этого экземпляра.

Однако у такого подхода масса минусов, которым посвящён этот материал.

Хотя применение стрелочной функции здесь, безусловно, представляет собой удобно выглядящий способ привязки функции, поведение этой функции во многих аспектах оказывается далёким от интуитивной понятности, мешая тестированию и создавая проблемы в ситуациях, когда, например, соответствующий объект пытаются использовать как прототип.

В подобных случаях, вместо стрелочных функций, используйте обычные функции, и, если нужно, привязывайте к ним экземпляр объекта в конструкторе:

class Counter {
  counter = 0;

  handleClick() {
    this.counter++;
  }

  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }
}

▍Длинные цепочки вызовов

Стрелочные функции могут стать источником проблем в том случае, если их планируется использовать во множестве различных комбинаций, в частности — в длинных цепочках вызовов функций.

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

Это не так уж и плохо, если, например, имеется лишь один уровень вложенности вызовов функций, скажем, если речь идёт о функции, используемой в итераторе. Однако если все используемые функции объявлены как стрелочные, и эти функции друг друга вызывают, то, при возникновении ошибки, разобраться в происходящем будет нелегко. Сообщения об ошибках будут выглядеть примерно так:

{anonymous}()
{anonymous}()
{anonymous}()
{anonymous}()
{anonymous}()

▍Функции с динамическим контекстом

Последняя из обсуждаемых нами ситуаций, в которых стрелочные функции могут стать источником неприятностей, заключается в использовании их там, где нужна динамическая привязка this.

Если в подобных ситуациях применяются стрелочные функции, то динамическая привязка this работать не будет. Эта неприятная неожиданность может заставить поломать голову над причинами происходящего тех, кому придётся работать кодом, в котором стрелочными функциями пользуются неправильно.

Вот некоторые вещи, о которых нужно помнить, рассматривая возможность использования стрелочных функций:

  • Обработчики событий вызываются с this, привязанным к атрибуту события currentTarget.
  • Если вы всё ещё пользуетесь jQuery, учитывайте, что большинство методов jQuery привязывают this к выбранному элементу DOM.
  • Если вы пользуетесь Vue.js, то методы и вычисляемые функции обычно привязывают this к компоненту Vue.

Конечно, стрелочные функции можно использовать намеренно, для того, чтобы изменить стандартное поведение программных механизмов. Но, особенно в случаях с jQuery и Vue, подобное часто вступает в конфликт с обычным функционированием системы, что приводит к тому, что программист не может понять, почему некий код, выглядящий вполне нормальным, вдруг отказывается работать.

Итоги

Стрелочные функции — это замечательная свежая возможность JavaScript. Они позволяют, во многих ситуациях, писать более удобный код, чем раньше. Но, как и в случае с другими возможностями, у них есть и преимущества, и недостатки. Поэтому использовать стрелочные функции надо там, где они могут принести пользу, не рассматривая их в виде полной замены для обычных функций.

Уважаемые читатели! Сталкивались ли вы с ситуациями, в которых использование стрелочных функций приводит к ошибкам, неудобствам или к неожиданному поведению программ?

Стрелочные функции в JavaScript: зачем они нужны, как с ними обращаться, когда ими стоит пользоваться, а когда — нет - 2

Автор: ru_vds

Источник


* - обязательные к заполнению поля