Функция - это параметрический блок кода, определенный единожды и вызываемый несколько раз позже. В JavaScript функция состоит из множества компонентов и зависит от некоторых вещей:
- Самого кода JavaScript, формирующего тело функции;
- Списка параметров;
- Переменных, доступных из лексической области видимости;
- Возвращаемого значения;
- Контекста this при вызове функции;
- Именованной или анонимной функции;
- Переменной, содержащей объект функции;
- объекта arguments
В этой статье вы узнаете шесть подходов к объявлению функций в JavaScript, их синтаксис, примеры и распространенные ошибки. Более того, вы поймете, когда использовать определенный тип функции в определенных обстоятельствах.
1. Объявление функции (Function declaration)
Объявление функции состоит из ключевого слова function, за которым следует обязательное имя функции, списка параметров в круглых скобках (param1, ..., paramN) и фигурных скобок {...}, ограничивающих основной код.
Пример объявления функции:
function isEven(num) {
return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false
Данная запись декларирует функцию isEven, которая определяет, является ли число чётным.
Объявление функции создает переменную в текущей области видимости с идентификатором, равным имени функции. Эта переменная содержит объект функции.
Переменная функции поднимается в верхнюю часть текущей области видимости, что означает, что функция может быть вызвана до объявления (процесс хоистинг).
Созданной функции присваивается имя, что означает, что свойство name объекта функции содержит ее имя. Это полезно при просмотре стека вызовов: при отладке или чтении сообщений об ошибках.
Давайте посмотрим на эти свойства на примере:
// Поднятие функции позволяет её выполнить
console.log(hello('Мир')); // => 'Привет Мир!'
// Функции присвоено свойство name
console.log(hello.name); // => 'hello'
// Переменная содержит объект функции
console.log(typeof hello); // => 'function'
function hello(name) {
return `Привет ${name}!`;
}
Что произошло: декларирование функции function hello() создало переменную hello, произошло её поднятие вверх текущей области видимости. Переменная hello содержит объект функции, а hello.name содержит имя функции: 'hello'.
Обычная функция
Объявление функции подходит для случаев, когда требуется обычная функция. Обычная означает, что вы объявляете функцию один раз, а затем вызываете ее во многих разных местах. Это базовый сценарий:
function sum(a, b) {
return a + b;
}
sum(5, 6); // => 11
([3, 7]).reduce(sum) // => 10
Поскольку объявление функции создает переменную в текущей области, наряду с обычными вызовами функций, это полезно для рекурсии или привязке к слушателям событий. Отличие от функциональных выражений или стрелочных функций в том, что те не создают привязку к переменной функции по ее имени.
Например, чтобы рекурсивно вычислить факториал, вам нужно получить доступ к функции внутри:
function factorial(n) {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
factorial(4); // => 24
Внутри factorial() выполняется рекурсивный вызов с использованием переменной, содержащей функцию factorial(n - 1).
Можно использовать выражение функции и присвоить его регулярной переменной, например const factorial = function(n) {...}. Но объявление функции function factorial(n) компактно (т.е. нет необходимости в const).
Важным свойством объявления функции является механизм ее подъема. Это позволяет использовать функцию перед объявлением в той же области.
Подъем полезен в некоторых ситуациях. Например, если вы хотите увидеть, как функция вызывается в начале скрипта, не читая её содержание. Само создание функции может быть расположено ниже в файле.
Подробней про хоистинг и область видимости можно ознакомиться в другой статье.
Отличие от выражения функции
Очень легко спутать объявление функции и выражение функции. Они выглядят очень похоже, но создают функции с разными свойствами.
Правило, которое легко запомнить: объявление функции всегда начинается с ключевого слова function. В противном случае это функциональное выражение (function expression).
Следующий пример - это объявление функции, которое начинается с ключевого слова function:
function isNull(value) {
return value == null;
}
В случае функциональных выражений оператор JavaScript не начинается с ключевого слова function (оно присутствует где-то в середине кода оператора):
// Функциональное выражение: начинается с "const"
const isTruthy = function(value) {
return !!value;
};
// Функциональное выражение: аргумент для .filter()
const numbers = ([1, false, 5]).filter(function(item) {
return typeof item === 'number';
});
// Функциональное выражение (немедленно-вызываемая функция): начинается с "("
(function messageFunction(message) {
return message + ' Мир!';
})('Привет');
Объявление функции в условных выражениях
Некоторые среды JavaScript могут вызывать ошибку reference error при вызове функции, объявление которой появляется в блоках {...} операторов if, for или while.
Давайте включим строгий режим и посмотрим, что произойдет, если функция объявлена в условном выражении:
(function() {
'use strict';
if (true) {
function ok() {
return 'true';
}
} else {
function ok() {
return 'false';
}
}
console.log(typeof ok === 'undefined'); // => true
console.log(ok()); // => "ReferenceError: ok is not defined"
})();
При вызове ok() JavaScript выдает ошибку "ReferenceError: ok is not defined", потому что объявление функции находится внутри условного блока.
Объявление функции в условных выражениях разрешено в нестрогом режиме, что еще больше сбивает с толку.
Как правило, для таких ситуаций, когда функция должна быть создана по условиям - используйте выражение функции. Вот как это возможно:
(function() {
'use strict';
let ok;
if (true) {
ok = function() {
return 'true';
}
} else {
ok = function() {
return 'false';
}
}
console.log(typeof ok === 'undefined'); // => true
console.log(ok()); // => 'true'
})();
Поскольку функция является обычным объектом, достаточно присвоить ее переменной в зависимости от условия и вызов ok() отработает нормально, без ошибок.
2. Функциональное выражение (Function expression)
Выражение функции определяется ключевым словом function, за которым следует необязательное имя функции, списком параметров в круглых скобках (param1, ..., paramN) и парой фигурных скобок {...}, ограничивающих тело код.
Примеры выражения функции:
const count = function(array) { // Выражение функции
return array.length;
}
const methods = {
numbers: [1, 5, 8],
sum: function() { // Выражение функции
return this.numbers.reduce(function(acc, num) { // Выражение функции
return acc + num;
});
}
}
count([5, 7, 8]); // => 3
methods.sum(); // => 14
Выражение функции создает объект функции, который можно использовать в различных ситуациях:
- Присваивание переменной как объект count = function(...) {...}
- Создание метода для объекта sum: function() {...}
- Использование функции в качестве обратного вызова (колбек) .reduce(function(...) {...})
Выражение функции - это рабочая лошадка в JavaScript. Обычно используют этот тип объявления функции, иногда вместе со стрелочной функцией (если вы предпочитаете короткий синтаксис и лексический контекст).
Именованные функциональные выражения
Функция является анонимной, если у нее нет имени (свойство name - это пустая строка " "):
(
function(variable) {return typeof variable;}
).name; // => ''
Это анонимная функция, name которой - пустая строка.
Иногда name функции можно вывести. Например, когда анонимная функция назначена переменной:
const myFunctionVar = function(variable) {
return typeof variable;
};
myFunctionVar.name; // => 'myFunctionVar'
Имя анонимной функции - myFunctionVar, потому что для вывода name функции используется имя переменной myFunctionVar.
Если в выражении указано имя, это именованное функциональное выражение. У него есть некоторые дополнительные свойства по сравнению с простым выражением функции:
- Создается именованная функция, т.е. свойство name содержит имя функции;
- Внутри тела функции переменная с тем же именем содержит объект функции.
Давайте воспользуемся приведенным выше примером, но зададим имя в функциональном выражении:
const getType = function funName(variable) {
console.log(typeof funName === 'function'); // => true
return typeof variable;
}
console.log(getType(3)); // => 'number'
console.log(getType.name); // => 'funName'
console.log(typeof funName); // => 'undefined'
function funName(variable) {...} - это именованное функциональное выражение. Переменная funName доступна в области видимости функции, но не за ее пределами. Свойство name объекта функции содержит имя funName.
Польза именованных функциональных выражений
Когда выражение функции присваивается переменной (const fun = function() {}), некоторые движки определяют имя функции из этой переменной. Однако обратные вызовы могут передаваться как анонимные функциональные выражения без сохранения в переменных и движок не может определить имя.
Разумно отдавать предпочтение именованным функциям и избегать анонимных, чтобы получить такие преимущества, как:
- Сообщения об ошибках и консольные вызовы показывают более подробную информацию при использовании имен функций;
- Более удобная отладка за счет уменьшения количества анонимных имен стека;
- Название функции говорит о том, что функция делает;
- Вы можете получить доступ к функции внутри ее области для рекурсивных вызовов или отключения прослушивателей событий.
3. Сокращенное определение метода
Сокращенное определение метода можно использовать в объявлении метода в литеральных объектах и классах ES2015. Вы можете определить метод, используя имя функции, за которым следует список параметров в круглых скобках (param1, ..., paramN) и пара фигурных скобок {...}, разделяющих операторы тела.
В следующем примере используется сокращенное определение метода в литеральном объекте:
const collection = {
items: [],
add(...items) {
this.items.push(...items);
},
get(index) {
return this.items[index];
}
};
collection.add('C', 'Java', 'PHP');
collection.get(1) // => 'Java'
Методы add() и get() в объекте collection определяются с использованием сокращенного определения метода. Эти методы вызываются как обычно: collection.add(...) и collection.get(...).
Краткий подход к определению метода имеет несколько преимуществ по сравнению с традиционным определением функции add: function(...) {...}:
- Более короткий синтаксис легче понять
- Сокращенное определение метода создает именованную функцию, в отличие от выражения функции. Это полезно для отладки.
Синтаксис класса требует объявления методов в краткой форме:
class Hello {
constructor(name) {
this.name = name;
}
getMessage(message) {
return this.name + message;
}
}
const hello = new Hello('Привет');
hello.getMessage(' мир!') // => 'Привет мир!'
Вычисляемые имена свойств и методов
ECMAScript 2015 добавил приятную вещь - вычисляемые имена свойств в литеральных объектах и классах.
Вычисляемые имена свойств используют немного другой синтаксис - [methodName]() {...}, поэтому определение метода выглядит следующим образом:
const addMethod = 'add';
const getMethod = 'get';
const collection = {
items: [],
[addMethod](...items) {
this.items.push(...items);
},
[getMethod](index) {
return this.items[index];
}
};
collection[addMethod]('C', 'Java', 'PHP');
collection[getMethod](1) // => 'Java'
[addMethod](...) {...} и [getMethod](...) {...} - это сокращенные объявления методов с вычисляемыми именами свойств.
4. Стрелочная функция
Стрелочная функция определяется с помощью пары круглых скобок, которые содержат список параметров (param1, ..., paramN), за которым следует стрелка => и пара фигурных скобок {...}, ограничивающих тело функции.
Давайте посмотрим на базовое использование стрелочной функции:
const absValue = (number) => {
if (number < 0) {
return -number;
}
return number;
}
absValue(-10); // => 10
absValue(5); // => 5
absValue - это стрелочная функция, которая вычисляет значение числа по модулю.
Функция, объявленная стрелкой, имеет следующие свойства:
- Стрелочная функция не создает свой контекст выполнения, но принимает его лексически (в отличие от выражения функции или объявления функции, которые создают собственный this в зависимости от вызова);
- Стрелочная функция анонимна. Однако движок может вывести её имя из переменной, содержащей функцию;
- Объект arguments недоступен в стрелочной функции (в отличие от других типов объявлений, которые предоставляют объект arguments). Однако вы можете использовать остальные параметры (... params).
Если стрелочная функция имеет только один параметр, пару скобок можно опустить. Если она содержит один оператор, фигурные скобки также можно опустить.
Прозрачность контекста
Ключевое слово this сбивает с толку в JavaScript. Поскольку функции создают собственный контекст выполнения, часто бывает трудно определить значение this.
ECMAScript 2015 улучшил использование this, введя стрелочную функцию, которая принимает контекст лексически (или просто использует его из внешней области видимости). Это хорошо, потому что вам не нужно использовать .bind(this) или сохранять контекст let _this = this, когда функции нужен свой контекст.
Давайте посмотрим, как this наследуется от внешней функции:
class Numbers {
constructor(array) {
this.array = array;
}
addNumber(number) {
if (number !== undefined) {
this.array.push(number);
}
return (number) => {
console.log(this === numbersObject); // => true
this.array.push(number);
};
}
}
const numbersObject = new Numbers([]);
const addMethod = numbersObject.addNumber();
addMethod(1);
addMethod(5);
console.log(numbersObject.array); // => [1, 5]
Класс Numbers содержит массив чисел и предоставляет метод addNumber() для вставки новых чисел. Когда addNumber() вызывается без аргументов, возвращается замыкание, которое позволяет вставлять числа. Это замыкание представляет собой стрелочную функцию, которая имеет this как экземпляр numbersObject, потому что контекст лексически берется из метода addNumber().
Без стрелочной функции вам придется вручную исправить контекст. А это использование обходных путей, подобных .bind():
//...
return function(number) {
console.log(this === numbersObject); // => true
this.array.push(number);
}.bind(this);
//...
Либо контекст можно поместить в переменную:
//...
const _this = this;
return function(number) {
console.log(_this === numbersObject); // => true
_this.array.push(number);
};
//...
Прозрачность контекста используют, когда хотят оставить this таким же, т.е. равным окружающему контексту.
Короткие обратные вызовы
Как упоминалось, при создании стрелочной функции круглые скобки и фигурные скобки необязательны для одного параметра и одного оператора тела. Это помогает в создании очень коротких функций обратного вызова.
Пример функции, которая определяет, содержит ли массив 0:
const numbers = [1, 5, 10, 0];
numbers.some(item => item === 0); // => true
item => item === 0 - это простая стрелочная функция.
Обратите внимание, что вложенные короткие стрелочные функции трудно читать.
При необходимости используйте расширенный синтаксис стрелочных функций при написании вложенных стрелочных функций. Просто читать легче.
5. Функция-генератор (Generator function)
Функция-генератор в JavaScript возвращает объект Generator. Его синтаксис аналогичен выражению функции, объявлению функции или объявлению метода, только для него требуется символ звездочки *.
Функция генератора может быть объявлена в следующих формах:
- Форма объявления функции (Function declaration form) function* <name> ():
function* indexGenerator(){
var index = 0;
while(true) {
yield index++;
}
}
const g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
- Форма функционального выражения (Function expression form) function* ():
const indexGenerator = function* () {
let index = 0;
while(true) {
yield index++;
}
};
const g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
- Форма краткого определения метода (Shorthand method definition form) *<name>():
const obj = {
*indexGenerator() {
var index = 0;
while(true) {
yield index++;
}
}
}
const g = obj.indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
Во всех трех случаях функция-генератор возвращает объект генератора g. Буква g используется для создания серии увеличивающихся чисел.
6. Использование new Function
В JavaScript функции - это объекты первого класса, т.е. функция - это обычный объект типа function. Описанные выше способы объявления создают один и тот же тип объекта - function. Давайте посмотрим на пример:
function sum1(a, b) {
return a + b;
}
const sum2 = function(a, b) {
return a + b;
}
const sum3 = (a, b) => a + b;
console.log(typeof sum1 === 'function'); // => true
console.log(typeof sum2 === 'function'); // => true
console.log(typeof sum3 === 'function'); // => true
Тип объекта function имеет конструктор: Function. Когда Function вызывается как конструктор new Function(arg1, arg2, ..., argN, bodyString), создается новая функция. Аргументы arg1, args2, ..., argN, переданные конструктору, становятся именами параметров для новой функции, а последний аргумент bodyString используется как код тела функции.
Давайте создадим функцию, которая суммирует два числа:
const numberA = 'numberA', numberB = 'numberB';
const sumFunction = new Function(numberA, numberB,
'return numberA + numberB'
);
sumFunction(10, 15) // => 25
Функция sumFunction, созданная с помощью вызова конструктора, имеет параметры numberA и numberB, а тело возвращает numberA + numberB.
Созданные таким образом функции не имеют доступа к текущей области видимости, поэтому замыкания не могут быть созданы. Они всегда создаются в глобальной области.
Одно из возможных лучших применений new Function - доступ к глобальному объекту в браузере или скрипте NodeJS:
(function() {
'use strict';
const global = new Function('return this')();
console.log(global === window); // => true
console.log(this === window); // => false
})();
Поэтому старайтесь никогда не объявлять функции с помощью new Function(). Поскольку важность тела функции определяется во время выполнения, а такой подход наследует многие проблемы использования eval(): риски безопасности, более сложная отладка, отсутствие возможности применить оптимизацию движка, отсутствие автозаполнения редактора.
Так какой же из способов лучший?
Нет победителя или проигравшего. Решение о том, какой тип объявления выбрать, зависит от ситуации.
Однако есть некоторые правила, которым вы можете следовать в обычных ситуациях.
Если функция использует this из замывающей функции, стрелочная функция - хорошее решение. Когда функция обратного вызова имеет один короткий оператор, стрелочная функция также является хорошим вариантом, поскольку она создает короткий и легкий код.
Для более короткого синтаксиса при объявлении методов в объектных литералах предпочтительнее сокращенное объявление метода.
new Function не следует использовать для объявления функций. В основном потому, что он открывает потенциальные угрозы безопасности, не позволяет выполнять автозаполнение кода в редакторах и теряет оптимизацию движка.
Комментарии (0)