Come evitare le variabili globali in JavaScript?


85

Sappiamo tutti che le variabili globali sono tutt'altro che best practice. Ma ci sono diversi casi in cui è difficile programmare senza di loro. Quali tecniche usi per evitare l'uso di variabili globali?

Ad esempio, dato il seguente scenario, come non useresti una variabile globale?

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

Markup pertinente:

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

Questo codice proviene da un modulo Web con più file <input type="file">. Carica i file uno alla volta per evitare richieste enormi. A tale scopo, POST nell'iframe, in attesa della risposta che attiva l'onload dell'iframe e quindi attiva l'invio successivo.

Non devi rispondere in modo specifico a questo esempio, lo sto solo fornendo come riferimento a una situazione in cui non sono in grado di pensare a un modo per evitare le variabili globali.


3
Usa l'espressione di funzione immediatamente invocata (IIFE) Puoi leggere di più qui: codearsenal.net/2014/11/…

Risposte:


70

Il modo più semplice è racchiudere il codice in una chiusura ed esporre manualmente solo le variabili di cui hai bisogno globalmente all'ambito globale:

(function() {
    // Your code here

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

Per rispondere al commento di Crescent Fresh: per rimuovere completamente le variabili globali dallo scenario, lo sviluppatore dovrebbe modificare una serie di cose assunte nella domanda. Sarebbe molto più simile a questo:

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>

Non hai bisogno di un gestore di eventi inline su<iframe> , verrà comunque attivato a ogni caricamento con questo codice.

Per quanto riguarda l'evento di caricamento

Ecco un caso di test che dimostra che non hai bisogno di un onloadevento in linea . Ciò dipende dal riferimento a un file (/emptypage.php) sullo stesso server, altrimenti dovresti essere in grado di incollarlo in una pagina ed eseguirlo.

<!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>

L'avviso si attiva ogni volta che faccio clic sul pulsante di invio in Safari, Firefox, IE 6, 7 e 8.


Oppure fornisci una sorta di accessorio. Sono d'accordo.
Upperstage

3
È utile quando le persone votano per loro per spiegare il motivo per cui hanno votato.
mancanza di palpebre

6
Non ho votato per difetto. Tuttavia, pronunciare window ['varName'] = varName equivale a fare una dichiarazione var globale al di fuori della chiusura. var foo = "bar"; (function() { alert(window['foo']) })();
Josh Stodola

Hai risposto al titolo, non alla domanda. Non mi è piaciuto. Mettere l'idioma di chiusura nel contesto dei riferimenti dal gestore di eventi inline (come sta arrivando il nocciolo della domanda) sarebbe stato più bello.
Crescent Fresh

1
Crescent Fresh, ho risposto alla domanda. Le ipotesi della domanda dovrebbero essere ridisegnate per evitare funzioni globali. Questa risposta prende, come linea guida, i presupposti della domanda (un gestore di eventi in linea, ad esempio) e consente allo sviluppatore di scegliere solo i punti di accesso globali necessari piuttosto che tutto ciò che è nell'ambito globale.
mancanza di palpebre

60

Suggerisco il modello del modulo .

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
Faccio +1 perché lo capisco ed è estremamente utile, ma non sono ancora chiaro quanto sarebbe efficace nella situazione in cui utilizzo solo una variabile globale. Correggimi se sbaglio, ma eseguendo questa funzione e restituendola, l'oggetto restituito viene memorizzato in YAHOO.myProject.myModule, che è una variabile globale. Destra?
Josh Stodola

11
@ Josh: la variabile globale non è malvagia. La variabile globale_S_ è malvagia. Mantieni il conteggio delle globali il meno possibile.
erenon

L'intera funzione anonima verrebbe eseguita ogni volta che si desidera accedere a una delle proprietà / metodi pubblici dei "moduli", giusto?
UpTheCreek

@UpTheCreek: no, non lo farà. Viene eseguito solo una volta, quando il programma incontra la chiusura () sull'ultima riga, e l'oggetto restituito verrà assegnato alla proprietà myModule con la chiusura contenente myPrivateVar e myPrivateMethod.
erenon

Bello, esattamente quello che cercavo anch'io. Ciò mi consente di separare la logica della mia pagina dalla gestione degli eventi jquery. Una domanda, con un attacco XSS un utente malintenzionato avrebbe comunque accesso a YAHOO.myProject.myModule corretto? Non sarebbe meglio lasciare semplicemente la funzione esterna senza nome e inserire una (); alla fine, intorno al $ (documento). già pure? Forse la modifica del meta oggetto delle proprietà di YAHOO.myProct.myModule? Ho appena investito un bel po 'di tempo nella teoria di js e sto cercando di adattarla ora.
Dale

8

Prima di tutto, è impossibile evitare JavaScript globale, qualcosa starà sempre penzolando l'ambito globale. Anche se crei uno spazio dei nomi, che è comunque una buona idea, quello spazio dei nomi sarà globale.

Ci sono molti approcci, tuttavia, per non abusare della portata globale. Due dei più semplici sono usare la chiusura, o poiché hai solo una variabile di cui devi tenere traccia, basta impostarla come una proprietà della funzione stessa (che può quindi essere trattata come unastatic variabile).

Chiusura

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

* Notare che l'incremento di uploadCount sta avvenendo internamente qui

Proprietà della funzione

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

Non sono sicuro del perché uploadCount++; if(uploadCount > 1) ...sia necessario, poiché sembra che la condizione sarà sempre vera. Ma se hai bisogno dell'accesso globale alla variabile, il metodo della proprietà della funzione che ho descritto sopra ti consentirà di farlo senza che la variabile sia effettivamente globale.

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

Tuttavia, se questo è il caso, allora dovresti probabilmente usare un oggetto letterale o un oggetto istanziato e farlo nel normale modo OO (dove puoi usare il modello del modulo se ti piace).


1
Puoi assolutamente evitare l'ambito globale. Nel tuo esempio di "Chiusura", rimuovi semplicemente "var startUpload =" dall'inizio e quella funzione sarà completamente racchiusa, senza accessibilità a livello globale. In pratica, molte persone preferiscono esporre una singola variabile, all'interno della quale sono contenuti riferimenti a tutto il resto
derrylwc

1
@derrylwc In questo caso, startUploadè quella singola variabile a cui ti riferisci. La rimozione var startUpload = dall'esempio di chiusura significa che la funzione interna non potrà mai essere eseguita perché non vi è alcun riferimento ad essa. La questione di evitare l'inquinamento di portata globale riguarda la controvariabile interna, uploadCountche viene utilizzata da startUpload. Inoltre, penso che l'OP stia cercando di evitare di inquinare qualsiasi ambito al di fuori di questo metodo con la uploadCountvariabile utilizzata internamente .
Justin Johnson

Se il codice all'interno della chiusura anonima aggiunge l'uploader come listener di eventi, ovviamente "potrà essere eseguito" ogni volta che si verifica l'evento appropriato.
Damian Yerrick

6

A volte ha senso avere variabili globali in JavaScript. Ma non lasciarli appesi direttamente dalla finestra in questo modo.

Invece, crea un singolo oggetto "spazio dei nomi" per contenere le tue variabili globali. Per i punti bonus, inserisci tutto lì, compresi i tuoi metodi.


Come lo posso fare? creare un singolo oggetto dello spazio dei nomi per contenere i miei globali?
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);  
    }       
}

Come faccio a incrementare l'uploadCount all'interno del mio onloadgestore sull'iframe ? Questo è fondamentale.
Josh Stodola

1
OK, vedo cosa hai fatto qui. Purtroppo non è la stessa cosa. Questo attiva tutti i caricamenti separatamente, ma allo stesso tempo (tecnicamente, ci sono 100 ms tra di loro). La soluzione corrente li carica in sequenza, il che significa che il secondo caricamento non viene avviato fino al completamento del primo caricamento. Ecco perché onloadè richiesto il gestore in linea . L'assegnazione del gestore a livello di codice non funziona, perché viene attivato solo la prima volta. Il gestore inline viene attivato ogni volta (per qualsiasi motivo).
Josh Stodola

Continuerò a fare +1 però, perché vedo che questo è un metodo efficace per nascondere una variabile globale
Josh Stodola

Puoi creare un iframe per ogni caricamento per mantenere i singoli callback.
Justin Johnson

1
Penso che questa sia in definitiva un'ottima risposta, solo leggermente offuscata dai requisiti del particolare esempio. Fondamentalmente l'idea è di creare un modello (oggetto) e utilizzare "new" per istanziarlo. Questa è probabilmente la risposta migliore perché evita davvero una variabile globale, cioè senza compromessi
PandaWood

3

Un altro modo per farlo è creare un oggetto e quindi aggiungere metodi ad esso.

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

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

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

In questo modo, viene esposto solo l'oggetto "obj" e ad esso vengono associati metodi. È equivalente ad aggiungerlo nello spazio dei nomi.


2

Alcune cose saranno nello spazio dei nomi globale, vale a dire, qualunque funzione tu chiami dal tuo codice JavaScript inline.

In generale, la soluzione è avvolgere tutto in una chiusura:

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

ed evitare il gestore in linea.


In questa situazione è necessario il gestore inline. Quando il modulo viene inviato a iframe, il tuo gestore onload impostato a livello di programmazione non si attiverà.
Josh Stodola

1
@ Josh: whoa, davvero? iframe.onload = ...non è equivalente a <iframe onload="..."?
Crescent Fresh

2

L'utilizzo di chiusure potrebbe essere OK per progetti medio-piccoli. Tuttavia, per grandi progetti, potresti voler dividere il tuo codice in moduli e salvarli in file diversi.

Pertanto ho scritto il plugin jQuery Secret per risolvere il problema.

Nel tuo caso con questo plugin il codice sarebbe simile al seguente.

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

Markup pertinente:

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

Apri il tuo Firebug , non troverai affatto globali, nemmeno la funzione :)

Per la documentazione completa, vedere qui .

Per una pagina demo, vedere questo .

Codice sorgente su GitHub .


1

Usa chiusure. Qualcosa di simile ti offre un ambito diverso da quello globale.

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

Il mio approccio in passato è stato quello di definire uno specifico oggetto delle variabili globali e di associarvi tutte le variabili globali. Come lo avvolgerei in una chiusura? Purtroppo, il limite della casella dei commenti non mi consente di incorporare il codice tipico.
David Edwards,

1

Per "proteggere" le variabili globali individuali:

function gInitUploadCount() {
    var uploadCount = 0;

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

gInitUploadCount();
gAddUploadCount();

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

Sono un principiante di JS, attualmente lo utilizzo in un progetto. (Apprezzo qualsiasi commento e critica)


1

Lo uso in questo modo:

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

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

    localFunc();
}

Per tutti gli ambiti globali utilizzare "var" e per gli ambiti locali utilizzare "let".

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.