Стрелочные функции (Arrow functions) в ECMAScript 6

в 7:08, , рубрики: ecmascript 6, ecmascript harmony, javascript, Блог компании Mail.Ru Group, Веб-разработка, метки: , ,

Arrow functions madness
Одной из самых интересных частей нового стандарта ECMAScript 6 являются стрелочные функции. Стрелочные функции, как и понятно из названия определяются новым синтаксисом, который использует стрелку =>. Однако, помимо отличного синтаксиса, стрелочные функции отличаются от традиционных функций и в других моментах:

  • Лексическое связывание. Значения специальных переменных this, super и arguments определяются не тем, как стрелочные функции были вызваны, а тем, как они были созданы.
  • Неизменяемые this, super и arguments. Значения этих переменных внутри стрелочных функций остаются неизменными на протяжении всего жизненного цикла функции.
  • Стрелочные функции не могут быть использованы как конструктор и кидают ошибку при использовании с оператором new.
  • Недоступность «собственного» значения переменной arguments.

Было несколько причин для введения этих отличий. Первоочередная — это то, что связывание (binding) используется довольно часто в JavaScript. Очень легко потерять нужное значение this при использовании традиционных функций, что может привести к непредсказуемым последствиям. Другая причина, это то, что JS-движки смогут легко оптимизировать выполнение стрелочных функций за счет этих ограничений (в противоположность традиционным функциям, которые могут быть использованы в качестве конструктора и которые свободны для модификации специальных переменных).


Примечание: Данная статья — это компиляция из вольного перевода статьи Understanding ECMAScript 6 arrow functions и чтения последнего черновика спецификации (January 20, 2014 Draft Rev 22).

Оглавление

Синтаксис


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

var fun = (x) => x;

Он очень похож на аналогичный синтаксис в таких языках как Scala, CoffeeScript и на синтаксис lambda-выражений из C#.

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

Один параметр

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

var reflect = value => value;
// эквивалент
var reflect = function(value) { return value; }

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

Несколько параметров

Но если вы хотите объявить более одного параметра, то должны обрамить список параметров в круглые скобки:

var sum = (num1, num2) => num1 + num2;
// эквивалент
var sum = function(num1, num2) { return num1 + num2; };

Функция sum просто суммирует два аргумента. Единственное отличие от предыдущего примера в наличии круглых скобок и запятой (прямо как в традиционных функциях).

Без параметров

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

var sum = () => 1 + 2;
// эквивалент
var sum = function() { return 1 + 2; };

Традиционный синтаксис тела функции

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

var sum = (num1, num2) => { return num1 + num2; }
// эквивалент
var sum = function(num1, num2) { return num1 + num2; };

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

Литерал объекта

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

var getTempItem = id => ({ id: id, name: "Temp" });
// эквивалент
var getTempItem = function(id) { return { id: id, name: "Temp" } };

Помещение литерала объекта в круглые скобки указывает парсеру, что фигурные скобки это не начало традиционного синтаксиса для тела функции, а начало литерала.

Переменное число параметров

Так как «собственный» объект arguments не доступен внутри стрелочной функции (значение arguments лексически связано со значением arguments традиционной функции, внутри которой стрелочная функция была объявлена), то для стрелочных функций с переменным числом параметров нужно использовать rest-паттерн из шаблонов деструктуризации. Пример:

var getTempItems = (...rest) => rest;
// эквивалент
var getTempItems = function() { return [].slice.apply(arguments) };

Шаблон деструктуризации в качестве параметра

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

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

var a = ({a}) => a;
var b = ([b]) => b;

Использование стрелочных функций


Установка контекста

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

var pageHandler = {
    id: "123456"
    , init: function() {
        document.addEventListener("click", function(event) {
            this.doSomething(event.type);     // ошибка
        });
    }
    , doSomething: function(type) { console.log("Handling " + type  + " for " + this.id) }
};

В приведённом коде объект pageHandler должен обрабатывать клики на странице. Метод init() навешивает обработчик на нужное событие, который внутри себя вызывает this.doSomething(). Однако код отработает неправильно. Ссылка на this.doSomething() не валидна, поскольку this указывает на объект document внутри обработчика события вместо планируемого pageHandler. При попытке выполнить этот код, вы получите ошибку, поскольку объект document не имеет метода doSomething.

Вы можете завязать значение this на объекте pageHandler используя handleEvent или вызвав у функции стандартный метод bind():

var pageHandler = {
    id: "123456"
    , init: function() {
        document.addEventListener("click", (function(event) {
            this.doSomething(event.type);     // error
        }).bind(this));
    }
    , doSomething: function(type) { console.log("Handling " + type  + " for " + this.id) }
};

Теперь код работает так, как и задумывалось, но выглядит более громоздко. Кроме того, вызывая bind(this) вы каждый раз создаёте новую функцию, значение this которой завязано на значении pageHandler, но зато код работает так, как вы задумывали.

Стрелочные функции решают проблему более элегантным способом, поскольку используют лексическое связывание значения this (а также super и arguments) и его значение определяется значением this в том месте, где стрелочная функция была создана. Например:

var pageHandler = {
    id: "123456"
    , init: function() {
        document.addEventListener("click", event => this.doSomething(event.type));
    }
    , doSomething: function(type) { console.log("Handling " + type  + " for " + this.id) }
};

В этом примере обработчик это стрелочная функция в которой вызывается this.doSomething(). Значение this будет тем же, что и в функции init(), и код в данном примере отработает правильно, аналогично тому, который использовал bind(). Вне зависимости от того, возвращает вызов this.doSomething() значение или нет, выражение внутри тела стрелочной функции не нужно обрамлять в фигурные скобки.

Кроме того, пример выше ещё и эффективнее вызова bind(), потому что для браузера он аналогичен следующему коду:

var pageHandler = {
    id: "123456"
    , init: function() {
        var self = this;
        document.addEventListener("click", function(event) {
            return self.doSomething(event.type)
        });
    }
    , doSomething: function(type) { console.log("Handling " + type  + " for " + this.id) }
};

То есть не происходит создание новой функции, как в случае с вызовом bind().

«Прокидывание» контекста между несколькими вызовами

Очевидно, что можно вкладывать одну стрелочную функцию в другую, тем самым «прокидывая» значение this через них:

var obj = {
    arr1: [1, 2, 3]
    , arr2: ['a', 'b', 'c']
    , concatenate: function(a, b){ return a + "|" + b }
    , intersection: function() {
        return this.arr1.reduce( (sum, v1) => // arrow function 1
            this.arr2.reduce( (sum, v2) => { // arrow function 2
                sum.push( this.concatenate( v1, v2 ) )
                return sum;
            }
            , sum )
        , [] )
    }
};
var arrSum = obj.intersection();//['1|a', '1|b', '1|c', '2|a', '2|b', '2|c', '3|a', '3|b', '3|c']

Использование в качестве аргумента

Короткий синтаксис стрелочных функций делает их идеальными кандидатами на передачу в качестве аргументов в вызов других функций. Например, если вы хотите отсортировать массив, то обычно пишете что-то типа такого:

var result = values.sort(function(a, b) { return a - b });

Довольно многословно для простой операции. Сравните с короткой записью стрелочной функции:

var result = values.sort((a, b) => a - b);

Использование таких методов, как массивные sort(), map(), reduce() и так далее, может быть упрощено с использованием короткого синтаксиса стрелочной функции.

Другие особенности стрелочных функций


Несмотря на то, что стрелочные функции отличаются от традиционных функций, у них есть общие черты:

  • Оператор typeof вернёт "function" для стрелочной функции
  • Стрелочная функция также экземпляр «класса» Function, поэтому instanceof сработает так же как, и с традиционной функцией
  • Вы всё ещё можете использовать методы call(), apply(), и bind(), однако помните, что они не будут влиять на значение this
  • Вы можете использовать метод toMethod(), однако он не будет менять значение super (метод toMethod() введён в es6 и не рассматривается в рамках данной статьи).

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

Итог


Стрелочные функции это одно из интереснейших нововведений в ECMAScript 6, которое, имея краткий синтаксис определения, упростит передачу функций в качестве значения параметра другой функции.

Лаконичный синтаксис позволит писать сложные вещи ещё сложнеепроще. Например, так будет выглядеть генератор идентификаторов (который на es5 выглядит куда многословней):


let idGen = 
    (start = 0, id = start, reset = (newId = start) => id = newId, next = () => id++) =>
        ({reset, next});

let gen = idGen(100);
console.log(gen.next(), gen.next(), gen.reset(10), gen.next());//100 101 10 10

А лексическое связывание закроет один из самых больших источников боли и разочарования для разработчиков, а так же улучшит производительность за счёт оптимизации на уровне js-движка.
Madness in FF
Если вы хотите попробовать стрелочные функции, то можете выполнить вышеуказанные примеры в консоли Firefox, который на данный момент (02.2014 FF28) почти полноценно поддерживает стрелочные функции (FF28 неправильно вычисляет значение arguments).

Также вы можете попробовать стрелочные функции и другие возможности es6 в онлайн трансляторе Traceur.

Автор: termi

Источник

Поделиться

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