Замыкание, которое можно понять тремя кусками кода, почему оно такое сложное?

JavaScript опрос
Замыкание, которое можно понять тремя кусками кода, почему оно такое сложное?

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

На самом деле, сначала я думал, что замыкание - это очень высокая и непостижимая точка знаний, но это не так.Если вы просто посмотрите на описание, это оченьофициальное определениетак же какОгромный индикатор закрытия статьиЕсли вы это сделаете, вас действительно уговорят сначала уволить😰. Далее я постараюсь максимально просто описать этот пункт знаний на примерах, ОК, приступим!

Прежде чем начать, если выобъем,контекст выполненияа такжеЛексическое окружениеЕсли концепция не ясна, рекомендуется сначала прочитать:Подробно объясните область действия JavaScript и цепочку областей видимости 🔗,Тщательно понимать объем, контекст выполнения, лексическое окружение 🔎, иначе некоторые описания в этой статье могут быть вам не очень дружелюбны 🤔

определение

Во-первых, выбросим определение замыкания в MDN:

функция и окружающее ее состояние (лексическое окружение, лексическое окружение) объединены вместе (или функции окружены ссылками), такая комбинацияЗакрытие(closure). То есть замыкания позволяют вам получить доступ к области действия внешней функции изнутри внутренней функции. В JavaScript всякий раз, когда создается функция, замыкание создается одновременно с созданием функции.

Хорошо, не имеет значения, если вы не понимаете вышеизложенного, давайте сначала рассмотрим пример:

Пример 1: вернуть функцию в функции

function makeFunc() {
    var name = "Mozilla"
    function displayName() {
        alert(name)
    }
    return displayName
}

var myFunc = makeFunc() 
myFunc()  // Mozilla

var myFunc = makeFunc()серединаmakeFunc()Создается соответствующий контекст выполнения функции, в котором объявляется локальная переменнаяnameи объявляет функциюdisplayname, и, наконец, вернутьсяdisplaynameУказатель (или ссылка) на функцию, т.е.function displayName(){alert(name);}вернулся вmyFuncПеременная

var myFunc = makeFunc()После завершения выполнения соответствующий контекст выполнения функции извлекается из стека, в обычных условиях его переменные также уничтожаются, ноmyFunc()называетсяmyFunc, то есть выполняетсяfunction displayName(){alert(name);},здесьnameцитируетсяmakeFuncВ переменнойname, поэтому его переменные не будут уничтожены вместе с ним, что эквивалентно инкапсуляции приватной переменной.

Это закрытие.

Хорошо, помимо конкретных примеров, давайте опишем понятие замыканий человеческими словами:

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

Описание Ян Цзинь:

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

Хорошо, вернемся к приведенному выше примеру, функцияmakeFuncи его лексическое окружение (включая переменныеnameи функцияdisplayName) называется замыканием. в лексическом окруженииnameПеременные ссылаются и не будут уничтожены.

Расширение: Далее мы рассмотрим похожий, но немного другой код:

function makeAdder(x) {
  return function(y) {
    return x + y
  }
}

var add5 = makeAdder(5)
var add10 = makeAdder(10)

console.log(add5(2))  // 7
console.log(add10(2)) // 12

add5 = null          // 释放对闭包的引用 
console.log(add5(1)) //Uncaught TypeError: add5 is not a function

В этом примере мы определяемmakeAdder(x)функция, которая принимает один параметрxи возвращает новую функцию. Возвращаемая функция принимает один параметрy, и вернутьсяx+yценность .

По сути,makeAdderявляется фабрикой функций — она создает функции, которые добавляют и суммируют указанное значение и его аргументы. В приведенном выше примере мы использовали фабрику функций для создания двух новых функций — одна суммирует свои аргументы с 5, а другая с 10.

надadd5 а также add10Все замыкания. Они используют одно и то же определение функции, но сохраняют другое лексическое окружение (переменная x).. существует add5в окружающей среде,xэто 5. пока вadd10 середина,xэто 10.

Пример 2: Немедленно выполнить функцию

var add = (function () { 
    var counter = 0; 
    return function () { 
        return counter += 1; 
    } 
})(); 

add(); 
add(); 
add(); // 3

Приведенное выше немедленно выполняет функцию и, наконец, помещаетfunction () {return counter += 1;}Вернуться кaddпеременная, выполнитьadd()когда он выполняетсяfunction () {return counter += 1;}но не заявляетcounterЭта переменная, ранее объявленная в функции немедленного выполнения, когда выполнение функции немедленного выполнения будет завершено, соответствующий контекст выполнения функции будет извлечен из стека вызовов, а переменная в немcounterбудет уничтожен, но мы можем нормально выполнитьadd()вывод кода3, проиллюстрироватьcounterПеременные не уничтожаются, что тоже является проявлением замыканий.

Пример 3: проблема с печатью по таймеру

Чтобы лучше описать каждую часть кода, он разделен на три абзаца, которые можно написать здесь:

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

Ожидается, что приведенный выше код будет выводиться отдельно каждую секунду.0, 1, 2, 3, 4, но на самом деле вывод, в свою очередь,5, 5, 5, 5, 5.

В приведенном выше кодеПеременнаяiобъявляется командой var и действует в глобальной области видимости, так что есть только одна переменная глобальноi. В каждом раунде цикла переменнаяiЗначение перепишет значение предыдущего раунда.

Обратный вызов вышеуказанной функции таймера не будет выполняться до конца цикла. Конечным условием этого цикла являетсяiбольше никогда< 5, когда условие впервые установленоiЗначение5, поэтому вывод показывает окончательное значение i в конце цикла. Поэтому каждый раз выводите 5.

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

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

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

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

Замена var на let с блочной областью действия ES6 также послужит нашей цели.

В приведенном выше кодеПеременнаяiдаletзаявленный, текущийiДействует в этом цикле, поэтому каждый циклiНа самом деле это новая переменная, поэтому окончательный вывод0, 1, 2, 3, 4. Вы можете спросить, является ли переменная для каждого раунда циклаiВсе они повторно объявлены, так как же узнать значение предыдущего цикла, чтобы вычислить значение этого цикла? Это связано с тем, что движок JavaScript внутренне запомнит значение предыдущего цикла и инициализирует переменные этого цикла.iрассчитывается на основе предыдущего цикла.

Расширение: Давайте посмотрим на аналогичный фрагмент кода:

var data = []
for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i)
  }
}
data[0]()
data[1]()
data[2]()
// 3 3 3

// 使用闭包
var data = []
for (var i = 0; i < 3; i++) {
  data[i] = (function (j) {
        return function () {
            console.log(j)
        }
  })(i)
}
data[0]()
data[1]()
data[2]()
// 0 1 2

// 使用let
var data = []
for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i)
  }
}
data[0]()
data[1]()
data[2]()
// 0 1 2

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

Другие типы сценариев, в которых используются замыкания

1. Функция как параметр

var a = 'Rocky'
function foo(){
    var a = 'foo'
    function fo(){
        console.log(a)
    }
    return fo
}

function f(p){
    var a = 'f'
    p()
}
f(foo()) // foo

Первый взглядf(foo())внутриfoo(), После его выполнения, которое является переменнойaне был переработан, потому чтоfo()цитируется, такfoo()Его лексическое окружение — замыкание.

2. Использование функций обратного вызова — это использование замыканий

window.name = 'Rocky'
setTimeout(function timeHandler(){
  console.log(window.name);
}, 100)

3. Дросселирование и защита от тряски

// 节流
function throttle(fn, timeout) {
    let timer = null
    return function (...arg) {
        if(timer) return
        timer = setTimeout(() => {
            fn.apply(this, arg)
            timer = null
        }, timeout)
    }
}

// 防抖
function debounce(fn, timeout){
    let timer = null
    return function(...arg){
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arg)
        }, timeout)
    }
}

Роль замыканий

Хорошо, давайте, наконец, подытожим роль замыканий (отКлассические сценарии использования замыканий JS и обязательные вопросы с замыканиями)

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

Некоторые студенты могут жаловаться: где код из трех частей?

На самом деле, кроме使用闭包的其他场景а также示例一和三的扩展, разве это не три абзаца😝 Эти две части просто эквивалентны расширению, я добавил их позже, это не влияет, если вы не читаете это, вы можете лучше понять это после прочтения😉

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

Ссылаться на

Документация MDN: замыкания
Классические сценарии использования замыканий JS и обязательные вопросы с замыканиями
Глубокое понимание того, что замыкания JavaScript являются замыканиями

Предыдущая серия JS

Подробно объясните область действия JavaScript и цепочку областей видимости 🔗
Тщательно понимать объем, контекст выполнения, лексическое окружение 🔎
Выбранные 30+ кейсов и 10 вопросов помогут вам освоить это 👊
Краткое изложение Promise/async/await✨ [Выбранные выходные вопросы и рукописные вопросы]