Zaskoczony, że zmienna globalna ma nieokreśloną wartość w JavaScript

87

Dzisiaj byłem całkowicie zaskoczony, gdy zobaczyłem, że zmienna globalna ma undefinedw pewnym przypadku wartość.

Przykład:

var value = 10;
function test() {
    //A
    console.log(value);
    var value = 20;

    //B
    console.log(value);
}
test();

Daje dane wyjściowe jako

undefined
20

Tutaj, dlaczego silnik JavaScript traktuje globalną wartość jako undefined. Wiem, że JavaScript jest językiem interpretowanym. W jaki sposób może uwzględniać zmienne w funkcji?

Czy to pułapka związana z silnikiem JavaScript?

iamjustcoder
źródło

Odpowiedzi:

175

Zjawisko to jest znane jako: JavaScript Variable Hoisting .

W żadnym momencie nie uzyskujesz dostępu do zmiennej globalnej w swojej funkcji; zawsze uzyskujesz dostęp tylko do valuezmiennej lokalnej .

Twój kod jest równoważny z następującym:

var value = 10;

function test() {
    var value;
    console.log(value);

    value = 20;
    console.log(value);
}

test();

Wciąż jesteś zaskoczony undefined?


Wyjaśnienie:

Jest to coś, na co prędzej czy później wpada każdy programista JavaScript. Mówiąc najprościej, wszelkie zadeklarowane zmienne są zawsze przenoszone na szczyt lokalnego zamknięcia. Tak więc, nawet jeśli zadeklarowałeś swoją zmienną po pierwszym console.logwywołaniu, nadal uważa się, że zadeklarowałeś ją wcześniej.
Jednak podnoszona jest tylko część deklaracji; z drugiej strony zadanie to nie jest.

Tak więc, kiedy pierwszy raz dzwoniłeś console.log(value), odnosiłeś się do swojej lokalnie zadeklarowanej zmiennej, która nie ma jeszcze nic przypisanego; stąd undefined.

Oto kolejny przykład :

var test = 'start';

function end() {
    test = 'end';
    var test = 'local';
}

end();
alert(test);

Jak myślisz, co to ostrzeże? Nie, nie tylko czytaj dalej, pomyśl o tym. Jaka jest wartość test?

Jeśli powiedziałeś coś innego niż start, byłeś w błędzie. Powyższy kod jest równoważny z tym:

var test = 'start';

function end() {
    var test;
    test = 'end';
    test = 'local';
}

end();
alert(test);

tak, aby nigdy nie wpływać na zmienną globalną.

Jak widać, bez względu na to, gdzie umieścisz swoją deklarację zmiennej, zawsze jest ona podnoszona na szczyt lokalnego zamknięcia.


Dygresja:

Dotyczy to również funkcji.

Rozważ ten fragment kodu :

test("Won't work!");

test = function(text) { alert(text); }

co da ci błąd odniesienia:

Uncaught ReferenceError: test nie jest zdefiniowany

To wyrzuca wielu programistów, ponieważ ten fragment kodu działa dobrze:

test("Works!");

function test(text) { alert(text); }

Jak już wspomniano, powodem tego jest to, że część przypisania nie jest podnoszona. Tak więc w pierwszym przykładzie, kiedy test("Won't work!")została uruchomiona, testzmienna została już zadeklarowana, ale nie ma jeszcze przypisanej do niej funkcji.

W drugim przykładzie nie używamy przypisywania zmiennych. Zamiast tego używamy prawidłowej składni deklaracji funkcji, która całkowicie podnosi funkcję.


Ben Cherry napisał doskonały artykuł na ten temat, odpowiednio zatytułowany JavaScript Scoping and Hoisting .
Przeczytaj to. Daje ci to pełny obraz ze szczegółami.

Joseph Silber
źródło
27
To dobre wyjaśnienie. Brakuje mi jednak rozwiązania, w którym można uzyskać dostęp do zmiennych globalnych wewnątrz funkcji.
DuKes0mE
1
@ DuKes0mE - zawsze możesz uzyskać dostęp do zmiennych globalnych wewnątrz funkcji.
Joseph Silber
3
tak, a dlaczego przypadek A z wpisu otwierającego nie działa i mówi, że jest niezdefiniowany? Rozumiem, że będzie to interpretowane jako zmienna lokalna, a nie globalna, ale jak to będzie wtedy globalne?
DuKes0mE
4
aby uzyskać dostęp do globalnej zmiennej użyj window.value
Venkat Reddy
1
kiedy wprowadzono ten styl zmiennego podnoszenia? czy to zawsze było standardowe w javascript?
Dieskim
54

Byłem nieco rozczarowany, że problem został tutaj wyjaśniony, ale nikt nie zaproponował rozwiązania. Jeśli chcesz uzyskać dostęp do zmiennej globalnej w zakresie funkcji bez tworzenia przez funkcję najpierw niezdefiniowanej zmiennej lokalnej, odwołaj się do zmiennej jakowindow.varName

Amalgovinus
źródło
8
Tak, szkoda, że ​​pozostali nie zaproponowali rozwiązania, ponieważ to pierwszy wynik w wynikach Google. PRAWIE odpowiedzieli na rzeczywiste pytanie zadane o to, że jest to pułapka w silniku js ... westchnienie -> dzięki za odpowiedź
użytkownik1567453
2
Taka jest różnica między wiedzą teoretyczną a robieniem rzeczy. Dzięki za odpowiedź!
hansTheFranz
Dla mnie to błąd, że zmieniłem nazwę globalną w dowolnym miejscu w dowolnej funkcji. Przynajmniej wprowadza zamieszanie. W najgorszym przypadku wymaga wyszukiwania w Google. Dzięki
dcromley
Javascript codziennie mnie zaskakuje, co mija. Dzięki stary, odpowiedź była pomocna.
Raf
Dzięki za rozwiązanie, znalazłem to po tym, jak globalna zmienna była niezdefiniowana, ale tylko w Safari. Pojawiły się inne pliki „include” i zmienne globalne, takie jak „google”, więc skopiowałem podejście używane przez Google: window.globalVarFromJS = window.globalVarFromJS || {}; Potem znalazłem twoje rozwiązanie, więc pomyślałem, że dodam do niego.
Ralph Hinkley,
10

Zmienne w JavaScript zawsze mają zasięg obejmujący całą funkcję. Nawet jeśli zostały zdefiniowane w środku funkcji, są widoczne wcześniej. Podobne zjawisko można zaobserwować przy podnoszeniu funkcji.

Biorąc to pod uwagę, pierwszy console.log(value)widzi valuezmienną (wewnętrzną, która przesłania zewnętrzną value), ale nie została jeszcze zainicjowana. Można o tym myśleć tak, jakby wszystkie deklaracje zmiennych zostały niejawnie przeniesione na początek funkcji (a nie najbardziej wewnętrzny blok kodu), podczas gdy definicje pozostają w tym samym miejscu.

Zobacz też

Tomasz Nurkiewicz
źródło
Zawsze uwielbiam proste słowa +1 :)
Jashwant
3

Istnieje zmienna globalna value, ale gdy sterowanie wchodzi do testfunkcji, valuedeklarowana jest inna zmienna, która przesłania zmienną globalną. Ponieważ deklaracje zmiennych ( ale nie przypisania ) w JavaScript są podnoszone na początek zakresu, w którym są zadeklarowane:

//value == undefined (global)
var value = 10;
//value == 10 (global)

function test() {
    //value == undefined (local)
    var value = 20;
    //value == 20 (local)
}
//value == 10 (global)

Zauważ, że to samo dotyczy deklaracji funkcji, co oznacza, że ​​możesz wywołać funkcję, zanim pojawi się ona w kodzie:

test(); //Call the function before it appears in the source
function test() {
    //Do stuff
}

Warto również zauważyć, że gdy połączysz te dwa elementy w wyrażenie funkcyjne, zmienna będzie obowiązywać undefineddo momentu przypisania, więc nie możesz wywołać funkcji, dopóki tak się nie stanie:

var test = function() {
    //Do stuff
};
test(); //Have to call the function after the assignment
James Allardice
źródło
0
  1. Najprostszym sposobem zachowania dostępu do zmiennych zewnętrznych (nie tylko o zasięgu globalnym) jest oczywiście próba niezgłaszania ich ponownie pod tą samą nazwą w funkcjach; po prostu nie używaj tam var . Zaleca się stosowanie odpowiednich reguł nazewnictwa opisowego . Dzięki nim trudno będzie skończyć ze zmiennymi nazwanymi takimi jak wartość (ten aspekt niekoniecznie jest powiązany z przykładem w pytaniu, ponieważ nazwa zmiennej mogła zostać podana dla uproszczenia).

  2. Jeśli funkcja może zostać ponownie wykorzystana w innym miejscu, a zatem nie ma gwarancji, że zmienna zewnętrzna faktycznie zdefiniowana w tym nowym kontekście, można użyć funkcji Eval . Działa wolno w tej operacji, więc nie jest zalecany do funkcji wymagających dużej wydajności:

    if (typeof variable === "undefined")
    {
        eval("var variable = 'Some value';");
    }
    
  3. Jeśli zmienna o zasięgu zewnętrznym, do której chcesz uzyskać dostęp, jest zdefiniowana w nazwanej funkcji, może być najpierw dołączona do samej funkcji, a następnie dostępna z dowolnego miejsca w kodzie - czy to z głęboko zagnieżdżonych funkcji, czy programów obsługi zdarzeń poza wszystko inne. Zauważ, że dostęp do właściwości jest znacznie wolniejszy i wymagałby zmiany sposobu programowania, więc nie jest to zalecane, chyba że jest to naprawdę konieczne: Zmienne jako właściwości funkcji (JSFiddle) :

    // (the wrapper-binder is only necessary for using variables-properties
    // via "this"instead of the function's name)
    var functionAsImplicitObjectBody = function()
    {
        function someNestedFunction()
        {
            var redefinableVariable = "redefinableVariable's value from someNestedFunction";
            console.log('--> functionAsImplicitObjectBody.variableAsProperty: ', functionAsImplicitObjectBody.variableAsProperty);
            console.log('--> redefinableVariable: ', redefinableVariable);
        }
        var redefinableVariable = "redefinableVariable's value from someFunctionBody";
        console.log('this.variableAsProperty: ', this.variableAsProperty);
        console.log('functionAsImplicitObjectBody.variableAsProperty: ', functionAsImplicitObjectBody.variableAsProperty);
        console.log('redefinableVariable: ', redefinableVariable);
        someNestedFunction();
    },
    functionAsImplicitObject = functionAsImplicitObjectBody.bind(functionAsImplicitObjectBody);
    functionAsImplicitObjectBody.variableAsProperty = "variableAsProperty's value, set at time stamp: " + (new Date()).getTime();
    functionAsImplicitObject();
    
    // (spread-like operator "..." provides passing of any number of arguments to
    // the target internal "func" function in as many steps as necessary)
    var functionAsExplicitObject = function(...arguments)
    {
        var functionAsExplicitObjectBody = {
            variableAsProperty: "variableAsProperty's value",
            func: function(argument1, argument2)
            {
                function someNestedFunction()
                {
                    console.log('--> functionAsExplicitObjectBody.variableAsProperty: ',
                        functionAsExplicitObjectBody.variableAsProperty);
                }
                console.log("argument1: ", argument1);
                console.log("argument2: ", argument2);
                console.log("this.variableAsProperty: ", this.variableAsProperty);
                someNestedFunction();
            }    
        };
        return functionAsExplicitObjectBody.func(...arguments);
    };
    functionAsExplicitObject("argument1's value", "argument2's value");
    
DDRRSS
źródło
0

Miałem ten sam problem nawet przy zmiennych globalnych. Odkryłem, że mój problem polega na tym, że zmienna globalna nie utrzymuje się między plikami html.

<script>
    window.myVar = 'foo';
    window.myVarTwo = 'bar';
</script>
<object type="text/html" data="/myDataSource.html"></object>

Próbowałem odwołać się do myVar i myVarTwo w załadowanym pliku HTML, ale otrzymałem niezdefiniowany błąd. Krótko mówiąc, odkryłem, że mogę odnieść się do zmiennych za pomocą:

<!DOCTYPE html>
<html lang="en">
    <!! other stuff here !!>
    <script>

        var myHTMLVar = this.parent.myVar

        /* other stuff here */
    </script>
</html>
Nathan Sutherland
źródło