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

85

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

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

Код 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, а затем запускает следующую отправку.

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

Джош Стодола
источник
3
Использовать выражение немедленно вызываемой функции (IIFE). Подробнее см. Здесь: codearsenal.net/2014/11/…

Ответы:

70

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

(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.

без век
источник
Или предоставьте какой-нибудь аксессуар. Я согласен.
Upperstage
3
Это полезно, когда люди голосуют против, потому что они объясняют, почему они проголосовали против.
век 03
6
Я не голосовал против. Однако сказать window ['varName'] = varName - это то же самое, что сделать глобальное объявление var за пределами закрытия. var foo = "bar"; (function() { alert(window['foo']) })();
Джош Стодола,
Вы ответили на заголовок, а не на вопрос. Мне это не понравилось. Помещение идиомы закрытия в контекст ссылок из встроенного обработчика событий (поскольку суть вопроса уже доходит) было бы лучше.
Crescent Fresh
1
Crescent Fresh, я ответил на вопрос. Предположения вопроса необходимо будет переработать, чтобы избежать глобальных функций. В этом ответе в качестве ориентира используются предположения вопроса (например, встроенный обработчик событий) и он позволяет разработчику выбирать только необходимые глобальные точки доступа, а не все, что находится в глобальной области.
век 03
60

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

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
эренон
источник
3
Я поставлю +1, потому что понимаю это, и это очень полезно, но мне все еще неясно, насколько это будет эффективно в ситуации, когда я использую только одну глобальную переменную. Поправьте меня, если я ошибаюсь, но выполнение этой функции и ее возвращение приводят к тому, что возвращаемый объект сохраняется в YAHOO.myProject.myModule, который является глобальной переменной. Правильно?
Джош Стодола,
11
@Josh: глобальная переменная - это не зло. Глобальная переменная_S_ зла. Сведите количество глобальных объектов как можно меньше.
erenon 03
Вся анонимная функция будет выполняться каждый раз, когда вы захотите получить доступ к одному из общедоступных свойств / методов «модулей», верно?
UpTheCreek
@UpTheCreek: нет, не будет. Он выполняется только один раз, когда программа встречает закрытие () в последней строке, а возвращаемый объект будет назначен свойству myModule с закрытием, содержащим myPrivateVar и myPrivateMethod.
erenon 05
Красиво, именно то, что я тоже искал. Это позволяет мне отделить логику моей страницы от обработки событий jquery. Вопрос: при XSS-атаке злоумышленник все равно будет иметь доступ к YAHOO.myProject.myModule правильно? Не лучше ли оставить внешнюю функцию без имени и поставить (); в конце, вокруг $ (document) .ready? Может быть, модификация мета-объекта свойств YAHOO.myProct.myModule? Я только что потратил немного времени на теорию js и сейчас пытаюсь совместить ее.
Дейл
8

Во-первых, невозможно избежать глобального 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) ...это нужно, так как похоже, что условие всегда будет выполняться. Но если вам действительно нужен глобальный доступ к переменной, то метод свойства функции, описанный выше, позволит вам сделать это без фактического глобального использования переменной.

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

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

Джастин Джонсон
источник
1
Вы абсолютно можете избежать глобального охвата. В вашем примере «Закрытие» просто удалите «var startUpload =» с самого начала, и эта функция будет полностью закрытой, без доступа на глобальном уровне. На практике многие люди предпочитают выставлять одну переменную, в которой содержатся ссылки на все остальное
derrylwc
1
@derrylwc В данном случае startUploadэто та единственная переменная, на которую вы ссылаетесь. Удаление var startUpload = из примера закрытия означает, что внутренняя функция никогда не сможет быть выполнена, потому что на нее нет ссылки. Проблема предотвращения загрязнения глобальной области видимости связана с внутренней переменной счетчика, uploadCountкоторую использует startUpload. Более того, я думаю, что OP пытается избежать загрязнения любой области за пределами этого метода внутренней uploadCountпеременной.
Джастин Джонсон
Если код в анонимном закрытии добавляет загрузчика в качестве прослушивателя событий, конечно, он «сможет быть выполнен» всякий раз, когда произойдет соответствующее событие.
Дамиан Йеррик
6

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

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

Носредна
источник
Как мне это сделать? создать единый объект пространства имен, содержащий мои глобальные объекты?
p.matsinopoulos
5
window.onload = function() {
  var frm = document.forms[0];
  frm.target = "postMe";
  frm.onsubmit = function() {
    frm.onsubmit = null;
    var uploader = new LazyFileUploader();
    uploader.startUpload();
    return false;
  }
}

function LazyFileUploader() {
    var uploadCount = 0;
    var total = 10;
    var prefix = "FileUpload";  
    var upload = function() {
        var fil = document.getElementById(prefix + 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();
        uploadCount++;

        if (uploadCount < total) {
            setTimeout(function() {
                upload();
            }, 100); 
        }
    }

    this.startUpload = function() {
        setTimeout(function() {
            upload();
        }, 100);  
    }       
}
ХаосПандион
источник
Как мне увеличить uploadCount внутри onloadобработчика iframe? Это очень важно.
Джош Стодола,
1
Хорошо, я вижу, что вы здесь сделали. К сожалению, это не то же самое. Это запускает все загрузки по отдельности, но в одно и то же время (технически между ними 100 мс). Текущее решение загружает их последовательно, то есть вторая загрузка не начинается, пока первая загрузка не будет завершена. Вот почему onloadтребуется встроенный обработчик. Программно назначить обработчик не получится, потому что он срабатывает только в первый раз. Встроенный обработчик срабатывает каждый раз (по любой причине).
Джош Стодола,
Я все еще собираюсь набрать +1, потому что вижу, что это эффективный метод сокрытия глобальной переменной
Джош Стодола,
Вы можете создать iframe для каждой загрузки, чтобы поддерживать отдельные обратные вызовы.
Джастин Джонсон,
1
Я думаю, что это, в конечном счете, отличный ответ, только слегка затуманенный требованиями конкретного примера. По сути, идея состоит в том, чтобы создать шаблон (объект) и использовать «новый» для его создания. Это, вероятно, лучший ответ, потому что он действительно избегает глобальной переменной, то есть без компромиссов
PandaWood
3

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

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

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

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

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

Аджай Пошак
источник
2

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

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

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

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

Джимми
источник
В этой ситуации требуется встроенный обработчик. Когда форма отправляется в iframe, ваш обработчик загрузки программно не срабатывает.
Джош Стодола,
1
@ Джош: правда? iframe.onload = ...не эквивалентно <iframe onload="..."?
Crescent Fresh
2

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

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

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

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 .

Бен
источник
1

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

(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
})();
NilColor
источник
В прошлом мой подход заключался в определении конкретного объекта глобальных переменных и присоединении к нему всех глобальных переменных. Как бы я завернул это в закрытие? К сожалению, ограничение на поле для комментариев не позволяет мне встраивать типичный код.
Дэвид Эдвардс
1

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

function gInitUploadCount() {
    var uploadCount = 0;

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

gInitUploadCount();
gAddUploadCount();

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

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

Koenees
источник
1

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

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

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

    localFunc();
}

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

Эшвин
источник