links: JS MOC


The (Not So) secret life of variables

When Can I use a variable

A variable is available to use within it’s scope even before you declare it.

getUserInfo()
 
function getUserInfo() {
	// some logic
}

Here you are able to call getUserInfo even before function declaration. The reason behind this is function hoisting. During the compile time JS engine hoists (basically make all declarations available to use in the enclosing scope)

The function hoisting is a special characteristic where the function’s reference is auto initialized to the identifier. In normal hoisting the values are auto initialized to undefined, but for function declaration, the value is auto initialized to it’s function reference. So getUserInfo() is possible

Hoisting: Declaration vs Expression

Function Hoisting only applies to function declaration but not function expression

getUserInfo()
 
var getUserInfo = function() {
	// some logic
}

In above example, the getUserInfo gets hoisted and auto initialized to undefined. so on line 1 you see TypeError, because the value of getUserInfo is still undefined and not referenced to the function.

Variable Hoisting

Consider this example

dosa = 'Onion Dosa'
console.log(dosa)
// Onion Dosa
 
var dosa = 'Ghee Dosa'

Though dosa isn’t declared by the time it accessed, it is available mainly for two reasons

  1. the identifier dosa is hoisted
  2. and it’s automatically initialized to undefined from the top of the scope.

Hoisting: Yet another metaphor

The hoisting is just a mental model rather than what’s actually happens in JS engine. In reality JS Engine does parse the code in single phase rather than a two phase.

The mental model of two phase and moving variables to top(hoisting) is useful to understand the program flow.

Re-declaration?

What happens when you re-declare a variable in JS program.

Consider the following example

var studentName = 'Subramanya'
console.log(studentName) // Subramanya
 
var studentName
console.log(studentName) // Subramanya

One might expect the re-declaration might reset it to undefined, but that’s not the case.

let’s visualize the above program in terms of hoisting metaphor

var studentName
var studentName
 
studentName = 'Subramanya'
console.log(studentName) // Subramanya
 
console.log(studentName) // Subramanya

Hoisting is all about registering variables, so when a variable studentName is already registered, it ignores the re-declared studentName.

But what happens when you re-declare a variable with different initialized value.

var cityName = 'Tokyo'
console.log(cityName) // Tokya
 
var cityName
console.log(cityName) // Tokyo
 
var cityName = undefined
console.log(cityName)
 
// Add initialization exlicitly
var cityName = 'Berlin'
console.log(cityName) // Berlin

So Basically when you re-declare a variable with an initialized value, the value gets reassigned (updated) to that value. The repeated declaration of variable even in terms of function remains same as do nothing operation

What about repeating a declaration within a scope using let or const?

let cityName = 'Tokyo'
console.log(cityName)
let cityName = 'Berlin'

It throws SyntaxError: Identifier ‘cityName’ has already been declared

It’s the same case when you mix let with var and const and make re-declaration. A syntax error is thrown

The reason for disallowing re-declaration is more of a Social Engineering rather than a technical decision. Many developers make bugs because of re declaring variables

Constants

The const keyword is more restricted than let

const empty; // SyntaxError: Missing initializer in const declaration

So basically there is a restriction to initialize const with a value.

const variables cannot be reassigned

const cityName = 'Tokyo'
console.log(cityName) // Tokyo
cityName = 'Berlin' // TypeError: Assignment to constanr variable

The cityName variable can’t be re-assigned because it’s declared as const

So if const declarations can’t be re-assigned, and const declarations always requires assignments, then we have clear technical reason for const to disallow any “re-declarations”

const cityName = 'Tokyo'
console.log(cityName) // Tokyo
const cityName = 'Berlin' // shouldn't be allowed

Loops

Let’s understand in terms of repeated execution of declaration statements in loops

var keepGoing = true
while(keepGoing) {
	let value = Math.random()
	if (value > 0.5) {
		keepGoing = false
	}
}

One might think that let value = Math.random() as re-declaration as while loop keeps executing the block. But it’s not, the reason is everytime a loop starts execution it’s block scope, it’s new execution context which means a new scope is created. So the value is declared only once.

What if we change value to var

var keepGoing = true
while (keepGoing) {
	var value = Math.random()
	if (value > 0.5) {
		keepGoing = false
	}
}

var allows re-declaration and not a block scoped. The var value will be registered along with keepGoing so there will be one value registered at global scope

One way to remember during loops is to strip var, let and const keywords from the code by the time you execute it.

If you mentally erase the declarator key words and try to process the code, it should help decide if and when re-declaration might occur

What about “re-declaration” with other loop forms, like for loop?

for (let i = 0; i < 3; i++) {
	let value = i * 10
	console.log(`${i}: ${value}`)
}

It should be clear that value is declared only once per scoped instance, but what about i is it being re-declared?

The scope of i belongs to for loop body, just like value, so no re-declaration

What about other for-loop forms?

for (let index in students) {
	// this is fine
}
for (let student of students) {
	// this is fine
}

The let is only declared once per iteration instance, so no re-declaration

How does const impacts loops?

var keepGoing = true
while (keepGoing) {
	const value = Math.random()
	if (value > 0.5) {
		keepGoing = false
	}
}

Since value is declared only once per iteration so no re-declaration problems here.

But what about for-loops?

for (const student of students) {
	// this is fine
}
 
for (let index in students) {
	// this is fine
}

But not the general for-loops

for(const i = 0; i < 3; i++) {
	// This is going to fail with TypeError
	// since i is being reassigned which const doesn't allow
}

Interestingly, if there is no re-assignment then there is no problem

var keepGoing = true;
 
for (const i = 0; keepGoing; /* nothing here */ ) {
    keepGoing = (Math.random() > 0.5);
    // ..
}

Uninitialized variables (aka, TDZ)

With var declaration, the variable is “hoisted” to the top of it’s scope, But it’s also automatically initialized with undefined, So that the variable can be accessed throughout the entire scope

However let and const are not quite same the same in this respect

Consider:

console.log(cityName) // ReferenceError
 
let cityName = 'Kadapa'

The result of the program is that a ReferenceError is thrown at first line, the error message say something like “Cannot access cityName before initialization”

What if we initialize on first line

cityName = 'Kadapa'
console.log(cityName) // ReferenceError
 
let cityName

We still get ReferenceError

The real question is how to initialize a uninitialized variable? For let/const the only way to initialize is with an assignment attached to it’s declaration statement.

An assignment itself is insufficient.

let cityName = 'Kadapa'
console.log(cityName)

Here we are initializing cityName by way of let declaration coupled with assignment

Alternatively:

let cityName
// Or
// let cityName = undefined
 
cityName = 'Kadapa'
 
console.log(cityName)
Note:
Let’s recall from earlier that var studentName is not same as var studentName = undefined, but here with let they behave same. The difference comes down to the face that var studentName automatically initializes at the top of the scope, where let studentName does not

The compiler ends up removing var/let/const declarators from program and replaces them with instructions at the top of each scope to register the appropriate identifiers.

So if we analyze the above example, we see that an additional naunce is that Compiler is also adding an instruction in the middle of the program, at the point where the variable studentName is declared, to handle the declaration’s auto initialization. We cannot use the variable prior to at any point initializing is occurring. The same goes for const as it is for let

The term coined by TC39 to refer to this period of time from the entering of a scope to auto-initialization of the variable occurs is Temporal Dead Zone (TDZ)

The TDZ is the time window where the variable exists, but is still uninitialized, and therefore cannot be accessed in any way. Only the execution of instructions left by Compiler at the point of the original declaration can do that initialization, After that moment TDZ is done and the variable is free to use for the rest of the scope.

A var also has technically has a TDZ, but it’s zero in length thus unobservable in programs. Only let and const have observable TDZ

By the way “temporal” in TDZ does indeed refer to time not position in code. Consider:

askQuestion() // ReferenceError
 
let studentName = 'chakri'
 
function askQuestion() {
	console.log(`${studentName} do you know?`)
}

Even though positionally the console.log(..) referencing studentName comes after let studentName declaration, timing wise askQuestion() is invoked before the let statement is encountered, with the studentName is still in it’s TDZ! hence the error

There’s a common misconception that let and const do not hoist, but it’s not true, both will hoist

The actual difference is the let/const does not automatically initialize at the beginning of the scope, the way var does. hoisting is about auto-registration of the variable but not auto-initialization

Consider this example to prove let/const do hoist (auto register at the top of the scope), courtesy of our friend shadowing

var studentName = "subramanya"
 
{
	console.log(studentName)
	// ???
	
	let studentName = "Mamatha"
	
	console.log(studentName)
	// Mamatha
}
 

What would the first console.log statement would print, one might think the studentName = "subramanya" as it’s the nearest available identifier.

But it will throw TDZ error, because let studentName did hoist (auto-registered at the top of the scope). What didn’t happen yet is the auto-initialization of that inner studentName it’s still uninitialized hence the TDZ violation

So to summarize, TDZ errors occurs because let and const declarations do hoist their declarations at the top of the scope, but unlike var they defer the auto-initialization of the variables until the moment in the code’s sequencing where the original declaration appeared. This window of time (hint: temporal) whatever the length is called TDZ

How to avoid TDZ errors?

By declaring let and const at the top of the scope and shrinking the TDZ window length to zero

Finally Initialized

Working with variables has much more naunce than it seems at first glance. Hoisting, Re-declaration and TDZ are the common sources of confusion for developers

Hoisting is generally cited as explicit mechanism of the JS engine, but it’s more of a metaphor to describe the various ways JS handles variable declarations during compilation, But even as a metaphor hoisting offers useful structure for thinking about the life-cycle of a variable — when it’s created, when it’s available and when it goes away

Declaration and re-declaration of variables tend to cause confusion when thought of as an runtime operations, but if you shift to compile time thinking of these operations, the quirks and shadows diminish

The TDZ error is strange and frustrating, Fortunately TDZ is straight forward to avoid, if you place let/const at the top f the scope


tags: javascript , ydkjs , variables, scope