JavaScript: how does it work?

what actually happens when we use JavaScript?

·

5 min read

Intro

Before going through the following, this article covers:

  • the JavaScript Engine

    • (the interpreter that compiles JS code into machine language)
  • the JavaScript Runtime

    • (the JS Engine user and provider of additional functionalities)

Let's try to understand how JavaScript works with the help of some very nicely written articles from JavaScript Tutorial:

Execution context

What actually happens when we declare variables and functions, as well as call them?

When we declare a variable as such:

let x = 10

...the "JavaScript engine" uses an execution context made up of two phases in order to:

  • declare the existence of the variable x (the creation phase)

    • so this variable actually begins with a value of undefined

    • setup a space in memory (the memory heap) to store this variable

  • finally assign x a value (in this case, 10 ) (the execution phase)

(...the article also explains a smaller-scale version of execution contexts within local scopes)

Call stack

When we have a lot of execution contexts, leap-frogging from here to there and everywhere, what gets called first? Well, the JavaScript engine uses the call stack to ensure priority:

  • When we create a new local execution context, we add ("push") it to the call stack

  • When we add another context on top of that, we stack that on top of the call stack

  • When we finish with the latest context, we remove ("pop") it from the call stack

  • The last context on the stack gets popped off first, until all contexts get popped off

    • "Last in, first out" (LIFO)

Event loop

If everything is all so LIFO in the call stack, then JavaScript can do only one thing at a time! So, how do we get it to multi-task? Some functions might take a very long time to execute (think of a file download) and this can cause blockage! Just how do we make things more efficient?

By using special functions called callback functions:

  • we can let the web browser make use of other components

    • (e.g. the Web API and callback queue)
  • we can then allow some "slower" functions to by-pass the call stack of the JavaScript runtime

Hoisting

When we declare global variables, all variables act as though they appeared (hoisted) at the top of the code; functions get hoisted above the variables, so the code behaves in a rearranged fashion as such:

  • function declarations

  • variable declarations

  • function calls

Variable scopes

So, where does a variable live?

Global scope‌

  • A variable not inside any function exists in the global scope

    • We can access this variable from anywhere in the script
  • On a browser, this scope is the entire browser window

Local (lexical) scoping‌

  • A variable inside a function will exist only inside its curly braces

    • We cannot use this variable from outside of the braces!

    • We can only take its value outside if

      • something in the global scope calls the function

      • the function includes the value in its return statement

  • Block scoping occurs when

    • We declare a variable with the let keyword inside curly braces

      • this includes if blocks

Global scoping versus local scoping

If a variable appears in the global scope and then appears in a local scope, then what happens?

let x = 2

function k() {
  let x = 1
  return x
}

console.log(k()) // 1 (local scope)
console.log(x)  // 2 (global scope)
  • We would expect the first console.log to return 1

    • the return x there refers to the x inside the local scope of k()

      • the value of x inside the brackets!
  • We would then expect the second console.log to return 2

    • the console has no idea that the x inside of k() exists

    • it has no memory of the first log

      • only the global x exists!

So, as soon as another x gets declared inside a local scope, a variable such as x takes on a new context!

Memory leaks

As discussed in the article, weird stuff happens when we assign a value to a variable that we never declared (or "declare without a keyword"):

function getCounter() {
    counter = 10
    return counter
}

console.log(getCounter()); // outputs 10

In this case, the JavaScript engine:

  • looks for the counter variable in the local scope

    • finds none
  • looks for the counter variable in the global scope

    • finds none
  • creates a counter variable in the global scope (!!!)

In order to avoid this "weird problem" of memory leaks into the global scope, we would append this even weirder string literal at the top of the code, called use strict:

'use strict'

function getCounter() {
    counter = 10
    return counter
}

console.log(getCounter()); // outputs a ReferenceError

Closures

A function has a closure when it can access a variable outside of its scope - in this example, cat is available via this in printInfo:

const cat = {

   name: "Gus",
   color: "gray",
   age: 15,

   printInfo: function() {

      console.log("Name:", this.name, 
          "Color:", this.color, 
          "Age:", this.age
      ) 
      // prints correctly - "this" refers to cat

      nestedFunction = function() {
          console.log("Name:", this.name, 
              "Color:", this.color, 
               "Age:", this.age
          )
      }
      // loses cat's "this" scope and refers to the global scope

      nestedFunction()
   }

}

cat.printInfo() 
/* prints: 
    Name: window Color: undefined Age: undefined
*/

Yet, a nested function within a parent function won't have access to the variables accessible by the parent function! Instead, the nested function gets bound to the global scope (!!)

The article then explains a way to mitigate by using a helper variable:

const cat = {

  name: "Gus",
  color: "gray",
  age: 15,

  printInfo: function() {

    // helper variable
    const that = this;

    console.log("Name:", this.name, 
        "Color:", this.color, 
        "Age:", this.age
    ) 
    // prints correctly - "this" refers to cat

    nestedFunction = function() {
      console.log("Name:", that.name, 
          "Color:", that.color, 
          "Age:", that.age
      )    
    }
    // loses cat's "this" scope but we can use "that"

    nestedFunction()

  }

}

cat.printInfo() 
/* prints: 
Name: window Color: undefined Age: undefined
*/