Функции

1. Понятие функции

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

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

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

function as a box

Функции - это инструмент для структурирования больших программ, уменьшения повторений и изолирования кода.

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

Функциональное выражение (function expression) — обычное объявление переменной, значение которой будет функция.

Объявим переменную add, и присвоим ей функцию, принимающую 3 значения и возвращающую результат сложения этих значений.

const add = function (a, b, c) {
  return a + b + c;
};
Copy

Определение функции начинается с ключевого слова function, за которым может следовать необязательное имя функции. В круглых скобках идут параметры — перечисление данных, которые функция будет получать из вне. Параметров может быть несколько или вообще ни одного, тогда записываются просто пустые круглые скобки ().

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

Оператор return без выражения возвращает значение undefined. При отсутствии return в теле функции, она все равно вернет значение undefined.

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

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

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

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

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

// a, b, c это параметры
const add = function (a, b, c) {
  return a + b + c;
};

// 1, 2, 3 это аргументы
const result = add(1, 2, 3);
console.log(result); // 6

// 5, 10, 15 это аргументы
console.log(add(5, 10, 15)); // 30
Copy

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

3. Порядок выполнения кода

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

const fnA = function () {
  console.log('Начала выполняться [fnA]');
  fnB();
  console.log('Продолжила выполняться [fnA] после выхода из [fnB]');
};

const fnB = function () {
  console.log('Выполняется [fnB]');
};

console.log('Начал выполнение [main]');
fnA();
console.log('Продолжил выполняться [main] после выхода из [fnA]');

/*
 * Начал выполнение [main]
 * Начала выполняться [fnA]
 * Выполняется [fnB]
 * Продолжила выполняться [fnA] после выхода из [fnB]
 * Продолжил выполняться [main] после выхода из [fnA]
 */
Copy

Пошагово разберем выполнение кода примера:

  • При входе в js-файл, код начинает выполняться сверху вниз.

  • Вызов fnA() заставляет интерпретатор приостановить исполнение кода и зайти в тело функции fnA.

  • В функции fnA интерпретатор встречает вызов функции fnB, которая перехватывает контроль и исполняется.

  • Как только интерпретатор выполнил функцию fnB, то есть в ней произошел return, в нашем случае не явный, интерпретатор возвращается в функцию fnA и продолжает выполнять ее с того места где остановился.

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

Вызовы console.log(), так как это функция, тоже перехватывают контроль. Они были пропущены для более понятного описания процесса.

4. Параметры по умолчанию

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

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

const count = function (countFrom = 0, countTo = 10, step = 1) {
  console.log(`countFrom = ${countFrom}, countTo = ${countTo}, step = ${step}`);

  for (let i = countFrom; i <= countTo; i += step) {
    console.log(i);
  }
};

count(1, 5); // countFrom = 1, countTo = 5, step = 1
count(2); // countFrom = 2, countTo = 10, step = 1
count(undefined, 5, 2); // countFrom = 0, countTo = 5, step = 2
count(); // countFrom = 0, countTo = 10, step = 1
Copy

5. Псевдомассив arguments

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

Псевдомассив — коллекция, имеющая свойство length и возможность обратиться к элементу по индексу, но отсутствием большинства методов для работы с массивом.

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

Рассмотрим пример использования arguments в функции, которая суммирует любое количество аргументов:

const sum = function () {
  let total = 0;

  for (const argument of arguments) {
    total += argument;
  }

  return total;
};

console.log(sum(1, 2, 3)); //  6
console.log(sum(1, 2, 3, 4)); //  10
console.log(sum(1, 2, 3, 4, 5)); //  15
Copy

5.1. Способы преобразования псевдомассив

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

Используя метод Array.from(), который создаст массив из итерируемого объекта.

const fn = function () {
  // В переменной args будет полноценный массив
  const args = Array.from(arguments);
};
Copy

Используя операцию ... (rest), она позволяет собрать произвольное количество элементов, в нашем случае аргументов, в массив и сохранить его в переменную. Собираем все аргументы используя операцию rest прямо в подписи функции.

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

const fn = function (...args) {
  // В переменной args будет полноценный массив
};
Copy

6. Паттерн Guard Clause

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

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

const withdraw = function (amount, balance) {
  if (amount === 0) {
    console.log('Для проведения операции введите сумму больше нуля.');
  } else if (amount > balance) {
    console.log('Недостаточно средств на счету.');
  } else {
    console.log('Операция снятия средств проведена.');
  }
};

withdraw(0, 300); // Для проведения операции введите сумму больше нуля.
withdraw(500, 300); // Недостаточно средств на счету.
withdraw(100, 300); // Операция снятия средств проведена.
Copy

Даже в таком простом примере есть группа вложенных условных операторов, среди которых не сразу можно выделить нормальный ход выполнения кода из-за особенности синтаксиса if...else.

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

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

const withdraw = function (amount, balance) {
  /*
   * Проверяется условие. Если оно выполняется, происходит
   * console.log и выход из функции. Код идущий после тела if
   * не выполнится.
   */
  if (amount === 0) {
    console.log('Для проведения операции введите сумму больше нуля.');
    return;
  }

  /*
   * Если условие первого if не выполнилось, его тело пропускается
   * и интерпретатор доходит до этого if.
   * Проверяется условие. Если оно выполняется, происходит
   * console.log и выход из функции. Код идущий после тела if
   * не выполнится.
   */
  if (amount > balance) {
    console.log('Недостаточно средств на счету.');
    return;
  }

  /*
   * Если ни один из предыдущих if не выполнился,
   * интерпретатор доходит до этого кода и выполняет его.
   */
  console.log('Операция снятия средств проведена.');
};

withdraw(0, 300); // Для проведения операции введите сумму больше нуля.
withdraw(500, 300); // Недостаточно средств на счету.
withdraw(100, 300); // Операция снятия средств проведена.
Copy

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

7. Объявление функции

Другой способ создать функцию — использовать ключевое слово function в начале инструкции. Такая запись называется объявление функции (function declaration).

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

function add(a, b, c) {
  return a + b + c;
}
Copy

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

add(1, 2, 3); // 6
print('text'); // text

function add(a, b, c) {
  return a + b + c;
}

function print(str) {
  console.log(str);
}
Copy

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

// ❌ Плохо
function fn() {}

// ✅ Хорошо
const fn = function () {};

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

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

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

// Обычное функциональное выражение
const add = function (a, b, c) {
  return a + b + c;
};

// Тоже самое записано как стрелочная функция
const add = (a, b, c) => a + b + c;
Copy

Слово function не используется, вместо этого сразу идет объявление параметров, за которыми всегда следует символ =>.

// Если параметров несколько, то они перечисляются через запятую в круглых скобках.
const fn = (a, b, c) => {
  return a + b + c;
};

// Если параметр один, то он может быть без круглых скобок.
const fn = x => {
  return x * 2;
};

// Если параметров нет, то обязательно должны быть пустые круглые скобки.
const fn = () => {
  console.log('Hello! :]');
};
Copy

После => идет тело функции. Здесь может быть два варианта: с фигурными скобками или без них.

/*
 * Если фигурные скобки после => есть, значит необходимо явно указать то,
 * что должна вернуть функция - поставить return и выражение.
 * Это называется явный возврат (explicit return)
 */
const fn = (a, b, c) => {
  return a + b + c;
};

/*
 * Если фигурных скобок нет, то возвращается результат выражения стоящего после =>
 * Это называется неявный возврат (implicit return)
 * В примере ниже вернется результат выражения сложения a, b и c
 */
const fn = (a, b, c) => a + b + c;
Copy

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

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

const add = (...args) => {
  console.log(args);
};

add(1, 2, 3); // [1, 2, 3]

Last updated