JavaScript — очень функционально-ориентированный язык. Это дает нам много свободы. Функцию можно создать в любой момент, передать в качестве аргумента другой функции, а затем вызвать позже из совершенно другого места кода.
Мы уже знаем, что функция может обращаться к внешним переменным («внешним» переменным).
Но что произойдет, если внешние переменные изменятся после создания функции? Будет ли функция получать новые значения или старые?
А что, если функция будет передана в качестве аргумента и вызвана из другого места кода, получит ли она доступ к внешним переменным в новом месте?
Давайте расширим наши знания, чтобы понять эти и более сложные сценарии.
Здесь мы поговорим о переменных let/const
В JavaScript существует три способа объявления переменной: let
, const
(современные) и var
(пережиток прошлого).
В этой статье мы будем использовать переменные let
в примерах.
Переменные, объявленные с помощью const
, ведут себя одинаково, поэтому эта статья также посвящена const
.
У старого var
есть некоторые заметные отличия, они будут рассмотрены в статье Старый «var».
Если переменная объявлена внутри блока кода {...}
, она видна только внутри этого блока.
Например:
{ // выполняем некоторую работу с локальными переменными, которые не должны быть видны снаружи let message = «Привет»; // видно только в этом блоке предупреждение (сообщение); // Привет } предупреждение (сообщение); // Ошибка: сообщение не определено
Мы можем использовать это, чтобы изолировать фрагмент кода, выполняющий свою собственную задачу, с переменными, которые принадлежат только ему:
{ // показываем сообщение let message = «Привет»; предупреждение (сообщение); } { // показываем другое сообщение let message = «До свидания»; предупреждение (сообщение); }
Без блоков была бы ошибка
Обратите внимание: без отдельных блоков будет ошибка, если мы используем let
с существующим именем переменной:
// показываем сообщение let message = «Привет»; предупреждение (сообщение); // показываем другое сообщение let message = «До свидания»; // Ошибка: переменная уже объявлена предупреждение (сообщение);
Ибо if
, for
, while
и т. д. переменные, объявленные в {...}
, также видны только внутри:
если (истина) { let фраза = "Привет!"; предупреждение (фраза); // Привет! } предупреждение (фраза); // Ошибка, нет такой переменной!
Здесь, после завершения if
, alert
ниже не увидит phrase
, отсюда и ошибка.
Это здорово, поскольку позволяет нам создавать блочные локальные переменные, специфичные для ветки if
.
То же самое справедливо и для циклов for
и while
:
для (пусть я = 0; я <3; я++) { // переменная i видна внутри только для предупреждение (я); // 0, затем 1, затем 2 } предупреждение (я); // Ошибка, нет такой переменной
Визуально let i
находится за пределами {...}
. Но конструкция for
здесь особенная: переменная, объявленная внутри нее, считается частью блока.
Функция называется «вложенной», если она создается внутри другой функции.
Это легко сделать с помощью JavaScript.
Мы можем использовать его для организации нашего кода, например:
функция SayHiBye(firstName, LastName) { // вспомогательная вложенная функция, которую можно использовать ниже функция getFullName() { вернуть имя + " " + последнее имя; } alert("Привет, " + getFullName()); alert( "Пока, " + getFullName()); }
Здесь для удобства создана вложенная функция getFullName()
. Он может получить доступ к внешним переменным и поэтому может вернуть полное имя. Вложенные функции довольно распространены в JavaScript.
Что гораздо интереснее, вложенную функцию можно вернуть: либо как свойство нового объекта, либо как результат сам по себе. Затем его можно будет использовать где-нибудь еще. Независимо от того, где он находится, он по-прежнему имеет доступ к одним и тем же внешним переменным.
Ниже makeCounter
создает функцию «счетчик», которая возвращает следующее число при каждом вызове:
функция makeCounter() { пусть счет = 0; функция возврата() { количество возврата++; }; } пусть счетчик = makeCounter(); предупреждение(счетчик()); // 0 предупреждение(счетчик()); // 1 предупреждение(счетчик()); // 2
Несмотря на простоту, слегка модифицированные варианты этого кода имеют практическое применение, например, в качестве генератора случайных чисел для генерации случайных значений для автоматических тестов.
Как это работает? Если мы создадим несколько счетчиков, будут ли они независимыми? Что здесь происходит с переменными?
Понимание таких вещей полезно для общего знания JavaScript и полезно для более сложных сценариев. Итак, давайте углубимся.
Здесь будут драконы!
Подробное техническое объяснение впереди.
Насколько я бы хотел избежать языковых подробностей низкого уровня, любое понимание без них было бы недостаточным и неполным, так что будьте готовы.
Для ясности объяснение разбито на несколько этапов.
В JavaScript каждая выполняющаяся функция, блок кода {...}
и скрипт в целом имеют внутренний (скрытый) связанный объект, известный как лексическая среда .
Объект «Лексическая среда» состоит из двух частей:
Запись среды — объект, который хранит все локальные переменные в качестве своих свойств (и некоторую другую информацию, например значение this
).
Ссылка на внешнюю лексическую среду , связанную с внешним кодом.
«Переменная» — это всего лишь свойство специального внутреннего объекта Environment Record
. «Получить или изменить переменную» означает «получить или изменить свойство этого объекта».
В этом простом коде без функций есть только одно лексическое окружение:
Это так называемая глобальная лексическая среда, связанная со всем сценарием.
На рисунке выше прямоугольник означает запись среды (хранилище переменных), а стрелка означает внешнюю ссылку. Глобальная лексическая среда не имеет внешней ссылки, поэтому стрелка указывает на null
.
Когда код начинает выполняться и продолжается, лексическая среда меняется.
Вот немного более длинный код:
Прямоугольники справа демонстрируют, как глобальная лексическая среда изменяется во время выполнения:
При запуске сценария лексическая среда предварительно заполняется всеми объявленными переменными.
Изначально они находятся в состоянии «Неинициализированные». Это особое внутреннее состояние: оно означает, что движок знает об этой переменной, но на нее нельзя ссылаться, пока она не будет объявлена с помощью let
. Это почти то же самое, как если бы переменная не существовала.
Затем let phrase
. Присвоения пока нет, поэтому его значение undefined
. С этого момента мы можем использовать переменную.
phrase
присвоено значение.
phrase
меняет значение.
Сейчас все выглядит просто, не так ли?
Переменная — это свойство специального внутреннего объекта, связанного с выполняемым в данный момент блоком/функцией/скриптом.
Работа с переменными фактически означает работу со свойствами этого объекта.
Лексическое окружение — это объект спецификации.
«Лексическая среда» — это объект спецификации: она существует только «теоретически» в спецификации языка и описывает, как все работает. Мы не можем получить этот объект в нашем коде и напрямую манипулировать им.
Механизмы JavaScript также могут оптимизировать его, отбрасывать неиспользуемые переменные для экономии памяти и выполнять другие внутренние трюки, пока видимое поведение остается описанным.
Функция также является значением, как и переменная.
Разница в том, что объявление функции мгновенно полностью инициализируется.
Когда создается лексическое окружение, объявление функции сразу становится готовой к использованию функцией (в отличие от let
, которую невозможно использовать до объявления).
Вот почему мы можем использовать функцию, объявленную как Объявление функции, даже до самого объявления.
Например, вот начальное состояние глобальной лексической среды, когда мы добавляем функцию:
Естественно, такое поведение применимо только к объявлениям функций, а не к выражениям функций, где мы присваиваем функцию переменной, например, let say = function(name)...
.
При запуске функции в начале вызова автоматически создается новая лексическая среда для хранения локальных переменных и параметров вызова.
Например, say("John")
это выглядит так (выполнение происходит на строке, отмеченной стрелкой):
Во время вызова функции у нас есть два лексических окружения: внутреннее (для вызова функции) и внешнее (глобальное):
Внутреннее лексическое окружение соответствует текущему выполнению say
. У него есть одно свойство: name
, аргумент функции. Мы вызвали say("John")
, поэтому значение name
— "John"
.
Внешняя лексическая среда — это глобальная лексическая среда. В нем есть переменная phrase
и сама функция.
Внутренняя лексическая среда имеет ссылку на outer
.
Когда код хочет получить доступ к переменной — сначала ищется внутренняя лексическая среда, затем внешняя, затем более внешняя и так далее до глобальной.
Если переменная нигде не найдена, это ошибка в строгом режиме (без use strict
присвоение несуществующей переменной создает новую глобальную переменную для совместимости со старым кодом).
В этом примере поиск происходит следующим образом:
Что касается переменной name
, alert
внутри say
немедленно находит ее во внутренней лексической среде.
Когда он хочет получить доступ к phrase
, локально phrase
нет, поэтому он следует по ссылке на внешнюю лексическую среду и находит ее там.
Вернемся к примеру makeCounter
.
функция makeCounter() { пусть счет = 0; функция возврата() { количество возврата++; }; } пусть счетчик = makeCounter();
В начале каждого вызова makeCounter()
создается новый объект лексической среды для хранения переменных для этого запуска makeCounter
.
Итак, у нас есть две вложенные лексические среды, как в примере выше:
Отличие состоит в том, что во время выполнения makeCounter()
создается крошечная вложенная функция, состоящая всего из одной строки: return count++
. Мы его пока не запускаем, только создаем.
Все функции запоминают лексическую среду, в которой они были созданы. Технически здесь нет никакой магии: все функции имеют скрытое свойство с именем [[Environment]]
, которое хранит ссылку на лексическую среду, в которой была создана функция:
Итак, counter.[[Environment]]
имеет ссылку на лексическую среду {count: 0}
. Таким образом функция запоминает, где она была создана, независимо от того, где она была вызвана. Ссылка [[Environment]]
устанавливается один раз и навсегда во время создания функции.
Позже, когда вызывается counter()
, для вызова создается новая лексическая среда, а ее внешняя ссылка на лексическую среду берется из counter.[[Environment]]
:
Теперь, когда код внутри counter()
ищет переменную count
, он сначала ищет свою собственную лексическую среду (пустую, поскольку там нет локальных переменных), затем лексическую среду внешнего вызова makeCounter()
, где находит и изменяет ее. .
Переменная обновляется в лексической среде, в которой она находится.
Вот состояние после выполнения:
Если мы вызовем counter()
несколько раз, переменная count
будет увеличена до 2
, 3
и т. д. в одном и том же месте.
Закрытие
Существует общий программный термин «замыкание», который обычно должны знать разработчики.
Замыкание — это функция, которая запоминает свои внешние переменные и может обращаться к ним. В некоторых языках это невозможно, или для этого функцию следует написать особым образом. Но, как объяснялось выше, в JavaScript все функции по своей природе являются замыканиями (есть только одно исключение, которое будет описано в синтаксисе «новой функции»).
То есть: они автоматически запоминают, где были созданы, используя скрытое свойство [[Environment]]
, и тогда их код может получить доступ к внешним переменным.
Когда на собеседовании фронтенд-разработчик получает вопрос «что такое замыкание?», правильным ответом будет определение замыкания и объяснение того, что все функции в JavaScript являются замыканиями, и, возможно, еще несколько слов о технических деталях: свойство [[Environment]]
и принцип работы лексического окружения.
Обычно лексическое окружение удаляется из памяти со всеми переменными после завершения вызова функции. Потому что на него нет никаких упоминаний. Как и любой объект JavaScript, он хранится в памяти только до тех пор, пока доступен.
Однако если есть вложенная функция, которая по-прежнему доступна после завершения функции, то у нее есть свойство [[Environment]]
, которое ссылается на лексическое окружение.
В этом случае лексическая среда по-прежнему доступна даже после завершения функции, поэтому она остается активной.
Например:
функция е() { пусть значение = 123; функция возврата() { предупреждение (значение); } } пусть g = f(); // g.[[Environment]] хранит ссылку на лексическое окружение // соответствующего вызова f()
Обратите внимание, что если f()
вызывается много раз и результирующие функции сохраняются, то все соответствующие объекты лексического окружения также будут сохранены в памяти. В приведенном ниже коде все три из них:
функция е() { пусть значение = Math.random(); функция возврата () { оповещение (значение); }; } // 3 функции в массиве, каждая из них связана с лексическим окружением // из соответствующего запуска f() пусть arr = [f(), f(), f()];
Объект лексической среды умирает, когда он становится недоступным (как и любой другой объект). Другими словами, он существует только тогда, когда на него ссылается хотя бы одна вложенная функция.
В приведенном ниже коде после удаления вложенной функции ее лексическое окружение (и, следовательно, value
) очищается из памяти:
функция е() { пусть значение = 123; функция возврата() { предупреждение (значение); } } пусть g = f(); // пока функция g существует, значение остается в памяти г = ноль; // ...и теперь память очищена
Как мы видели, теоретически, пока функция жива, все внешние переменные также сохраняются.
Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если из кода видно, что внешняя переменная не используется — она удаляется.
Важным побочным эффектом в V8 (Chrome, Edge, Opera) является то, что такая переменная станет недоступна при отладке.
Попробуйте запустить приведенный ниже пример в Chrome с открытыми инструментами разработчика.
Когда он приостановится, в консоли введите alert(value)
.
функция е() { пусть значение = Math.random(); функция г() { отладчик; // в консоли: введите alert(value); Нет такой переменной! } вернуть г; } пусть g = f(); г();
Как вы могли видеть – такой переменной нет! Теоретически он должен быть доступен, но движок его оптимизировал.
Это может привести к забавным (если не таким трудоемким) проблемам с отладкой. Один из них — вместо ожидаемой мы видим одноимённую внешнюю переменную:
let value = "Сюрприз!"; функция е() { let value = "ближайшее значение"; функция г() { отладчик; // в консоли: введите alert(value); Сюрприз! } вернуть г; } пусть g = f(); г();
Эту особенность V8 полезно знать. Если вы отлаживаете с помощью Chrome/Edge/Opera, рано или поздно вы с этим столкнетесь.
Это не ошибка отладчика, а особенность V8. Возможно, когда-нибудь оно будет изменено. Вы всегда можете проверить это, запустив примеры на этой странице.
важность: 5
Функция SayHi использует имя внешней переменной. Какое значение она будет использовать при запуске функции?
пусть имя = "Джон"; функция SayHi() { alert("Привет, " + имя); } name = "Пит"; сказатьПривет(); // что он покажет: «Джон» или «Пит»?
Такие ситуации распространены как в браузерной, так и в серверной разработке. Функцию можно запланировать для выполнения позже, чем она была создана, например, после действия пользователя или сетевого запроса.
Итак, вопрос: улавливает ли он последние изменения?
Ответ: Пит .
Функция получает внешние переменные такими, какие они есть сейчас, она использует самые последние значения.
Старые значения переменных никуда не сохраняются. Когда функции требуется переменная, она берет текущее значение из своей собственной лексической среды или внешней.
важность: 5
Функция makeWorker
ниже создает другую функцию и возвращает ее. Эту новую функцию можно вызвать откуда-то еще.
Будет ли он иметь доступ к внешним переменным из места своего создания, места вызова или того и другого?
функция makeWorker() { let name = "Пит"; функция возврата() { оповещение (имя); }; } пусть имя = "Джон"; // создаем функцию пусть работает = makeWorker(); // вызовем это работа(); // что он покажет?
Какое значение он покажет? «Пит» или «Джон»?
Ответ: Пит .
Функция work()
в приведенном ниже коде получает name
из места своего происхождения через ссылку на внешнее лексическое окружение:
Итак, здесь в результате получается "Pete"
.
Но если бы в makeWorker()
не было let name
, то поиск вышел бы наружу и взял бы глобальную переменную, как мы видим из цепочки выше. В этом случае результатом будет "John"
.
важность: 5
Здесь мы создаем два счетчика: counter
и counter2
используя одну и ту же функцию makeCounter
.
Они независимы? Что покажет второй счетчик? 0,1
или 2,3
или что-то еще?
функция makeCounter() { пусть счет = 0; функция возврата() { количество возврата++; }; } пусть счетчик = makeCounter(); пусть counter2 = makeCounter(); предупреждение(счетчик()); // 0 предупреждение(счетчик()); // 1 предупреждение(счетчик2()); // ? предупреждение(счетчик2()); // ?
Ответ: 0,1.
Функции counter
и counter2
создаются разными вызовами makeCounter
.
Таким образом, у них есть независимые внешние лексические среды, каждая из которых имеет свой собственный count
.
важность: 5
Здесь объект-счетчик создается с помощью функции-конструктора.
Будет ли это работать? Что это покажет?
функция Счетчик() { пусть счет = 0; this.up = функция() { вернуть ++счет; }; this.down = функция() { вернуть --count; }; } пусть счетчик = новый счетчик (); оповещение( counter.up() ); // ? оповещение( counter.up() ); // ? оповещение( counter.down() ); // ?
Конечно, это будет работать нормально.
Обе вложенные функции создаются в одной внешней лексической среде, поэтому они имеют общий доступ к одной и той же переменной count
:
функция Счетчик() { пусть счет = 0; this.up = функция() { вернуть ++счет; }; this.down = функция() { вернуть --count; }; } пусть счетчик = новый счетчик (); оповещение( counter.up() ); // 1 оповещение( counter.up() ); // 2 оповещение( counter.down() ); // 1
важность: 5
Посмотрите на код. Каков будет результат вызова последней строки?
let фраза = "Привет"; если (истина) { пусть пользователь = "Джон"; функция SayHi() { alert(`${phrase}, ${user}`); } } сказатьПривет();
Результат — ошибка .
Функция sayHi
объявлена внутри if
, поэтому она существует только внутри него. Снаружи нет sayHi
.
важность: 4
Напишите функцию sum
, которая работает следующим образом: sum(a)(b) = a+b
.
Да, именно так, используя двойные круглые скобки (это не опечатка).
Например:
сумма(1)(2) = 3 сумма(5)(-1) = 4
Чтобы вторые скобки работали, первые должны возвращать функцию.
Так:
функция сумма(а) { возвращаемая функция(б) { вернуть а + б; // берет "a" из внешнего лексического окружения }; } предупреждение(сумма(1)(2)); // 3 предупреждение(сумма(5)(-1)); // 4
важность: 4
Каков будет результат этого кода?
пусть х = 1; функция func() { консоль.журнал(х); // ? пусть х = 2; } функция();
PS В этой задаче есть подводный камень. Решение не очевидно.
Результат: ошибка .
Попробуйте запустить его:
пусть х = 1; функция func() { консоль.журнал(х); // Ошибка ссылки: невозможно получить доступ к 'x' перед инициализацией пусть х = 2; } функция();
В этом примере мы можем наблюдать своеобразную разницу между «несуществующей» и «неинициализированной» переменной.
Как вы могли прочитать в статье Область видимости, замыкание, переменная запускается в «неинициализированном» состоянии с момента, когда исполнение входит в блок кода (или функцию). И он остается неинициализированным до соответствующего оператора let
.
Другими словами, технически переменная существует, но ее нельзя использовать до let
.
Код выше демонстрирует это.
функция func() { // локальная переменная x известна движку с самого начала функции, // но "неинициализирован" (непригоден для использования) до тех пор, пока не будет выпущен ("мертвая зона") // отсюда и ошибка консоль.журнал(х); // Ошибка ссылки: невозможно получить доступ к 'x' перед инициализацией пусть х = 2; }
Эту зону временной непригодности переменной (от начала блока кода до let
) иногда называют «мертвой зоной».
важность: 5
У нас есть встроенный метод arr.filter(f)
для массивов. Он фильтрует все элементы через функцию f
. Если он возвращает true
, то этот элемент возвращается в результирующий массив.
Сделайте набор «готовых к использованию» фильтров:
inBetween(a, b)
– между a
и b
или равным им (включительно).
inArray([...])
– в заданном массиве.
Использование должно быть таким:
arr.filter(inBetween(3,6))
– выбирает только значения от 3 до 6.
arr.filter(inArray([1,2,3]))
– выбирает только элементы, соответствующие одному из членов [1,2,3]
.
Например:
/* .. ваш код для inBetween и inArray */ пусть arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6 alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
Откройте песочницу с тестами.
функция inBetween(a, b) { возвращаемая функция (х) { return x >= a && x <= b; }; } пусть arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6
функция inArray(arr) { возвращаемая функция (х) { вернуть arr.includes(x); }; } пусть arr = [1, 2, 3, 4, 5, 6, 7]; alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
Откройте решение с тестами в песочнице.
важность: 5
У нас есть массив объектов для сортировки:
пусть пользователи = [ { имя: «Джон», возраст: 20, фамилия: «Джонсон» }, { имя: «Пит», возраст: 18, фамилия: «Петерсон» }, { имя: «Энн», возраст: 19, фамилия: «Хэтэуэй» } ];
Обычный способ сделать это:
// по имени (Энн, Джон, Пит) users.sort((a, b) => a.name > b.name? 1: -1); // по возрасту (Пит, Энн, Джон) users.sort((a, b) => a.age > b.age ? 1: -1);
Можем ли мы сделать это еще менее многословным, вот так?
users.sort(byField('имя')); users.sort(byField('возраст'));
Итак, вместо того, чтобы писать функцию, просто введите byField(fieldName)
.
Напишите функцию byField
, которую можно использовать для этого.
Откройте песочницу с тестами.
функция byField(fieldName){ return (a, b) => a[имя поля] > b[имя поля]? 1:-1; }
Откройте решение с тестами в песочнице.
важность: 5
Следующий код создает массив shooters
.
Каждая функция предназначена для вывода своего номера. Но что-то не так…
функция makeArmy() { пусть стрелки = []; пусть я = 0; в то время как (я < 10) { let Shooter = function() { // создаем функцию-стрелок, предупреждение(я); // это должно показать его номер }; Shooters.push(стрелок); // и добавляем его в массив я++; } // ...и возвращаем массив стрелков ответные стрелки; } пусть армия = makeArmy(); // все стрелки показывают 10 вместо цифр 0, 1, 2, 3... армия[0](); // 10 от стрелка номер 0 армия[1](); // 10 от стрелка №1 армия[2](); // 10 ... и так далее.
Почему все стрелки показывают одинаковую ценность?
Исправьте код, чтобы он работал как задумано.
Откройте песочницу с тестами.
Давайте разберем, что именно происходит внутри makeArmy
, и решение станет очевидным.
Он создает пустой массив shooters
:
пусть стрелки = [];
Заполняет его функциями через shooters.push(function)
в цикле.
Каждый элемент является функцией, поэтому результирующий массив выглядит следующим образом:
стрелки = [ функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); }, функция () {предупреждение (я); } ];
Массив возвращается из функции.
Затем, позже, вызов любого члена, например, army[5]()
, получит элемент army[5]
из массива (который является функцией) и вызовет его.
Почему же все такие функции показывают одно и то же значение 10
?
Это потому, что внутри функций shooter
нет локальной переменной i
. Когда такая функция вызывается, она берет i
из своего внешнего лексического окружения.
Тогда каково будет значение i
?
Если мы посмотрим на источник:
функция makeArmy() { ... пусть я = 0; в то время как (я < 10) { let Shooter = function() { // функция стрелка предупреждение(я); // должен показать его номер }; Shooters.push(стрелок); // добавляем функцию в массив я++; } ... }
Мы видим, что все функции shooter
создаются в лексическом окружении функции makeArmy()
. Но когда вызывается army[5]()
, makeArmy
уже завершила свою работу, и окончательное значение i
равно 10
( while
останавливается на i=10
).
В результате все функции- shooter
получают одно и то же значение из внешнего лексического окружения, то есть последнее значение i=10
.
Как вы можете видеть выше, на каждой итерации блока while {...}
создается новое лексическое окружение. Итак, чтобы исправить это, мы можем скопировать значение i
в переменную внутри блока while {...}
, например:
функция makeArmy() { пусть стрелки = []; пусть я = 0; в то время как (я < 10) { пусть j = я; let Shooter = function() { // функция стрелка оповещение (Дж); // должен показать его номер }; Shooters.push(стрелок); я++; } ответные стрелки; } пусть армия = makeArmy(); // Теперь код работает корректно армия[0](); // 0 армия[5](); // 5
Здесь let j = i
объявляет «локальную для итерации» переменную j
и копирует в нее i
. Примитивы копируются «по значению», поэтому мы фактически получаем независимую копию i
, принадлежащую текущей итерации цикла.
Стрелки работают правильно, потому что значение i
теперь немного ближе. Не в лексическом окружении makeArmy()
, а в лексическом окружении, соответствующем текущей итерации цикла:
Такую проблему также можно было бы избежать, если бы мы использовали for
вначале, например:
функция makeArmy() { пусть стрелки = []; for(пусть я = 0; я <10; я++) { let Shooter = function() { // функция стрелка предупреждение(я); // должен показать его номер }; Shooters.push(стрелок); } ответные стрелки; } пусть армия = makeArmy(); армия[0](); // 0 армия[5](); // 5
По сути, это то же самое, потому что for
на каждой итерации генерирует новую лексическую среду со своей собственной переменной i
. Таким образом, shooter
сгенерированный на каждой итерации, ссылается на свой собственный i
из этой самой итерации.
Теперь, когда вы приложили столько усилий, чтобы прочитать это, а окончательный рецепт настолько прост – просто используйте for
, вы можете задаться вопросом – стоило ли оно того?
Что ж, если бы вы могли легко ответить на вопрос, вы бы не читали решение. Итак, надеемся, что это задание помогло вам лучше понять ситуацию.
Кроме того, действительно бывают случаи, когда предпочитают while
вместо for
и другие сценарии, когда такие проблемы реальны.
Откройте решение с тестами в песочнице.