ES6 в деталях: стрелочные функции

ES6 в деталях — это цикл статей о новых возможностях языка программирования JavaScript, появившихся в 6 редакции стандарта ECMAScript, кратко — ES6.

Стрелки были частью JavaScript с самого начала. Первые учебники по JavaScript советовали оборачивать встроенные скрипты в комментарии HTML. Это не позволяло тем браузерам, что не поддерживали JS, ошибочно отображать код JS как текст. В то время вы бы писали примерно так:

<script language="javascript">
<!--
  document.bgColor = "brown";  // red
// -->
</script>

Старые браузеры видели два неподдерживаемых тега и комментарий, и только новые браузеры видели в этом JS.

Чтобы поддерживать этот костыль, движок JavaScript в браузере рассматривает символы <!-- как начало однострочного комментария. Кроме шуток. Это действительно всё время было частью языка и работает по сей день, не только сразу после открывающего тега <script>, но и вообще в любом месте JS-кода. Даже в Node.js работает.

И, между прочим, такой стиль комментариев был впервые стандартизован в ES6. Но статья вовсе не про эти стрелки.

Последовательность символов в виде стрелки --> также обозначает однострочный комментарий. Интересно, что в HTML комментарием считаются символы перед -->, а в JS комментарий — это всё, что после --> и до конца строки.

А вот что ещё интересней. Эта стрелка обозначает комментарий только если находится в начале строки. Потому, что в других контекстах в JS --> — оператор «стремится к»!

function countdown(n) {
  while (n --> 0)  // "n стремится к нулю"
    alert(n);
  blastoff();
}

Этот код действительно работает. Цикл выполняется, пока n не достигнет 0. Это тоже не новая возможность ES6, а комбинация старых в новом контексте и небольшой фокус с записью операторов. Сможете разобраться, как это работает? Как обычно, разгадку можно найти на Stack Overflow.

Разумеется, есть ещё оператор «меньше или равно», <=. Возможно, вы сможете, как в игре Поиск Предметов, отыскать в исходном коде программ написанных на JavaScript и другие виды стрелок, но давайте на этом остановимся, как бы вы не искали, одной стрелки всё равно не хватает.

<!-- однострочный комментарий
--> оператор «стремится к»
<= меньше или равно
=> ???

Что за =>? Сейчас разберемся.

Но сначала немного о функциях.

Функции-выражения повсюду

Забавная особенность JavaScript состоит в том, что если вам нужна функция, то вы можете написать функцию прямо посреди исполняемого кода.

Например, предположим, вы хотите сказать браузеру, что ему следует делать, если пользователь нажмёт определённую кнопку. Вы начинаете печатать:

$("#confetti-btn").click(

Метод .click() jQuery принимает один аргумент — функцию. Без проблем. Вы можете впечатать функцию прямо туда:

$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

Мы уже привыкли писать так, это для нас уже вполне естественно. И странно вспоминать, что до того как, благодаря JavaScript, такой подход к программированию стал популярен, во многих языках не было такой возможности. Само собой, в Lisp были функции-выражения, они же лямбда-функции, ещё с 1958. Но C++, Python, C# и Java просуществовали годы без них.

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

Хотя немного жаль, что из всех упомянутых языков в JavaScript синтаксис лямбд оказался самым многословным.

// Очень простые функции на шести языках.
function (a) { return a > 0; } // JS
[](int a) { return a > 0; }  // C++
(lambda (a) (> a 0))  ;; Lisp
lambda a: a > 0  # Python
a => a > 0  // C#
a -> a > 0  // Java

Новая стрела в ваш колчан

В ES6 появился новый синтаксис функций.

// ES5
var selected = allJobs.filter(function (job) {
  return job.isSelected();
});

// ES6
var selected = allJobs.filter(job => job.isSelected());

Если вам нужна простая функция с одним аргументом, то синтаксис новых, стрелочных функций — это просто Идентификатор => Выражение. Не нужно печатать ни function, ни return, ни круглых скобок с фигурными и точкой с запятой.

(Лично я очень благодарен за этот синтаксис. Для меня очень важно, что печатать function больше не надо, потому что у меня постоянно вместо этого получается functoin, и мне приходится возвращаться и исправлять опечатку.)

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

// ES5
var total = values.reduce(function (a, b) {
  return a + b;
}, 0);

// ES6
var total = values.reduce((a, b) => a + b, 0);

Мне кажется, выглядит очень неплохо.

Стрелочные функции точно так же великолепно работают с функциональными утилитами из библиотек наподобие Underscore.js и Immutable. В сущности, все примеры кода в документации Immutable написаны на ES6, так что многие из них уже используют стрелочные функции.

А что насчёт не столь функциональных случаев? Стрелочные функции могут содержать блок инструкций вместо одиночного выражения. Вернёмся к более раннему примеру:

// ES5
$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

Вот так это будет выглядеть в ES6:

// ES6
$("#confetti-btn").click(event => {
  playTrumpet();
  fireConfettiCannon();
});

Небольшое улучшение. Эффект при использовании промисов может быть более заметным из-за нагроможения строчек }).then(function (result) {.

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

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

// создаём каждому щенку по пустому объекту в качестве игрушки
var chewToys = puppies.map(puppy => {});   // БАГ!
var chewToys = puppies.map(puppy => ({})); // всё хорошо

Увы, пустой объект {} и пустой блок {} выглядят абсолютно одинаково. Правила ES6 гласят: { сразу после стрелки всегда трактуется как начало блока и никогда не считается началом объекта. Поэтому код puppy => {} молча интерпретируется как стрелочная функция, которая ничего не делает и возвращает undefined.

Ещё больше сбивает с толку то, что литерал вроде {key: value} выглядит в точности как блок, содержащий инструкцию с меткой; по крайней мере, он так выглядит для движка JavaScript. К счастью, { — это единственный неоднозначный символ, так что единственный приём, который вам следует запомнить,— это оборачивание литералов объектов в скобки.

Что такое this?

Есть одно хитрое отличие в поведении обычных функций-function и стрелочных функций. У стрелочных функций нет собственного значения this. Внутри стрелочной функции this всегда наследуется из окружающего лексического окружения.

Перед тем как мы постараемся выяснить, что это значит на практике, давайте ненадолго обратимся к основам.

Как в JavaScript работает this? Откуда берётся это значение? На этот вопрос нет короткого ответа. Если для вашего мозга это просто - это лишь из-за того, что вы с этим долго работали!

Одна из причин, почему этот вопрос всплывает так часто - это то, что функции-function получают значение this автоматически, неважно, нужно оно им или нет. Вы когда-нибудь применяли такой приём?

{
  ...
  addAll: function addAll(pieces) {
    var self = this;
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },
  ...
}

Здесь вам бы хотелось написать внутреннюю функцию просто как this.add(piece). К несчастью, внутренняя функция не наследует this внешней. Во внутренней функции this будет window или undefined. Временная переменная self нужна, чтобы протащить внешнее значение this во внутреннюю функцию. (Ещё один способ — использовать .bind(this) на внутренней функции. И оба эти способа особым изяществом не отличаются.)

В ES6 трюки с this по большей части не нужны, если вы придерживаетесь этих правил:

// ES6
{
  ...
  addAll: function addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

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

Что приятно, ES6 также предоставляет более краткий способ записи методов в литералах объектов! Так что код выше можно сделать ещё проще:

// ES6 с сокращённым синтаксисом методов
{
  ...
  addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

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

Есть ещё одна небольшая разница между стрелочными и не-стрелочными функциями: стрелочные функции не получают собственного объекта arguments. Разумеется, в ES6 вы и так скорее предпочтёте остаточные параметры или значения по умолчанию.

Пронзаем стрелами тёмное сердце информатики

Мы уже обсудили множество практических применений стрелочных функций. Есть ещё одно применение, о котором я хочу рассказать — стрелочные функции ES6 как инструмент для обучения, как способ раскрыть глубинные тайны природы вычисления. Практично это или нет — вам решать самим.

В 1936 Алонзо Чёрч и Алан Тьюринг независимо друг от друга разработали мощные математические вычислительные модели. Тьюринг назвал свою модель а-машины, но остальные немедленно окрестили их машинами Тьюринга. Чёрч, напротив, писал о функциях. Его модель называлась λ-исчисление. (λ — это строчная греческая буква лямбда.) Его работа послужила причиной тому, что в Lisp для обозначений функций использовалось слово LAMBDA, и поэтому наши дни мы называем функции-выражения лямбдами.

Но что такое λ-исчисление? И что имеется в виду под вычислительной моделью?

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

И он обнаружил, что в его модели нужно только одно — функции.

Только представьте, насколько необычайно это заявление! Без объектов, без массивов, без чисел, без инструкций if, циклов while, точек с запятыми, присваиваний, логических операторов или событийных циклов, возможно с нуля при помощи одних лишь функций воплотить любой вид вычислений, какой только возможен в JavaScript.

Например, вот такую «программу» могут написать математики в λ-нотации Чёрча:

fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

Эквивалентная функция JavaScript выглядит так:

var fix = f => (x => f(v => x(x)(v)))
              (x => f(v => x(x)(v)));

То есть JavaScript содержит работающую реализацию λ-исчисления. λ-исчисление есть в JavaScript.

Истории о том, как Алонзо Чёрч и поздние исследователи развивали λ-исчисление, и о том, как оно незаметно проникло в практически все заметные языки программирования, находятся уже за пределами тематики этой статьи. Но если вы заинтересовались основателями информатики или хотели бы взглянуть на то, как в языке, в котором нет ничего, кроме функций, можно делать вещи вроде циклов и рекурсии, то вы могли бы в какой-нибудь пасмурный день почитать про нотацию Чёрча и комбинаторы неподвижной точки и поиграться с ними в консоли Firefox или Scratchpad. Со стрелочными функциями и другими его сильными сторонами, JavaScript можно с уверенностью назвать лучшим языком для ознакомления с λ-исчислением.

Когда я смогу пользоваться стрелками?

Стрелочные функции из ES6 были реализованы в Firefox мной ещё в 2013. Ян де Мойж (Jan de Mooij) сделал их быстрыми. Спасибо Тоору Фуджисава (Tooru Fujisawa) и ziyunfei за патчи.

Стрелочные функции также реализованы в предварительной версии Microsoft Edge. Они также доступны в Babel, Traceur и TypeScript, если вы хотите начать использовать их в вебе прямо сейчас.

Нашей следующей темой будет одна из странных особенностей ES6. Мы увидим, что typeof x возвращает совершенно новое значение. Мы зададимся вопросом: когда имя не является строкой? Мы переосмыслим понятие равенства. Это будет необычно. Так что присоединяйтесь на следующей неделе, и мы рассмотрим символы ES6 в деталях.