Функции в JavaScript
В статье начну с базовых понятий, затем расскажу про все виды функций и их отличия со множеством примеров. Узнаете про то, как использовать функции, про Functional Declaration, Functional Expression, Arrow Function, Pure, High Ordered, Recursion функции. Подробно расскажу про отличия традиционной и стрелочной функции.
Что такое функция
Функция - это часть кода, которая может быть вызвана во время жизненного цикла приложения для выполнения задачи или возврата значения.
Рассмотрим пример функции:
function sumNumbers(a,b) {
const sum = a+b;
return sum;
}
Объявление функции можно разбить на следующие части.
- Имя. В примере выше имя функции —
sumNumbers
- Параметры. Список входных данных, которые могут быть переданы в функцию.
a
иb
в скобках — это параметры. - Тело. Логика или утверждения, которые выполняют вычисления. Мы объявляем переменную
sum
, которая хранит в себе значениеa+b
. В конце мы возвращаем значение этой переменной.
Объявление функции само по себе ничего не делает, только описывает некоторую логику. Чтобы использовать эту логику, нужно вызвать функцию.
sumNumbers(2,3);
- Вызов функции выполняет код внутри тела функции.
- Аргументы — значения, которые будут использоваться в качестве параметров в функции. Мы как бы говорим — вызови функцию со значениями a=2, b=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;
};
Традиционные функции или выражения?
Не существует общепризнанной лучшей практики, но выражения функций обычно предпочтительнее, поскольку:
- они могут быть переназначены,
- гибкость при составлении функций более высокого порядка,
- они не загрязняют глобальную область видимости.
Стрелочные функции (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
:
В окружении браузера мы вызываем функции в глобальной области видимости, где значение this
ссылается на объект Window
, который отображает информацию об окне браузера.
Однако, в окружении Node.js
(в strict mode), стандартное значение this
в глобальной области видимости равно undefined
В любом случае, если функция объявлена в глобальной области видимости – мы не увидим разницы в значении 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.
- Традиционная и стрелочная функция отличаются в первую очередь контекстом. У традиционной контекст зависит от того, как она была вызвана и может быть привязан, а у стрелочной наследуется окружающий контекст и не может быть привязан выбранный.