Функции - это вызываемые объекты в JavaScript. Очень важно отметить, что в JavaScript функции являются объектами. Это может ввести в заблуждение, потому что когда вы используете оператор typeof
в функции, вы получаете function
в качестве вывода. Это один из случаев, когда JavaScript обманывает вас. Результат typeof function () {}
должен быть object
, потому что функции являются объектами в JavaScript. Это делает функции очень мощными, потому что вы можете думать о них как о вызываемых объектах. Вы можете использовать функцию как объект, фрагмент кода многократного использования или функцию, которая создает объекты.
Создание функций
Есть два основных способа определения функций:
- объявление функции
- выражение функции
Если оператор начинается с ключевого слова function
, значит, у вас есть объявление функции:
function myFn() {}
Но если вы назначите функцию переменной, у вас будет выражение функции:
const fnRef = function myFn() {};
Обратите внимание, что вам нужна точка с запятой при создании выражения функции, но вы не должны включать ее при создании объявления функции.
Функциональные входы и выходы
Вы можете передавать значения и возвращать значения из функции. Например, вы можете создать функцию, которая принимает два числа и returns
результат их сложения:
function add(a, b) {
return a + b;
}
Если вы заметили, входные данные функции помещены в круглые скобки и разделены запятыми. Обратите внимание, что a
и b
представляют входные данные функции. Если вы не укажете никаких возвращаемых значений для своей функции, JavaScript по умолчанию вернет undefined
:
function fn() {
const a = 1;
// other stuff.
// no return.
}
fn(); // `undefined` is returned.
Обратите внимание, что аргументы всегда передаются функциям по значению. Значение может быть примитивным или ссылочным значением в случае непримитивов. Это означает, что если вы назначите аргумент другому значению или объекту, это не повлияет на объект вне функции:
const n = 1;
function change(x) {
x = 5;
}
console.log(n); // 1;
change(n);
console.log(n); // 1;
Не имеет значения, что передается, поведение единообразно, даже если ввод не примитивен (объект):
const n = {value: 1};
function change(x) {
x = {};
}
console.log(n); // {value: 1};
change(n);
console.log(n); // {value: 1};
Однако, если вы решите изменить свойство объекта, на который указывает ссылка, объект будет изменен:
const n = {value: 1};
function mutate(x) {
x.value = 22;
}
console.log(n); // {value: 1}
mutate(n);
console.log(n); // {value: 22}
Как видите, аргументы, передаваемые функциям, всегда копируются. В случае примитивов фактические значения копируются, а в случае непримитивов ссылка копируется и становится доступной в функции, а присвоение аргумента другой вещи не изменяет исходное значение.
Объект arguments
Каждая функция в JavaScript имеет доступ к волшебному объекту arguments
внутри тела функции, который содержит аргументы, переданные функции. Давайте посмотрим на базовый пример, чтобы продемонстрировать, как вы можете получить доступ к объекту arguments
.
В этом примере мы собираемся создать функцию с именем sum
, которая будет просто возвращать объект arguments
при его вызове:
function sum() {
return arguments;
}
Когда вы вызываете функцию с sum(1,2,3)
, вы можете видеть, что arguments
- это объект с тремя ключами и значениями:
{
'0': 0,
'1': 1,
'2': 2
}
Поэтому важно отметить, что объект arguments
не является объектом массива. Это означает, что arguments
не наследует методы массива, и следующее возвращает false:
function sum() {
return arguments;
}
const args = sum(1,2,3);
Object.getPrototypeOf(args) === Array.prototype; // -> false
Array.isArray(args); // -> false
Начиная с ES2015, вы можете использовать метод Array.from
для преобразования итерируемого объекта, такого как объект arguments
, в массив. В качестве альтернативы вы можете вызвать метод slice
для Array.prototype
в контексте объекта arguments
, чтобы вернуть массив, содержащий значения:
function sum() {
return arguments;
}
const args = sum(1,2,3);
const argArray = Array.prototype.slice.call(args); // -> [1,2,3]
Object.getPrototypeOf(argArray) === Array.prototype; // -> true
Array.isArray(Array.from(args)); // -> true
Выполнение функции
Функция может быть выполнена, это обычно называется вызовом функции. Вы можете вызвать функцию, используя имя функции, за которым следует скобка ()
:
add();
Обратите внимание, что мы вызываем функции без каких-либо входных данных. Давайте дадим функцию числам в качестве входных данных:
add(1, 2); // -> 3
Помимо оператора ()
, есть еще 3 способа вызова функции. То есть, используя:
call
apply
- и ключевое слово
new
Итак, для нашего простого примера давайте вызовем функцию, используя все эти методы, и исследуем, когда вы хотите использовать каждый метод.
Function.prototype.call
Когда вы используете call
для выполнения функции, первым аргументом является значение объекта контекста. А сброс значений - это аргументы функции, разделенные запятыми:
add.call({}, 1,2); // -> 3
В приведенном выше фрагменте мы вызываем функцию add
с пустым объектом в качестве контекста, двумя аргументами, 1
и 2
.
Function.prototype.apply
Использование apply
похоже на call
, вы передаете объект контекста в качестве первого аргумента. Но вы передаете аргументы функции в виде массива вместо того, чтобы передавать их по одному:
add.apply({}, [1,2]); // -> 3
В приведенном выше фрагменте мы передаем пустой объект для контекста и массив для аргументов. Первый аргумент - 1
, второй - 2
.
Вызов функции с new
Цель ключевого слова new
- вызвать функцию как конструктор объектов. Таким образом, вы не стали бы на самом деле вызывать функцию sum
, указанную выше, с ключевым словом new
. Когда функция вызывается с ключевым словом new
, за кулисами происходит несколько вещей:
- Создается новый пустой объект
- Объект контекста
this
привязан к новому пустому объекту - Новый объект связан со свойством прототипа функции.
this
возвращается автоматически, если иное не примитивное значение не возвращается явным образом из функции.
Это механизм, предоставляемый JavaScript для связывания объектов с объектами-прототипами. Вы можете узнать больше о прототипах в моей другой статье, а пока просто запомните, что эти функции известны как функции-конструкторы и по соглашению их имя начинается с заглавной буквы. Давайте посмотрим на простую функцию-конструктор, которую можно использовать для создания Car
объектов:
function Car() {
this.color = 'black';
}
const myCar = new Car();
myCar.color; // -> 'black'
Как видите, в функции мы используем объект контекста this
для назначения свойства color
. Затем мы вызываем функцию с помощью ключевого слова new
и присваиваем результат переменной myCar
. Теперь myCar
- это объект, созданный функцией конструктора Car
.
Объект контекста, this
Контекстный объект this
- это динамический объект, который устанавливается динамически в зависимости от способа вызова функции. Чтобы понять, к чему привязан this
, нужно следовать нескольким правилам.
Сначала проверьте, вызывается ли интересующая функция с ключевым словом new
. Если это так, то this
привязан к новому объекту, созданному функцией. Если нет, проверьте, вызывается ли функция с call
или apply
, если да, первый аргумент сообщает вам, к чему привязано значение this
. Если нет, проверьте, вызывается ли функция в контексте другого объекта, (someObj.fn())
, если да, то this
привязан к объекту. Если ни один из этих случаев не выполняется и функция просто вызывается без какого-либо контекста, this
будет привязан к глобальному объекту в режиме non-strict
. Однако в strict mode
значение this
будет undefined
.
Давайте рассмотрим несколько примеров и посмотрим, как объект контекста привязан к.
Позвоните с new
Первый пример - это просто обзор из предыдущего раздела. То есть вызов функции с ключевым словом new
. Когда вы можете использовать функцию с ключевым словом new
, объект контекста привязывается к объекту экземпляра, созданному функцией:
function Car() {
this.color = 'Black';
}
const myCar = new Car();
Чтобы выяснить, как привязан this
, мы смотрим на строку, в которой вызывается функция. В этом случае функция вызывается с ключевым словом new
, поэтому мы знаем, что контекст привязан к новому объекту, который будет возвращен функцией. Это тот случай, когда вы всегда должны сначала проверять, потому что это преобладает над другими.
В следующем примере мы собираемся вызвать функцию с call
и apply
.
Использование call
или apply
Давайте посмотрим, как значение this
привязывается, когда мы вызываем функцию с call
или apply
. В этом примере мы собираемся создать простую функцию с именем printMessage
, которая будет читать свойство name
и возвращать сообщение:
function printMessage(msg) {
return msg + ' ' + this.name;
}
const message = printMessage.call({name: 'AJ'}, 'Welcome!');
// -> 'Welcome! AJ'
В приведенном выше примере мы смотрим на строку, в которой вызывается функция, и вы видите, что функция вызывается call
, а значение this
явно передается в качестве первого параметра. Итак, внутри функции this
будет привязан к {name: 'AJ'}
, и когда мы вызовем функцию, она вернет следующий результат:
'Welcome! AJ'
Случай apply
очень похож на случай выше, за исключением того, что мы передаем аргументы в массиве, но значение this
по-прежнему будет привязано к первому переданному аргументу, то есть: {name : 'AJ'}
:
function printMessage(msg) {
return msg + ' ' + this.name;
}
const message = printMessage.apply({name: 'AJ'}, ['Welcome!']);
Вывод такой же, как в примере call
, и здесь важно отметить, что когда вы вызываете функцию с call
или apply
, первым аргументом является объект контекста, который будет использоваться внутри функции как this
.
Неявная привязка
Следующий случай, который следует рассмотреть по порядку, - это когда функция вызывается в контексте другого объекта. Итак, сначала давайте создадим объект и определим для него метод, а затем вызовем метод в контексте объекта:
const util = {
name: 'Utility',
getName: function () {
return this.name;
}
};
const name = util.getName(); // -> Utility
Снова сначала мы смотрим на строку, в которой вызывается функция, чтобы определить, как this
привязан к ней. Когда вы посмотрите, как вызывается функция, вы увидите, что функция вызывается в контексте объекта util
, то есть util.getName()
. Таким образом, объект контекста неявно привязан к объекту util
.
Последний случай, когда функция вызывается без какого-либо контекста. Используя пример объекта util
выше, мы можем назначить функцию getName
переменной, а затем вызвать функцию без какого-либо контекста:
const util = {
name: 'Utility',
getName: function () {
return this.name;
}
};
const getName = util.getName;
const name = getName();
В этом случае при вызове функции объект контекста по умолчанию устанавливается на глобальный объект. Таким образом, если вы находитесь в браузере, this
будет привязан к объекту window
, а если вы находитесь на сервере, this
будет привязан к объекту global
.
Однако важно отметить, что в strict mode
значение this
будет неопределенным, если вы вызываете функцию как есть:
function setName(name) {
'use strict';
this.name = name;
}
setName();
Когда вы запустите функцию выше, мы получим следующую ошибку:
TypeError: Cannot set property 'name' of undefined
Причина в том, что в strict mode
this
будет установлено в undefined
, когда вы вызываете функцию без контекста. Таким образом, установка свойства на что-то undefined
вызовет ошибку. Это хорошо, потому что тогда вы не станете случайно настраивать свойства глобального объекта.
Связывание контекста с bind
Стоит отметить, что в случаях, когда вы хотите явно привязать функцию к контексту, вы можете использовать bind
. Метод bind
доступен для всех функций без стрелок, принимает объект контекста в качестве аргументов и возвращает новую функцию, привязанную к данному объекту:
const util = {
name: 'Utility',
getName: function () {
return this.name;
}
};
const getName = util.getName.bind(util);
const name = getName(); // -> 'Utility'
В приведенном выше фрагменте мы вызываем bind
для util.getName
, чтобы явно привязать его к объекту util
. Тогда не имеет значения, как вы можете использовать функцию, она всегда будет привязана к объекту util
. Попробуйте использовать bind
разумно, поскольку он создает копию данной функции и может не захотеть копировать функцию только для установки ее контекста. Лучше попытайтесь использовать то, что предоставляет язык, и используйте bind
, когда все другие механизмы не могут предоставить вам то, что вам нужно.
Функции как объекты
Поскольку функции являются объектами, вы можете использовать функции как простые объекты:
var fnRef = function fnObject () {};
fnRef.someProp = 'foo';
fnRef.hello = function () {
return 'hello';
};
Обратите внимание, что затем вы можете получить доступ к свойствам и методам этого функционального объекта:
fnRef.someProp; // -> 'foo'
fnRef.hello(); // -> 'hello'
Важно отметить, что когда вы запрашиваете тип функции, JavaScript возвращает вам function
. Хоть и полезно, но не совсем точно. Более точным ответом будет object
, поскольку в JavaScript нет непримитивного function
типа:
function a() {}
console.log(typeof a);
Приведенный выше фрагмент выведет function
на консоль, хотя a
- это объект, связанный с объектом Function.prototype
:
Object.getPrototypeOf(a); //-> [Function]
Для получения дополнительной информации о прототипах ознакомьтесь с другой моей статьей Прототипы JavaScript.
Стрелочные функции
Стрелочные функции были введены в ES2015 и упрощают создание функций, не относящихся к методам. Стрелочные функции, в отличие от обычных функций, не создают новый контекст. Это означает, что объект this
не привязан к функции при создании стрелочной функции. Кроме того, стрелочные функции нельзя использовать в качестве конструкторов, и у них нет свойства prototype
. Более того, у них нет собственного объекта arguments
, и из-за этого вам нужно будет использовать параметр rest для чтения нескольких аргументов. В следующих разделах мы рассмотрим, как создавать стрелочные функции, поведение объекта this
и многое другое.
Создание функции стрелки
Вы можете создать стрелочную функцию, используя набор круглых скобок для аргументов функции, за которым следует стрелка =>
и набор фигурных скобок для определения тела функции:
const fn = (a, b) => {
const x = a + 1;
const y = b + 2;
return x + y;
};
const sum = fn(1, 2);
Указанная выше стрелочная функция называется fn
и принимает два аргумента: a
и b
. Мы делаем некоторые операции в теле функции и, наконец, return
результат. Затем мы вызываем функцию и сохраняем результат в sum
. Вы также можете использовать сокращенную форму и опустить фигурные скобки. Затем данное значение оператора неявно возвращается:
const add = (a, b) => a + b;
Приведенный выше фрагмент такой же, как следующий:
const add = (a, b) => {
return a + b;
};
Обратите внимание, что если вы возвращаете литерал объекта из стрелочной функции, вы должны использовать набор круглых скобок для обозначения возвращаемого значения:
const register = (name) => ({
id: 0,
name: name,
});
Сокращенные стрелочные функции очень полезны для однострочных операторов, которые вы обычно видите в map
, filter
или reduce
:
const nums = [1, 2, 3, 4];
const plusOne = nums.map(v => v + 1);
В приведенном выше фрагменте мы используем сокращенную форму стрелочной функции, чтобы передать функцию map
, чтобы добавить ее к каждому члену массива и вернуть новый массив. Новому массиву присваивается plusOne
. Также обратите внимание, что вы можете при желании удалить круглые скобки аргументов, если вы ссылаетесь только на один аргумент, в противном случае вам нужно будет включить круглые скобки:
const nums = [1, 2, 3, 4];
const sum = nums.reduce((c, v) => c + v);
В приведенном выше фрагменте мы должны включить набор скобок вокруг c
и v
, поскольку мы используем более одного аргумента для обратного вызова map
.
Объект стрелочных функций и аргументов
Стрелочные функции не получают объект arguments
, объект arguments
всегда будет относиться к внешней области arguments
:
const fn = () => console.log(arguments);
fn('hello', 'world');
Когда вы вызываете функцию, указанную выше, например, в среде Node, она печатает аргументы вызова сценария, а не аргументы функции. Если у вас есть стрелочная функция внутри обычной функции, объект arguments
будет ссылаться на аргументы, переданные внешней функции, но не на стрелочную функцию:
function outer() {
const inner = () => console.log(arguments);
return inner;
}
const r = outer('hello', 'world');
r('inner', 'arguments');
В приведенном выше фрагменте мы определяем стрелочную функцию в outer
функции, регистрируем arguments
и возвращаем внутреннюю функцию. Когда мы вызываем внешнюю функцию с некоторыми аргументами, эти аргументы будут записаны в консоль, а не аргументы, переданные внутренней функции.
Если вы хотите прочитать аргументы, переданные стрелочной функции (или даже для обычных функций), лучше всего использовать параметр rest:
const sum = (...values) => values.reduce((c, v) => c + v);
sum(1, 2, 3, 4);
Остальной аргумент выше - это values
, который представляет собой массив JavaScript и не требует каких-либо дополнительных манипуляций для преобразования в массив. Мы просто вызываем для него метод reduce, складываем все числа и возвращаем результат.
Стрелочные функции и контекст
Стрелочные функции не создают новый контекст, в отличие от обычных функций. Когда вы используете ключевое слово this
для доступа к контексту в стрелочной функции, оно всегда будет ссылаться на контекст во внешней области. Это очень полезно, если вы хотите сохранить контекст в местах, где вы вызываете функции, которые принимают обратный вызов. Самый простой пример - когда вы вызываете setTimeout
с обратным вызовом:
function hello() {
setTimeout(() => {
console.log(this.name);
}, 200);
}
hello.call({name: 'tom'});
В приведенном выше фрагменте мы определяем функцию с именем hello
, которая регистрирует this.name
через 200 мс. Обратите внимание: поскольку мы используем стрелочную функцию в качестве обратного вызова для setTimeout
, мы можем использовать ключевое слово this
для ссылки на внешнюю лексическую область видимости, в данном случае на функцию hello
. Глядя на то, как вызывается функция hello
, мы видим, что контекст установлен на объект {name: 'tom'}
. Если бы вы не использовали стрелочную функцию, у вас были бы следующие параметры:
- Использование переменной во внешней функции:
function hello() {
var self = this;
setTimeout(function() {
console.log(self.name);
}, 200);
}
hello.call({name: 'tom'});
- Использование
bind
для явной привязки обратного вызова, переданного вsetTimeout
:
function hello() {
setTimeout(function() {
console.log(this.name);
}.bind(this), 200);
}
hello.call({name: 'tom'});
Однако использование стрелочной функции предпочтительнее, поскольку они предназначены для таких ситуаций. А теперь давайте посмотрим на другой пример:
function Person(name) {
this.name = name;
}
Person.prototype.talk = function() {
setTimeout(() => {
console.log('My name is', this.name);
}, 100);
};
const tom = new Person('tom');
tom.talk();
В приведенном выше примере мы используем стрелочную функцию внутри setTimeout
, и мы используем this
для ссылки на внешний контекст, которым является функция Person
. Давайте посмотрим на другой пример с обещаниями:
function defineTypes(options) {
const types = {
md: 'markdown',
pdf: 'pdf',
};
return new Promise((r, j) => {
if(!this.prmary) {
return j(new Error('no primary context given'));
}
r(types[this.primary]); // <-- referencing `this`
});
}
defineTypes.call({primary: 'pdf'}, {minCount: 1})
.then(r => console.log(r))
.catch(e => console.log(e));
В приведенном выше фрагменте у нас есть функция с именем defineTypes
. Эта функция определяет карту типов, а затем возвращает новое созданное обещание. В обратном вызове обещания мы ссылаемся на this
при разрешении данного типа r(types[this.primary])
. Поскольку мы используем функцию стрелки для обещания, this
относится к внешнему контексту функции. Глядя на то, как вызывается defineTypes
, мы видим, что контекст явно определяется с помощью объекта: defineTypes.call({primary: 'pdf'})
.
Позвоните и подайте заявку
Как мы видели ранее, вы можете вызвать функцию с помощью call
или apply
и передать объект для обозначения контекста функции. Поскольку стрелочные функции не создают новый контекст, call
или apply
не будут иметь никакого эффекта, и вы не сможете определить контекст для функции. Он просто передаст переданные аргументы, и this
будет установлено в undefined
:
const sum = (toAdd) => {
return this.value + toAdd;
};
const r = sum.call({value: 1}, 100);
console.log(r);
В приведенном выше фрагменте мы определяем функцию sum
, которая добавляет this.value
с переданным аргументом toAdd
. Затем мы вызываем sum
с call
и передаем {value: 1}
в качестве первого параметра и 100
в качестве второго. Когда мы регистрируем значение, мы получим NaN
, потому что добавление undefined
к любому числу приведет к NaN
. То же самое верно, если вы попытаетесь вызвать стрелочную функцию sum
с apply
с заданным контекстом:
const r = sum.apply({value: 1}, [100]);
Стрелочные функции и конструкторы
Поскольку стрелочные функции не создают контекст и у них нет свойства prototype
, мы не можем использовать их в качестве конструкторов. Например, приведенный ниже код вызовет ошибку типа:
const Car = () => {
this.name = 'car';
};
const c = new Car();
Если вы запустите приведенный выше код, вы получите следующую ошибку:
TypeError: Car is not a constructor
Как мы видели ранее, вы можете использовать функцию с контекстом для создания объектов, связанных с прототипами:
const Car = function() {
this.name = 'car';
};
const c = new Car();
Вы также можете использовать класс:
class Car {
constructor() {
this.name = 'car';
}
}
const c = new Car();
Стрелочные функции и цепочки обещаний
Стрелочные функции очень удобно использовать при работе с последовательными цепочками обещаний. При работе с цепочками обещаний вы всегда должны возвращать значение из каждой последовательности, чтобы гарантировать разрешение асинхронных значений, прежде чем переходить к следующему шагу в последовательности. Сокращенные стрелочные функции позволяют очень удобно выражать последовательные цепочки обещаний:
readFile('myfile.txt', 'utf-8')
.then(content => replaceValues(content))
.then(newContent => writeFile(newContent, 'file.txt'))
.then(() => console.log('done'));
В приведенном выше фрагменте мы сначала читаем файл, затем заменяем некоторые значения и, наконец, записываем содержимое в новый файл. Обратите внимание, как мы используем сокращенные стрелочные функции, чтобы возвращать результаты на каждом шаге для разрешения значений, прежде чем переходить к следующим шагам. Приведенный выше код эквивалентен следующему:
readFile('myfile.txt', 'utf-8')
.then(content => {
return replaceValues(content);
})
.then(newContent => {
return writeFile(newContent, 'file.txt');
})
.then(() => {
return console.log('done');
});
Функции высшего порядка
В JavaScript, поскольку функции являются объектами, они могут быть переданы функциям или возвращены из функций. Функция более высокого порядка - это функция, которая либо возвращает функцию, либо принимает функцию в качестве одного из своих аргументов. Давайте посмотрим на пример функции более высокого порядка, которая возвращает функцию:
const writeTo = (path) => {
return (content) => writeFile(path, content);
};
В приведенном выше фрагменте мы определили функцию с именем writeTo
, которая принимает путь к файлу и возвращает другую функцию. Возвращенная функция принимает аргумент для содержимого файла и вызывает writeFile
для записи содержимого в данный файл. Функция writeTo
особенно полезна при работе с цепочками обещаний. Предположим, мы определили readFile
и writeFile
, которые возвращают обещания:
readFile('file.txt', 'utf-8')
.then(writeTo('newfile.txt'));
В приведенном выше фрагменте сначала мы вызываем readFile
, чтобы прочитать содержимое файла. Блок then принимает функцию в следующем виде:
(previousResult) => { }
Теперь, когда мы вызываем нашу writeTo
функцию, она передает значение пути и возвращает другую функцию, которую можно передать блоку then
, что делает ее очень удобной. Вы также можете расширить функцию writeTo
, не беспокоясь о том, где она вызывается, если вы поддерживаете тот же открытый интерфейс.
Мы рассмотрим другие примеры функций более высокого порядка в следующем разделе, где мы поговорим о замыканиях.
Закрытие
Вообще говоря, замыкание - это динамическая ссылка, которая создается, когда внутренняя функция использует переменные внешней функции. А чтобы получить доступ к замыканию, вы просто вызываете внутренние функции, возвращаемые внешней функцией. Продемонстрируем это на простом примере. Во-первых, нам нужна внешняя функция, которая определяет переменную с именем stateVal
:
function makeClosure() {
let stateVal = 0;
}
После этого нам нужно создать внутри другую функцию для ссылки на stateVal
:
function makeClosure() {
let stateVal = 0;
function cl() {
stateVal += 1;
return stateVal;
}
}
и, наконец, мы возвращаем внутреннюю функцию:
function makeClosure() {
let stateVal = 0;
function cl() {
stateVal += 1;
return stateVal;
}
return cl;
}
Теперь, когда у нас есть функция создания замыкания, мы можем вызвать ее и получить доступ к созданному замыканию:
const inc = makeClosure();
Обратите внимание, что inc
является замыканием, потому что он ссылается на переменную stateVal
внутри внешней функции, поэтому stateVal
сохраняет свое состояние каждый раз, когда вызывается inc
. Поэтому, если мы вызываем функцию inc
3 раза, мы ожидаем, что значение stateVal
будет увеличиваться каждый раз:
inc(); // 1
inc(); // 2
inc(); // 3
В конце последнего вызова inc
возвращается значение 3
.
Стрелочные функции и замыкания
Стрелочные функции ведут себя так же, как функции с контекстом. Давайте перепишем приведенный выше пример с помощью стрелочных функций:
const makeClosure = () => {
let stateVal = 0;
const cl = () => ++stateVal;
return cl;
};
const inc = makeClosure();
console.log(inc()); // -> 1
console.log(inc()); // -> 2
console.log(inc()); // -> 3
Как видите, идея та же, мы просто использовали стрелочные функции для создания cl
внутреннего замыкания.
Как вы думаете, что произойдет, если мы создадим еще одно inc
замыкание и вызовем его?
const makeClosure = () => {
let stateVal = 0;
const cl = () => stateVal++;
return cl;
};
const inc = makeClosure();
const inc2 = makeClosure();
console.log(inc()); // -> 1
console.log(inc()); // -> 2
console.log(inc()); // -> 3
console.log(inc()); // -> 4
console.log(inc2()); // -> 1
console.log(inc2()); // -> 2
console.log(inc2()); // -> 3
console.log(inc2()); // -> 4
Когда мы создаем новый inc
, он создает копию закрытия, и счетчик снова начинает с 0. Но это не мешает нашему первому закрытию inc
. Как вы понимаете, вы можете создать столько копий, сколько захотите, но у вас должна быть очень веская причина для этого, потому что каждое создаваемое вами закрытие потребляет дополнительный объем памяти.
Замыкания и инкапсуляция
Замыкания очень полезны для инкапсуляции функций и переменных и создания пространств имен, которые вы не хотите передавать в другие области. Распространенным шаблоном является использование немедленно вызываемой самостоятельно функции для инкапсуляции всего в области действия функции и возврата объекта, содержащего то, что вы хотите раскрыть:
const lib = (() => {
const VERSION = '1.1';
const last = (arr) => arr[arr.length - 1];
const uniq = (v, i, self) => self.indexOf(v) === i;
return {
last, uniq,
};
})();
const lastVal = lib.last([1,2,3,4]);
const uniqVals = [1,1,1,2,2,3].filter(lib.uniq);
Вы можете представить, что приведенный выше фрагмент является частью служебной библиотеки. Как видите, мы определяем немедленно вызываемую стрелочную функцию (() => {})()
и скрываем реализацию нашей библиотеки от внешнего мира, то есть от глобальной области видимости. Кроме того, мы решаем, что мы хотим раскрыть, возвращая объект и включая только то, что мы хотим раскрыть. Обратите внимание, что мы раскрываем только функции last
и uniq
. Таким образом, мы можем использовать только то, что библиотека решает раскрыть, и не иметь доступа к внутренним компонентам библиотеки извне.
Стоит отметить, что вам может не понадобиться такой код в среде Node, поскольку вы можете инкапсулировать свой код с помощью модулей. Кроме того, для кода, который запускается в браузере, вам не нужно вручную скрывать реализации, если вы используете комплекты модулей, которые позволяют создавать инкапсулированные модули. Тем не менее, это полезный метод, особенно в сторонних или устаревших базах кода, когда вы хотите убедиться, что ваш код не будет конфликтовать с другими частями базы кода.
Замыкания внутри петель
Распространенной ошибкой является создание функций внутри классических циклов for или while без какой-либо области видимости блока:
const myFns = [];
const arr = ['hello', 'world'];
for(var i = 0; i < arr.length; i++) {
myFns[i] = function() {console.log(i)}
}
myFns[0](); // -> 2
myFns[1](); // -> 2
В приведенном выше фрагменте мы используем цикл for и ключевое слово var
для определения индекса итерации. Когда используется var
, объявление поднимается до ближайшей функции. Следовательно, при использовании var
область видимости блока отсутствует. Кроме того, мы определяем функции внутри цикла, но проблема в том, что функция закроется через переменную i
, и в цикле значение будет последним значением i
. Вот почему, когда вы запускаете определенные функции, все они будут регистрировать 2
, потому что цикл идет от 0 до 2, но, очевидно, останавливается на 2. Есть несколько способов обойти это. Самое простое решение - использовать область видимости блока с помощью ключевого слова let
:
const myFns = [];
const arr = ['hello', 'world'];
for(let i = 0; i < arr.length; i++) {
myFns[i] = function() {console.log(i)}
}
myFns[0](); // -> 0
myFns[1](); // -> 1
В этом случае каждая итерация получит свою собственную область видимости, а функциям будет присвоено правильное значение i
. Вы также можете использовать метод массивов forEach
для создания области видимости для каждой итерации:
const myFns = [];
const arr = ['hello', 'world'];
arr.forEach((v, i) => {
myFns[i] = function() {console.log(i)}
})
myFns[0](); // -> 0
myFns[1](); // -> 1
Любой из вышеперечисленных методов верен, но если вы застряли со старой версией JavaScript, вы можете создать немедленно вызываемую функцию для создания контекста для каждой итерации:
const myFns = [];
const arr = ['hello', 'world'];
for(var i = 0; i < arr.length; i++) {
(function(v) {
myFns[v] = function() {console.log(v);}
}(i));
}
myFns[0](); // -> 0
myFns[1](); // -> 1
Внутри цикла for мы создаем самозаполняющуюся функцию, которая принимает значение i
на каждой итерации и передает его как v
в цикл, и мы получаем ожидаемое поведение. Стоит отметить, что это поведение не только для циклов for, оно также применимо к циклам while:
const myFns = [];
const arr = ['hello', 'world'];
var i = 0;
while(i < arr.length) {
myFns[i] = function() {console.log(i);}
i++
}
myFns[0](); //-> 2
myFns[1](); //-> 2
В этом случае мы не можем просто использовать let
, потому что он не создаст объем блока, который нам нужен для каждой итерации. В этом случае нам нужно использовать самозапускающуюся функцию для обработки значения i
:
const myFns = [];
const arr = ['hello', 'world'];
var i = 0;
while(i < arr.length) {
((i) => {
myFns[i] = function() {console.log(i);}
})(i);
i++;
}
myFns[0](); // -> 0
myFns[1](); // -> 1
В приведенном выше фрагменте мы создаем самовызывающуюся стрелочную функцию для инкапсуляции значения i
на каждой итерации.