agrim mittal (Everything is a file)

Diving Deep into JavaScript (Part 1)

I recently got acquainted with some advanced concepts in JavaScript and believe me understanding these are vital if you are developing a large scale application on JavaScript. So, this post discusses some of those concepts in detail.

The strength of JavaScript is that you can do anything.
The weakness is that you will.
- Reg Braithwaite

Topics covered in this post:

  • Closures
  • Scopes
  • Hoisting

This is going to be a long post! So, stretch your neck and let the journey begin!

Closures

A closure is the combination of a function and the lexical environment within which that function was declared.

Before diving into closures we need to know what is a lexical environment.

What is lexical scoping?

Consider a function

function myFunc() {
    var myVar = 'Yay!'   // myVar is a local variable

    function myInnerFunc() {
        console.log(myVar)
    }

    myInnerFunc()
}

myFunc()      // Yay!

myFunc creates a local variable myVar and a function myInnerFunc. Both of these are only accessible inside the block of myFunc. However, myInnerFunc can access myVar!

This concept is called lexical scoping, which describes how a parser resolves variable names when functions are nested. Nested functions have access to variables declared in their outer scope.

What is a closure?

Let’s modify the above example a bit

function myFunc() {
    var myVar = 'Yay!'   // myVar is a local variable

    function myInnerFunc() {
        console.log(myVar)
    }

    return myInnerFunc
}

let a = myFunc()

console.log(a)   // ƒ myInnerFunc() { console.log(myVar) }

a()             // Yay!

Here, a reference to the function is returned to a.

In JavaScript, using a function inside another function is closure. Local variables of outer function remain accessible to the inner functions even after the parent function has returned.

Another definition of closures may be

A closure is a stack frame which is allocated when a function starts its execution, and not freed after the function returns.

In JavaScript, a function reference also has a secret reference to the closure it was created in.

A misconception may arise here, that the local variables are copied, they are not, instead, they are stored by reference.

function myFunc() {
    var myVar = 1   // myVar is a local variable

    function myInnerFunc() {
        console.log(myVar)
    }

    myVar++

    return myInnerFunc
}

myFunc()()        // 2

A closure in JavaScript is like keeping a copy of all the local variables, just as they were when the function exited.

So, before myFunc function exited, myVar was incremented and its value was stored as 2.

Global functions and closures

var increment, get, set   // global variables

function myFunc() {
    var myVar = 0     // local variable

    // The three global functions have access to same closure.
    increment = function() { myVar++ }
    get = function() { console.log(myVar) }
    set = function(newVar) { myVar = newVar }
}

myFunc()

get()         // 0
increment()
get()         // 1
set(4)
get()         // 4
increment()
get()         // 5

/* Again calling myFunc creates a new closure and 
old variables are overwritten by new functions with new closures
*/
myFunc()

get()        // 0

In JavaScript, whenever you declare a function inside another function, the inside function(s) is/are recreated again each time the outside function is called.

We can extend the above example to private methods,

let c = (function() {
    // Private variables
    let _privateVar = 0
    function _changeBy(x) {
        _privateVar += x
    }

    // Public variables
    return {
        increment: function() {
            _changeBy(1)
        },
        decrement: function() {
            _changeBy(-1)
        },
        get: function() {
            return _privateVar
        },
        set: function(x) {
            _privateVar = x
        }
    }
})()            // Immediately invoked function

console.log(c)  // {increment: ƒ, decrement: ƒ, get: ƒ, set: ƒ}

c.get()        // 0
c.set(1)
c.get()        // 1
c.increment()
c.get()        // 2
c.decrement()
c.get()        // 1 

Using closures in this way is known as the module pattern. We have created a single lexical environment that is shared by four functions: increment, decrement, get and set. Neither of _privateVar nor _changeBy is accessible to the world.

Scopes

Global Scope

let a = 'Tyrion'
let a = 'Cerci'  // Uncaught SyntaxError: Identifier 'a' has already been declared

var b = 'Boltons'
var b = 'Starks'  // Be careful!
console.log(b)   // Starks

Local Scope

Variables that are usable only in a specific part of the code are considered to be in a local scope. These variables are also called local variables.

Block Scope

{
    const a = 'Var 1'
}

console.log(a)      // Uncaught ReferenceError: a is not defined

Function Scope

function a() {
    const b = 'Winterfell'

    console.log(b)
}

a()                 // Winterfell

console.log(b)      // Uncaught ReferenceError: b is not defined

Functions do not have access to each other’s scopes!

function first() {
    const a = 'Heat'
}

function second() {
    first()

    console.log(a) // Uncaught ReferenceError: a is not defined
}

Hoisting

Hoisting is a mechanism where variables and function declarations are moved to the top of their scope before code execution.

This means no matter where we declare functions or variables, they are moved to the top of their scope!

Hoisting mechanism only moves the declaration. The assignments are left in place.

A slight heads up

console.log(typeof a) // undefined

console.log(a)        // ReferenceError: a is not defined

Undeclared variable is assigned undefined at execution and is also of type undefined.

ReferenceError is thrown when trying to access a previously undeclared variable.

Hoisting variables

var a

console.log(a)    // 1
a = 1

OR

var a, b

add(1, 2)     // 3

function add(a, b) {
    console.log(a + b)
}

All variable and function declarations are hoisted to the top of their scope. Also variable declarations are processed before any code is executed. However, undeclared variables do not exist until code assigning them is executed.

All undeclared variables are global variables.

Let’s consider a tricky example:

function trick() {
    a = 1
    var b = 2
}

trick()

console.log(a)   // 1
console.log(b)   // ReferenceError: b is not defined

Both a and b are hoisted to the top of their scopes. b is declared inside trick so it is not accessible outside its scope but because a is not declared inside trick so it is available to global scope!

ES5 way

The scope of a variable declared with var is its current execution context.

Case of global scope
console.log(a)  // undefined

var a = 'Yay!'

We expected ReferenceError but got undefined! This happens as JavaScript has hoisted the variables a. This is what the code above looks like to the interpreter:

var a

console.log(a)  // undefined

a = 'Yay!'

Because of this, a variable can be used before declaring it. However, we have to be careful because the hoisted variable is initialized with a value of undefined. This can introduce unintended behavior in the program and difficult to trace!

Case of function scope

Similar concept can be extended to function scopes:

function myFunc() {
    console.log(a)
    var a = 'Yay!'
}

myFunc()      // undefined

To avoid this we should declare and initialize the variable before using it.

The "use strict" enforcement

By enabling strict mode, we opt into a restricted variant of JavaScript that does not allow the usage of variables before they are declared.

'use strict'

console.log(a)   // ReferenceError: a is not defined
a = 'Yay!' 

ES6 way

ES6 introduced let and const.

Variables declared with let are block scoped not function scoped.

It means that the variable’s scope is bound to the block in which it is declared and not the function in which it is declared.

Let’s analyse let’s behaviour:

console.log(a)  // ReferenceError: a is not defined

let a = 'Yay!'

This ensures that we always declare our variables first.

However, we still need to be careful:

let a

console.log(a)  // undefined
a = 'Yay!'

Hence, we should declare then assign our variables before using them.

The const keyword allows immutable variables(one those cannot be modified once assigned). With const also, the variable is hoisted to top the block.

const a = 0

a = 1    // TypeError: Assignment to constant variable.

const behaves same as let

console.log(a)  // ReferenceError: a is not defined

const a = 'Yay!'

An interesting error arises if we try to do something like this:

const a

console.log(a) // SyntaxError: Missing initializer in const declaration
a = 0

Therefore, we need to both declare and initialize a constant variable before use.

Hoisting Functions

JavaScript functions can be loosely classified as the following:

  • Function declarations
  • Function expressions

JavaScript hoists function declarations, but not function expressions.

Function Declaration

If a function is declared but isn’t assigned to a variable, it will be hoisted to the top of its scope.

myFunc()     // Joy!

function myFunc() {
  console.log('Joy!')
}

Function Expression

A function expression, will not be hoisted.

myFunc()    // TypeError: myFunc is not a function

var myFunc = function () { 
  console.log('Sad!') 
}

The journey continues! I will talk about some more advanced concepts in JavaScript in future posts.

JavaScript is the only language that I’m aware of that people feel they don’t need to learn before they start using it.
- Douglas Crockford

À bientôt.