Prototypal inheritance
With the exception of null
and undefined
, each primitive data type has a
prototype, a corresponding object wrapper that provides methods for working
with values. When a method or property lookup is invoked on a primitive,
JavaScript wraps the primitive behind the scenes and calls the method or
performs the property lookup on the wrapper object instead.
For example, a string literal has no methods of its own, but you can call the
.toUpperCase()
method on it thanks to the corresponding String
object
wrapper:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
This is called prototypal inheritance—inheriting properties and methods from a value's corresponding constructor.
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
You can create primitives using these constructors, instead of just defining
them by their value. For example, using the String
constructor creates a
string object, not a string literal: an object that not only contains our string
value, but all the inherited properties and methods of the constructor.
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
For the most part, the resulting objects behave as the values we've used to
define them. For example, even though defining a number value using the
new Number
constructor results in an object containing all the methods and
properties of the Number
prototype, you can use mathematical operators on
those objects just as you would on number literals:
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
You'll very rarely need to use these constructors, because JavaScript's built-in prototypal inheritance means they provide no practical benefit. Creating primitives using constructors can also lead to unexpected results, because the result is an object, not a simple literal:
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
This can complicate the use of strict comparison operators:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
Automatic semicolon insertion (ASI)
While parsing a script, JavaScript interpreters can use a feature called automatic semicolon insertion (ASI) to try to correct instances of omitted semicolons. If the JavaScript parser encounters a token that isn't allowed, it tries to add a semicolon before that token to fix the potential syntax error, as long as one or more of the following conditions is true:
- That token is separated from the previous token by a line break.
- That token is
}
. - The previous token is
)
, and the inserted semicolon would be the ending semicolon of ado
…while
statement.
For more information, refer to the ASI rules.
For example, omitting semicolons after the following statements won't cause a syntax error because of ASI:
const myVariable = 2
myVariable + 3
> 5
However, ASI can't account for multiple statements on the same line. If you write more than one statement on the same line, make sure to separate them with semicolons:
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
ASI is an attempt at error correction, not a kind of syntactic flexibility built into JavaScript. Make sure to use semicolons where appropriate so you don't rely on it to produce correct code.
Strict mode
The standards that govern how JavaScript is written have evolved far beyond anything considered during the early design of the language. Every new change to JavaScript's expected behavior must avoid causing errors in older websites.
ES5 addresses some long-standing issues with JavaScript semantics without
breaking existing implementations by introducing "strict mode," a way to opt
into a more restrictive set of language rules for either an entire script or an
individual function. To enable strict mode, use the string literal
"use strict"
, followed by a semicolon, on the first line of a script or
function:
"use strict";
function myFunction() {
"use strict";
}
Strict mode prevents certain "unsafe" actions or deprecated features, throws
explicit errors in place of common "silent" ones, and prohibits the use of
syntaxes that might collide with future language features. For example, early
design decisions around variable scope
made it more likely for developers to mistakenly "pollute" the global scope when
declaring a variable, regardless of the containing context, by omitting the
var
keyword:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
Modern JavaScript runtimes can't correct this behavior without the risk of breaking any website that relies on it, either mistakenly or deliberately. Instead, modern JavaScript prevents it by letting developers opt into strict mode for new work, and enabling strict mode by default only in the context of new language features where they won't break legacy implementations:
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
You must write "use strict"
as a
string literal.
A template literal
(use strict
) won't work. You must also include "use strict"
before any
executable code in its intended context. Otherwise, the interpreter ignores it.
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
By-reference, by-value
Any variable, including properties of an object, function parameters, and elements in an array, set, or map, can contain either a primitive value or a reference value.
When a primitive value is assigned from one variable to another, the JavaScript engine creates a copy of that value and assigns it to the variable.
When you assign an object (class instances, arrays, and functions) to a variable, instead of creating a new copy of that object, the variable contains a reference to the object's stored position in memory. Because of this, changing an object referenced by a variable changes the object being referenced, not just a value contained by that variable. For example, if you initialize a new variable with a variable containing an object reference, then use the new variable to add a property to that object, the property and its value are added to the original object:
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
This is important not only for altering objects, but also for performing strict
comparisons, because strict equality between objects requires both variables to
reference the same object to evaluate to true
. They can't reference
different objects, even if those objects are structurally identical:
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
Memory allocation
JavaScript uses automatic memory management, meaning that memory doesn't need to be explicitly allocated or deallocated during the course of development. While the details of JavaScript engines' approaches to memory management are beyond the scope of this module, understanding how memory is allocated provides useful context for working with reference values.
There are two "areas" in memory: the "stack" and the "heap." The stack stores static data—primitive values and references to objects—because the fixed amount of space needed to store this data can be allocated before the script executes. The heap stores objects, which need dynamically-allocated space because their size can change during execution. Memory is freed by a process called "garbage collection," which removes objects with no references from memory.
The main thread
JavaScript is a fundamentally single-threaded language with a "synchronous" execution model, meaning it can execute only one task at a time. This sequential execution context is called the main thread.
The main thread is shared by other browser tasks, such as parsing HTML, rendering and re-rendering parts of the page, running CSS animations, and handling user interactions ranging from the simple (like highlighting text) to the complex (like interacting with form elements). Browser vendors have found ways to optimize the tasks performed by the main thread, but more complex scripts can still use too much of the main thread's resources and impact overall page performance.
Some tasks can be executed in background threads called Web Workers, with some limitations:
- Worker threads can only act on standalone JavaScript files.
- They have severely reduced or no access to the browser window and UI.
- They're limited in how the can communicate with the main thread.
These limitations make them ideal for focused, resource-intensive tasks that might otherwise occupy the main thread.
The call stack
The data structure used to manage "execution contexts"—the code being actively executed—is a list called the call stack (frequently just "the stack"). When a script is first executed, the JavaScript interpreter creates a "global execution context" and pushes it to the call stack, with statements inside that global context executed one at a time, from top to bottom. When the interpreter encounters a function call while executing the global context, it pushes a "function execution context" for that call onto the top of the stack, pauses the global execution context, and executes the function execution context.
Each time a function is called, the function execution context for that call is pushed onto the top of the stack, just above the current execution context. The call stack operates on a "first in, first out" basis, meaning that the most recent function call, which is highest in the stack, is executed and continues until it resolves. When that function is complete, the interpreter removes it from the call stack, and the execution context that contains that function call becomes the highest item in the stack again and resumes execution.
These execution contexts capture any values necessary to their execution. They
also establish the variables and functions available within the scope of the
function based on its parent context, and determine and set the value of the
this
keyword in the context of the function.
The event loop and callback queue
This sequential execution means that asynchronous tasks that include callback
functions, such as fetching data from a server, responding to user interaction,
or waiting for timers set with setTimeout
or setInterval
, would either block
the main thread until that task is complete, or unexpectedly interrupt the
current execution context the moment the callback function's execution context
is added to the stack. To address this, JavaScript manages asynchronous tasks
using an event-driven "concurrency model" made up of the "event loop" and the
"callback queue" (sometimes referred to as the "message queue").
When an asynchronous task is executed on the main thread, the callback function's execution context is placed in the callback queue, not on top of the call stack. The event loop is a pattern sometimes called a reactor, which continuously polls the status of the call stack and the callback queue. If there are tasks in the callback queue and the event loop determines that the call stack is empty, tasks from the callback queue are pushed to the stack one at a time to be executed.