Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't this assignment throw a ReferenceError?

Consider three cases, where both a and k are undefined:

if (a) console.log(1); // ReferenceError

and

var a = k || "value"; // ReferenceError

seems reasonable, but...

var a = a || "value"; // "value"

Why doesn't the last case throw a ReferenceError? Isn't a being referenced before it's defined?

like image 646
Wilson Luniz Avatar asked Jan 04 '23 20:01

Wilson Luniz


1 Answers

This is because of one of var's "features" called hoisting. Per the link:

Because variable declarations (and declarations in general) are processed before any code is executed, declaring a variable anywhere in the code is equivalent to declaring it at the top. This also means that a variable can appear to be used before it's declared. This behavior is called "hoisting", as it appears that the variable declaration is moved to the top of the function or global code. (emphasis mine)

So, for example:

console.log(a);
var a = "foo";

Instead of throwing a ReferenceError as you might expect, since a is referenced before it is defined, it logs undefined. This is because, as mentioned earlier, the declaration is processed first and essentially happens at the top, which means it's the same as:

var a;
console.log(a);
a = "foo";

The same goes for functions as mentioned earlier:

function foo() {
    console.log(a);
    var a = "foo";
}

That's the same as:

function foo() {
    var a;
    console.log(a);
    a = "foo";
}

To see why, look into the ECMAScript 2015 Language Specification:

13.3.2 Variable Statement

NOTE

A var statement declares variables that are scoped to the running execution context’s VariableEnvironment. Var variables are created when their containing Lexical Environment is instantiated and are initialized to undefined when created.

[...]

A variable defined by a VariableDeclaration with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the VariableDeclaration is executed, not when the variable is created. (emphasis mine)

From this, we can gather that the var declarations are created before any code is executed (in their lexical environment) and are scoped to the containing VariableEnvironment, which is either in the global scope, or in a function. They are initially given the value undefined. The next part explains that the value that the var is assigned to is the value of the right hand side when the declaration is executed, not when the variable is created.

This applies to your situation because in your example, a is referenced like it is in the example. Using the information earlier, your code:

var a = a || "value";

Can be rewritten as:

var a;
a = a || "value";

Remember that all declarations are processed before any code is executed. The JavaScript engine sees that there's a declaration, variable a and declares it at the top of the current function or global scope. It is then given the value undefined. Since undefined is falsey, a is assigned to value.

In contrast, your second example throws a ReferenceError:

var a = k || "value";

It can also be rewritten as:

var a;
a = k || "value";

Now you see the problem. Since k is never a declared anywhere, no variable with that identifier exists. That means, unlike with a in the first example, k is never declared and throws the ReferenceError because it is referenced before declaration.

But how do you explain var a = "123"; var a = a || "124"; // a = "123"?

From the ES2015 Language Specification again:

Within the scope of any VariableEnvironment a common BindingIdentifier may appear in more than one VariableDeclaration but those declarations collective define only one variable.

To put it plainly: variables can be defined in the same function or global scope more than once, but always define the same variable. Thus, in your example:

var a = "123";
var a = a || "124";

It can be rewritten as:

var a;
a = "123";
a = a || "124";

Declaring a in the same function or global scope again collectively only declares it once. a is assigned to "123", then it is assigned to "123" again because "123" is truthy.


As of ES2015, you should, in my opinion, no longer use var. It has function scoping and can cause unexpected assignments like those mentioned in the question. Instead, if you still want mutability, try using let:

let a = a || "value";

This will throw a ReferenceError. Even though all variables are hoisted, no matter which declarator you use (var, let, or const), with let and const it is invalid to reference an uninitialized variable. Also, let and const have block scope, not function scope. This is more clear and normal regarding other languages:

function foo() {
    {
        var a = 3;
    }
    console.log(a); //logs 3
}

Versus:

function foo() {
    {
        let a = 3;
    }
    console.log(a); //ReferenceError
}
like image 111
Andrew Li Avatar answered Jan 07 '23 15:01

Andrew Li