Как избежать глобальных переменных в JavaScript?

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

Например, учитывая следующий сценарий, как бы вы не использовали глобальную переменную?

Код JavaScript:

var uploadCount = 0;

window.onload = function() {
    var frm = document.forms[0];

    frm.target = "postMe";
    frm.onsubmit = function() {
        startUpload();
        return false;
    }
}

function startUpload() {
    var fil = document.getElementById("FileUpload" + uploadCount);

    if (!fil || fil.value.length == 0) {
        alert("Finished!");
        document.forms[0].reset();
        return;
    }

    disableAllFileInputs();
    fil.disabled = false;
    alert("Uploading file " + uploadCount);
    document.forms[0].submit();
}

Соответствующая разметка:

<iframe src="test.htm" name="postHere" id="postHere"
  onload="uploadCount++; if(uploadCount > 1) startUpload();"></iframe>

<!-- MUST use inline JavaScript here for onload event
     to fire after each form submission. -->

Этот код взят из веб-формы с несколькими <input type="file">. Он загружает файлы по одному, чтобы предотвратить огромные запросы. Это делается путем POST в iframe, ожидая ответа, который запускает iframe onload, а затем запускает следующую отправку.

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


person Josh Stodola    schedule 03.12.2009    source источник
comment
Использовать выражение немедленно вызываемой функции (IIFE). Подробнее см. Здесь: codearsenal.net/2014/11/   -  person    schedule 27.11.2014


Ответы (11)


Самый простой способ - заключить ваш код в закрытие и вручную предоставить только те переменные, которые вам нужны глобально, в глобальную область видимости:

(function() {
    // Your code here

    // Expose to global
    window['varName'] = varName;
})();

Чтобы ответить на комментарий Crescent Fresh: чтобы полностью удалить глобальные переменные из сценария, разработчику потребуется изменить ряд вещей, предполагаемых в вопросе. Это выглядело бы примерно так:

Javascript:

(function() {
    var addEvent = function(element, type, method) {
        if('addEventListener' in element) {
            element.addEventListener(type, method, false);
        } else if('attachEvent' in element) {
            element.attachEvent('on' + type, method);

        // If addEventListener and attachEvent are both unavailable,
        // use inline events. This should never happen.
        } else if('on' + type in element) {
            // If a previous inline event exists, preserve it. This isn't
            // tested, it may eat your baby
            var oldMethod = element['on' + type],
                newMethod = function(e) {
                    oldMethod(e);
                    newMethod(e);
                };
        } else {
            element['on' + type] = method;
        }
    },
        uploadCount = 0,
        startUpload = function() {
            var fil = document.getElementById("FileUpload" + uploadCount);

            if(!fil || fil.value.length == 0) {    
                alert("Finished!");
                document.forms[0].reset();
                return;
            }

            disableAllFileInputs();
            fil.disabled = false;
            alert("Uploading file " + uploadCount);
            document.forms[0].submit();
        };

    addEvent(window, 'load', function() {
        var frm = document.forms[0];

        frm.target = "postMe";
        addEvent(frm, 'submit', function() {
            startUpload();
            return false;
        });
    });

    var iframe = document.getElementById('postHere');
    addEvent(iframe, 'load', function() {
        uploadCount++;
        if(uploadCount > 1) {
            startUpload();
        }
    });

})();

HTML:

<iframe src="test.htm" name="postHere" id="postHere"></iframe>

Вам не нужен встроенный обработчик событий на <iframe>, он все равно будет запускаться при каждой загрузке с этим кодом.

Что касается события загрузки

Вот тестовый пример, демонстрирующий, что вам не нужно встроенное событие onload. Это зависит от ссылки на файл (/emptypage.php) на том же сервере, иначе вы сможете просто вставить его на страницу и запустить.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>untitled</title>
</head>
<body>
    <script type="text/javascript" charset="utf-8">
        (function() {
            var addEvent = function(element, type, method) {
                if('addEventListener' in element) {
                    element.addEventListener(type, method, false);
                } else if('attachEvent' in element) {
                    element.attachEvent('on' + type, method);

                    // If addEventListener and attachEvent are both unavailable,
                    // use inline events. This should never happen.
                } else if('on' + type in element) {
                    // If a previous inline event exists, preserve it. This isn't
                    // tested, it may eat your baby
                    var oldMethod = element['on' + type],
                    newMethod = function(e) {
                        oldMethod(e);
                        newMethod(e);
                    };
                } else {
                    element['on' + type] = method;
                }
            };

            // Work around IE 6/7 bug where form submission targets
            // a new window instead of the iframe. SO suggestion here:
            // http://stackoverflow.com/q/875650
            var iframe;
            try {
                iframe = document.createElement('<iframe name="postHere">');
            } catch (e) {
                iframe = document.createElement('iframe');
                iframe.name = 'postHere';
            }

            iframe.name = 'postHere';
            iframe.id = 'postHere';
            iframe.src = '/emptypage.php';
            addEvent(iframe, 'load', function() {
                alert('iframe load');
            });

            document.body.appendChild(iframe);

            var form = document.createElement('form');
            form.target = 'postHere';
            form.action = '/emptypage.php';
            var submit = document.createElement('input');
            submit.type = 'submit';
            submit.value = 'Submit';

            form.appendChild(submit);

            document.body.appendChild(form);
        })();
    </script>
</body>
</html>

Предупреждение срабатывает каждый раз, когда я нажимаю кнопку отправки в Safari, Firefox, IE 6, 7 и 8.

person eyelidlessness    schedule 03.12.2009
comment
Или предоставьте какой-нибудь аксессуар. Я согласен. - person Upperstage; 03.12.2009
comment
Когда люди голосуют против, это полезно для объяснения причин, по которым они проголосовали против. - person eyelidlessness; 03.12.2009
comment
Я не голосовал против. Однако сказать window ['varName'] = varName - это то же самое, что сделать глобальное объявление var за пределами закрытия. var foo = "bar"; (function() { alert(window['foo']) })(); - person Josh Stodola; 03.12.2009
comment
Вы ответили на заголовок, а не на вопрос. Мне это не понравилось. Было бы лучше поместить идиому закрытия в контекст ссылок из встроенного обработчика событий (поскольку доходит до сути вопроса). - person Crescent Fresh; 03.12.2009
comment
Джош Стодола, верно. Это потому, что ваш встроенный обработчик событий вызывает глобальную функцию. При закрытии вы должны объявить, какие переменные (или функции) являются глобальными, а какие нет. Это дает вам полный контроль над тем, что (если что) сделать глобальным. - person eyelidlessness; 03.12.2009
comment
Crescent Fresh, я ответил на вопрос. Предположения вопроса необходимо будет переработать, чтобы избежать глобальных функций. В этом ответе в качестве ориентира используются предположения вопроса (например, встроенный обработчик событий) и он позволяет разработчику выбирать только необходимые глобальные точки доступа, а не все, что находится в глобальной области. - person eyelidlessness; 03.12.2009
comment
@eyelidlessness: Черт возьми, хорошее редактирование, +1. @Josh упомянул в комментарии к другому вопросу: в этой ситуации требуется встроенный обработчик. Когда форма отправляется в iframe, ваш обработчик загрузки, установленный программно, не срабатывает. Может кто-нибудь подтвердить, iframe.onload = ... не эквивалентно <iframe onload="..."? - person Crescent Fresh; 03.12.2009
comment
Я тоже ненавижу голосование слишком старых болельщиков. Вчера я потратил довольно много времени, назначая onload для ifframe из кода программной части, и он просто не работал после начальной загрузки. Как только он отобразил ответ от первой загрузки, все. Я также обнаружил, что невозможно заставить форму правильно работать с внедрением Iframe в DOM. Это должно быть жестко запрограммировано в разметке. - person Josh Stodola; 03.12.2009
comment
Постой, я создам быстрый тестовый пример для события load. Я действительно не думаю, что он должен вести себя так, как вы описываете. - person eyelidlessness; 03.12.2009
comment
Я провел некоторое тестирование, и да, вы можете назначить событие загрузки программно, используя функцию addEvent. И я знаю, что делал вчера не так. Это действительно стыдно. Я делал $(frames["postMe"]).load(), поэтому я был объектом кадра, а не элементом! - person Josh Stodola; 04.12.2009
comment
Думаю, я просто немного опоздал со своим тестовым примером! Ну да ладно, это все равно есть для справки в будущем. - person eyelidlessness; 04.12.2009
comment
Я чувствую себя таким идиотом. Я знаю, что вчера я попробовал простой getElementById (postMe) .onload = function () {}, но я предположил, что это не удалось, потому что я забыл удалить логику, необходимую для обхода самой начальной загрузки страницы (родительской), которая казалась чтобы всегда запускать встроенную загрузку. Спасибо за помощь, сегодня я многому научился! - person Josh Stodola; 04.12.2009
comment
Итак, я наконец могу проголосовать за, Cripes. @eyelidlessness: отличная работа. - person Crescent Fresh; 04.12.2009
comment
Однажды я видел что-то подобное (function(window, document) { ... })(window, document);. В чем причина этого? - person Iulian Onofrei; 04.04.2014

Я предлагаю шаблон модуля.

YAHOO.myProject.myModule = function () {

    //"private" variables:
    var myPrivateVar = "I can be accessed only from within YAHOO.myProject.myModule.";

    //"private" method:
    var myPrivateMethod = function () {
        YAHOO.log("I can be accessed only from within YAHOO.myProject.myModule");
    }

    return  {
        myPublicProperty: "I'm accessible as YAHOO.myProject.myModule.myPublicProperty."
        myPublicMethod: function () {
            YAHOO.log("I'm accessible as YAHOO.myProject.myModule.myPublicMethod.");

            //Within myProject, I can access "private" vars and methods:
            YAHOO.log(myPrivateVar);
            YAHOO.log(myPrivateMethod());

            //The native scope of myPublicMethod is myProject; we can
            //access public members using "this":
            YAHOO.log(this.myPublicProperty);
        }
    };

}(); // the parens here cause the anonymous function to execute and return
person erenon    schedule 03.12.2009
comment
Я поставлю +1, потому что я понимаю это, и это очень полезно, но мне все еще неясно, насколько это будет эффективно в ситуации, когда я использую только одну глобальную переменную. Поправьте меня, если я ошибаюсь, но выполнение этой функции и ее возврат приводит к тому, что возвращаемый объект сохраняется в YAHOO.myProject.myModule, который является глобальной переменной. Правильно? - person Josh Stodola; 03.12.2009
comment
@Josh: глобальная переменная - это не зло. Глобальная переменная_S_ зла. Держите количество глобальных объектов как можно меньше. - person erenon; 03.12.2009
comment
Вся анонимная функция будет выполняться каждый раз, когда вы захотите получить доступ к одному из общедоступных свойств / методов «модулей», верно? - person UpTheCreek; 05.09.2011
comment
@UpTheCreek: нет, не будет. Он выполняется только один раз, когда программа встречает закрытие () в последней строке, и возвращаемый объект будет назначен свойству myModule с закрытием, содержащим myPrivateVar и myPrivateMethod. - person erenon; 05.09.2011
comment
Красиво, именно то, что я тоже искал. Это позволяет мне отделить логику моей страницы от обработки событий jquery. Вопрос, при XSS-атаке злоумышленник все равно будет иметь доступ к YAHOO.myProject.myModule правильно? Не лучше ли оставить внешнюю функцию без имени и поставить (); в конце, вокруг $ (document) .ready? Возможно изменение метаобъекта свойств YAHOO.myProct.myModule? Я только что потратил немного времени на теорию js и сейчас пытаюсь совместить ее. - person Dale; 03.08.2012
comment
@Dale: Нет смысла предотвращать успешную XSS-атаку таким способом. JavaScript на стороне клиента небезопасен, точка. - person erenon; 04.08.2012
comment
Неработающая ссылка на шаблон модуля? - person Olle Härstedt; 20.10.2020
comment
@ OlleHärstedt: да. клон можно найти здесь: yuuuuc.me/module_pattern - person erenon; 21.10.2020

Во-первых, невозможно избежать глобального JavaScript, что-то всегда будет болтаться в глобальной области видимости. Даже если вы создадите пространство имен, что по-прежнему является хорошей идеей, это пространство имен будет глобальным.

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

Закрытие

var startUpload = (function() {
  var uploadCount = 1;  // <----
  return function() {
    var fil = document.getElementById("FileUpload" + uploadCount++);  // <----

    if(!fil || fil.value.length == 0) {    
      alert("Finished!");
      document.forms[0].reset();
      uploadCount = 1; // <----
      return;
    }

    disableAllFileInputs();
    fil.disabled = false;
    alert("Uploading file " + uploadCount);
    document.forms[0].submit();
  };
})();

* Обратите внимание, что увеличение uploadCount здесь происходит внутри

Свойство функции

var startUpload = function() {
  startUpload.uploadCount = startUpload.count || 1; // <----
  var fil = document.getElementById("FileUpload" + startUpload.count++);

  if(!fil || fil.value.length == 0) {    
    alert("Finished!");
    document.forms[0].reset();
    startUpload.count = 1; // <----
    return;
  }

  disableAllFileInputs();
  fil.disabled = false;
  alert("Uploading file " + startUpload.count);
  document.forms[0].submit();
};

Я не уверен, зачем uploadCount++; if(uploadCount > 1) ... необходим, так как похоже, что условие всегда будет истинным. Но если вам действительно нужен глобальный доступ к переменной, то метод function property, который я описал выше, позволит вам сделать это без фактического глобального использования переменной.

<iframe src="test.htm" name="postHere" id="postHere"
  onload="startUpload.count++; if (startUpload.count > 1) startUpload();"></iframe>

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

person Justin Johnson    schedule 03.12.2009
comment
Вы абсолютно можете избежать глобального охвата. В вашем примере «Закрытие» просто удалите «var startUpload =» с самого начала, и эта функция будет полностью закрытой и недоступной на глобальном уровне. На практике многие люди предпочитают предоставлять одну переменную, в которой содержатся ссылки на все остальное. - person derrylwc; 26.06.2013
comment
@derrylwc В данном случае startUpload - это та единственная переменная, на которую вы ссылаетесь. Удаление var startUpload = из примера закрытия означает, что внутренняя функция никогда не сможет быть выполнена, потому что на нее нет ссылки. Проблема предотвращения загрязнения глобальной области видимости связана с внутренней переменной счетчика uploadCount, которая используется startUpload. Более того, я думаю, что OP пытается избежать загрязнения любой области за пределами этого метода внутренней переменной uploadCount. - person Justin Johnson; 18.07.2013
comment
Если код в анонимном закрытии добавляет загрузчика в качестве прослушивателя событий, конечно, он сможет выполняться всякий раз, когда происходит соответствующее событие. - person Damian Yerrick; 24.05.2018

Иногда имеет смысл иметь глобальные переменные в JavaScript. Но не оставляйте их вот так свисать прямо с окна.

Вместо этого создайте единый объект «пространство имен», содержащий ваши глобальные объекты. Чтобы получить бонусные баллы, положите туда все, включая свои методы.

person Nosredna    schedule 03.12.2009
comment
Как мне это сделать? создать единый объект пространства имен, содержащий мои глобальные объекты? - person p.matsinopoulos; 17.08.2013

Некоторые вещи будут находиться в глобальном пространстве имен, а именно, любая функция, которую вы вызываете из своего встроенного кода JavaScript.

В общем, решение состоит в том, чтобы завернуть все в укупорку:

(function() {
    var uploadCount = 0;
    function startupload() {  ...  }
    document.getElementById('postHere').onload = function() {
        uploadCount ++;
        if (uploadCount > 1) startUpload();
    };
})();

и избегайте встроенного обработчика.

person Jimmy    schedule 03.12.2009
comment
В этой ситуации требуется встроенный обработчик. Когда форма отправляется в iframe, ваш обработчик загрузки, установленный программно, не срабатывает. - person Josh Stodola; 03.12.2009
comment
@ Джош: правда? iframe.onload = ... не эквивалентно <iframe onload="..."? - person Crescent Fresh; 03.12.2009

Другой способ сделать это - создать объект, а затем добавить к нему методы.

var object = {
  a = 21,
  b = 51
};

object.displayA = function() {
 console.log(object.a);
};

object.displayB = function() {
 console.log(object.b);
};

Таким образом, доступен только объект obj и прикрепленные к нему методы. Это эквивалентно добавлению его в пространство имен.

person Ajay Poshak    schedule 06.10.2017

Замыкания подходят для малых и средних проектов. Однако для больших проектов вы можете разделить код на модули и сохранить их в разных файлах.

Поэтому я написал jQuery Secret plugin решить проблему.

В вашем случае с этим плагином код будет выглядеть примерно так.

JavaScript:

// Initialize uploadCount.
$.secret( 'in', 'uploadCount', 0 ).

// Store function disableAllFileInputs.
secret( 'in', 'disableAllFileInputs', function(){
  // Code for 'disable all file inputs' goes here.

// Store function startUpload
}).secret( 'in', 'startUpload', function(){
    // 'this' points to the private object in $.secret
    // where stores all the variables and functions
    // ex. uploadCount, disableAllFileInputs, startUpload.

    var fil = document.getElementById( 'FileUpload' + uploadCount);

    if(!fil || fil.value.length == 0) {
        alert( 'Finished!' );
        document.forms[0].reset();
        return;
    }

    // Use the stored disableAllFileInputs function
    // or you can use $.secret( 'call', 'disableAllFileInputs' );
    // it's the same thing.
    this.disableAllFileInputs();
    fil.disabled = false;

    // this.uploadCount is equal to $.secret( 'out', 'uploadCount' );
    alert( 'Uploading file ' + this.uploadCount );
    document.forms[0].submit();

// Store function iframeOnload
}).secret( 'in', 'iframeOnload', function(){
    this.uploadCount++;
    if( this.uploadCount > 1 ) this.startUpload();
});

window.onload = function() {
    var frm = document.forms[0];

    frm.target = "postMe";
    frm.onsubmit = function() {
        // Call out startUpload function onsubmit
        $.secret( 'call', 'startUpload' );
        return false;
    }
}

Соответствующая разметка:

<iframe src="test.htm" name="postHere" id="postHere" onload="$.secret( 'call', 'iframeOnload' );"></iframe>

Откройте свой Firebug, вы вообще не найдете глобальных объектов, даже функции :)

Полную документацию см. здесь.

Для демонстрационной страницы см. это.

Исходный код на GitHub.

person ben    schedule 06.04.2011

Я использую это так:

{
    var globalA = 100;
    var globalB = 200;
    var globalFunc = function() { ... }

    let localA = 10;
    let localB = 20;
    let localFunc = function() { ... }

    localFunc();
}

Для всех глобальных областей используйте var, а для локальных областей - let.

person Ashwin    schedule 31.08.2020

Используйте закрытия. Что-то вроде этого дает вам область, отличную от глобальной.

(function() {
    // Your code here
    var var1;
    function f1() {
        if(var1){...}
    }

    window.var_name = something; //<- if you have to have global var
    window.glob_func = function(){...} //<- ...or global function
})();
person NilColor    schedule 03.12.2009
comment
В прошлом мой подход заключался в определении конкретного объекта глобальных переменных и присоединении к нему всех глобальных переменных. Как бы мне обернуть это закрытием? К сожалению, ограничение на поле для комментариев не позволяет мне встроить типичный код. - person David Edwards; 18.09.2017

Для "защиты" индивидуальных глобальных переменных:

function gInitUploadCount() {
    var uploadCount = 0;

    gGetUploadCount = function () {
        return uploadCount; 
    }
    gAddUploadCount= function () {
        uploadCount +=1;
    } 
}

gInitUploadCount();
gAddUploadCount();

console.log("Upload counter = "+gGetUploadCount());

Я новичок в JS, в настоящее время использую это в одном проекте. (ценю любые комментарии и критику)

person Koenees    schedule 14.09.2016

person    schedule
comment
Как увеличить значение uploadCount в моем обработчике onload в iframe? Это очень важно. - person Josh Stodola; 03.12.2009
comment
Хорошо, я вижу, что вы здесь сделали. К сожалению, это не то же самое. Это запускает все загрузки по отдельности, но в одно и то же время (технически между ними 100 мс). Текущее решение загружает их последовательно, то есть вторая загрузка не начинается, пока первая загрузка не будет завершена. Вот почему требуется встроенный обработчик onload. Программно назначить обработчик не получится, потому что он срабатывает только в первый раз. Встроенный обработчик срабатывает каждый раз (по любой причине). - person Josh Stodola; 03.12.2009
comment
Я по-прежнему собираюсь использовать +1, потому что вижу, что это эффективный метод сокрытия глобальной переменной. - person Josh Stodola; 03.12.2009
comment
Вы можете создать iframe для каждой загрузки, чтобы поддерживать отдельные обратные вызовы. - person Justin Johnson; 03.12.2009
comment
Я думаю, что это, в конечном счете, отличный ответ, только слегка затуманенный требованиями конкретного примера. По сути, идея состоит в том, чтобы создать шаблон (объект) и использовать «новый» для его создания. Это, вероятно, лучший ответ, потому что он действительно избегает глобальной переменной, то есть без компромиссов - person PandaWood; 09.05.2011