Перехват переменной jQuery в цикле - PullRequest
2 голосов
/ 20 ноября 2011

Мое понимание JS и jQuery довольно ограничено, и я пришел из C # фона. Но я знаю, что такое захват переменных, и я знаю, что если я захвачу переменную внутри цикла, которая была объявлена ​​вне цикла, каждый раз, когда выполняется делегат, я получу последнее значение захваченной переменной, а не время захвата. Тем не менее, это явно не проблема здесь, все же Я все еще получаю последнее значение :

for (var i = 0; i < dialogs.length; i++) {

    var dialog_button = dialogs[i];

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
     $(dialog_button).click(function (event) {
            $(dialog_panel).dialog('open');
            return false;
        });

}

Так как dialog_button был объявлен внутри области видимости цикла, я ожидал, что получу правильное значение в обработчике кликов.
JS делает что-то другое?

Ответы [ 2 ]

9 голосов
/ 20 ноября 2011

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

Лучшая ставка в описываемой вами конкретной ситуации - использовать функцию $.each в jQuery, а не for loop (спасибо @Esailija и обратите внимание, что Tadeck предложил $.each до того, как я сделал - стоит повышенного голоса - я добавил его по предложению @ Esailija, потому что это, безусловно, лучшее решение в данной конкретной ситуации) :

$.each(dialogs, function(index, dialog_button) {

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    $(dialog_button).click(function (event) {
         $(dialog_panel).dialog('open');
         return false;
    });
});

... потому что теперь каждый вызов функции, которую мы передаем $.each, получает собственный уникальный аргумент dialog_button, поэтому каждая сгенерированная функция (замыкание) закрывается над своей собственной копией, и мы не сталкиваемся с проблема новых значений, присваиваемых переменной.

Я предложил функцию jQuery, потому что вы уже используете jQuery, и поэтому она наверняка доступна. Начиная с ECMAScript5, есть встроенная функция Array#forEach, которая делает то же самое, но пока не у всех двигателей.

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

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

for (var i = 0; i < dialogs.length; i++) {

    var dialog_button = dialogs[i];

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    $(dialog_button).click(createHandler(dialog_button));
}

function createHandler(dlg) {
    return function (event) {
        $(dlg).dialog('open');
        return false;
    };
}

Там цикл вызывает createHandler, что создает функцию-обработчик как замыкание в контексте вызова createHandler, поэтому обработчик ссылается на dlg. Каждый вызов createHandler получит свой уникальный контекст и, следовательно, собственный уникальный аргумент dlg. Таким образом, замыкания относятся к ожидаемым значениям.

Вы можете расположить createHandler внутри вашей общей функции, если хотите (убедитесь, что она не находится внутри каких-либо ветвей, она должна быть на верхнем уровне функции), например:

function createDialogs() {
    for (var i = 0; i < dialogs.length; i++) {

        var dialog_button = dialogs[i];

        var ix_parts = $(dialog_button).attr("id").split("_");
        var index_tag = ix_parts[1];
        var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
        $(dialog_button).click(createHandler(dialog_button));
    }

    function createHandler(dlg) {
        return function (event) {
            $(dlg).dialog('open');
            return false;
        };
    }
}

... или если вам нужно сделать то же самое в другом месте, вы можете переместить его на уровень:

function createDialogs() {
    for (var i = 0; i < dialogs.length; i++) {

        var dialog_button = dialogs[i];

        var ix_parts = $(dialog_button).attr("id").split("_");
        var index_tag = ix_parts[1];
        var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
        $(dialog_button).click(createHandler(dialog_button));
    }
}

function createHandler(dlg) {
    return function (event) {
        $(dlg).dialog('open');
        return false;
    };
}

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

Подробнее:


В комментариях вы спрашивали:

"(убедитесь, что он не находится внутри каких-либо веток, он должен находиться на верхнем уровне функции)" Не могли бы вы уточнить это? Я объявил функцию внутри цикла for, и она, похоже, работает! Я что-то не так делаю?

JavaScript имеет две различные конструкции function: функция объявления и функция выражения . Синтаксически они очень похожи, но они допустимы в разных местах и ​​происходят в разное время.

TL; DR: My createHandler является примером функции объявление . Они не могут быть внутри контрольных структур. Ваша function конструкция, которую вы передаете в click, является функцией expression , которая может быть. Разница в том, является ли конструкция function значением для правой руки или нет (у меня нет, у вас есть). Правильное понимание объявлений и выражений является ключом к умелому программированию на JavaScript.

Длинная версия:

Вот функция объявление :

function foo() {
}

Вот функция выражение , назначаемое переменной:

var foo = function() {
};

Вот еще одно выражение функции, на этот раз используемое как инициализатор свойства в литерале объекта:

var obj = {
    foo: function() {
         }
};

И еще одно выражение функции, на этот раз передаваемое в функцию в качестве аргумента:

bar(function() {
});

Как видите, различие между ними заключается в том, что объявления функций стоят отдельно, тогда как выражение функции используется как правое значение в содержащем выражении & mdash; в качестве правой части присваивания (=) или инициализатора (:) или переданного в функцию в качестве аргумента.

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

function bar() {

    function foo() {
    }

    return foo;
}

... когда вызывается bar, перед выполнением любого пошагового кода создается функция foo. Только после этого запускается пошаговый код, который в этом случае возвращает ссылку на функцию foo. Следовательно, вышеприведенное в точности эквивалентно этому:

function bar() {

    return foo;

    function foo() {
    }
}

Хотя это выглядит так, как будто foo не должно существовать с оператором return, оно существует.

Поскольку объявления функций выполняются перед пошаговым кодом, они не могут находиться внутри операторов потока управления:

function bar(condition) {
    if (condition) {
        function foo() {    // <== INVALID
            return alert("A");
        }
    }
    else {
        function foo() {    // <== INVALID
            return alert("B");
        }
    }

    return foo;
}
var f = bar(true);
f(); // alerts what?

Вы бы подумали, что в предупреждении будет написано "А", верно? Потому что мы передали true для condition, и поэтому происходит первая ветвь. Но это не то, что происходит вообще. Технически, вышесказанное является синтаксической ошибкой, чистой и простой. Но большинство браузеров не рассматривают его как один (движок Firefox [SpiderMonkey] - единственный, который мне известен). Так что они делают? Это зависит. Большинство механизмов продолжают обрабатывать их как объявления функций, и когда у вас есть два объявления функций для одной и той же функции в одной и той же области видимости, в спецификации говорится, что выигрывает второй. Таким образом, эти двигатели будут предупреждать "B". Но другие механизмы (IE один) переписывают ваш код на лету, превращая эти объявления в выражения, и поэтому эти механизмы будут предупреждать «A». Здесь Там Пока Драконы. Не делай этого. : -)

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

function bar() {
    var foo;

    return foo;

    foo = function() {
    };
}

Здесь bar возвращает undefined, поскольку для оператора return foo равно undefined и, конечно, следующий оператор присваивания никогда не встречается. Точно так же это верно и его поведение четко определено:

function bar(condition) {
    var foo;

    if (condition) {
        foo = function() {
            return alert("A");
        };
    }
    else {
        foo = function() {
            return alert("B");
        };
    }

    return foo;
}
var f = bar(true);
f(); // alerts "A"
f = bar(false);
f(); // alerts "B"

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

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

for (var i = 0; i < dialogs.length; i++) {

    var dialog_button = dialogs[i];

    var ix_parts = $(dialog_button).attr("id").split("_");
    var index_tag = ix_parts[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    $(dialog_button).click((function(dlg) {
        return function (event) {
            $(dlg).dialog('open');
            return false;
        };
    })(dialog_button));
}

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

И последнее замечание: все приведенные выше выражения функций создали анонимные функции (функции без имен). Я не люблю анонимные функции ; назначение имен функций помогает вашим инструментам. Технически законно давать им имена:

var f = function foo() { // <== WARNING, see below
};

... но вы не можете сделать это в дикой природе в настоящее время из-за ошибок в IE до IE9 .IE8 и ниже увидят эту конструкцию дважды, один раз как объявление функции, затем снова как выражение функции.Это действительно создаст два функциональных объекта, которые могут вызвать всевозможные проблемы.: -)

5 голосов
/ 20 ноября 2011

Замыкания и for цикл

Да, он делает что-то другое, что подтверждается следующим (и this jsfiddle ):

var tests = [1,2,3,4,5];

for (var i=0; i<tests.length; i++){
    var test = tests[i];
};

alert(test);

Выше будет оповещать последнее значение из массива tests, который в точности равен 5.Это связано с тем, что цикл for не является замыканием - переменные, определенные в нем, доступны и вне его.

jQuery.each() как возможное решение

Кстати, в jQuery есть вспомогательная функция jQuery.each(), которая поможет вам перебирать объекты и массивы без необходимости в цикле for.И внутри обратного вызова локальные переменные останутся (будут) локальными .

Решение

Итак, в основном, следующие проблемы должны решить ваши проблемы (однако это не проверено, поэтому, пожалуйстапроверьте сначала):

jQuery.each(dialogs, function(ind, dialog){
    var dialog_button = $(dialog);
    var index_tag = dialog_button.attr("id").split("_")[1];
    var dialog_panel = $(dialog_panel_selector.replace("$ix$", index_tag));
    dialog_button.click(function(event){
        event.preventDefault();
        dialog_panel.dialog('open');
    });
});

Помогло ли это?Есть вопросы?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...