Wie vermeide ich globale Variablen in JavaScript?

84

Wir alle wissen, dass globale Variablen alles andere als bewährte Verfahren sind. Es gibt jedoch mehrere Fälle, in denen es schwierig ist, ohne sie zu codieren. Welche Techniken verwenden Sie, um die Verwendung globaler Variablen zu vermeiden?

Wie würden Sie beispielsweise im folgenden Szenario keine globale Variable verwenden?

JavaScript-Code:

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

Relevantes Markup:

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

Dieser Code stammt aus einem Webformular mit mehreren <input type="file">. Die Dateien werden einzeln hochgeladen, um große Anforderungen zu vermeiden. Dazu wird ein POST an den Iframe gesendet, auf die Antwort gewartet, die den Iframe-Onload auslöst, und dann die nächste Übermittlung ausgelöst.

Sie müssen dieses Beispiel nicht speziell beantworten. Ich verweise nur auf eine Situation, in der ich mir keine Möglichkeit vorstellen kann, globale Variablen zu vermeiden.

Josh Stodola
quelle
3
Sofort aufgerufenen Funktionsausdruck (IIFE) verwenden Weitere Informationen finden Sie hier: codearsenal.net/2014/11/…

Antworten:

69

Am einfachsten ist es, Ihren Code in einen Abschluss zu packen und nur die Variablen, die Sie global benötigen, manuell für den globalen Bereich verfügbar zu machen:

(function() {
    // Your code here

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

Um den Kommentar von Crescent Fresh anzusprechen: Um globale Variablen vollständig aus dem Szenario zu entfernen, müsste der Entwickler eine Reihe von in der Frage angenommenen Dingen ändern. Es würde viel mehr so ​​aussehen:

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>

Sie müssen nicht brauchen einen Inline - Event - Handler auf das <iframe>, wird es immer noch das Feuer auf jede Last mit diesem Code.

In Bezug auf das Ladeereignis

Hier ist ein Testfall, der zeigt, dass Sie kein Inline- onloadEreignis benötigen . Dies hängt davon ab, ob Sie auf eine Datei (/emptypage.php) auf demselben Server verweisen. Andernfalls sollten Sie diese einfach in eine Seite einfügen und ausführen können.

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

Die Warnung wird jedes Mal ausgelöst, wenn ich in Safari, Firefox, IE 6, 7 und 8 auf die Schaltfläche "Senden" klicke.

Augenlidlosigkeit
quelle
Oder stellen Sie eine Art Accessor bereit. Genau.
Upperstage
3
Es ist hilfreich, wenn Leute für sie abstimmen, um ihre Gründe für die Abstimmung zu erklären.
Augenlidlosigkeit
6
Ich habe nicht herabgestimmt. Das Sagen von window ['varName'] = varName entspricht jedoch dem Erstellen einer globalen var-Deklaration außerhalb des Abschlusses. var foo = "bar"; (function() { alert(window['foo']) })();
Josh Stodola
Sie haben den Titel beantwortet, nicht die Frage. Das hat mir nicht gefallen. Es wäre schöner gewesen, das Abschluss-Idiom in den Kontext der Referenzen des Inline-Event-Handlers zu stellen (da das Fleisch der Frage auf den Punkt kommt).
Crescent Fresh
1
Crescent Fresh, ich beantwortete die Frage. Die Annahmen der Frage müssten neu gestaltet werden, um globale Funktionen zu vermeiden. Diese Antwort basiert auf den Annahmen der Frage (z. B. einem Inline-Ereignishandler) und ermöglicht es dem Entwickler, nur die erforderlichen globalen Zugriffspunkte auszuwählen, anstatt alles, was sich im globalen Bereich befindet.
Augenlidlosigkeit
59

Ich schlage das Modulmuster vor .

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
erenon
quelle
3
Ich werde +1 geben, weil ich das verstehe und es äußerst hilfreich ist, aber ich bin mir immer noch nicht sicher, wie effektiv dies in der Situation sein würde, in der ich nur eine globale Variable verwende. Korrigieren Sie mich, wenn ich falsch liege. Wenn Sie diese Funktion jedoch ausführen und zurückgeben, wird das zurückgegebene Objekt in YAHOO.myProject.myModuleeiner globalen Variablen gespeichert . Richtig?
Josh Stodola
11
@ Josh: Globale Variable ist nicht böse. Globale Variable_S_ sind böse. Halten Sie die Anzahl der Globals so gering wie möglich.
Erenon
Die gesamte anonyme Funktion wird jedes Mal ausgeführt, wenn Sie auf eine der öffentlichen Eigenschaften / Methoden der 'Module' zugreifen möchten, oder?
UpTheCreek
@UpTheCreek: Nein, das wird es nicht. Es wird nur einmal ausgeführt, wenn das Programm in der letzten Zeile auf das Schließen () trifft und das zurückgegebene Objekt der Eigenschaft myModule zugewiesen wird, wobei der Abschluss myPrivateVar und myPrivateMethod enthält.
Erenon
Schön, genau das, wonach ich auch gesucht habe. Dadurch kann ich meine Seitenlogik von meiner jquery-Ereignisbehandlung trennen. Eine Frage, bei einem XSS-Angriff hätte ein Angreifer immer noch Zugriff auf YAHOO.myProject.myModule richtig? Wäre es nicht besser, die äußere Funktion namenlos zu lassen und ein () zu setzen? am Ende auch um das $ (Dokument) herum? Möglicherweise Änderung des Metaobjekts der YAHOO.myProct.myModule-Eigenschaften? Ich habe gerade ziemlich viel Zeit in js Theorie investiert und versuche jetzt, sie zusammenzufügen.
Dale
8

Zunächst einmal ist es unmöglich, globales JavaScript zu vermeiden, etwas wird immer den globalen Bereich baumeln lassen. Selbst wenn Sie einen Namespace erstellen, was immer noch eine gute Idee ist, ist dieser Namespace global.

Es gibt jedoch viele Ansätze, um den globalen Geltungsbereich nicht zu missbrauchen. Zwei der einfachsten sind, entweder den Abschluss zu verwenden, oder da Sie nur eine Variable haben, die Sie verfolgen müssen, legen Sie sie einfach als Eigenschaft der Funktion selbst fest (die dann als staticVariable behandelt werden kann).

Schließung

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

* Beachten Sie, dass das Inkrementieren uploadCounthier intern erfolgt

Funktionseigenschaft

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

Ich bin mir nicht sicher, warum dies uploadCount++; if(uploadCount > 1) ...notwendig ist, da es so aussieht, als ob die Bedingung immer wahr sein wird. Wenn Sie jedoch globalen Zugriff auf die Variable benötigen, können Sie dies mit der oben beschriebenen Funktionseigenschaftsmethode tun, ohne dass die Variable tatsächlich global ist.

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

Wenn dies jedoch der Fall ist, sollten Sie wahrscheinlich ein Objektliteral oder ein instanziiertes Objekt verwenden und dies auf normale OO-Weise tun (wobei Sie das Modulmuster verwenden können, wenn es Ihnen gefällt).

Justin Johnson
quelle
1
Sie können den globalen Geltungsbereich absolut vermeiden. Entfernen Sie in Ihrem 'Closure'-Beispiel einfach' var startUpload = 'von Anfang an, und diese Funktion wird vollständig eingeschlossen und ist auf globaler Ebene nicht zugänglich. In der Praxis ziehen es viele Menschen vor, eine einzelne Variable
verfügbar zu machen
1
@derrylwc In diesem Fall startUploadhandelt es sich um die einzelne Variable, auf die Sie sich beziehen. Das Entfernen var startUpload = aus dem Schließungsbeispiel bedeutet, dass die innere Funktion niemals ausgeführt werden kann, da kein Verweis darauf vorhanden ist. Das Problem der Vermeidung einer Verschmutzung durch den globalen Geltungsbereich betrifft die interne Zählervariable, uploadCountdie von verwendet wird startUpload. Darüber hinaus denke ich, dass das OP versucht, zu vermeiden, dass ein Bereich außerhalb dieser Methode mit der intern verwendeten uploadCountVariablen verschmutzt wird .
Justin Johnson
Wenn Code innerhalb des anonymen Abschlusses den Uploader als Ereignis-Listener hinzufügt, kann er natürlich "ausgeführt werden", wenn das entsprechende Ereignis eintritt.
Damian Yerrick
6

Manchmal ist es sinnvoll, globale Variablen in JavaScript zu haben. Aber lassen Sie sie nicht so direkt am Fenster hängen.

Erstellen Sie stattdessen ein einzelnes "Namespace" -Objekt, das Ihre Globals enthält. Geben Sie für Bonuspunkte alles ein, einschließlich Ihrer Methoden.

Nosredna
quelle
wie kann ich das machen? ein einzelnes Namespace-Objekt erstellen, das meine Globals enthält?
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);  
    }       
}
ChaosPandion
quelle
Wie erhöhe ich den uploadCount in meinem onloadHandler auf dem Iframe? Das ist entscheidend.
Josh Stodola
1
OK, ich sehe, was du hier gemacht hast. Leider ist das nicht dasselbe. Dies löst alle Uploads separat aus, aber gleichzeitig (technisch gesehen liegen 100 ms dazwischen). Die aktuelle Lösung lädt sie nacheinander hoch, dh der zweite Upload startet erst, wenn der erste Upload abgeschlossen ist. Deshalb ist der Inline- onloadHandler erforderlich. Das programmgesteuerte Zuweisen des Handlers funktioniert nicht, da er nur beim ersten Mal ausgelöst wird. Der Inline-Handler wird jedes Mal ausgelöst (aus welchem ​​Grund auch immer).
Josh Stodola
Ich gehe aber immer noch auf +1, weil ich sehe, dass dies eine effektive Methode ist, um eine globale Variable zu verbergen
Josh Stodola
Sie können für jeden Upload einen Iframe erstellen, um die einzelnen Rückrufe zu verwalten.
Justin Johnson
1
Ich denke, dies ist letztendlich eine großartige Antwort, die durch die Anforderungen des jeweiligen Beispiels nur leicht getrübt wird. Grundsätzlich besteht die Idee darin, eine Vorlage (ein Objekt) zu erstellen und diese mit 'new' zu instanziieren. Dies ist wahrscheinlich die beste Antwort, da sie eine globale Variable wirklich vermeidet, dh ohne Kompromisse
PandaWood
3

Eine andere Möglichkeit, dies zu tun, besteht darin, ein Objekt zu erstellen und ihm dann Methoden hinzuzufügen.

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

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

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

Auf diese Weise wird nur das Objekt 'obj' verfügbar gemacht und Methoden daran angehängt. Dies entspricht dem Hinzufügen im Namespace.

Ajay Poshak
quelle
2

Einige Dinge werden sich im globalen Namespace befinden - nämlich welche Funktion Sie aus Ihrem Inline-JavaScript-Code aufrufen.

Im Allgemeinen besteht die Lösung darin, alles in einen Verschluss zu wickeln:

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

und vermeiden Sie den Inline-Handler.

Jimmy
quelle
In dieser Situation ist der Inline-Handler erforderlich. Wenn das Formular an iframe gesendet wird, wird Ihr programmgesteuert festgelegter Onload-Handler nicht ausgelöst.
Josh Stodola
1
@ Josh: whoa, wirklich? iframe.onload = ...ist nicht gleichbedeutend mit <iframe onload="..."?
Crescent Fresh
2

Die Verwendung von Schließungen kann für kleine bis mittlere Projekte in Ordnung sein. Bei großen Projekten möchten Sie Ihren Code jedoch möglicherweise in Module aufteilen und in verschiedenen Dateien speichern.

Deshalb habe ich das jQuery Secret Plugin geschrieben , um das Problem zu lösen.

In Ihrem Fall mit diesem Plugin würde der Code ungefähr so ​​aussehen.

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

Relevantes Markup:

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

Öffne deinen Firebug , du wirst überhaupt keine Globals finden, nicht einmal die Funktion :)

Eine vollständige Dokumentation finden Sie hier .

Eine Demoseite finden Sie hier .

Quellcode auf GitHub .

ben
quelle
1

Verwenden Sie Verschlüsse. So etwas gibt Ihnen einen anderen Bereich als global.

(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
quelle
Mein Ansatz in der Vergangenheit war es, ein bestimmtes globales Variablenobjekt zu definieren und alle globalen Variablen daran anzuhängen. Wie würde ich das in einen Verschluss wickeln? Leider erlaubt mir das Kommentarfeldlimit nicht, typischen Code einzubetten.
David Edwards
1

Zum "Sichern" einzelner globaler Variablen:

function gInitUploadCount() {
    var uploadCount = 0;

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

gInitUploadCount();
gAddUploadCount();

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

Ich bin ein Neuling bei JS und verwende dies derzeit in einem Projekt. (Ich schätze jeden Kommentar und jede Kritik)

Koenees
quelle
1

Ich benutze es so:

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

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

    localFunc();
}

Verwenden Sie für alle globalen Bereiche 'var' und für lokale Bereiche 'let'.

Ashwin
quelle