Функции - это вызываемые объекты в 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'}. Если бы вы не использовали стрелочную функцию, у вас были бы следующие параметры:

  1. Использование переменной во внешней функции:
function hello() {
  var self = this;
  setTimeout(function() {
    console.log(self.name);
  }, 200);
}
hello.call({name: 'tom'});
  1. Использование 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 на каждой итерации.

Далее: прототипы JavaScript, карманный справочник