Jak unikać zmiennych globalnych w JavaScript?


84

Wszyscy wiemy, że zmienne globalne nie są najlepszą praktyką. Ale jest kilka przypadków, w których trudno jest kodować bez nich. Jakich technik używasz, aby uniknąć używania zmiennych globalnych?

Na przykład, biorąc pod uwagę następujący scenariusz, w jaki sposób nie użyłbyś zmiennej globalnej?

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

Odpowiednie znaczniki:

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

Ten kod pochodzi z formularza internetowego z wieloma plikami <input type="file">. Przesyła pliki pojedynczo, aby zapobiec ogromnym żądaniom. Robi to przez POST w elemencie iframe, czekając na odpowiedź, która uruchamia element iframe podczas ładowania, a następnie wyzwala następne przesłanie.

Nie musisz konkretnie odpowiadać na ten przykład, podam go tylko jako odniesienie do sytuacji, w której nie jestem w stanie wymyślić sposobu na uniknięcie zmiennych globalnych.


3
Użyj natychmiast wywoływanego wyrażenia funkcyjnego (IIFE) Możesz przeczytać więcej tutaj: codearsenal.net/2014/11/ ...

Odpowiedzi:


69

Najłatwiejszym sposobem jest zawinięcie kodu w zamknięcie i ręczne udostępnienie globalnie tylko tych zmiennych, których potrzebujesz:

(function() {
    // Your code here

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

Aby odnieść się do komentarza Crescent Fresh: aby całkowicie usunąć zmienne globalne ze scenariusza, deweloper musiałby zmienić szereg założeń w pytaniu. Wyglądałoby to bardziej tak:

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>

Nie potrzebujesz wbudowanej obsługi zdarzeń w programie <iframe>, będzie on nadal uruchamiany przy każdym ładowaniu z tym kodem.

Odnośnie zdarzenia obciążenia

Oto przypadek testowy pokazujący, że nie potrzebujesz onloadzdarzenia inline . Zależy to od odniesienia do pliku (/emptypage.php) na tym samym serwerze, w przeciwnym razie powinieneś być w stanie po prostu wkleić to na stronę i uruchomić.

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

Alert jest uruchamiany za każdym razem, gdy klikam przycisk przesyłania w Safari, Firefox, IE 6, 7 i 8.


Lub zapewnij jakiś akcesorium. Zgadzam się.
Upperstage

3
Jest to pomocne, gdy ludzie głosują za nie, aby wyjaśnić powody głosowania.
powiek

6
Nie głosowałem przeciw. Jednak powiedzenie window ['varName'] = varName jest tym samym, co zrobienie globalnej deklaracji zmiennej poza zamknięciem. var foo = "bar"; (function() { alert(window['foo']) })();
Josh Stodola

Odpowiedziałeś w tytule, a nie na pytanie. Nie podobało mi się to. Umieszczenie idiomu zamknięcia w kontekście odniesień z wbudowanego modułu obsługi zdarzeń (do którego dochodzi sedno pytania) byłoby przyjemniejsze.
Crescent Fresh

1
Crescent Fresh, odpowiedziałem na pytanie. Założenia pytania musiałyby zostać przeprojektowane, aby uniknąć funkcji globalnych. Ta odpowiedź przyjmuje jako wskazówkę założenia pytania (na przykład wbudowaną procedurę obsługi zdarzeń) i pozwala programiście wybrać tylko niezbędne globalne punkty dostępu, a nie wszystko, co znajduje się w zasięgu globalnym.
powiek

59

Proponuję wzór modułu .

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
Dam +1, ponieważ to rozumiem i jest to niezwykle pomocne, ale nadal nie jestem pewien, jak skuteczne byłoby to w sytuacji, gdy używam tylko jednej zmiennej globalnej. Popraw mnie, jeśli się mylę, ale wykonanie tej funkcji i zwrócenie jej powoduje, że zwracany obiekt jest przechowywany w YAHOO.myProject.myModulektórym jest zmienna globalna. Dobrze?
Josh Stodola

11
@Josh: zmienna globalna nie jest zła. Globalna zmienna_S_ jest zła. Utrzymuj jak najmniejszą liczbę globali.
erenon

Cała anonimowa funkcja byłaby wykonywana za każdym razem, gdy chciałbyś uzyskać dostęp do jednej z publicznych właściwości / metod „modułów”, prawda?
UpTheCreek

@UpTheCreek: nie, nie będzie. Wykonywane jest tylko raz, gdy program napotka funkcję close () w ostatniej linii, a zwrócony obiekt zostanie przypisany do właściwości myModule z zamknięciem zawierającym myPrivateVar i myPrivateMethod.
erenon

Piękne, dokładnie to, czego szukałem. To pozwala mi oddzielić logikę mojej strony od obsługi zdarzeń jquery. Pytanie, w przypadku ataku XSS osoba atakująca nadal miałaby dostęp do YAHOO.myProject.myModule, prawda? Czy nie byłoby lepiej pozostawić zewnętrzną funkcję bez nazwy i wstawić (); na końcu, wokół $ (document) .ready? Może modyfikacja meta obiektu właściwości YAHOO.myProct.myModule? Właśnie zainwestowałem sporo czasu w teorię js i teraz próbuję to dopasować.
Dale

8

Po pierwsze, nie da się uniknąć globalnego JavaScript, zawsze coś będzie wisiało w zasięgu globalnym. Nawet jeśli utworzysz przestrzeń nazw, co nadal jest dobrym pomysłem, będzie ona globalna.

Istnieje jednak wiele podejść, aby nie nadużywać zasięgu globalnego. Dwa z najprostszych to użycie domknięcia lub, ponieważ masz tylko jedną zmienną, którą musisz śledzić, po prostu ustaw ją jako właściwość samej funkcji (która może być następnie traktowana jako staticzmienna).

Zamknięcie

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

* Zwróć uwagę, że przyrost wartości uploadCountodbywa się tutaj wewnętrznie

Właściwość funkcji

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

Nie jestem pewien, dlaczego uploadCount++; if(uploadCount > 1) ...jest to konieczne, ponieważ wygląda na to, że warunek zawsze będzie prawdziwy. Ale jeśli potrzebujesz globalnego dostępu do zmiennej, metoda właściwości funkcji , którą opisałem powyżej, pozwoli ci to zrobić bez rzeczywistego globalnego dostępu do zmiennej .

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

Jeśli jednak tak jest, to prawdopodobnie powinieneś użyć dosłownego obiektu lub obiektu, dla którego utworzono instancję i przejść do tego w normalny sposób OO (gdzie możesz użyć wzorca modułu, jeśli ci się spodoba).


1
Absolutnie możesz uniknąć zasięgu globalnego. W przykładzie „Zamknięcia” po prostu usuń „var startUpload =” od początku, a ta funkcja zostanie całkowicie zamknięta, bez dostępu na poziomie globalnym. W praktyce wiele osób woli eksponować jedną zmienną, w której zawarte są odniesienia do wszystkiego innego
derrylwc

1
@derrylwc W tym przypadku startUploadjest to pojedyncza zmienna, do której się odnosisz . Usunięcie var startUpload = z przykładu zamknięcia oznacza, że ​​funkcja wewnętrzna nigdy nie będzie mogła zostać wykonana, ponieważ nie ma do niej odniesienia. Kwestia unikania zanieczyszczenia o zasięgu globalnym dotyczy zmiennej licznika wewnętrznego, z uploadCountktórej korzysta startUpload. Co więcej, myślę, że PO stara się uniknąć zanieczyszczenia jakiegokolwiek zakresu poza tą metodą uploadCountzmienną używaną wewnętrznie .
Justin Johnson,

Jeśli kod w ramach anonimowego zamknięcia dodaje przesyłającego jako nasłuchiwanie zdarzeń, to oczywiście „będzie można go wykonać” za każdym razem, gdy nastąpi odpowiednie zdarzenie.
Damian Yerrick

6

Czasami warto mieć zmienne globalne w JavaScript. Ale nie zostawiaj ich tak zwisających bezpośrednio z okna.

Zamiast tego utwórz pojedynczy obiekt „przestrzeni nazw”, który będzie zawierał Twoje elementy globalne. Aby otrzymać punkty bonusowe, umieść tam wszystko, w tym metody.


jak mogę to zrobić? utworzyć pojedynczy obiekt przestrzeni nazw, który będzie zawierał moje zbiory globalne?
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);  
    }       
}

Jak zwiększyć liczbę uploadCount w moim module onloadobsługi w elemencie iframe? To jest kluczowe.
Josh Stodola

1
OK, widzę, co tu zrobiłeś. Niestety to nie to samo. Powoduje to oddzielne wysyłanie wszystkich plików, ale w tym samym czasie (technicznie rzecz biorąc, jest między nimi 100 ms). Obecne rozwiązanie przesyła je sekwencyjnie, co oznacza, że ​​drugie przesyłanie nie rozpocznie się, dopóki nie zostanie ukończone pierwsze. Dlatego onloadwymagany jest wbudowany moduł obsługi. Programowe przypisywanie programu obsługi nie działa, ponieważ jest uruchamiany tylko za pierwszym razem. Wbudowany program obsługi uruchamia się za każdym razem (z dowolnego powodu).
Josh Stodola

Nadal jednak zamierzam +1, ponieważ widzę, że jest to skuteczna metoda ukrywania zmiennej globalnej
Josh Stodola

Możesz utworzyć element iframe dla każdego przesyłania, aby zachować indywidualne wywołania zwrotne.
Justin Johnson,

1
Myślę, że to ostatecznie świetna odpowiedź, tylko trochę zaciemniona przez wymagania konkretnego przykładu. Zasadniczo chodzi o utworzenie szablonu (obiektu) i użycie „new” do jego utworzenia. To prawdopodobnie najlepsza odpowiedź, ponieważ naprawdę unika zmiennej globalnej, czyli bez kompromisów
PandaWood

3

Innym sposobem jest utworzenie obiektu, a następnie dodanie do niego metod.

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

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

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

W ten sposób ujawniany jest tylko obiekt „obj” i metody do niego dołączane. Jest to równoważne dodaniu go w przestrzeni nazw.


2

Niektóre rzeczy będą znajdować się w globalnej przestrzeni nazw - mianowicie, niezależnie od funkcji, którą wywołujesz z wbudowanego kodu JavaScript.

Ogólnie rozwiązaniem jest zawinięcie wszystkiego w zamknięcie:

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

i unikaj obsługi wbudowanej.


W tej sytuacji wymagana jest obsługa wbudowana. Gdy formularz jest przesyłany do iframe, program obsługi ładowania ustawiony w sposób programowy nie zostanie uruchomiony.
Josh Stodola

1
@Josh: zaraz, naprawdę? iframe.onload = ...nie jest równoważne <iframe onload="..."?
Crescent Fresh

2

Używanie domknięć może być OK w przypadku małych i średnich projektów. Jednak w przypadku dużych projektów warto podzielić kod na moduły i zapisać je w różnych plikach.

Dlatego napisałem wtyczkę jQuery Secret, aby rozwiązać problem.

W twoim przypadku z tą wtyczką kod wyglądałby mniej więcej tak.

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

Odpowiednie znaczniki:

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

Otwórz swojego Firebuga , nie znajdziesz w ogóle żadnych globali, nawet funkcji :)

Pełną dokumentację można znaleźć tutaj .

Aby zobaczyć stronę demonstracyjną, zobacz to .

Kod źródłowy na GitHub .


1

Użyj zamknięć. Coś takiego daje ci zakres inny niż globalny.

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

Moje podejście w przeszłości polegało na definiowaniu określonego obiektu zmiennych globalnych i dołączaniu do niego wszystkich zmiennych globalnych. Jak bym to zawinął w zamknięcie? Niestety, limit pola komentarza nie pozwala mi na osadzenie typowego kodu.
David Edwards,

1

W celu „zabezpieczenia” poszczególnych zmiennych globalnych:

function gInitUploadCount() {
    var uploadCount = 0;

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

gInitUploadCount();
gAddUploadCount();

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

Jestem nowicjuszem w JS, obecnie używam tego w jednym projekcie. (doceniam wszelkie komentarze i krytykę)


1

Używam tego w ten sposób:

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

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

    localFunc();
}

Dla wszystkich zasięgów globalnych użyj „var”, a dla zakresów lokalnych użyj „let”.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.