Когда и почему стоит использовать стрелочные функции ES6, а когда нет +29


Привет, Хабр! Представляю вашему вниманию перевод статьи "When (and why) you should use ES6 arrow functions?—?and when you shouldn’t" автора Cynthia Lee.

Стрелочные функции — наиболее популярная фишка ES6. Это новый, лаконичный способ написания функций.

function timesTwo(params) {
  return params * 2
}
timesTwo(4);  // 8

Теперь то же самое при помощи стрелочной функции.

var timesTwo = params => params * 2
timesTwo(4);  // 8

Намного короче! Мы можем опустить фигурные скобки и оператор return ( если нет блока, но об этом позже).

Давайте разберемся, чем отличается новый способ от привычного.

Синтаксис


Первое, на что вы быстро обратите внимание, различные вариации синтаксиса. Давайте посмотрим на основные:

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


Если у функции нет параметров, вы можете просто написать пустые круглые скобки перед =>

() => 42

На самом деле, можно вообще без скобок!

_ => 42

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


Круглые скобки тоже не обязательны

x => 42  || (x) => 42

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


Вот тут уже нужны скобки

(x, y) => 42

4. Инструкции


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

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

var feedTheCat = (cat) => {
  if (cat === 'hungry') {
    return 'Feed the cat';
  } else {
    return 'Do not feed the cat';
  }
}

5. Тело фунцкии — блок


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

var addValues = (x, y) => {
  return x + y
}

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


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

x =>({ y: x })

Стрелочные функции — анонимные


Обратите внимание, что стрелочные функции – анонимны, у них нет имени.

Это создает некоторые сложности:

  1. Трудно дебажить

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

    Если вам нужна ссылка внутри функции на саму себя для чего-то (рекурсия, обработчик событий, который необходимо отменить), ничего не выйдет

Главное преимущество: нет своего this


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

Например, посмотрите на функцию setTimeout ниже:

// ES5
var obj = {
  id: 42,
  counter: function counter() {
    setTimeout(function() {
      console.log(this.id);
    }.bind(this), 1000);
  }
};

В примере выше требуется использовать .bind(this), чтобы передать контекст в функцию. Иначе this будет undefined.

// ES6
var obj = {
  id: 42,
  counter: function counter() {
    setTimeout(() => {
      console.log(this.id);
    }, 1000);
  }
};

В этом примере не нужно привязывать this. Стрелочная функция возьмет значение this из замыкания.

Когда не следует использовать стрелочные функции


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

Вот несколько примеров, когда вам вряд ли захочется их использовать.

1. Методы объекта


Когда вы вызываете cat.jumps, количество жизней не уменьшается. Это происходит потому, что this не привязан ни к чему, и наследует значение из замыкания.

var cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

2. Функции обратного вызова с динамическим контекстом


Если вам нужен динамический контекст, стрелочная функция — плохой вариант.

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

Если нажать на кнопку, мы получим TypeError. Это связано с тем, что this не привязан к кнопке.

3. Когда ухудшается читаемость кода


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

Когда точно стоит использовать стрелочные функции


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

Также мне очень нравится использовать стрелочные функции во всяких map и reduce — код так лучше читается.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (24):

  1. yea
    /#18766805 / +2

    Нельзя присвоить переменной

    Если вам нужна ссылка на функцию для чего-то (рекурсия, обработчик событий, который необходимо отменить), ничего не выйдет


    В оригинале не «ссылка на функцию», а именно «self-reference», ссылка внутри функции на саму себя. Соответственно, обработчик событий там рассматривается такой, который сам себя должен при каких-то условиях отвязать. А с самой ссылкой на функцию как раз всё в порядке.

  2. Wroud
    /#18767543

    this стрелочной функции такой же как this окружения, в котором вызвана стрелочная функция.

    Скорее создана или объявлена, а не вызвана

  3. Dimd13
    /#18767555 / -1

    var cat = {
      lives: 9,
      jumps: () => {
        this.lives--;
      }
    }
    ES6 Определение методов объекта

    var cat = {
      lives: 9,
      jumps() {
        this.lives--;
      }
    };
    console.log(cat.lives); // => 9
    cat.jumps();
    console.log(cat.lives); // => 8
    

    • kahi4
      /#18767817

      Это сахар для конструкции


      var cat = {
         lives: 9,
         jumps: function() {
           this.lives--;
         }
      };

      Так что все правильно. При этом this так же потеряется при передаче cat.jumps как переменной.


      var cat = {
        lives: 9,
        jumps() {
          this.lives--;
        }
      };
      console.log(cat.lives); // => 9
      const jump = cat.jumps;
      jump();
      console.log(cat.lives); // => все еще 9

  4. Fragster
    /#18768109

    Почему во всех примерах с this используют setTimeout, но не используют его третий параметр?

    • lazyboa
      /#18769143

      Не работает в IE.
      Он, конечно, уже почти умер.

      • Chamie
        /#18769253 / +1

        Так он и стрелочные функции не поддерживает.

    • justboris
      /#18771491

      Третий параметр в setTimeout — это аргумент для функции, а не контекст.


      setTimeout(function(that) {
        console.log(that.id);
      }, 1000, this);

      Придется вводить новую переменную по имени that/self и т.п. Стрелочные функции позволяют оставить this как есть, и не назначать ему дополнительных алиасов

  5. Juma
    /#18768659

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

  6. Sabubu
    /#18769001

    Какой ужасный синтаксис. Люди почему-то бездумно используют эти стрелочные функции, не понимая, для чего они придуманы, и думая, что «обычные» функции «устарели в 2018» (да, манера писать «xxx в 2018» выдает неопытного кодера).

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

    var heavyGoods = goods.filter(g => g.getWeight() > 100);
    var names = users.map(u => u.name);
    


    Получается аккуратный и лаконичный код. Он хорошо читается: «Отбери товары, у которых вес больше 100 чего-то». Но если у вас «обычная» функция, то лучше писать ее с помощью «классического синтаксиса»:

    
    function feedTheCat(cat) {
      if (cat === 'hungry') {
        return 'Feed the cat';
      } else {
        return 'Do not feed the cat';
      }
    }
    


    Тут все понятно: мы, прочитав первое слово, видим, что перед нами функция. А прочитав первую строку, знаем ее прототип. Сравните это с уродливым примером из статьи:

    
    var feedTheCat = (cat) => {
      if (cat === 'hungry') {
        return 'Feed the cat';
      } else {
        return 'Do not feed the cat';
      }
    }
    


    Попробуйте прочитать начало функции: «переменная feedTheCat равна проекции cat на блок кода». Такое ощущение, что мы видим результат работы обфускатора. Более того, я иногда вижу, как такие функции вкладывают друг в друга для большего ада или пишут несколько стрелок: (x) => (y) => { 100 строк кода };

    Кому в здравом уме придет в голову объявлять функцию как переменную?

    Я подозреваю, что люди пытаются таким образом пародировать языки вроде Хаскелл в своем коде. Это неправильно. Если вам нравится Хаскелл, то просто пишите на нем, а не превращайте код в нечитаемую лапшу.

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

    • Chamie
      /#18769309 / +1

      Сравните это с уродливым примером из статьи:
      var feedTheCat = (cat) => {
        if (cat === 'hungry') {
          return 'Feed the cat';
        } else {
          return 'Do not feed the cat';
        }
      }
      Тут вообще пример ради примера. Если уж делать стрелочной функцией, то через тернарный оператор (и без скобок вокруг единственного аргумента):
      var feedTheCat = cat => cat === 'hungry' ? 'Feed the cat': 'Do not feed the cat'

      Но всё равно остаётся странным, что кошка равна строке, а функция «покормить кошку» возвращает строку, но не делает самого действия. С ходу красивого названия не придумывается, но должно быть скорее как-то так:
      const shouldIFeedTheCat = cat => cat.isHungry ? 'Feed the cat': 'Do not feed the cat';

      • Sabubu
        /#18774901

        Вы попробуйте прочитать начало вашего кода:

        var feedTheCat = cat => cat === 'hungry' ...


        5 знаков равенства! «Переменная feedTheCat равна проекции переменной cat на выражение, если cat равняется 'hungry'....». какая-то галиматья, если честно. Не стоит так уплотнять код.

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

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

    • argonavtt
      /#18774579

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

      • Sabubu
        /#18774933

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

        Я предпочитаю решать проблему this так: 1) использовать that или self 2) не вкладывать несколько функций друг в друга и вообще не писать код с большой (больше 3-4) глубиной отступов. Коллбеки нужны не так уж и часто и уж точно нет необходимости вкладывать несколько функций друг в друга.

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

        • argonavtt
          /#18774997

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

          Вы хоть представляете что бы было если бы что то поменяли с самим словом? Сколько старого кода стало бы работать по другому.

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

  7. paratagas
    /#18769367 / +4

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

    • Aries_ua
      /#18774303

      Но нас спасет:

      (...args) => { console.log(...args) }

  8. Paran01d
    /#18770125 / +1

    Скоро на JS станет писать write-only код легче, чем на Perl)
    Не холивара ради, но, имхо, яву должен подталкивать писать легко читаемый код, а не превращать это в удел гуру, постигшего науку применять сахар и особенности синтаксиса там, где это уместно.
    ЗЫ: Наверное по этому сменил Perl на Python 5 лет назад, хотя до этого Perl привлекал меня именно синтаксической необузданностью.

  9. x-foby
    /#18771973 / +2

    Высосанные из пальца примеры — это, конечно, наше всё))

    var button = document.getElementById('press');
    button.addEventListener('click', () => {
        this.classList.toggle('on');
    });

    Зачем здесь использовать this? Зачем мы объявляли button, если потом не используем? Забавно, конечно)
    var button = document.getElementById('press');
    button.addEventListener('click', () => button.classList.toggle('on'));
    

    • Sabubu
      /#18774943

      button.addEventListener('click', function (e) { self.onButtonClick(e); });


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

  10. aleksandy
    /#18772699

    По-моему, тут вообще замыкания не нужно. Можно же ссылку на кнопку получить из события.

    document.getElementById('press').addEventListener('click', e => e.target.classList.toggle('on'));

  11. iShatokhin
    /#18774481

    Трудно дебажить

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

    Не совсем так.
    (() => {
         throw new Error('Some error');
    })();
    
    // Uncaught Error: Some error
    //    at <anonymous>:2:12 <--- Номер строки никуда не делся
    
    const funcName = () => {
        throw new Error('Some error');
    };
    
    funcName ();
    
    // Uncaught Error: Some error
    //    at funcName (<anonymous>:2:11) <--- А теперь еще и имя появилось
    
    funcName.name; 
    // "funcName"