Как сказано в книге «JavaScript, которого вы не знаете»:
Замыкания похожи на таинственный нецивилизованный мир, отпочковавшийся от JavaScript, куда могут попасть только самые смелые!
Это также верно в реальной разработке, за исключением сценария интервью или нескольких других конкретных сценариев (таких как функция «анти-тряска и дроссель»), мы поняли, что — это «закрытие»! В других случаях он в основном не используется или используется, не зная об этом.
Как жаль! ! Такая хорошая вещь используется, не зная об этом.
Эта статья опирается на 50 000 лайков на Medium.I never understood JavaScript closuresВозьмите вас, чтобы понять "закрытие" один раз! (Бесполезно искать меня, если я этого не понимаю, и бесполезно искать Иисуса, я сказал QAQ)
Прочитав эту статью, вы с удивлением обнаружите, что даже в следующем коде есть замыкание? !
let a = 1
function b(){
return a
}
console.log(b())
«Я никогда не пойму закрытие JS, потому что оно везде...»
контекст выполнения
Прежде чем узнать «замыкание», мы должны понять важную концепцию —"контекст выполнения"!
Контекст выполнения делится на:
- Глобальный контекст выполнения (Глобальный контекст выполнения): когда файл JS загружается в браузер для запуска, он входит в глобальный контекст выполнения. Глобальные переменные находятся в этом контексте выполнения. Код доступен в любом месте.
- Контекст выполнения функции (контекст функционального выполнения): контекст, определенный в конкретном методе. Доступен только в этом методе и внутри методов внутри этого метода.
Например, когда мы вызываем функцию в глобальном контексте выполнения, процесс разбора JS, вероятно, будет выглядеть так:
- JS создает новый контекст выполнения функции (который можно понимать как временный «контекст выполнения»), который имеет локально доступный набор переменных;
- Контекст выполнения будет помещен в [стек выполнения] для выполнения (здесь [стек выполнения] рассматривается как механизм отслеживания позиции выполнения программы);
- при встрече
returnили}, считается, что исполнение заканчивается. - Контекст выполнения извлекается из стека выполнения;
- Выполняемая функция отправит возвращаемое значение в контекст выполнения, который ее вызвал, в данном случае в глобальный контекст выполнения;
- Контекст выполнения функции будет уничтожен, а его набор переменных станет недоступен, поэтому он называется временным «контекстом выполнения»;
Что касается работы стека выполнения, вы можете увидеть следующее понимание анимации:
Давайте объединим анализ следующего примера кода, чтобы увидеть, как JS делает это шаг за шагом:
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
-
В первой строке объявляется переменная a в глобальном контексте выполнения и ей присваивается значение 3;
-
Строки со 2 по 5 — это контекст выполнения функции. Функция addTwo объявляется в глобальном контексте выполнения, код внутри функции не выполняется, а сохраняется для последующего вызова;
-
В шестой строке объявляется переменная b, а присваивание b является возвращаемым значением выполнения функции addTwo;
-
Найдите функцию addTwo в глобальном контексте выполнения для выполнения и передайте параметр 3;
-
В этот момент контекст выполнения переключается! Создайте временный контекст выполнения функции с именем addTwo и поместите его в стек выполнения;
-
на третью строчку, а дальше что? Создать переменную ret ? Нет, на самом деле сначала создается переменная x, а затем ей присваивается значение 3;
-
В третьей строке объявляется переменная ret, а затем присваивается результат операции x+2.
-
Где JS ищет термин добавления x? Оказывается, x уже существует в контексте выполнения функции addTwo, его значение равно 3, а затем выполняет операцию сложения с 2, после чего присваивает его ret.
-
Затем дошел до четвертой строки, возврат рет, чтобы вернуться;
-
В четвертой и пятой строках выполнение функции addTwo заканчивается, временный контекст выполнения уничтожается, переменная x и переменная ret очищаются, контекст выполнения функции выталкивается из стека вызовов, а затем функция будет возвращен контекст выполнения, вызвавший его, здесь То есть глобальный контекст выполнения;
-
Затем вернитесь к пункту 4 и присвойте возвращаемое значение функции b;
-
Седьмая строка, печать;
Хотя это очень длинное объяснение и оно не включает в себя «замыкания», это то, что мы должны написать заранее и полностью понять.
лексический объем
Тогда «контекст исполнения», есть концепция, которую мы должны понимать, это«лексический объем»!
Взгляните на следующий пример кода:
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
Можете ли вы описать этот код со ссылкой на объяснение в предыдущем разделе?
-
В первой строке объявляется переменная vall в глобальном контексте выполнения и ей присваивается значение 2;
-
Строки со 2 по 5 объявляют контекст выполнения функции multipleThis, внутренний код не выполняется и сохраняется для вызова;
-
Шестая строка объявляет переменную multiplied и присваивает ей значение, возвращаемое выполнением функцииmultimpleThis;
-
Найдите функциюmultiThis в глобальном контексте выполнения для выполнения и передайте параметр 6;
-
В этот момент контекст выполнения переключается! Создайте временный контекст выполнения функции с именемmultiThis и поместите его в стек выполнения;
-
Перейти к третьей строке, объявить переменную n в контексте выполнения функции и присвоить значение 6;
-
Третья строка объявляет переменную ret и присваивает ей результат умножения n на vall. n равно 6, а как насчет val? val не найден в текущем контексте выполнения функции!
-
В этот момент JS перейдет к глобальному контексту выполнения функцииmultiThis, чтобы найти vall! Нашел! Его значение 2, 6 * 2 = 12. назначить рет;
-
Затем дошел до четвертой строки, возврат рет, чтобы вернуться;
-
В четвертой и пятой строках завершается функцияmultiThis, временный контекст выполнения уничтожается, переменная n и переменная ret очищаются, но vall не уничтожается, так как существует в глобальном контексте выполнения функции;
-
Вернитесь к шестой строке и присвойте возвращаемое значение 12 переменной multiplied;
-
итоговая распечатка;
В этом описании этапы поседения в основном такие же, как и в предыдущем разделе.
Самое главное: когда JS ищет переменную в текущем контексте выполнения, если он не может ее найти, он будет вызывать предыдущий контекст выполнения этого контекста выполнения, чтобы найти ее.
Это не сложно понять, процесс нахождения переменных в такой цепочке и есть [scope chain] JS.
функция возврата функции
Функция может возвращать что угодно, включая возврат другой функции, конечно.
Давайте снова разберем следующий код, как указано выше:
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
-
В первой строке объявляется переменная val в глобальном контексте выполнения и ей присваивается значение 7;
-
Строки со 2 по 8 объявляют контекст выполнения функции createAdder, внутренний код не выполняется, а сохраняется для вызова;
-
Одиннадцатая строка объявляет переменную adder и присваивает ей возвращаемое значение выполнения функции createAdder;
-
Найдите функцию createAdder в глобальном контексте выполнения для выполнения, она вызывается во второй строке, ок;
-
Пришел ко второй строке, создайте новый временный контекст выполнения функции, чтобы снести стек выполнения;
-
Так как функция createAdder не передает параметры непосредственно в тело функции, которая объявляет функцию addNumbers, только оператор, не выполняется;
-
Переходим к седьмой строке, возвращаем addNumbers в глобальный контекст выполнения, который является функцией, затем выталкиваем контекст выполнения временной функции createAdder из стека выполнения;
-
Контекст выполнения функции createAdder уничтожается, и addNumbers в это время существовать не будет;
-
Перейти к десятой строке, объявить переменную sum в глобальном контексте выполнения, а ее значение — возвращаемое значение выполнения adder(val, 8);
-
Мы идем в глобальный контекст выполнения, чтобы искать сумматор и находим его, так получилось, что это функция, и мы можем ее вызвать;
-
У него два параметра, первый — val, который объявляется и копируется в первой строке, равной 7, и второй параметр — 8;
-
Затем мы подошли к третьей строке, к пятой строке создаем две переменные A и B, присваивая им значения из 8 и 7 соответственно;
-
Четвертая строка, объявить переменную RET, присвоение 8 + 7, 15;
-
Возвращается переменная ret, уничтожается временный контекст выполнения функции, а также уничтожаются переменные a, b и ret;
-
Возвращаемое значение выполнения функции сумматора присваивается переменной sum;
-
итоговая распечатка;
Главный герой закрыт! ! !
Прочитав подробный анализ конкретных шагов в трех предыдущих абзацах, я думаю, что дам вам аналогичный код вызова, и вы сможете понять подсказки и провести аналогичный анализ!
1:let counter
2: function createCounter() {
3: let counter = 0
4: const myFunction = function() {
5: counter = counter + 1
6: return counter
7: }
8: return myFunction
9: }
10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
Приходите попробовать?
-
Первая строка объявляет переменную counter и присваивает ей значение undefined ;
-
Со второй строки по девятую в глобальном контексте выполнения объявляется функция createCounter, которая не выполняется и сохраняется для вызова;
-
Строка 10 объявляет приращение переменной в глобальном контексте выполнения и присваивает ее возвращаемому значению выполнения функции createCounter;
-
Вызовите функцию createCounter, которая была объявлена на шаге 2, выполните ее!
-
Строки со 2 по 9 создают новый контекст выполнения временной функции;
-
Объявите переменную counter в этом временном контексте выполнения функции и присвойте значение 0;
-
Строки с четвертой по седьмую объявляют функцию myFunction, не выполняют ее и сохраняют для вызова;
-
Верните myFunction и назначьте ее переменной increment. Контекст выполнения функции createCounter уничтожается, уничтожаются и myFunction, и counter;
-
В настоящее время в глобальном контексте выполнения нет функции myFunction, только функция приращения;
-
Одиннадцатая строка объявляет переменную c1 и присваивает ей возвращаемое значение выполнения функции приращения;
-
Функция приращения — это возвращаемое значение выполнения функции createCounter, которое определено в строках с четвертой по седьмую;
-
Создайте новый контекст выполнения функции, без передачи параметров, напрямую введите функцию для выполнения;
-
Пятая строка,
counter = counter + 1Переменная счетчика выполняет контекст, чтобы найти вновь созданную функцию? Не могу найти это! Я должен вернуться в глобальный контекст исполнения, чтобы найти его, выполнить счетчик контекста undefined в глобаке, поэтому выполнитьcounter = undefined + 1; -
Шестая строка возвращает счетчик, значение равно 1, уничтожает новую функцию и выполняет текст;
-
Вернемся к одиннадцатой строке, c1 присваивается значение 1;
-
Двенадцатая строка, повторите шаги с 10 по 14, с2 присваивается то же значение, что и 1;
-
В тринадцатой строке повторите шаги с 10 по 14 и таким же образом присвойте с3 значение 1;
-
итоговая распечатка;
Это действительно так?Печатаем в консоль:
Результат оказался вопреки ожидаемому отпечатку нашего анализа!
что случилось?
На работе должно быть что-то таинственное! Правильно, это «замыкание»!недостающая часть!
Его принцип таков:
Когда мы объявляем функцию, мы храним его для вызова, а не только определение функции, но и «закрытие» функции, которая включает в себя лексическую область всех переменных в контексте выполнения функции. Они определяются при создании функции.
Чтобы провести неуместную аналогию, подумайте о функции как о человеке.Когда человек рождается (когда функция создается), он также приходит с рюкзаком (закрытием), в который входит семейное окружение (лексический объем).
Так что наш пошаговый анализ в предыдущем абзаце неверен!
Давайте сделаем правильный анализ снова!
1:let counter
2: function createCounter() {
3: let counter = 0
4: const myFunction = function() {
5: counter = counter + 1
6: return counter
7: }
8: return myFunction
9: }
10: const increment = createCounter()
11: const c1 = increment()
12: const c2 = increment()
13: const c3 = increment()
14: console.log('example increment', c1, c2, c3)
-
Первая строка объявляет переменную counter и присваивает ей значение undefined ;(то же, что и выше)
-
Со второй строки по девятую в глобальном контексте выполнения объявляется функция createCounter, которая не выполняется и сохраняется для вызова;(то же, что и выше)
-
Строка 10 объявляет приращение переменной в глобальном контексте выполнения и присваивает ее возвращаемому значению выполнения функции createCounter;(то же, что и выше)
-
Вызовите функцию createCounter, которая была объявлена на шаге 2, выполните ее!(то же, что и выше)
-
Строки со 2 по 9 создают новый контекст выполнения временной функции;(то же, что и выше)
-
Объявите переменную counter в этом временном контексте выполнения функции и присвойте значение 0;(то же, что и выше)
-
Строки с четвертой по седьмую объявляют функцию myFunction,Также создается замыкание, включая лексическую область видимости всех переменных в контексте выполнения этой функции. В данном случае это счетчик, который просматривается по цепочке областей видимости и имеет значение 0..Здесь также нет исполнения и хранилища для вызова;
-
поставить мою функциюи его закрытиеВместе вернитесь и присвойте его переменной increment. Контекст выполнения функции createCounter уничтожается, уничтожаются и myFunction, и counter;
-
В настоящее время в глобальном контексте выполнения нет функции myFunction, только функция приращения;(то же, что и выше)
-
Одиннадцатая строка объявляет переменную c1 и присваивает ей возвращаемое значение выполнения функции приращения;(то же, что и выше)
-
Функция приращения — это возвращаемое значение выполнения функции createCounter, которое определено в строках с четвертой по седьмую;(то же, что и выше)
-
Создайте новый контекст выполнения функции, без передачи параметров, напрямую введите функцию для выполнения;(то же, что и выше)
-
Пятая строка,
counter = counter + 1, найдена ли переменная counter во вновь созданном контексте выполнения функции? Не могу найти!Мы собираемся заглянуть внутрь его «затвора»!, он действительно существует! Его значение равно 0, тогда можно выполнить counter = 0 + 1, что равно 1; -
Шестая строка возвращает счетчик, значение равно 1, уничтожает новую функцию и выполняет текст;(то же, что и выше)
-
Вернемся к одиннадцатой строке, c1 присваивается значение 1;(то же, что и выше)
-
Строка 12, повторите шаги с 10 по 14,Когда мы идем к «замыканию», чтобы найти его снова, потому что счетчик стал 1 после операции на предыдущем шаге, мы снова выполняем операцию сложения, и результат счетчика равен 2 ;
-
В тринадцатой строке повторите шаги с 10 по 14 и таким же образом назначьте c3 на 3;
-
итоговая распечатка;
О~ так оно и есть!
Когда функция объявляется, это не только объявление, но и «замыкание» за ней! Закрытие содержит лексическую область видимости всех переменных в контексте выполнения этой функции!
Оказывается, это закрытие!
У вас может возникнуть вопрос: если функция объявлена, она содержит замыкание, а есть ли замыкание и у глобальной функции?
Ответ: ДА!
Когда объявляется глобальная функция, также происходит замыкание! Просто его переменная лексическая область видимости является глобальной, поэтому не очевидно, что она не «закрыта».
«Замыкания» наиболее очевидны, когда функция возвращает другую функцию! Лексическая область видимости переменной возвращаемой функции имеет доступ к переменным с неглобальной областью видимости, они помещаются только в ее замыкание!
тестовое задание
Вот еще несколько завершающих вопросов для самопроверки:
// 题目一
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
// 题目二
function multiply(number1, number2) {
if (number2 !== undefined) {
return number1 * number2;
}
return function doMultiply(number2) {
return number1 * number2;
};
}
multiply(4, 5);
const double = multiply(2);
double(5);
// 题目三
function showBiBao() {
for (var i = 0; i < 5; i++) {
setTimeout( timer => () {
console.log(i);
}, 1000 );
}
console.log(i)
}
showBiBao()
function showListNumber() {
for(var i = 0; i < 5; i++) {
let ret = function(i) {
setTimeout(function timerr() {
console.log(i)
}, 1000)
}
ret(i)
}
console.log(i)
}
showListNumber()
Я считаю, что если вы будете внимательно следовать шагам анализа от начала до конца, это не должно быть большой проблемой~
Добро пожаловать на обсуждение ~
Суммировать
Из этой статьи мы знаем: пока есть объявление функции, есть и замыкание! Просто иногда "закрытость" не так очевидна.
Почему в заголовке написано «Я никогда не пойму закрытие JS»? На самом деле, вы также видели, что в разработке, даже если вы не используете замыкания или вообще не знаете их, вы все равно можете зарегистрироваться и уйти с работы.Но разве суть дела в закрытии?
Не делайте!
Как вы думаете, эта статья о закрытии? неправильно! Эта статья о контексте выполнения!
Как вы думаете, в этой статье говорится о контексте исполнения? неправильно! Эта статья о лексическом объеме!
Как вы думаете, в этой статье речь идет о лексическом объеме? неправильно! Эта статья о динамических языковых возможностях JS!
Когда вы спросите, в этой статье говорится о динамическом языке JS? неправильно! Эта статья о возможности однопоточной работы JS!
......
Правильно, эй, просто для удовольствия~
Писать не просто✍, ставьте лайки и поощряйте 👍, ваши отзывы 💬, моя мотивация 🚀
Я Наггетс Энтони, обратите внимание на публичный аккаунт [Наггетс Энтони], обратите внимание на передовые технологии, а также обратите внимание на жизнь и продолжайте выводить!