Области видимости переменных в JavaScript и их подводные камни

В этом посте мы разберемся с областями видимости в JavaScript. Обратим внимание на две вещи, которые могут приводить к проблемам, если вы о них не позаботитесь: область видимости переменных в JavaScript - функции, и они могут быть захвачены в замыкания.

Область видимости переменных определяет, где эта переменная доступна. Например, если переменная определена в начале функции, то она доступная внутри этой функции, но не снаружи, и обычно "умирает" с завершением функции. В этом случае, область видимости переменной - функция. Когда поток управления доходит до области видимости, создается новое окружение, которое отображает имена переменных на их значения. Области видимости могут быть вложенными. Переменная доступна в своей области видимости и во всех вложенных областях видимости.

Ловушка 1: Область видимости переменных функций

Большинство популярных языков имеют блочную область видимости: новое окружение создается при входе в блок и области видимости составлены из вложенных блоков. В отличии от JavaScript, где переменные имеют область видимости в функции, новое окружение создается только при входе в функцию и составлено из вложенных функций. Это значит, что даже если вы определите переменную внутри блока "тогда" оператора if, то она будет доступна везде в окружающей функции. Код ниже демонстрирует это:

var myvar = "global";
function f() {
    print(myvar); // (*)
    if (true) {
        var myvar = "local"; // (**)
    }
    print(myvar);
}
> f();
undefined
local
> myvar
global

Как вы видите, даже первое обращение к myvar ссылается на локальную переменную (значение которой еще не присвоено на (*)). Причина в том, что определение var переменной в JavaScript поднимается (hoisting): var myvar = "local" равносильно определению переменной myvar на начале функции f(), а присвоение происходит на строке (**). Поэтому, передовые практики в JavaScript рекомендуют использовать только один var в начале функции.

Ловушка 2: Замыкания

В JavaScript области видимости статические, они определены вложенностью синтаксических конструкций. Для обеспечения статической области видимости, окружение связано с значениями, которые имеют доступ к переменными в окружении. Пример такого значения - возвращенная функция в следующем коде:

function f() {
    var x = "abc";
    return function() {
        return x;
    }
}

Использование:

> var g = f();
> g()
abc

Переменная x доступная в возвращенной функции, и к ней невозможно получить доступ, кроме как в этой функции. Присоединив окружение, можно получить значение x, как мы ожидали, с учетом статической области видимости. Эта пара значения и окружения называется замыкание, потому что переменные замкнуты. Замыкания в JavaScript очень мощны. Вы их можете использовать даже для сохранения свойств объектов, как продемонстрировано в коде ниже:

function objMaker(color) {
    return {
        getColor: function() {
            return color;
        },
        setColor: function(c) {
            color = c;
        }
    };
}

Использование:

> var c = objMaker("blue");
> c.getColor()
blue
> c.setColor("green");
> c.getColor()        
green

Значение цвета сохраняется в окружении, которое было создано при вызове objMaker(). Это окружение присоединяется к возвращаемому значению, поэтому color все еще доступен даже после того, как закончилось исполнение objMaker().

Нечаянный обмен средой

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

function f() {
    var arr = [ "red", "green", "blue" ];
    var result = [];
    for (var i=0; i < arr.length-1; i++) {
        var func = function() {
            return arr[i];
        };
        result.push(func);
    }
    return result;
}

Эта функция возвращает коллекцию из двух функций. Обе функции все еще имеют доступ к окружению f и, как следствие, к arr. По факту, они имеют одинаковые окружения. Но в том окружении i имеет значение 2, и поэтому функции возвращают значение "blue" при вызове:

> f()[0]()
blue

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

Создание нового окружения

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

function f() {
    var arr = [ "red", "green", "blue" ];
    var result = [];
    for(var i=0; i<arr.length-1; i++) {
        // Новое окружение для каждой итерации? Только не в JS!
        var color = arr[i]; // НЕ новая копия в JS!
        var func = function() {
            return color;
        };
        result.push(func);
    }
    return result;
}

JavaScript имеет функциональную область видимости, поэтому color обрабатывается так, если бы был определен в начале функции f(), и мы не получим другого окружения для каждой возвращаемой функции. Только функции могут создавать окружения, поэтому мы используем функцию для симуляции блока. Это выглядит следующим образом:

(function() { // open block
    // inside block
}()); // close block

Так, как мы сразу вызываем функцию, поведение обернутого кода такое же, как и в блока: код выполняется сразу. Зачем нужны круглые скобки вокруг конструкции? Они там для избежания синтаксической ошибки. Если выражение начинается с функции, то ожидается определение функции. Таким образом, без открытия круглых скобок, синтаксический анализатор будет жаловаться на отсутствующее имя. Вас может посетить идея дописать необходимое имя - в таком случае, анализатор будет жаловаться на круглые скобки, которые следуют за функцией. В обоих случаях, функция не будет выполнена. Таким образом открытие круглых скобок необходимо для того, чтоб указать, что далее будет следовать выражение-функция-выражение. Это называется Немедленно Вызываемые Функции (Immediately Invoked Function Expression aka IIFE). Переписав в пример выше, на псевдокоде с помощью IIFE получим:

function f() {
    var arr = [ "red", "green", "blue" ];
    var result = [];
    for(var i=0; i<arr.length-1; i++) {
        (function() {
            var color = arr[i]; // fresh copy
            var func = function() {
                return color;
            };
            result.push(func);
        }());
    }
    return result;
}

Теперь новое окружение будет создано для каждой итерации в цикле. Теперь получим ожидаемый результат:

> f()[0]()
red

Так же вы можете использовать color как параметр для IIFE:

function f() {
    var arr = ["red", "green", "blue"];
    var result = [ ];

    for (var i = 0; arr.length - 1 > i; i++) {
        (function(color) {
            var func = function() {
                return color;
            };
            result.push(func);
        } (arr[i]));
    }
    return result;
}

Обратите внимание, пример имеет практический смысл, потому что похожи сценарии имеют место при добавлении обработчиков на DOM элементы.


Источник

Добавить комментарий