Замыкание — это основная концепция JavaScript Джона Сины, которая была у вас перед глазами, но, конечно же, вы ее не видите. Итак, давайте немного осветим замыкания (в буквальном смысле) в этой статье и поймем, что они могут предложить.

Все, чего касается свет, является нашим королевством, как сказал Муфаса в «Короле Льве». Точно так же все, что связано с функциями в JavaScript, предлагает вам Closure. Так что, если вы новичок, продвинутый разработчик или разработчик JavaScript-ниндзя, вероятность того, что вы использовали замыкания, очень высока. Итак, прежде чем мы начнем с кода, давайте разберемся, что Google говорит о слове «закрытие»:

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

Замыкание — это не что иное, как функции, привязанные к своему лексическому окружению. Лексическое окружение — это место, где фактически написан код. Что касается кода, у нас есть функции, которые могут иметь локальные и глобальные переменные, к которым он может иметь доступ; таким образом, закрытие в основном позволяет функции получить доступ к вещам, присутствующим в ее лексической области видимости; эта концепция применима ко всей вселенной JavaScript.

Итак, если замыкания везде, то давайте начнем отовсюду в следующей последовательности:

  1. Локальный, глобальный охват и замыкания
  2. Внутренние функции и замыкания
  3. setTimeout и замыкания
  4. Функции высшего порядка и замыкания
  5. Применение замыкания — инкапсуляция/скрытие данных

Локальный, глобальный охват и замыкания

Локальная область: переменные внутри функции имеют функциональную/локальную область, в которой они доступны. Функциональные переменные больше не могут быть доступны за пределами границ функции. Так что функция запоминает свою переменную во время выполнения, круто. Проверьте приведенный ниже код:

function foo() {
    let local = 10;

    console.log(local);
}

foo();

// 10

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

let global = 20;

function foo() {
    let local = 10;

    console.log(local);
    console.log(global);
}

foo();

// 10
// 20

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

Внутренние функции и замыкания

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

let value0 = 1;

function outer() {
    let value1 = 10;

    function inner() {
        let value2 = 100;
        console.log(value0, value1, value2);
    }

    inner();
}

outer();

// 1 10 100

Здесь функция inner() формирует замыкание с внешней функцией и глобальной областью видимости. Функция inner() запоминает собственное окружение (лексическое окружение), в котором она присутствует, и, следовательно, сможет вспомнить ее при вызове. Мы вызываем внутреннюю функцию непосредственно внутри функции external() и делаем вызов функции external() в корневой области видимости.

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

let value0 = 1;

function outer() {
    let value1 = 10;

    function inner() {
        let value2 = 100;
        console.log(value0, value1, value2);
    }

    return inner;
}

const innerRef = outer();

innerRef();

// 1 10 100

Это тоже приведет к тому же результату; здесь функция external() не вызывает сразу функцию inner(), вместо этого она возвращает ссылку на функцию inner(). Позже мы вызываем функцию external(), которая вернет ссылку на функцию inner(), а затем вызовем функцию inner() вне области видимости ее родителя. Интересно, что функция inner() не проявляет никаких признаков болезни Альцгеймера и запоминает свое лексическое окружение даже за пределами родительской границы. Очень трогательный!

Вы даже можете проверить это, поместив точку отладки либо в VSCode, либо в Chrome (или в любом браузере) на вкладке «Источник» в строке журнала консоли внутренней функции. Отладчик покажет следующие замыкания на вкладке отладки:

Проверьте приведенные ниже замыкания со ссылками на функцию inner(). Это то же самое, что и приведенный выше код, но с немного другим подходом к реализации, как указано в комментариях. Интернет — это просто страшное место, где одни и те же вещи показаны по-разному, не волнуйтесь, мы это покрыли.

Внутренний 1:

// creating inner function and
// passing the inline reference of inner function from outer function

let value0 = 1;

function outer() {
 let value1 = 10;
 return function () {
  let value2 = 100;
  console.log(value0, value1, value2);
 };
}

const innerRef = outer();

innerRef();

// 1 10 100

Внутренняя 2: переменная внешней функции

// creating outer function variable

let value0 = 1;

const outer = function () {
 let value1 = 10;
 return function () {
  let value2 = 100;
  console.log(value0, value1, value2);
 };
};

const innerRef = outer();

innerRef();

// 1 10 100

Внутренний 3: IIFE — немедленно вызываемое функциональное выражение

// creating inner ref directly by immediately invoking the outer function

let value0 = 1;

const innerRef = (function () {
 let value1 = 10;
 return function () {
  let value2 = 100;
  console.log(value0, value1, value2);
 };
})();

innerRef();

// 1 10 100

Внутренний 4: IIFE с функцией стрелки

// creating inner ref directly by immediately invoking the outer function (arrow)

let value0 = 1;

const innerRef = (() => {
 let value1 = 10;
 return () => {
  let value2 = 100;
  console.log(value0, value1, value2);
 };
})();

innerRef();

// 1 10 100

setTimeout и замыкания

Жизнь хороша, когда все синхронно, но не раньше, чем появится асинхронность. Функция setTimeout() является таким примером чего-то, что по своей природе является асинхронным. Важно изучить поведение замыканий в таких неверных средах (асинхронный мир). Давайте сначала вспомним функцию setTimeout() и неверную среду, о которой я говорю:

// setTimeout execution sequence

console.log('before setTimeout');

setTimeout(function () {
    console.log('within setTimeout');
}, 0);

console.log('after setTimeout');

// before setTimeout
// after setTimeout 
// within setTimeoutJavaScript handles the async operations on a different timeline altogether, even if you run setTimeout() for 0 milliseconds. The callback of setTimeout will execute after completing all the sync operations. An ideal scenario for us to explore about closures.

JavaScript обрабатывает асинхронные операции на другой временной шкале, даже если вы запускаете setTimeout() на 0 миллисекунд. Обратный вызов setTimeout будет выполняться после завершения всех операций синхронизации. Идеальный сценарий для нас, чтобы изучить замыкания.

Если вы смотрели фильм «Начало», то замыкания напоминают вам об этом; в котором у главного героя было много уровней снов, и персонажу было важно различать эти уровни. То же самое происходит и с функциями с замыканиями. Синхронная или асинхронная функция запоминает свое лексическое окружение благодаря замыканиям. Давайте проверим setTimeout на нескольких примерах:

// setTimeout can remember variables from its lexical scope 
// even they execute on a different timeline

let global = 1;

setTimeout(function () {
    let local = 10;
    console.log(global, local);
}, 1000);

// after 1000 milliseconds
// 1 10

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

Добавим к этому еще один уровень и проверим, как setTimeout() ведет себя внутри функции, а не на корневом уровне.

let value0 = 1;

function outer() {
    let value1 = 10;

    setTimeout(function () {
        let value2 = 100;
        console.log(value0, value1, value2);
    }, 1000);
}

outer();

// after 1000 milliseconds
// 1 10 100

Здесь мы добавили одну функцию external(), которая содержит setTimeout(). Таким образом, функция external() находится на уровне 1, а функция обратного вызова setTimeout() — на уровне 2, а замыкания все еще работают.

Для эксперимента я добавил больше уровней и протестировал закрытие функции setTimeout(), чтобы почувствовать эффект Inception и человек, который работает без вреда, и для справки я добавил некоторые варианты, такие как анонимные и стрелочные функции.

setTimeout 1: вызов внутренних функций внутри

let value0 = 1;

function level1() {
    let value1 = 10;
    function level2() {
        let value2 = 100;
        function level3() {
            let value3 = 1000;
            function level4() {
                let value4 = 10000;
            
                setTimeout(function () {
                    console.log(value0, value1, value2, value3, value4);
                }, 1000);
            }
            level4();
        }
        level3();
    }
    level2();
}

level1();

// after 1000 milliseconds
// 1 10 100 1000 10000

setTimeout 2: Возврат функций

let value0 = 1;

function level1() {
    let value1 = 10;
    return function () {
        let value2 = 100;
        return function () {
            let value3 = 1000;
            return function () {
                let value4 = 10000;
            
                setTimeout(function () {
                    let value5 = 100000;
                    console.log(value0, value1, value2, value3, value4, value5);
                }, 1000);
            }
        }
    }
}

const level2 = level1();
const level3 = level2();
const level4 = level3();

level4();

// level1()()()();

// after 1000 milliseconds
// 1 10 100 1000 10000

setTimeout 2: Возврат стрелочных функций

let value0 = 1;

const level1 = () => {
    let value1 = 10;
    return () => {
        let value2 = 100;
        return () => {
            let value3 = 1000;
            return () => {
                let value4 = 10000;
            
                setTimeout(() => {
                    let value5 = 100000;
                    console.log(value0, value1, value2, value3, value4, value5);
                }, 1000);
            }
        }
    }
}
const level2 = level1();
const level3 = level2();
const level4 = level3();

level4();

// level1()()()();

// after 1000 milliseconds
// 1 10 100 1000 10000

Не пугайтесь этих многочисленных скобок (level1()()()()), это всего лишь вызов функций, чтобы наконец вывести вас из самой внутренней функции.

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

Я не знаю, кому это нужно слышать, но совершенно не обязательно кайфовать, чтобы понимать функции высшего порядка в JavaScript. Функция называется функцией высшего порядка, если она может либо принимать функцию в качестве параметра, либо возвращать функцию. Наличие замыканий можно обнаружить с большого расстояния в функциях более высокого порядка.

Мы рассматриваем оба сценария функции высшего порядка:

  1. Передать функцию в качестве параметра
  2. Возврат функции из функции

Передать функцию в качестве параметра

// passing function as parameter

const num1 = 6;
const num2 = 3;

const compute = operation => operation();

const add = () => num1 + num2;
const sub = () => num1 - num2;

console.log(compute(add));
console.log(compute(sub));

// 9
// 3

Здесь мы передаем функцию в качестве параметра функции вычисления. Здесь мы хотим идентифицировать наличие замыкания. Внутри функций add() и sub() мы напрямую пытаемся использовать глобальные переменные num1 и num2. Прелесть замыканий в том, что они позволяют функции помнить свое лексическое окружение даже вдали от дома. В приведенном выше примере то же самое происходит с функциями add() и sub(), даже несмотря на то, что они не выполняются напрямую на корневом уровне, вместо этого они вызываются в границах функции calculate(), они по-прежнему помнят переменные num1 и num2.

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

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

Функции массива более высокого порядка являются чистым примером таких случаев; рассмотрим следующую структуру данных:

const employees = [
    { id: 1, firstName: 'John', lastName: 'Doe', salary: 1200 },
    { id: 2, firstName: 'Allen', lastName: 'Green', salary: 2200 },
    { id: 3, firstName: 'Smith', lastName: 'Woods', salary: 5400 },
    { id: 4, firstName: 'Berry', lastName: 'Allen', salary: 4300 },
    { id: 5, firstName: 'Clark', lastName: 'Kent', salary: 3300 }
];

Изучение некоторых запросов с функциями массива более высокого порядка:

// get employees having salary greater than 4k
const result1 = employees.filter(i => i.salary > 4000);
console.log(result1);

// get employee whose id is 3
const result2 = employees.find(i => i.id === 3);
console.log(result2);

// get employee index whose id is 4
const result3 = employees.findIndex(i => i.id === 4);
console.log(result3);

// get id and full name of employees
const result4 = employees.map(i => ({ id: i.id, fullName: `${i.firstName} ${i.lastName}` }));
console.log(result4);

/*
[
  { id: 3, firstName: 'Smith', lastName: 'Woods', salary: 5400 },
  { id: 4, firstName: 'Berry', lastName: 'Allen', salary: 4300 }
]
{ id: 3, firstName: 'Smith', lastName: 'Woods', salary: 5400 }
3
[
  { id: 1, fullName: 'John Doe' },
  { id: 2, fullName: 'Allen Green' },
  { id: 3, fullName: 'Smith Woods' },
  { id: 4, fullName: 'Berry Allen' },
  { id: 5, fullName: 'Clark Kent' }
]
*/

Возврат функции из функции

Такие случаи редки и в основном наблюдаются при реализации концепции каррирования в JavaScript; по которому ваша функция должна вернуть функцию, и эта цепочка продолжается и продолжается, и продолжается.

// returning function as parameter (currying)

const splitter = by => value => value.split(by);

const dashSplitter = splitter('-');
const commaSplitter = splitter(',');

const result1 = dashSplitter('thunder-bolt-attack');
const result2 = commaSplitter('thunder,bolt,attack');

console.log(result1);
console.log(result2);

/*
[ 'thunder', 'bolt', 'attack' ]
[ 'thunder', 'bolt', 'attack' ]
*/

Здесь мы пытаемся имитировать тот же случай, когда мы создали функцию-разделитель, которая возвращает другую функцию, которая принимает один аргумент с именем value (с помощью которого мы разделяем строку) и возвращает массив разделенных значений. Мы создали две интеллектуальные функции-разделители, используя основную функцию-разделитель, одна из которых разделяет строку на основе дефиса, а другая — на основе запятой.

Здесь также мы можем почувствовать суть замыканий, когда вызывается внутренняя функция-разделитель, она принимает значение «by» от своей внешней родительской функции, что выходит за рамки внутренней функции-разделителя; но все благодаря замыканию, даже не уверенному в том, когда будет выполнена функция интеллектуального разделителя, это заставляет их помнить о значении «by» их родительской функции.

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

Применение замыкания — инкапсуляция/скрытие данных

Наиболее распространенное и основное использование концепции замыкания — это наличие функции инкапсуляции, где мы можем иметь своего рода частные переменные в наших функциях замыкания. Синтаксис ES6 по умолчанию для классов не поддерживает эту функцию, поэтому сейчас нет приватных переменных для JavaScript (плачет в углу); но до тех пор вы можете добиться этого, используя функции закрытия. Мы попробуем реализовать функцию инкапсуляции, используя фиктивные функции Counter и Account.

Закрытие счетчика

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

// creating inner increment function and 
// passing the reference of inner increment function 
// from counter function

function counter() {
    let count = 0;
    return function () {
        return ++count;
    };
}

const counter1 = counter();

console.log(counter1())
console.log(counter1())
console.log(counter1())
console.log(counter1())

// 1
// 2
// 3
// 4

Здесь переменная count находится в локальной области видимости функции counter(), но для внутренней функции это замыкание, и она может получить доступ к родительской переменной count. Мы создаем ссылку на функцию counter1 для возвращаемой внутренней функции, которая может получить доступ к локальной переменной count, не позволяя внешнему миру получить ее; прохладный.

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

Счетчик 1:

// creating inner increment & decrement function and 
// passing object from counter function

function counter() {
    let count = 0;

    function increment() {
        return ++count;
    }
    function decrement() {
        return --count;
    }
    function value() {
        return count;
    }

    return ({ increment, decrement, value });
}

const counter1 = counter();

console.log(counter1.value())
console.log(counter1.increment())
console.log(counter1.increment())
console.log(counter1.increment())

console.log(counter1.decrement())
// 0
// 1
// 2
// 3
// 2

Счетчик 2: Использование стрелочных функций

// creating inner increment & decrement function and 
// passing object from counter function using arrow function

const counter = () => {
    let count = 0;
    return ({
        increment: () => ++count,
        decrement: () => --count,
        value: () => count
    });
}

const counter1 = counter();

console.log(counter1.value())
console.log(counter1.increment())
console.log(counter1.increment())
console.log(counter1.increment())

console.log(counter1.decrement())
// 0
// 1
// 2
// 3
// 2

Счетчик 3: использование конструктора

// creating inner increment & decrement function 
// for constructor Counter function

function Counter() {
    let count = 0;

    this.increment = () => ++count;
    this.decrement = () => --count;
    this.value = () => count;
}

const counter1 = new Counter();

console.log(counter1.value())
console.log(counter1.increment())
console.log(counter1.increment())
console.log(counter1.increment())

console.log(counter1.decrement())
// 0
// 1
// 2
// 3
// 2

Счетчик 4: использование класса

// creating inner increment & decrement function 
// for constructor Counter function with ES6 signature

class Counter {
    constructor() {
        let count = 0;

        this.increment = () => ++count;
        this.decrement = () => --count;
        this.value = () => count;
    }
}

const counter1 = new Counter();

console.log(counter1.value())
console.log(counter1.increment())
console.log(counter1.increment())
console.log(counter1.increment())

console.log(counter1.decrement())
// 0
// 1
// 2
// 3
// 2

Счетчик 4 - это просто синтаксический чудак, так как это всего лишь уловка, позволяющая использовать локальную переменную функции-конструктора как частную и инициализировать функции-члены класса внутри конструктора.

Закрытие счета

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

// basic encapsulation using closure

function account(number, type, baseAmount = 0) {
    let _number = number;
    let _type = type;
    let _balance = baseAmount;
    return {
        getNumber: () => _number,
        getType: () => _type,
        getBalance: () => _balance,
        withdraw: (amount) => amount > _balance ? 'Insufficient Balance' : (_balance = _balance - amount),
        deposit: (amount) => _balance = _balance + amount
    };
}

const account1 = account(2001, 'Savings', 1000);

console.log('Number:', account1.getNumber());
console.log('Type:', account1.getType());
console.log('Balance:', account1.getBalance());

console.log('Balance:', account1.withdraw(200));
console.log('Balance:', account1.deposit(300));
console.log('Balance:', account1.deposit(300));
console.log('Balance:', account1.withdraw(30000));
console.log();

const account2 = account(4001, 'Current', 500);

console.log('Number:', account2.getNumber());
console.log('Type:', account2.getType());
console.log('Balance:', account2.getBalance());

console.log('Balance:', account2.withdraw(200));
console.log('Balance:', account2.deposit(1000));

// Number: 2001
// Type: Savings
// Balance: 1000
// Balance: 800
// Balance: 1100
// Balance: 1400
// Balance: Insufficient Balance

// Number: 4001
// Type: Current
// Balance: 500
// Balance: 300
// Balance: 1300

Мы создаем 2 объекта учетных записей, с помощью которых мы можем выполнять операции, но без прямого доступа к закрытым членам, которые отмечены символом подчеркивания (например, _number, _type и _balance).

Мы можем ясно понять, что закрытие в основном Танос (неизбежно). Бойтесь этого. Беги от него. Закрытия все еще приходят. Так что лучше принять это, чем потом избегать и чесать волосы. Замыканий намного больше, и их очень трудно охватить в небольшом чтении.

Git-репозиторий

Проверьте репозиторий git для этого проекта или загрузите код.

Скачать код

Git-репозиторий

Краткое содержание

Мужчина! эта концепция потребовала много времени, чтобы понять ее с первого взгляда. Так просто, но так сложно уложить в голове таких смертных, как мы. Как только вы поняли, закройте ноутбук и идите прямо к кровати, чтобы сохранить его надолго (шутка).

Помимо шуток, очень важно знать эту концепцию (A), если вы не хотите, чтобы JavaScript пугал вас в любой момент времени (B) самый распространенный вопрос на собеседовании, и (C) конечно, вы заслуживаете завершения с JavaScript. Техника ниндзя, чтобы лучше понять эту концепцию, состоит в том, чтобы испачкать руку и попробовать все возможные результаты с замыканиями, которые вы можете.

Надеюсь, эта статья поможет.

Первоначально опубликовано на https://codeomelet.com.