DevSurge 💦

Функции в JavaScript

Cover Image for Функции в JavaScript
Mark Nelyubin
Mark Nelyubin

В статье начну с базовых понятий, затем расскажу про все виды функций и их отличия со множеством примеров. Узнаете про то, как использовать функции, про Functional Declaration, Functional Expression, Arrow Function, Pure, High Ordered, Recursion функции. Подробно расскажу про отличия традиционной и стрелочной функции.

Что такое функция

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

Рассмотрим пример функции:

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

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

  1. Имя. В примере выше имя функции — sumNumbers
  2. Параметры. Список входных данных, которые могут быть переданы в функцию. a и b в скобках — это параметры.
  3. Тело. Логика или утверждения, которые выполняют вычисления. Мы объявляем переменную sum, которая хранит в себе значение a+b. В конце мы возвращаем значение этой переменной.

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

sumNumbers(2,3);
  1. Вызов функции выполняет код внутри тела функции.
  2. Аргументы — значения, которые будут использоваться в качестве параметров в функции. Мы как бы говорим — вызови функцию со значениями a=2, b=3;
  3. Возвращаемое значение. По умолчанию функции возвращают неопределенное значение, но могут возвращать результат вычислений, если тело содержит оператор возврата. В нашем случае — содержит.

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

let result = sumNumbers(2,3); // вызов функции вернул значение, сохраняем в переменную result
console.log(result); // 5

Виды функций по способу определения

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

Традиционные, объявленные функции (Function Declaration)

Это традиционные функции, которые имеют имя и определяются с помощью ключевого слова "function". Они могут принимать ноль или более параметров и могут быть вызваны по своему имени.

function add(x, y) {
  return x + y;
}

console.log(add(2, 3)); // Outputs: 5

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

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

const add = function(x, y) {
  return x + y;
};

console.log(add(2, 3)); // Outputs: 5

Они могут быть и именованными (хотя такое редко встречается):

const add = function adder(x, y) {
  return x + y;
};

console.log(add); 
/*
ƒ adder(x, y) {
  return x + y;
}
*/

Важно упомянуть о следующей особенности — традиционные функции могут быть вызваны до объявления благодаря механизму "поднятия":

console.log(multi(2, 3)); // 6
function multi(x, y) {
  return x * y;
}

console.log(multiply(2, 3)); // Cannot access 'multiply' before initialization
const multiply = function (x, y) {
  return x * y;
};

Традиционные функции или выражения?

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

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

Стрелочные функции (Arrow Functions)

Это сокращенный способ определения функций, введенный в ES6 (ECMAScript 2015) и полезный для написания краткого, читабельного кода. Они имеют более компактный синтаксис, чем традиционные функции, а также несколько иные правила определения контекста. Об этих отличиях расскажу в следующем разделе статьи.

const add = (x, y) => x + y;

console.log(add(2, 3)); // Outputs: 5

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

Если же у нас несколько выражений, придется использовать return для возврата значения, как и в традиционной функции:

const squareRootOfSum = (x, y) => {
  const sum = x + y;
  const squareRoot = Math.round(Math.sqrt(sum));
  return squareRoot;
};

console.log(squareRootOfSum(5, 4)); // 3

IIFE (Immediately Invoked Function Expression)

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

(function() {
  var x = "Hello";
  console.log(x);
})(); 
// Hello

Генераторы (Generator Functions)

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

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let generator = generateSequence();

console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3

Пример использования — верни 3 разных значения с интервалом в секунду:

function* executeFunction() {
  yield 1;
  yield 2;
  yield 3;
}

function runFunctionThreeTimes() {
  const generator = executeFunction();
  const interval = setInterval(() => {
    const { value, done } = generator.next();
    if (done) {
      clearInterval(interval);
    } else {
      console.log(value);
    }
  }, 1000);
}

runFunctionThreeTimes();
// 1
// 2
// 3

Совет: если вы только начинаете изучение JS — не обязательно вникать в код выше и вообще углубляться в тему генераторов, т.к. в реальности они встречаются крайне редко.

Виды функций по способу использования

Чистые функции

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

let global = 0;
// грязь
const impure = () => {
    global++;
    return global ** 2;
}
// чистота
const pure = (x) => x ** 2;

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

Функции высшего порядка

Функция более высокого порядка создается путем объединения (или композиции) нескольких функций вместе, передавая функции в качестве аргументов или возвращая функции. Существует множество встроенных функций JS, которые используют HOF (Higher Order Functions), например, setTimeout и Array.map.

const log = () => console.log('hello');
setTimeout(log, 2000);


// функция map —  это higher-order функция. Применяет функцию, переданную в качестве аргумента для каждого элемента массива и возвращает новый массив в качестве результата
[1,2,3,4].map(v => v ** 2);

Более сложный пример:

function addOne(x) {
  return x + 1;
}

function double(x) {
  return x * 2;
}

// функция высшего порядка
// принимает две функции и создает замыкание
function compose(func1, func2) {
  // возвращает функцию с одним параметром
  return function(x) {
    // возвращает результат вызова второй функции, который служит аргументом для первой
    return func1(func2(x));
  };
}

const doubleAndAddOne = compose(addOne, double);
console.log(doubleAndAddOne(5)); // 12, 2*(5+1)

const addOneAndDouble = compose(double, addOne);
console.log(addOneAndDouble(5)); // 11 1+(5*2)

Рекурсивная функция

Рекурсивная функция - это функция, которая вызывает саму себя внутри собственного тела. Если не указать условие завершения, то это приведет к созданию бесконечного цикла. Рекурсивные функции обычно используются в реализации алгоритмов для эффективной обработки таких задач, как обход двоичного дерева (binary-tree traversal).

const sumToN = (n) => {
  // базовый случай, когда функция перестает вызывать сама себя
  if (n === 1) return 1;
  // функция вызывает саму себя с новым аргументом
  return n + sumToN(n-1);
}

sumToN(3); // 6

Почитайте мою статью про рекурсию.

Чем отличается традиционная и стрелочная функция

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

Чем же они отличаются? Начнем с простого:

1. Синтаксисом

Стрелочная функция может быть значительно лаконичнее

function stringify(val){
  return val + ''
}

const stringify = val => val+'';

Когда становится больше логики, лаконичность теряется:

// Принимает число num и возвращает массив четных цифр от 1 до num включительно
const getEvens = (num) => {
  const res = [];
  for (let n = 1; n <= num; n++) {
    if (!(n % 2)) {
      res.push(n);
    }
  }
  return res;
};

console.log(getEvens(10)); // [ 2, 4, 6, 8, 10 ]

Самое простое разобрали, перейдем к самому сложному и важному пункту:

2. Контекстом

У кода в момент выполнения есть «окружение». В какой среде запускается код — в браузере или при помощи Node.js? Какие переменные объявлены глобально? Какая функция сейчас запускается и каковы переменные в рамках ее функциональной области видимости? Всё это — контекст.

Чтобы получить доступ к контексту, нужно использовать ключевое слово this, которое хранит ссылку на объект с контекстом.

Как думаете, что будет в консоли, если мы запустим следующий код?

function named() {
  console.log(this);
}

named(); // ???

const arrow = () => {
  console.log(this);
};

arrow(); // ???

Правильный ответ — неизвестно, т.к. это зависит от того, в каком окружении мы запустили этот скрипт.

Если мы запустим его в браузере — в обоих случаях this в качестве значения получит глобальный объект Window:

Article Image

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

Однако, в окружении Node.js (в strict mode), стандартное значение this в глобальной области видимости равно undefined

Article Image

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

Чтобы увидеть разницу в том, как работает this для named и arrow функции, рассмотрим следующий пример, где функции объявлены в качестве методов объекта в окружении Node.js:

let user1 = {
  firstName: "Илья",
  sayHi() {
    console.log(this); 
  },
};

user1.sayHi(); // { firstName: 'Илья', sayHi: [λ: sayHi] }

let user2 = {
  firstName: "Илья",
  sayHi: () => {
    console.log(this); 
  },
};

user2.sayHi(); // undefined

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

Разберем на нашем примере:

  • Стрелочная функция sayHi объявлена внутри блока {} объекта user2
  • Значение this она наследует из окружающей области видимости. Наш блок окружает глобальная область видимости. ему равно значение this в глобальной области видимости Node.JS? Оно равно undefined. В контексте браузера — Window.

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

let user = {
  firstName: "Илья",
  sayHi() {
    let arrow = () => console.log(this); // { firstName: 'Илья', sayHi: [λ: sayHi] }
    arrow();
  },
};
// вызываем метод, который вызывает arrow-функцию
user.sayHi();

Теперь "окружающая" область видимости — не глобальная, а блок user. this стрелочной функции принял значение { firstName: 'Илья', sayHi: [λ: sayHi] }

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

const user = {
  name: 'Alex',
  greet() {
    console.log(this)
  },
}
// вызываем функцию, сохраненную внутри объекта, this = сам объект
user.greet(); // {name: 'Alex', greet: ƒ}

// сохраняем функцию в глобальную переменную
const greet = user.greet
// при вызове функции, объявленной глобально, this принимет значение Window (при вызове в браузере), или undefine (Node.js strict mode)
greet(); // Window

Фух, с самым сложным разобрались (если нет — почитайте доп.материалы про контекст и поиграйтесь с примерами), теперь можно перейти к оставшимся пунктам.

3. Объектом аргументов

Стрелочные функции не имеют собственного объекта аргументов, а традиционные — имеют. В стрелочных функциях вы можете получить доступ к аргументам, переданным функции, используя синтаксис rest-параметров (es6 фича).

function logArgs() {
  console.log(arguments); // { [Iterator]  0: 1, 1: 'Peter', 2: true }
}

logArgs(1, "Peter", true);

const logArgsArrow = (...args) => {
  console.log(arguments); // error: arguments is not defined
  console.log(args); // [ 1, 'Peter', true ]
};

logArgsArrow(1, "Peter", true);

4. Привязкой

Стрелочные функции нельзя привязать к другому значению this с помощью таких методов, как call(), apply() или bind(). Традиционные функции могут быть привязаны к другому значению this с помощью этих методов.

const person = {
  name: "Alice"
}

const sayNameTraditional = function() {
  console.log("My name is " + this.name);
}

sayNameTraditional.call(person); // Outputs: "My name is Alice"

const sayNameArrow = () => {
  console.log("My name is " + this.name);
}

sayNameArrow.call(person); // Throws an error: Cannot read property 'name' of undefined

5. Использованием в качестве конструктора объекта

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

function Person(name) {
  this.name = name;
}

const alice = new Person("Alice");

const PersonArrow = (name) => {
  this.name = name;
}

const bob = new PersonArrow("Bob"); // Throws an error: PersonArrow is not a constructor

Когда следует писать функции

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

DRY Do Not Repeat Yourself

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

WET Пишите все дважды, но не трижды

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

DRY, и WET - полезные принципы, но ни один из них не совершенен, с функциями просто нужна практика. Со временем вы поймете, когда функция нужна, а когда нет.

Итого

  • По способу объявления функции делятся на Functional Declaration, Functional Expression, Arrow Function.
  • По использованию на Pure, High Ordered, Recursion.
  • Традиционная и стрелочная функция отличаются в первую очередь контекстом. У традиционной контекст зависит от того, как она была вызвана и может быть привязан, а у стрелочной наследуется окружающий контекст и не может быть привязан выбранный.

Другие материалы

Cover Image for TypeScript Utility Types — вспомогательные типы и области их применения

TypeScript Utility Types — вспомогательные типы и области их применения

что такое Utility Types в TypeScript, расскажу про основные вспомогательные типы, и покажу, как применять их на реальных проектах.

Mark Nelyubin
Mark Nelyubin
Cover Image for Принципы SOLID с примерами на JS и Vue

Принципы SOLID с примерами на JS и Vue

Расскажу про принципы SOLID с актуальными примерами на JavaScript, Vue, React.

Mark Nelyubin
Mark Nelyubin