Як уникнути глобальних змінних у JavaScript?


84

Ми всі знаємо, що глобальні змінні - це все, що не найкраща практика. Але є кілька випадків, коли без них важко кодувати. Які прийоми ви використовуєте, щоб уникнути використання глобальних змінних?

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

Код 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/…

Відповіді:


69

Найпростіший спосіб - обернути свій код закриттям і вручну виставити лише ті змінні, які вам потрібні в усьому світі, до глобальної області:

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


Або надати якийсь аксесуар. Я згоден.
Верхня сцена

3
Корисно, коли люди голосують за них, пояснюючи свої аргументи проти.
безвічність

6
Я не голосував проти. Однак сказати window ['varName'] = varName - це те саме, що зробити глобальну декларацію var поза закриттям. var foo = "bar"; (function() { alert(window['foo']) })();
Джош Стодола

Ви відповіли на заголовок, а не на запитання. Мені це не сподобалось. Поміщення ідіоми закриття в контекст посилань з вбудованого обробника подій (як і полягає суть питання) було б приємнішим.
Crescent Fresh

1
Crescent Fresh, я відповів на запитання. Припущення запитання потребують переробки, щоб уникнути глобальних функцій. Ця відповідь бере як орієнтир припущення запитання (наприклад, вбудований обробник подій) і дозволяє розробнику вибирати лише необхідні глобальні точки доступу, а не все, що знаходиться в глобальній області.
безвічність

59

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

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_ - це зло. Зберігайте кількість глобалів якомога менше.
Еренон

Вся анонімна функція буде виконуватися кожного разу, коли ви хочете отримати доступ до одного із загальнодоступних властивостей / методів 'модулів'?
UpTheCreek

@UpTheCreek: ні, не буде. Він виконується лише один раз, коли програма зустрічає замикання () в останньому рядку, і повернутий об'єкт буде присвоєний властивості myModule із закриттям, що містить myPrivateVar та myPrivateMethod.
Еренон

Красиво, саме те, що я теж шукав. Це дозволяє мені відокремити логіку сторінки від моєї обробки подій jquery. Питання: при атаці XSS зловмисник все одно матиме доступ до YAHOO.myProject.myModule правильно? Чи не було б краще залишити зовнішню функцію безіменною і поставити (); наприкінці, навколо $ (документа). вже теж? Можливо, модифікація мета-об’єкта властивостей 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>

Однак, якщо це так, то вам, мабуть, слід використовувати об'єктний літерал або екземплярний об'єкт і робити це звичайним способом OO (де ви можете використовувати шаблон модуля, якщо це вам подобається).


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
Я думаю, що це, зрештою, чудова відповідь, лише трохи затьмарена вимогами конкретного прикладу. В основному ідея полягає в тому, щоб створити шаблон (об'єкт) і використовувати 'new' для створення його екземпляра. Це, мабуть, найкраща відповідь, оскільки вона справді уникає глобальної змінної, тобто без компромісів
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
})();

Мій підхід у минулому полягав у визначенні конкретного об'єкта глобальних змінних та приєднанні до нього всіх глобальних змінних. Як би я загорнув це в закриття? На жаль, обмеження поля для коментарів не дозволяє мені вбудовувати типовий код.
Девід Едвардс

1

Для "захисту" окремих глобальних змінних:

function gInitUploadCount() {
    var uploadCount = 0;

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

gInitUploadCount();
gAddUploadCount();

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

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


1

Я використовую це таким чином:

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

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

    localFunc();
}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.