ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Categories:

Учебник по JavaScript: ч.1, замыкания и циклы

Начало в предыдущем посте: Учебник по JavaScript: ч.1, параметры и замыкания.

Я думал, что понял взаимодействие замыканий и объектов-лексических окружений в языке JavaScript. Однако, при решении задачи «Армия функций» в подразделе 6.3 «Замыкание» оказалось, что понял не до конца и местами неправильно.

В указанной задаче дан следующий кусок кода:

function makeArmy() {
    let shooters = [];

    let i = 0;
    while (i < 10) {
        let shooter = function() { // функция shooter должна выводить
            alert( i );            // порядковый номер стрелка
        };
        shooters.push(shooter);
        i++;
    }

    return shooters;
}

let army = makeArmy();

army[0](); // выводит 10, а должна выводить 0
army[5](); // выводит 10, а должна выводить 5
// ... у всех стрелков будет номер 10 вместо 0, 1, 2, 3...

Во-первых, требуется объяснить, почему код не работает так, как планировалось. Во-вторых, нужно исправить этот код, чтобы он начал работать так, как планировалось.

Минут за десять я интуитивно сообразил, как исправить код (и мой вариант решения совпал со вторым из двух вариантов решения, предложенных авторами учебника). Однако, я не мог самому себе четко объяснить, почему исправленный код начал работать. Я решил перечитать текст подраздела в четвертый раз и только тогда картинка сложилась. Как оказалось, в первые три прочтения я пропустил неосмысленным следующий текст, цитата:

Для цикла у каждой итерации своё отдельное лексическое окружение.


Думаю, эту фразу авторам учебника стоит выделить жирным.

Как работает обычный цикл (не содержащий вложенных функций) с объектами-лексическими окружениями?

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

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

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

Как работает цикл, содержащий вложенную функцию, из приведенного выше кода?

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

Однако, из-за того, что ссылка на объект-лексическое окружение каждой итерации записывается в скрытое свойство Environment вложенной в цикл функции, после окончания каждой итерации ее объект-лексическое окружение остается достижимым из программы и поэтому не унитожается сборщиком мусора.

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

Итак, функция makeArmy после своего запуска возвращает в переменную army массив из десяти функций shooter (по-русски «стрелок»), каждая из которых имеет в своем скрытом свойстве Environment ссылку на объект-лексическое окружение соответствующей итерации цикла в функции makeArmy. У меня получилась такая иллюстрация:


Как видно на рисунке, после окончания работы функции makeArmy в памяти будет десять объектов-лексических окружений соответствующих итераций цикла, один объект-лексическое окружение функции makeArmy (потому что эта функция запускалась только один раз) и один объект-глобальное лексическое окружение. (Я не стал рисовать все десять элементов массива army и все десять объектов-лексических окружений итераций цикла. Нарисовал только несколько первых и последние элементы массива и объекты-лексические окружения итераций цикла. Кроме этого, я не стал приписывать какое-то определенное значение переменной i, ограничившись вместо этого многоточием. Перед запуском цикла там хранится значение 0, затем другие значения до 10 включительно.)

Из иллюстрации можно понять, наконец, почему вышеприведенный код не работает, как планировалось. Так как переменная i объявлена вне цикла, то ее значение хранится в объекте-лексическом окружении, предназначенном для локальных переменных функции makeArmy. При запуске функций shooter из массива army каждая функция ищет значение переменной i в цепочке объектов-лексических окружений и находит ее только в лексическом окружении, в котором хранятся локальные переменные функции makeArmy. А так как к моменту вызова любой функции shooter из массива цикл уже закончился (работа функции makeArmy, в которой содержится цикл, ведь, уже закончилась), то переменная i содержит значение 10. Это значение и будет выдаваться при запуске любой из функций shooter, хранящихся в массиве army.

Чтобы заставить код работать так, как планировалось, я добавил внутрь цикла локальную для цикла переменную j и присвоил ей значение внешней для цикла переменной i. Кроме этого, я заменил в тексте функции shooter переменную i на переменную j (исправления отмечены красным шрифтом):

function makeArmy() {
    let shooters = [];

    let i = 0;
    while (i < 10) {
        let j = i;
        let shooter = function() { // функция shooter должна выводить
            alert( j );            // порядковый номер стрелка
        };
        shooters.push(shooter);
        i++;
    }

    return shooters;
}

let army = makeArmy();

army[0](); // выводит 0
army[5](); // выводит 5

Вот как теперь будет выглядеть структура объектов-лексических окружений:


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

function makeArmy() {
    let shooters = [];

    for(let i = 0; i < 10; i++) {
        let shooter = function() { // функция shooter должна выводить
            alert( i );            // порядковый номер стрелка
        };
        shooters.push(shooter);
    }

    return shooters;
}

let army = makeArmy();

army[0](); // выводит 0
army[5](); // выводит 5

То есть переменная i переместилась внутрь цикла и стала локальной для цикла.

В этом случае структура объектов-лексических окружений будет выглядеть так:

Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments