Функциональные выражения и стрелочные функции в JavaScript

Александр Мальцев
Александр Мальцев
26K
9
Функциональные выражения и стрелочные функции в JavaScript
Содержание:
  1. Функциональные выражения
  2. Стрелочные функции
  3. Самовызывающаяся функция (IIFE)
  4. Почему не нужно использовать традиционные функции
  5. Отличия между различными способами объявления функций
  6. Комментарии

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

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

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

Ключевое слово function также применяется для определения функций в выражениях.

Функциональное выражение (от английского Function Expression) очень похоже на обычное объявление функции:

function(a, b) {
  const sum = a + b;
  return sum;
}

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

function funcDeclaration() {
  return 'Обычное объявление функции';
}

const funcExpression = function() {
  return 'Функциональное выражение';
}

Здесь функциональное выражение мы присвоили переменной funcExpression. В итоге, у функционального выражения по сути будет имя (название переменной). Затем, используя эту переменную мы можем вызвать данную функцию:

funcExpression();

В этом примере присвоим переменной sum функциональное выражение. А затем вызовем данную функцию, используя эту переменную:

const sum = function(num1, num2) {
  return num1 + num2;
};

// вызов функции, используя переменную sum
sum(7, 4);

Если вы хотите внутри тела функции сослаться на эту же функцию, то можно создать именованное функциональное выражение:

const factorial = function factorialInner(num) {
  if (num <= 1) {
    return 1;
  }
  // использование factorialInner для вызова функции
  return factorialInner(num - 1) * num;
};

// выведем результат вызова функции factorial(5) в консоль
console.log(factorial(5)); // 120

// при попытке вызвать функцию по имени factorialInner получим ошибку
console.log(factorialInner(5)); // Uncaught ReferenceError: factorialInner is not defined

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

При этом функциональное выражение присвоено переменной factorial, объявленной с помощью const. Используя эту переменную (т.е. factorial) мы можем вызвать данную функцию.

Но вызвать функцию внутри её тела можно не только по имени, но также с помощью свойства arguments.callee:

const factorial = function(num) {
  if (num <= 1) {
    return 1;
  }
  return arguments.callee(num - 1) * num;
};

Ещё очень часто функциональное выражение используется как колбэк-функция. Т.е. как функция, которая передаётся в качестве аргумента в другую функцию. И эта другая функция где-то внутри себя вызывает эту callback функцию.

Это связано с тем, что в этом случае не нужно создавать функцию с именем, когда вы хотите просто передать её в другую функцию. Можно использовать анонимную функцию.

Пример использования функционального выражение в вызове другой функции:

setTimeout(function() {
  console.log('Сообщение, которое будет выведено в консоль через 1 секунду!');
}, 1000);

В этом примере используется стандартная функция setTimeout, которая доступна как в браузере, так и в Node.js. Она принимает на вход колбэк-функцию и количество миллисекунд, через которые нужно вызвать эту колбэк-функцию. Здесь нет смысла давать имя вот этой функции. Достаточно просто использовать анонимное функциональное выражение.

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

Стрелочные функции (от английского arrow function) – это функции, которые имеют немного другой более современный синтаксис. При создании стрелочных функциях не используется ключевое слово function. Появились стрелочные функции в стандарте ECMAScript 2016 (6 редакции).

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

(num1, num2) => {
  const result = (num1 + num2) / 2;
  console.log(result);
}

У стрелочной функции нет имени. Начинается стрелочная функция сразу же с (), внутри которых при необходимости описываются параметры. Далее идёт специальная стрелочка, которая состоит из знака = и >. Этот специальный синтаксис как раз и делает эту функцию стрелочной. После этого идёт тело функции, внутри которого мы описываем действия, которая она будет выполнять при её вызове. В теле как в традиционной функции опционально с помощью return мы можем возвращать результат.

Как дать имя стрелочной функции? Точно также как анонимному функциональному выражению, т.е. путём его присваивания переменной.

const average = (num1, num2) => {
  const result = (num1 + num2) / 2;
  console.log(result);
}

В этом примере мы присвоили стрелочную функцию переменной average. То есть, по сути, дали ей имя.

После этого мы можем вызвать эту функцию используя данную переменную:

average(7, 5); // 6

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

setTimeout(() => {
  console.log('Это сообщение будет выведено в консоль через 1 секунду!');
}, 1000);

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

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

Сокращение синтаксиса в стрелочных функциях

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

const greeting = name => {
  console.log(`Привет, ${name}`);
};

Но для удобства чтения стрелочной функции круглые скобки лучше не опускать:

const greeting = (name) => {
  console.log(`Привет, ${name}`);
};

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

const average = (num1, num2) => (num1 + num2) / 2;

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

Этот же примере без сокращенного варианта:

const average = (num1, num2) => {
  return (num1 + num2) / 2;
}

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

this в стрелочных функциях

Функции, созданные с помощью ключевого слова function имеют свой собственный this. Значение this внутри таких функций зависит от того, как она вызывается, и делается ли это в строгом режиме или нет. Более подробно прочитать про это можно в этой статье.

В стрелочных функциях нет собственного this. Они берут его снаружи. Поэтому стрелочные функции не следует использовать в качестве методов:

const person = {
  firstName: 'Alexander',
  getFirstName: () => {
    return this.firstName;
  }
}

const firstName = person.getFirstName();
console.log(firstName); // undefined

В этом примере мы получили undefined, а не строку 'Alexander' как ожидали. Почему? Потому что при вызове функции person.getFirstName() она будет брать контекст снаружи, так как эта функция является стрелочной. Т.е. им не будет являться объект person. В случае, если мы этот выполняем в глобальной области видимости в браузере, то this в getFirstName будет являться объект window.

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

class Timer {
  constructor() {
    this.counter = 0;
    setInterval(() => {
      console.log(this.counter++);
    }, 1000);
  }
}

new Timer();

В этом примере в вызов setInterval() мы передали в качестве аргумента стрелочную функцию. Эта функция является методом объекта window. Но так как стрелочная функция не имеет собственного this, он берёт его снаружи, т.е. так как нам это нужно.

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

class Timer {
  constructor() {
    this.counter = 0;
    setInterval(function () {
      console.log(this.counter++);
    }, 1000);
  }
}

new Timer();

Как мы уже отмечали выше, setInterval() – это сокращенная запись от window.setInterval(), то this в setInterval() будет указывать на window.

Для того чтобы исправить этот код, можно, например, предварительно сохранить контекст в переменную that, а затем его использовать setInterval:

class Timer {
  constructor() {
    this.counter = 0;
    const that = this;
    setInterval(function () {
      console.log(that.counter++);
    }, 1000);
  }
}

new Timer();

Или воспользоваться методом bind():

class Timer {
  constructor() {
    this.counter = 0;
    setInterval(function () {
      console.log(this.counter++);
    }.bind(this), 1000);
  }
}

new Timer();

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

Стрелочные функции не подходят для call, apply и bind. Они также не имеют массивоподобного объекта arguments. Получить все аргументы для которых нет параметров в этом случае можно с помощью оператора ...:

const sum = (...nums) => {
  let result = 0;
  for (let num of nums) {
    result += typeof num === 'number' ? num : 0;
  }
  return result;
};

console.log(sum(2, 5, -7, 11)); // 11

Ещё примеры

Пример, в котором создадим стрелочную функцию, возвращающую массив определённой длины, заполненный случайными числами от 0 до 9.

const fillArr = (numElements) => {
  const arr = [];
  for (let i = 0; i < numElements; i++) {
    arr.push(parseInt(Math.random() * 10));
  }
  return arr;
};

// вызов функции fillArr
console.log(fillArr(5)); // [1, 4, 6, 4, 9]

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

// () - необходимо указывать при отсутствии параметров
const result = numElements = () => {
  console.log('Привет, мир!');
};

result(); // 'Привет, мир!'

Самовызывающаяся функция (IIFE)

Самовызывающаяся функция или IIFE - это функция, которая вызывается сразу же как только до неё дойдет интерпретатор кода.

Она используется для создания закрытой области видимости, и применяется в паттерне «модуль».

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

// num1 и num2 - параметры самовызывающейся функции
// 7 и 4 - аргументы самовызывающейся функции
(function (num1, num2) {
  console.log(num1 + num2); // 11
})(7, 4);

Паттерн «модуль»:


const userInfo = (function() {
  // имя пользователя по умолчанию
  let name = 'Аноним';

  // возвращаем объект, состоящий из 2 функций
  return {
    getName: function () {
      return name;
    },
    setName: function (newName) {
      name = newName;
    },
  };
})();

console.log(userInfo.getName()); // 'Аноним'
console.log(userInfo.setName('Дима')); // 'Дима'
console.log(userInfo.getName()); // 'Дима'

// обратиться напрямую к переменной name нельзя, только через «публичные» методы
console.log(userInfo.name); // undefined

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

1. Если вы создаёте функцию традиционным способом, то можете присвоить переменной (имени функции) новое значение:

function sum(a, b) {
  console.log(a + b);
}
sum(5, 3); // 8
sum = 7;
console.log(sum); // 7

Если функцию мы присвоим переменной, объявленной с помощью const, то затем присвоить новое значение этой переменной у нас уже не получится:

const sum = (a, b) => {
  console.log(a + b);
}
sum(5, 3); // 8
sum = 7; // Uncaught TypeError: Assignment to constant variable.

2. Функции, объявленные традиционным способом, всплывают, т.е. их можно использовать до их объявления:

sum(4, 3); // 7
function sum(a, b) {
  console.log(a + b);
}

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

sum(4, 3); // Uncaught ReferenceError: Cannot access 'sum' before initialization
const sum = (a, b) => {
  console.log(a + b);
}

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

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

1. Объявление и вызов функции:

// традиционное объявление функции
function squareA(side) {
  return side * side;
};
// функциональное выражение, которое присвоено переменной square2
const squareB = function(side) {
  return side * side;
};
// стрелочная функция, которая присвоена переменной square3
const squareC = (side) => side * side;

// вызов традиционной функции осуществляется по имени
console.log(squareA(5)); // 25
// вызов функционального выражения и стрелочной функции осуществляется с использованием имени переменной
console.log(squareB(5)); // 25
console.log(squareC(5)); // 25

2. Всплытие (hoisting). Функции, объявленные традиционным образом всплывают в отличие от функциональных выражений и стрелочных функций, т.е. их можно вызвать до объявления:

// функцию squareА можно вызвать до объявления
console.log(squareA(7)); // 49
console.log(squareB(7)); // Uncaught ReferenceError: Cannot access 'squareB' before initialization
console.log(squareC(7)); // Uncaught ReferenceError: Cannot access 'squareC' before initialization

function squareA(side) {
  return side * side;
};
const squareB = function(side) {
  return side * side;
};
const squareC = (side) => side * side;

3. Нельзя получить доступ к функции вне блока. В том числе и функции, объявленной традиционным способом, но только в строгом режиме 'use strict':

'use strict';
if (true) {
  function squareA(side) {
    return side * side;
  };
  // функциональное выражение, которое присвоено переменной square2
  const squareB = function (side) {
    return side * side;
  };
  // стрелочная функция, которая присвоена переменной square3
  const squareC = (side) => side * side;
}

console.log(squareA(9)); // Uncaught ReferenceError: squareA is not defined
console.log(squareB(9)); // Uncaught ReferenceError: squareB is not defined
console.log(squareC(9)); // Uncaught ReferenceError: squareC is not defined

Комментарии: 9

  1. Walks
    Walks
    2021-12-15 21:33:08
    Добрый день. Подскажите, имеет ли анонимная функция собственное свойство «this», и объект «arguments»?
    Например:
    function First(){
      console.log(arguments)
    }
    
    Function.prototype.delay = function(delay) {
      return function (...args) {
        setTimeout(() => {
          this(...args); //Здесь "this" имеет значение анонимной функции или "Function.prototype.delay"?
        }, delay);
      }.bind(this); //Здесь "bind" привязывает анонимную функцию к "Function.prototype.delay"? Если да то зачем?
    };
    
    const second = First.delay(2000)
    
    second(1, 2, 3) //[Arguments] { '0': 1, '1': 2, '2': 3 }
    И еще, буду благодарен если проясните: почему в данном выражении из кода выше «setTimeout(() => {this(...args);» не теряется «this», ведь используется в стрелочной функции, да и после оператора «setTimeout» свойство «this» будет «undefine»?

    Спасибо за ответ.
  1. Александр Мальцев
    Александр Мальцев
    2021-12-19 05:43:46
    this – это обращение к контексту. В функции this указывает на объект, в рамках которого она выполняется.
    Когда мы вызываем delay(2000) как метод First, то this будет указывать на First:
    const second = First.delay(2000)
    Т.е. в момент вызова delay ключевое слово this в данном коде везде будет указывать на First:
    Function.prototype.delay = function(delay) {
      return function (...args) {
        setTimeout(() => {
          this(...args); // здесь this указывает на First
        }, delay);
      }.bind(this); // здесь this указывает на First
    };
    Стрелочные функции не содержат собственный контекст this и не имеют собственного объекта arguments. Они его берут снаружи.
    Метод setTimeout по умолчанию выполняется в контексте объекта window, т.к. setTimeout – это сокращённая запись от window.setTimeout. Но если мы используем стрелочную функцию, то this будет браться снаружи и в данном примере нам не нужно будет привязывать необходимый нам контекст с помощью bind().
    Но, если мы бы не использовали стрелочную функцию, то bind() пришлось бы использовать, иначе бы this указывал бы на объект window:
    Function.prototype.delay = function(delay) {
      return function (...args) {
        setTimeout(function() {
          this(...args); //Здесь "this" имеет значение анонимной функции или "Function.prototype.delay"?
        }.bind(this), delay);
      }.bind(this); // здесь this указывает на контекст "bind" привязывает метод delay
    };
  • Vadim
    Vadim
    2020-10-22 10:54:15
    Есть задачка, в которой нужно создать функцию canBuyBeer. Эта функция должна параметром age принимать целое число и возвращать строку «You can buy beer», если переменная age больше или равна 18. Иначе возвращать строку «You can not buy beer».

    Наваял, но не работает:
    function canBuyBeer (age = 19) {
      if (age >= 18) {
        canBuyBeer = "You can buy beer";
      } else {
        canBuyBeer = "You can not buy beer";
      }
    }
    
    Подскажите, в чем ошибка? Как можно это выражение записать стрелочной функцией?
    Спасибо.
    1. Александр Мальцев
      Александр Мальцев
      2020-10-22 12:22:01
      Вот так:
      const canBuyBeer = (age = 19) => {
        return age >= 18 ? 'You can buy beer' : 'You can not buy beer';
      }
      
  • Александр
    Александр
    2017-03-26 10:46:43
    Товарищи, помогите решить задачу. Пожалуйста, сижу и понять не могу как ее решить.
    Напишите функцию, которая будет принимать на вход строку с надписью для гравировки. На выход функция отдает стоимость для гравировки. Если строка пустая или равна undefined, то цена гравировки равна 0.

    Запишите в переменную строку текста, которую нужно будет выгравировать.
    Создайте функцию, принимающую в качестве аргумента строку.
    Воспользуйтесь методом split, чтобы получить массив слов, по аналогии с примером ниже:
    var string = 'Это строка из нескольких слов';
    var words = string.split(' ');
    console.log(words);
    
    Предусмотрите универсальный способ расчета стоимости гравировки от количества слов. Мы не ограничиваем клиентов по длине надписи.
    Верните в качестве результата работы вычисленную стоимость.
    Выведите результат работы функции в консоль в формате:
    Подарочная упаковка и гравировка:
    1. Владимир
      Владимир
      2017-03-22 20:31:08
      А как же основное отличие function declaration от function expression(инициализация, когда можно их использовать в коде, обявление в блоках if, циклах и т.д. и видимость там) и можно про use strict упомятуть
      1. Александр Мальцев
        Александр Мальцев
        2017-03-23 15:55:49
        Благодарю за советы, с помощью которых можно улучшить статью. На днях добавлю эту информацию.
      2. Александр Мальцев
        Александр Мальцев
        2017-03-25 16:46:02
        Добавил информацию в статью.
    2. Иван
      Иван
      2016-08-10 16:44:19
      Лучшая статья по этой теме.Спасибо!