CSS-live.ru

ES6 изнутри: стрелочные функции

Перевод статьи ES6 In Depth: Arrow functions  с сайта hacks.mozilla.org, опубликовано на css-live.ru с разрешения автора — Джейсона Орендорфа.

ES6 изнутри — серия статей про новые возможности, добавленные в язык программирования JavaScript в 6-е издание стандарта ECMAScript, сокращенно ES6.

Стрелки существовали в JavaScript с самого начала. В первых руководствах по JavaScript советовали оборачивать встроенные скрипты в HTML-комментарии. Это помешало бы браузерам без поддержки JS ошибочно отобразить ваш код JS в виде текста.

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

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

Ради поддержки этого необычного хака движок JavaScript в браузере обрабатывает  символы <!‑‑  как начало однострочного комментария. Кроме шуток. Это действительно было частью языка с самого начала, и работает по сей день, и не только в верхней части встроенного <script>, но и везде в коде JS. И даже в Node.

Оказывается, этот стиль комментария стандартизирован в ES6 впервые. Но это не те стрелки, о которых эта статья.

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

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

function countdown(n) {
  while (n --> 0)  // "n достигает нуля"
    alert(n);
  blastoff();
}

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

Конечно, есть ещё оператор <= — «меньше или равно». Возможно в коде JS (как в загадках типа «найди то-то на картинке») найдутся и другие стрелки, но давайте остановимся и заметим, что одной стрелки не хватает.

<!‑‑ однострочный комментарий
‑‑>  оператор “достигает такого-то числа”
<=       меньше или равно
=>       ???

Что случилось с =>? Сегодня выясним.

Для начала немного поговорим о функциях.

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

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

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

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

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

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

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

Это уже не так. Теперь у всех четырёх есть лямбды. В современных языках повсеместно встроены лямбда-функции. За это надо сказать спасибо 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 => ({})); // ok

К сожалению пустой объект {} и пустой блок {} выглядят совершенно одинаково. Правило в 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 не понадобятся:

  • Используйте обычные (не стрелочные) функции для методов, вызванных синтаксисом object.method(). Те функции, которые получают из вызывающего их кода осмысленное значение this.
  • Применяйте стрелочные функции для всего остального.
// ES6
{
  ...
  addAll: function addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

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

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

// ES6 with method syntax
{
  ...
  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 — не худшее занятие в дождливый вечер. С ES6-стрелками сверх прочих своих достоинств JavaScript обосновано претендует на лучший язык для исследования λ-исчислений.

Где использовать стрелки сегодня?

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

Стрелочные функции также реализованы в предварительной версии Microsoft Edge. А если есть желание воспользоваться ими сегодня, то они доступны в BabelTraceur, и TypeScript.

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

P.S. Это тоже может быть интересно:

2 комментария

  1. Исправьте пожалуйста:


    // Очень простая функция на шести языках.
    function (a) { returna > 0; } // JS

Добавить комментарий для afdw Отменить ответ

Ваш e-mail не будет опубликован. Обязательные поля помечены *

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