ES6: стрелочные функции изнутри
Перевод статьи ES6 Arrow Functions in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Ежедневная эпопея статей «ES6 изнутри» продолжается. Сегодня мы обсудим стрелочные функции. В прошлых статьях мы познакомились с деструктированием и литералами шаблона. Я стараюсь разобрать всё, что относится к набору фич ES6, и наконец добраться до ES7. Также я заметил, что, когда я пишу про эти фичи, они навсегда врезаются мне в память.
Раз вы читаете эти статьи, предлагаю установить Babel и babel-node
, и повторять за мной, копируя отдельные примеры в файл. Можете затем запустить их при помощи babel-node yourfile
в терминале. Самостоятельный запуск этих примеров, и, возможно, небольшая их корректировочка помогут лучше усвоить эти новые фичи — даже если вы просто добавите оператор console.log
, чтобы разобраться в происходящем.
Итак, встречайте наших сегодняшних гостей.
В прошлых статьях мы уже затрагивали стрелочные функции вскользь, применяя их мимоходом и не рассуждая о происходящем. Эту статью мы посвятим преимущественно стрелочным функциям, а остальной ES6 отложим до лучших времен. Я считаю, что это лучший способ писать о ES6 — выводить на первый план одну фичу в каждой статье, постепенно добавляя другие и объединяя разные идеи, чтобы понимать, как они все взаимодействуют. Я часто наблюдал, как несколько фич ES6 вместе образуют суперфичу, и это потрясающе. И всё же важно погружаться в синтаксис и фичи ES6 постепенно, а не бросаться в воду, потому что чем дальше, тем она горячее, и к этому лучше привыкать постепенно — вероятно это была плохая аналогия, но двигаемся дальше.
Применение стрелочных функций в JavaScript
Стрелочные функции доступны во многих современных языках, и как же мне их недоставало несколько лет назад, когда я переходил с C# на JavaScript. К счастью, теперь они появились в ES6, а значит доступны нам в JavaScript. Их синтаксис довольно выразителен. У нас уже есть анонимные функции, но порой приятно иметь краткую альтернативу.
Вот как выглядит синтаксис, если есть один аргумент и хочется просто вернуть итоговое значение выражения.
[1, 2, 3].map(num => num * 2) // <- [2, 4, 6]
А вот как это выглядело бы в ES5:
[1, 2, 3].map(function (num) { return num * 2 }) // <- [2, 4, 6]
Если нужно объявить больше аргументов (или аргументов нет), то необходимы круглые скобки.
[1, 2, 3, 4].map((num, index) => num * 2 + index) // <- [2, 5, 8, 11]
Бывает, что надо не просто вернуть результат выражения, а выполнить перед этим еще несколько действий. В таком случае не обойтись без скобок.
[1, 2, 3, 4].map(num => { var multiplier = 2 + num return num * multiplier }) // <- [3, 8, 15, 24]
Также с помощью синтаксиса круглых скобок можно добавить больше аргументов.
[1, 2, 3, 4].map((num, index) => { var multiplier = 2 + index return num * multiplier }) // <- [2, 6, 12, 20]
Однако, здесь уже есть смысл посмотреть в сторону именованного объявления функции по нескольким причинам.
(num, index) =>
лишь чуть-чуть короче function (num, index)- Запись
function
позволяет именовать метод, улучшая качество кода. - Когда у функции есть множество аргументов и операторов, то, на мой взгляд, шесть дополнительных символов не сыграют особой роли.
- Однако, с именованным методом код станет настолько понятнее, что те шесть лишних символов (плюс имя метода) сполна себя оправдают
Далее, если нам нужно вернуть литерал объекта, то придётся обернуть выражение в круглые скобки. Тем самым литерал объекта не будет интерпретироваться, как блок оператора (что привело бы к незаметной ошибке, или того хуже, к ошибке синтаксиса, поскольку number: n
— недопустимое выражение в примере ниже. В первом примере код интерпретируется как метка number
, после которой идет выражение n
. Поскольку мы находимся в блоке и ничего не возвращаем, на выходе каждый элемент массива получит значение undefined
. Во втором случае, после метки и выражения n
, , something: 'else'
не имеет смысла для компилятора, и выбрасывается SyntaxError
.)
[1, 2, 3].map(n => { number: n }) // [undefined, undefined, undefined] [1, 2, 3].map(n => { number: n, something: 'else' }) // <- SyntaxError [1, 2, 3].map(n => ({ number: n })) // <- [{ number: 1 }, { number: 2 }, { number: 3 }] [1, 2, 3].map(n => ({ number: n, something: 'else' })) /* <- [ { number: 1, something: 'else' }, { number: 2, something: 'else' }, { number: 3, something: 'else' }] */
Огромный плюс стрелочных функций в ES6 — они связаны со своей лексической областью. Это значит, что можно попрощаться с var self = this
и похожими хаками, — например .bind(this)
— чтобы сохранить контекст в пределах глубоко вложенных методов.
function Timer () { this.seconds = 0 setInterval(() => this.seconds++, 1000) } var timer = new Timer() setTimeout(() => console.log(timer.seconds), 3100) // <- 3
Помните, что связывание лексического this
в стрелочных функциях ES6 означает, что .call
и .apply
не смогут изменить контекст. Однако, это скорее фича, а не баг.
Заключение
Стрелочные функции хороши, когда дело касается определения анонимных функций, которые должны быть в любом случае лексически связаны, и в ряде случаев они определённо сделают код более аккуратным.
Не стоит слепо менять все объявления функций на стрелочные, если только их аргументы и тело выражения не говорят сами за себя. Я большой сторонник именованных объявлений функций, поскольку они улучшают читаемость кода, не требуя комментариев — а значит, в большинстве случаев я лишь усложню себе жизнь, перейдя на стрелочные функции.
С другой стороны, я считаю, что стрелочные функции особенно полезны в большинстве задач функционального программирования, например, при использовании .map
, .filter
или .reduce
с коллекциями. Кроме того, по-настоящему полезными стрелочные функции окажутся в асинхронных скриптах, в которых, как правило, бесчисленные обратные вызовы только и делают, что передают друг другу аргумент — вот где стрелочные функции предстают во всей красе.
P.S. Это тоже может быть интересно: