Как избежать глобальных переменных в 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">, Он загружает файлы по одному, чтобы предотвратить огромные запросы. Это делается путем отправки сообщения в iframe, ожидания ответа, который запускает загрузку iframe, а затем запускает следующую отправку.

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

13 ответов

Решение

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

(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://stackru.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.

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

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

Во-первых, невозможно избежать глобального 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>

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

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

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

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);  
    }       
}

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

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

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

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

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

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

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

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

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

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

Поэтому я написал плагин 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.

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

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

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

    localFunc();
}

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

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

function gInitUploadCount() {
    var uploadCount = 0;

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

gInitUploadCount();
gAddUploadCount();

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

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

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

(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
})();

Что происходит, так это то, что внутри функции makeCounter:

      function makeCounter() {
  var i = 0;
  return function() {
    console.log( ++i );
  };
}

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

      const counter = makeCounter(); // execute function and return other function
counter(); // This executes the function that you returned

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

      function makeCounter() {
    var i = 0;
    console.log( ++i );
}

makeCounter(); // this execute logs

Один забавный (YDOFMV1) способ избежать беспорядка в области видимости не только для JavaScript, но также для HTML и даже CSS(!) — это шаблоны HTML. Вам нужно создать небольшой архитектурный код (который, конечно, не должен быть в глобальной области видимости). Рассмотрим этот HTML:

      <html lang="en">
    <head>
        <title>Templates Are Fun!</title>
        <script> // stuff!
        </script>
    </head>
    <body>
    <template>
        <style> these style directives are local!</style>
        <div> just about any html stuff! </div>
        <script>
            doStuff(); // this code ignored unless you eval it!
        </script>
    </template>
    </body>
</html>

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

Что касается второстепенной архитектуры, которую вы должны (дойти!) сделать самостоятельно:

Для каждого шаблона

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

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

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

      const template     = document.querySelector('template');
const templateRoot = template.content;
const scriptNode   = templateRoot.querySelector('script');
scriptNode.remove(); // take it out of the template
template.replaceWith(templateRoot); // replace template with its contents

После запуска этого кодаtemplateсодержимое теперь существует в вашем документе и больше не будет игнорироваться синтаксическим анализатором DOM. Элемент еще не выполнен, но у вас есть ссылка на него вscriptNode.

Управляйте этим сценарием

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

      const code = '"use strict";' + scriptNode.innerHTML;
const templateFunc = new Function(code);
// now run templateFunc when you want

Обратите внимание на инъекцию"use strict", так как оцениваемый код не наследует этот параметр из вызывающего кода.

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

Позвольте звонку извне

Шаблон «модуль» здесь отлично работает. Поскольку шаблон будет обернут конструктором в анонимную функцию, он может просто вернуть какой-нибудь интерфейс, который вам подходит:

      // from inside template <script>
function init() { outside calls me to let me initialize };
function render() { outside calls me to tell me to render };
return {init:init, render:render};

Затем ваш мир за пределами шаблона (ов) сохраняет это возвращаемое значение для вызова кода шаблона:

      const code = '"use strict";' + scriptNode.innerHTML;
const templateFunc = new Function(code);
const templateInterface = templateFunc();
// let's tell the template script to initialize
templateInterface.init();

Позвольте внутреннему зову наружу

Помимо необходимости сообщать шаблону, что делать, шаблону, вероятно, нужен некоторый ограниченный доступ к внешнему миру. Опять же, мы хотим осуществлять полный контроль над этим доступом. Еще раз, некоторая разновидность шаблона «модуль» работает нормально.

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

      // revised to give <script> code access to a 'require' function

// model defined elsewhere in this scope
function require(name){
    switch(name){
        case 'model' : return model;
    // etc.
    }
}
const code = '"use strict";' + scriptNode.innerHTML;
// anonymous function will have arg named "require"
const templateFunc = new Function("require", code);
const templateInterface = templateFunc(require);
// let's tell the template script to initialize
templateInterface.init();

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

      <script>
    const model = require('model');
    // remaining code has access to model forever more
//...
<script>

Обогащение окружающей среды

Конечно, вы можете значительно обогатить среду кода, а не просто дать емуrequireфункция. Вот почему существует миллиард фреймворков для JavaScript. Мне нравится генерировать на лету объект для каждого шаблона, который дает доступ к интересующим его элементам DOM внутри шаблона. Это устраняет раздражающую проблему, заключающуюся в том, что вы часто хотите найти элементы DOM с помощью , но тогда у вас возникает непреодолимое ощущение, что вы должны сделать все s уникальными во всем документе, поэтомуidфактически становится глобальной переменной, которую вы не можете модулировать.

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

      <template>
    <dialog id="PickOrCreateProject">
        <center>
        Project name: <input type="text" data-id="textProject" />
        <input type="button" value="Create" data-id="createButton">
        <br/>
        <div data-id="div">
            <select data-id="select"></select>
            <input type="button" value="Select" data-id="selectButton">
        </div>
        </center>
    </dialog>
    <script> etc.</script>
</template>

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

      const {textProject,createButton,selectButton} = viewNodes;

Вы можете сделать это так же просто, как:

      let viewNodes = {};
for(const element of templateRoot.querySelectorAll("[data-ID]"))
    viewNodes[element.dataset.id] = element;

И пересмотретьFunctionконструктор, чтобы поставить его наscriptкод:

      const templateFunc = new Function("require", "viewNodes", code);
const templateInterface = templateFunc(require, viewNodes);

Не имеет значения, если два разных шаблона используют конфликтующиеdata-idатрибуты, поскольку ни один шаблон никогда не ищет идентификаторы, которые ему нужны, вне себя.

Заключение

В заключение скажу, что модульность шаблонов — это весело, и у меня должна быть какая-то работа, которую я сильно откладываю, чтобы тратить время на извержение всего этого :-).


1 Ваше определение веселья может отличаться
Другие вопросы по тегам