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 bracesWe 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
- this includes
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 return1
the
return x
there refers to the x inside the local scope ofk()
- the value of
x
inside the brackets!
- the value of
We would then expect the second
console.log
to return2
the console has no idea that the
x
inside ofk()
existsit has no memory of the first log
- only the global
x
exists!
- only the global
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
*/