Понимание замыканий в JavaScript

JavaScript

предисловие

Следуя предыдущей статье «Понимание области видимости в JavaScript», я сразу же написал эту статью, потому что они связаны между собой, и вам нужно знать область действия, прежде чем разбираться в замыканиях.

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

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

что такое закрытие

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

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

Вот некоторый код, объясняющий это определение:

function foo(){
    var a = 2;
    function bar(){
        console.log(a); // 2
    }
    bar();
}
foo();

Очевидно, что это вложенная область, иbarОбласть применения также имеет доступ к внешнему объему, но это закрытие?

Нет, не совсем, но это важная часть закрытия: он может получить доступ к внешней области в соответствии с правилами поиска лексического охвата.

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

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 这就是闭包

из-заbarЛексическая область может получить доступfoo, то ставимbarСама эта функция используется как возвращаемое значение, а затем при вызовеfooвремяbarУказанная функция назначаетсяbaz(на самом деле два идентификатора относятся к одной и той же функции), поэтомуbazв состоянии получить доступfooвнутренний объем.

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

На самом деле, в нормальных условиях движок имеет сборщик мусора для освобождения пространства памяти, которое больше не используется.fooКогда выполнение будет завершено, оно, естественно, будет переработано, но волшебство замыкания в том, что оно может предотвратить это, потому что внутренняя область видимости все еще существует,barиспользуй это.

из-заbarПричина объявления местоположения, она охватываетfooЗакрытие внутренней области, так что область всегда может выжить дляbarЦитируйте в любое время после этого.

barСсылка на эту область все еще существует, и эта ссылка называется замыканием.

Следовательно, когдаbazПри вызове естественно получить доступfooвнутренний объем.

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

function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    bar(baz);
}
function bar(fn){
    fn(); // 2 —— 这也是闭包
}

поставить внутреннюю функциюbazтак какfnпараметр, переданныйbar, при звонкеfn, это может получить доступ кfooвнутренний объем.

Передаточные функции также могут быть косвенными:

var fn;
function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    fn = baz;
}
foo();
fn(); // 2 —— 这也是闭包

так:

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

Использование замыканий

Поскольку мы сказали, что замыкания повсюду, давайте взглянем на несколько фрагментов, которые мы часто видим, и увидим магию замыканий.

function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000);
}
wait("Hello, closure!");

поместите внутреннюю функцию (здесь она называетсяtimer) в качестве аргументаsetTimeout,а такжеtimerв состоянии получить доступwaitвнутренний объем.

если вы использовалиjQuery, нетрудно обнаружить, что замыкания также используются в следующем коде:

function setupBot(name,selector){
    $(selector).click(function activator(){
        console.log("Activating:" + name);
    })
}
setupBot("Closure Bot 1","#btn_1");
setupBot("Closure Bot 2","#btn_2");

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

Давайте взглянем на очень классический вопрос заключительного интервью:

for (var i=1; i<=5; i++){
	setTimeout(function(){
		console.log(i);
    },i*1000);
}

Обычно мы ожидаем, что этот код будет вести себя 1-5 раз в секунду.

Но на самом деле этот код будет выводить 6 пять раз со скоростью один в секунду, когда он работает.

Почему?

Сначала объясните, откуда берется 6, условие завершения этого циклаiбольше никогда<=5, поэтому при выполнении условияiРавно 6. Поэтому вывод отображается в конце цикла.iконечное значение .

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

Чтобы этот код работал так, как мы ожидали, обходной путь выглядит следующим образом:

for (var i=1; i<=5; i++){
    (function(j){
        setTimeout(function(){
            console.log(j);
        },j*1000);
    })(i)
}

В этом коде мы использовалиIIFE,Будуiкак параметрjпередано, на каждой итерацииIIFEсоздаст собственную область видимости, которая принимает аргументыjне то же самое, поэтому этот код работает так, как мы ожидаем.

Есть ли другое решение?

Да, используя новый ES6letМожет решить эту проблему:

for (let i=1; i<=5; i++){
	setTimeout(function(){
		console.log(i);
    },i*1000);
}

мы просто положилиvarзаменитьletПроблема была легко решена по следующим причинам:

  • forимеет собственную область видимости блока (()это родительская область,{}является дочерней областью).
  • использоватьletВозможность создавать блочные переменные.

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

Предположим, у нас есть такой объект:

var box = {
    age : 18,
}
console.log(box.age); // 18

Однако здесь есть проблема, и это свойствоageможно изменить по желанию, если мы используем закрытия, мы можем добиться приватизации, котораяageАтрибуты защищены, и производятся только разрешенные модификации.

var box = (function (){
    var age = 18;
    return {
        birthday : function(){
            age++;
        },
        sayAge : function(){
            console.log(age);
        }
    }
})();
box.birthday();
box.sayAge(); // 19

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

Уведомление:

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

постскриптум

На самом деле, когда вы разберетесь в замыканиях, вы обнаружите, что все так само собой разумеющееся, как будто так и должно быть.

Наконец, если вы уже понимаете замыкания и хотите попрактиковаться, я могу задать вам вопрос:

реализоватьaddфункция, функция:add(1)(2)(3); // 6

Более сложные:

реализоватьaddфункция, функция:add(3)(‘*’)(3); // 9

Несколько моментов:

  1. addФункции можно вызывать бесконечно.
  2. После завершения вызова результат выводится на консоль.

Спасибо за просмотр!

Примечание: Данная статья является оригинальной статьей, если вам необходимо перепечатать, пожалуйста, укажите источник.