When I was first learning JavaScript I would come across posts about how weird (or bad) the language was, and since it was the first programming language I learned and didn’t have anything else to compare it with, I didn’t understand why some people had a negative opinion of the language.
This is an attempt to assemble a list of those JavaScript quirks that give it a bad wrap, and were confusing to me when I was learning, as well as key concepts that are vital to understand in order to write good (i.e. working) JavaScript code. My goal is to create a concise list that would be helpful for those learning the language to reference in order to understand some of the “weird” behavior of JavaScript, and how to use the language more effectively while not having to learn these things “the hard way”, or learning them way too late. The list is meant to be an overview to make the student aware of these things and serve as a launching point for further exploration and research.
NOTE: This list is a work in progress and constantly expanding. It will be updated and is under construction, so if there are other quirks or important concepts that you would like to see included that would help one write more informed and well-written JavaScript code, drop a note in the comments section. It would be great to have a one stop shop that has as many of the quirks and confusing things about JavaScript in one place for people who are learning to have as a reference.
Table of Contents:
THE CREATION AND EXECUTION PHASE:
- When the JavaScript Engine runs in the browser, it runs in two phases – a Creation Phase and an Execution Phase.
- A Creation Phase and Execution Phase is run every time the JavaScript for the app initially loads to create the Global Execution Context, and also runs for each successive call to any function to create that function’s Execution Context.
Terms:
- Creation Phase: All variable names (including the built-in
this
variable), objects and functions are created and stored in memory for use in the Execution Phase. For example, on an app initialization (first load), the global window object is created and variable names, as well as functions in the code are created, stored in memory space and attached to the global window object. A Global Execution Context is created and the this
keyword is created and setup. In addition, an outer reference to the immediately outer Execution Context is set up which allows access to variables and objects in that context (In this case there wouldn’t be an outer context, since the global context is the top most level).
- Execution Phase: The JavaScript code is then run and executed, line by line, without stopping or pausing.
- Execution Context: The environment that is created during the Creation Phase that the JavaScript code runs inside of during the Execution Phase. It defines the variables, objects, functions and function arguments which are available and accessible, as well as the scope chain, and the
this
value.
*A new Execution Context is created for every function that runs in JavaScript which entails running through a Creation and Execution Phase for it, and setting up an outer reference to the immediately surrounding Execution Context which allows access to the outer context’s variables and objects (see Scope Chain below for more details).
- Execution Stack: A model or data structure in the JavaScript engine which keeps track of the order of the execution of code and it’s current Execution Context. Every time a function is called in the code, a new Execution Context (or Call Stack) is created and placed at the top of the Stack. JavaScript is a single threaded language (which means it runs one call stack at a time). When the execution of the function is completed, that Execution Context is “popped” (removed) off the top of the Stack, and the code of the previous Execution Context on the Stack beneath it is run, picking up from where it left off from before the code from the popped Execution Context began running. (Further resources: see this great Youtube video lecture on the Call Stack and the Event Loop)
HOISTING:
- Refers to the storing of functions and variables in memory by the JavaScript engine during the Creation Phase (see above) before the code actually runs. All variables are initially stored and set to a value of ‘undefined’, but all functions are stored complete in memory (including the declaration of their name as an identifier for a space in memory to look, and their value (the body of the function, etc.)).
- This is why you can define a function in your code AFTER you actually call it, but variables will return undefined if they are referenced before their declaration in the code.
Example:
// Calling the function in the code before it's defined:
hoistedFunction();
hoistedFunction() {
console.log("The function ran and worked!");
}
In the above Example, the call to hoistedFunction()
will log "The function ran and worked!"
even though it is called before it is defined. This is because during the Creation Phase, all functions defined in the code are “hoisted” into memory and made available before the code actually is run in the Execution Phase. Note that this is different from variables, which are initially set to undefined
and cannot be referenced before they are assigned in the code.
THE SCOPE CHAIN:
- If a variable is used in a function and a value is not found for it inside that function’s scope or block, then JavaScript will look for it’s value in the function’s outer environment context. If the value is not found there, then it will search the environment of the next outer context, and keep going all the way to the global environment to look for the value for the variable.
- It should be noted that looking for an outer scoped variable’s value stops when the first match is found.
- It’s possible to access a variable directly on the global scope with
window.[variable name]
.
- Lexical Scope: Scope that is assigned based on where variable and function declarations are written in the code (i.e. functions written inside other function body blocks, variables declared in the global space outside of any function blocks, or inside a function’s scope because they are declared in the body, etc.).
Example of traversing the scope chain:
// a variable created at the top level execution context (global):
const globalVar = "from top level";
// Execution Context is created for initial function:
const exampleFunction = function() {
const outerVar = "from outer context.";
// A new Execution Context is created when this inner function runs, but it has access to the immediate outer context it's created in, and also the outer context of it's outer context up the scope chain to the top global level, etc.:
(function() {
console.log(outerVar);
console.log(globalVar);
})();
}
exampleFunction();
// When called, the function logs "from outer context" and then "from top level" - the JS Engine went up the scope chain to find outerVar and globalVar and look for it's value all the way up to the top level global context since they were not defined and assigned a value in the inner function block.
*NOTE THIS QUIRK: When not in strict mode (‘use strict’), if a variable is not declared with var/let/const but assigned a value, then a variable of that name will be automatically created and implicitly declared in the global scope.
Example:
undeclared = 5;
// A global variable undeclared is created and assigned the value of 5 automatically by JavaScript.
NOTE: If 'use strict' is implemented, then a ReferenceError will be thrown and automatic declaration will not occur.
eval():
- Built in function in JavaScript that takes a string as an argument, and treats the contents of the string as code that was authored code at that point in the program, consequently altering the corresponding Lexical Scope.
- Can be used to execute dynamically created code (that’s not
hard coded initially).
In other words, you can dynamically generate code that is not hard coded at author time and eval() will inject it as if it were hard coded at author time – this is a way to cheat Lexical Scope (code that is scoped based on where it was authored), but has performance issues and is bad practice.
- If a string of code that
eval(..)
executes contains variable or function declarations, the existing lexical scope in which the eval(..)
resides will be modified.
- Note from reference book listed below: “The use-cases for dynamically generating code inside your program are incredibly rare, as the performance degradations are almost never worth the capability.”
Reference: You Don’t Know JS: Scope & Closures, Chaper 2.
PRIMITIVE TYPES:
Terms:
- Type: A category identifier for a piece of data which can be used by the JavaScript engine to determine how to handle the piece of data and what operations can be performed with it. For example, a Number type is assigned to a piece of data that is represented by an integer. Since the data has a type of Number, JavaScript knows that it can do things like arithmetic with it and another piece of data of the same type, etc.
- Primitive Type: Represents a single value in JavaScript that is not an Object type. (Everything in JavaScript is either a Primitive or an Object).
- DYNAMIC TYPING: JavaScript is a dynamically typed language. The engine figures out and determines what type of data a variable holds (without having to declare it explicitly) while the code is running. It’s possible for a variable to hold different types of values while the code is running. You don’t specify what type of data is in a variable (you just use
var
/const
/let
, and JavaScript figures out what it is – a Number, Boolean, String, etc.).
6 Primitive Types:
- Undefined: lack of existence (don’t use this or set variables to this)
- Null: lack of existence – use this to set variables to nothing (let the engine use undefined). Null is not coerced by JS to 0 for comparisons, but coerced to 0 in other contexts (i.e. with Number() function).
*What is the difference between NULL and UNDEFINED?
null
is read by JavaScript to mean that there is no value, but the developer intends it that way and set it to null
explicitly (i.e. let x = null;
), whereas undefined
may throw an error as being an unintended absence of value from not assigning anything to the variable (i.e. let x;
), and then accessing it (i.e. console.log(x);
), or by simply not declaring it at all in the first place. Setting a value to undefined
explicitly is not considered good practice. If you want a value to be assigned as undefined or empty, then set it to null
in the code.
- Boolean: A primitive that has a value of true or false.
- Number: An Integer or floating point number (decimal), i.e. 5 or 5.23 etc.
- String: Sequence of characters inside quotes (single or double).
- Symbol (used in ES6 ): May not be supported by some browsers.
THE EVENT LOOP:
Terms:
- Event Loop: A constantly running process that checks a Task Queue for any callbacks registered which are associated with asynchronous operations or Event Listeners, and pushes them to the Execution Stack whenever it is empty.
- Task Queue: A built-in feature of the Event Loop that keeps track of and stores registered callbacks for any asynchronous operatons (network requests for example) or Event Listeners (for a ‘click’ Event when a user clicks a specified button for example) which are ready to be run and awaiting a push to the Call Stack from the Event Loop.
- Web API: Features and methods that come built-in from the browser and that are provided and made accessible to the JavaScript Runtime Engine so you can access them in your JavaScript code. Examples are multi-threaded operations made available by Web API methods such as setTimeout() or AJAX requests over the wire using the XHR (XmlHttpRequest) Object provided by the browser. Javascript is a single-threaded language, so these features and methods allow access to additional threads which can be used to perform longer-running operations while not blocking the running of JavaScript code.
- Thread: A line (or “thread”) of instructions sent to a processor from an application. The set of instructions can be abandoned and come back to later where the processor left off with them last and then complete them (called context switching).
- Execution Stack (aka Call Stack, or just Stack): see Creation and Execution Phase entry above).
- Callback: a function that is registered to run when an asynchronous operation completes. These are collected in the Task Queue, which the Event Loop constantly checks to push them to the Stack to run.
- Event Listener: A feature which can be accessed in JavaScript code with
[element].addEventListener([event], [callback])
that registers a callback function to be run when a specified event occurs. The browser throws events that can occur which are associated with HTML elements in the DOM as the user interacts with the application (i.e. a ‘click’ event is thrown by the browser when a user clicks on a button element on the page). The callback registered is then pushed to the Task Queue when the event occurs and the Event Loop will see it and run the callback.
What the Event Loop does: It’s job is to look at the Task Qeue and the Execution Stack in the JavaScript Runtime and if the Stack is empty, and there is anything (i.e. any callbacks registered) in the Task Queue, then it pushes that callback function to the Execution Stack for JavaScript to run it.
CLOSURES:
- A closure is a feature of JavaScript which enables a function that is called outside of the scope it was created in (and after that scope’s execution context is cleared from the Stack) to have access to variables and values that were created in it’s original lexical scope (the function block where it was defined).
- Basically, it’s an encapsulation of values and variables from a scope where a function was defined, so that when that function is passed as a value and called outside of that scope (i.e. returned and used elsewhere in the code), those variables and values remain accessible to it and their values in tact and defined as they were in the original scope.
- Closures are a commonly used feature whenever you want to pass functions as values to be called at a later time in the code which maintain references to variables in the scope they were created in, and are also used in creating JavaScript Modules, for example, where you can expose methods on a returned object that have references to private, protected, internally defined and scoped variables by invoking a wrapper function which creates a closure, encapsulating those variable values which the exported object methods can reference.
- IIFE’s (Immediately Invoked Function Expressions) can be used to create a scope and consequently a closure (this can be useful in a for loop, for example to capture the value of i).
Example Use Case with Asynchronous operation in a for loop using a Closure to capture the value of i:
for (var i = 0; i <= 5; i++) {
(function(i) {
fetch(`/page${i}`)
.then(() => {
console.log(`fetched page ${i}`);
});
})(i);
}
// The value of i will be encapsulated by the IIFE on each loop iteration and a reference maintained to it in the asynchronous callback (otherwise the value of i would always be the terminal value since the async function callback runs after the loop is finished and references the value of i at that time.
REVEALING MODULE PATTERN Use Case:
function userModule() {
const username = "Brent";
const eyeColor = "brown";
function username() {
console.log( username );
}
function eyeColor() {
console.log( eyeColor );
}
return {
username: username,
eyeColor: eyeColor
};
}
// Invoke the function to create a closure:
const user = userModule();
user.username(); // "Brent"
user.eyeColor(); // "brown"
// The userModule invocation exposes functions in that module that utilize closures to retain references to private protected variables and values which can be accessed when the exposed functions are called outside of the context they were defined in.
‘USE STRICT’:
- By typing
'use strict;'
in your code, this changes JavaScript’s un-opinionated, loose rules and flexible behavior by preventing type coercion and requiring more explicit syntax.
- Considered good practice to implement in production code in order to catch potential errors.
Examples of behavior with ‘use strict’:
'use strict';
a = 1; // throws an error since var, let, const is not used to declare it.
17 = '17'; // will throw an error since type coercion is turned off and the string '17' will not be converted to a number or vice versa.
// When calling a function the shorthand way (instead of using fn.call()), this is set to undefined in strict mode:
fn("arg"); // this is undefined.
// Normally when not using strict mode, this is set to the window object in a shorthand function call.
// See this article recommended by Dan Abramov:
Understanding Javascript Invocation and "this" by Yehuda Katz
MISC:
- Everything in JavaScript is an Object or Primitive data type.
- JavaScript compiles (translates the human readable code to machine code that the computer can understand and execute) just before execution, as opposed to other languages which are compiled well before.
External Resources/Further Reading:
- You Don’t Know JS: Up & Going by Kyle Simpson – an excellent book series on Javascript to gain a more sophisticated understanding of how the language works – but written in a way that’s not to dense or difficult to digest. E-book format is free to read online.
- wtfjs.com – Funny website with many examples of strange JavaScript behavior.
- Good article on Scope and THIS – from Digital Web Magazine by Mike West.
…More Items Coming… This list will continue to be updated.
Items planned for future updates:
- Type Coercion
- truthy and falsy concept and rules
- Logical Operators
- Comparisons
- This keyword
- New keyword
- Prototypal Inheritance
- The Prototype Chain
- ES6 Classes
- Promises
- Synchronous vs. Asynchronous execution
- Web APIs that add features and asynchronous functionality to JavaScript