JavaScript is a cross-platform, prototype-based, object-oriented scripting language. The core language of JavaScript is standardized by the ECMA TC39 committee as a language named ECMAScript.

Among other things, ECMAScript defines:

  • Language syntax (parsing rules, keywords, control flow, object literal initialization, …​)

  • Error handling mechanisms (throw, try…​catch, ability to create user-defined Error types)

  • Types (boolean, number, string, function, object, …​)

  • A prototype-based inheritance mechanism

  • Built-in objects and functions, including JSON, Math, Array methods, parseInt, decodeURI, etc.

  • Strict mode

  • A module system

  • Basic memory model

The ECMAScript specification does not describe the Document Object Model (DOM), which is standardized by the World Wide Web Consortium (W3C) and/or WHATWG (Web Hypertext Application Technology Working Group).
"ECMAScript" is the term for the language standard, but "ECMAScript" and "JavaScript" can be used interchangeably.
TypeScript stands in an unusual relationship to JavaScript, that offers all of JavaScript’s features, and an additional layer on top of these: TypeScript’s type system.

1. Grammar and types

JavaScript is case-sensitive and uses the Unicode character set.

const Hello世界 = 'Hello World!';
console.log(Hello世界); // logs "Hello World!"
console.log(hello世界); // Uncaught ReferenceError: hello世界 is not defined

A semicolon (;) is not necessary after a statement if it is written on its own line. But if more than one statement on a line is desired, then they MUST be separated by semicolons.

const Hello世界 = 'Hello World!'; console.log(Hello世界) // logs "Hello World!"

1.1. Comments

The syntax of comments is the same as in C++ and in many other languages:

// a one line comment

/* this is a longer,
 * multi-line comment
 */

1.2. Variable declarations

JavaScript has three kinds of variable declarations.

  • var

    Declares both local and global variables, depending on the execution context, optionally initializing it to a value.

  • let

    Declares a block-scoped, local variable, optionally initializing it to a value.

  • const

    Declares a block-scoped, read-only named constant.

Variables should always be declared before they are used. JavaScript used to allow assigning to undeclared variables, which creates an undeclared global variable.

If a variable is declared without an initializer, it is assigned the value undefined.

let x;
console.log(x); // logs "undefined"

1.3. Scopes

A variable may belong to one of the following scopes:

  • Global scope: The default scope for all code running in script mode.

  • Module scope: The scope for code running in module mode.

  • Function scope: The scope created with a function.

  • Block scope: The scope created (let, const) with a pair of curly braces (a block).

When you declare a variable outside of any function, it is called a global variable, because it is available to any other code in the current document. When you declare a variable within a function, it is called a local variable, because it is available only within that function.

  • Global variables are in fact properties of the global object.

  • In web pages, the global object is window, so you can read and set global variables using the window.variable syntax.

  • In all environments, the globalThis variable (which itself is a global variable) may be used to read and set global variables. This is to provide a consistent interface among various JavaScript runtimes.

Blocks only scope let and const declarations, but not var declarations.

{
  var x = 1;
}
console.log(x); // 1
{
  const x = 1;
}
console.log(x); // ReferenceError: x is not defined

var-declared variables are hoisted, meaning the variable can be referred anywhere in its scope, even if its declaration isn’t reached yet.

console.log(x === undefined); // true
var x = 3;

Same as:

var x;
console.log(x === undefined); // true
x = 3;

1.4. Data types

The latest ECMAScript standard defines eight data types:

  • Seven data types that are primitives:

    1. Boolean. true and false.

    2. null. A special keyword denoting a null value. (Because JavaScript is case-sensitive, null is not the same as Null, NULL, or any other variant.)

    3. undefined. A top-level property whose value is not defined.

    4. Number. An integer or floating point number. For example: 42 or 3.14159.

    5. BigInt. An integer with arbitrary precision. For example: 9007199254740992n.

    6. String. A sequence of characters that represent a text value. For example: "Howdy".

    7. Symbol. A data type whose instances are unique and immutable.

  • and Object

JavaScript is a dynamically typed language, which means that data types are automatically converted as-needed during script execution.

let answer = 42;
answer = "Thanks for all the fish!";

x = "The answer is " + 42; // "The answer is 42"
y = 42 + " is the answer"; // "42 is the answer"
z = "37" + 7; // "377"

"37" - 7; // 30
"37" * 7; // 259

// An alternative method of retrieving a number from a string is with the `+` (unary plus) operator:
// Note: the parentheses are added for clarity, not required.
"1.1" + "1.1"; // '1.11.1'
(+"1.1") + (+"1.1"); // 2.2

1.5. Literals

An array literal is a list of zero or more expressions, each of which represents an array element, enclosed in square brackets ([]).

const coffees = ["French Roast", "Colombian", "Kona"];
  • If you put two commas in a row in an array literal, the array leaves an empty slot for the unspecified element. The following example creates the fish array:

    const fish = ["Lion", /* empty */, "Angel"];
    console.log(fish);
    // [ 'Lion', <1 empty item>, 'Angel' ]

    Note that the second item is "empty", which is not exactly the same as the actual undefined value. When using array-traversing methods like Array.prototype.map, empty slots are skipped. However, index-accessing fish[1] still returns undefined.

    const fish = ["Lion", /* empty */, "Angel"];
    fish.map(x => console.log(x));
    // Lion
    // Angel
  • If you include a trailing comma at the end of the list of elements, the comma is ignored.

    // Only the last comma is ignored.
    const myList = ["home", /* empty */, "school", /* empty */,];

Integer and BigInt literals can be written in decimal (base 10), hexadecimal (base 16), octal (base 8) and binary (base 2).

  • A decimal integer literal is a sequence of digits without a leading 0 (zero).

  • A leading 0 (zero) on an integer literal, or a leading 0o (or 0O) indicates it is in octal.

  • A leading 0x (or 0X) indicates a hexadecimal integer literal.

  • A leading 0b (or 0B) indicates a binary integer literal.

  • A trailing n suffix on an integer literal indicates a BigInt literal. The BigInt literal can use any of the above bases. Note that leading-zero octal syntax like 0123n is not allowed, but 0o123n is fine.

    0, 117, 123456789123456789n             (decimal, base 10)
    015, 0001, 0o777777777777n              (octal, base 8)
    0x1123, 0x00111, 0x123456789ABCDEFn     (hexadecimal, "hex" or base 16)
    0b11, 0b0011, 0b11101001010101010101n   (binary, base 2)

A floating-point literal can have the following parts:

[digits].[digits][(E|e)[(+|-)]digits]
  • An unsigned decimal integer,

  • A decimal point (.),

  • A fraction (another decimal number),

  • An exponent (e or E).

    3.1415926
    .123456789
    -.123456789 // -0.123456789
    3.1E+12
    .1e-23
Note that the language specification requires numeric literals to be unsigned. Nevertheless, code fragments like -123.4 are fine, being interpreted as a unary - operator applied to the numeric literal 123.4.

An object literal is a list of zero or more pairs of property names and associated values of an object, enclosed in curly braces ({}).

  • Object property names can be any string, including the empty string. If the property name would not be a valid JavaScript identifier or number, it must be enclosed in quotes.

  • Property names that are not valid identifiers cannot be accessed as a dot (.) property.

    const unusualPropertyNames = {
      '': 'An empty string',
      '!': 'Bang!'
    }
    console.log(unusualPropertyNames.'');   // SyntaxError: Unexpected string
    console.log(unusualPropertyNames.!);    // SyntaxError: Unexpected token !
  • Instead, they must be accessed with the bracket notation ([]).

    console.log(unusualPropertyNames[""]); // An empty string
    console.log(unusualPropertyNames["!"]); // Bang!
  • Object literals support a range of shorthand syntaxes that include setting the prototype at construction, shorthand for foo: foo assignments, defining methods, making super calls, and computing property names with expressions.

    const obj = {
      // __proto__
      __proto__: theProtoObj,
      // Shorthand for 'handler: handler'
      handler,
      // Methods
      toString() {
        // Super calls
        return "d " + super.toString();
      },
      // Computed (dynamic) property names
      ["prop_" + (() => 42)()]: 42,
    };

A regex literal is a pattern enclosed between slashes: /pattern/flags.

const re1 = /ab+c/; // new RegExp("ab+c");
const re2 = /\w+\s/g; // new RegExp("\\w+\\s", "g");

A string literal is zero or more characters enclosed in double (") or single (') quotation marks. A string must be delimited by quotation marks of the same type (that is, either both single quotation marks, or both double quotation marks).

'foo'
"bar"
'1234'
'one line \n another line'
"Joyo's cat"
"He read \"The Cremation of Sam McGee\" by R.W. Service.";

Template literals are literals delimited with backtick (`) characters, allowing for multi-line strings, string interpolation with embedded expressions, and special constructs called tagged templates.

`string text`

`string text line 1
 string text line 2`

`string text ${expression} string text`

tagFunction`string text ${expression} string text`

2. Control flow and error handling

The most basic statement is a block statement, which is used to group statements. The block is delimited by a pair of curly braces:

{
  statement1;
  statement2;
  // …
  statementN;
}

2.1. Conditional statements

A conditional statement is a set of commands that executes if a specified condition is true. JavaScript supports two conditional statements: if…​else and switch. The following values evaluate to false (also known as Falsy values):

  • the keyword false

  • undefined, null

  • 0, -0, 0n

  • NaN

  • the empty string ("")

All other values—including all objects—evaluate to true when passed to a conditional statement.

A falsy (sometimes written falsey) value is a value that is considered false when encountered in a Boolean context.

Note: Do not confuse the primitive boolean values true and false with the true and false values of the Boolean object!

For example:

const b = new Boolean(false);
if (b) {
  // this condition evaluates to true
}
if (b == true) {
  // this condition evaluates to false
}
  • Use the if statement to execute a statement if a logical condition is true. Use the optional else clause to execute a statement if the condition is false. Use the optional else if to have multiple conditions tested in sequence.

    if (condition1) {
      statement1;
    } else if (condition2) {
      statement2;
    } else if (conditionN) {
      statementN;
    } else {
      statementLast;
    }
  • A switch statement allows a program to evaluate an expression and attempt to match the expression’s value to a case label. If a match is found, the program executes the associated statement.

    switch (expression) {
      case label1:
        statements1;
        break;
      case label2:
        statements2;
        break;
      // …
      default:
        statementsDefault;
    }

2.2. Error handling

  • Use the throw statement to throw an exception. A throw statement specifies the value to be thrown: throw expression.

    throw "Error2"; // String type
    throw 42; // Number type
    throw true; // Boolean type
    throw {
      toString() {
        return "I'm an object!";
      },
    };
    throw new Error("Whoops!");

    While it is common to throw numbers or strings as errors, it is frequently more effective to use one of the exception types specifically created for this purpose: ECMAScript exceptions and DOMException.

  • The try…​catch statement marks a block of statements to try, and specifies one or more responses should an exception be thrown.

    • If an exception is thrown, the try…​catch statement catches it.

    • The finally block executes after the try and catch blocks execute but before the statements following the try…​catch statement.

  • Throwing a generic error

    try {
      throw new Error("Whoops!");
    } catch (e) {
      console.error(`${e.name}: ${e.message}`);
    }
  • Handling a specific error type

    try {
      foo.bar();
    } catch (e) {
      if (e instanceof EvalError) {
        console.error(`${e.name}: ${e.message}`);
      } else if (e instanceof RangeError) {
        console.error(`${e.name}: ${e.message}`);
      }
      // etc.
      else {
        // If none of our cases matched leave the Error unhandled
        throw e;
      }
    }
  • Using finally ensures that the file is never left open, even if an error occurs.

    openMyFile();
    try {
      writeMyFile(theData); // This may throw an error
    } catch (e) {
      handleError(e); // If an error occurred, handle it
    } finally {
      closeMyFile(); // Always close the resource
    }
  • If the finally block returns a value, this value becomes the return value of the entire try…catch…finally production, regardless of any return statements in the try and catch blocks:

    function f() {
      try {
        console.log(0);
        throw "bogus";
      } catch (e) {
        console.log(1);
        // This return statement is suspended
        // until finally block has completed
        return true;
        console.log(2); // not reachable
      } finally {
        console.log(3);
        return false; // overwrites the previous "return"
        console.log(4); // not reachable
      }
      // "return false" is executed now
      console.log(5); // not reachable
    }
    console.log(f()); // 0, 1, 3, false
  • Overwriting of return values by the finally block also applies to exceptions thrown or re-thrown inside of the catch block:

    function f() {
      try {
        throw "bogus";
      } catch (e) {
        console.log('caught inner "bogus"');
        // This throw statement is suspended until
        // finally block has completed
        throw e;
      } finally {
        return false; // overwrites the previous "throw"
      }
      // "return false" is executed now
    }
    
    try {
      console.log(f());
    } catch (e) {
      // this is never reached!
      // while f() executes, the `finally` block returns false,
      // which overwrites the `throw` inside the above `catch`
      console.log('caught outer "bogus"');
    }
    
    // Logs:
    // caught inner "bogus"
    // false
  • Custom error types

    class CustomError extends Error {
      constructor(foo = "bar", ...params) {
        // Pass remaining arguments (including vendor specific ones) to parent constructor
        super(...params);
    
        // Maintains proper stack trace for where our error was thrown (only available on V8)
        if (Error.captureStackTrace) {
          Error.captureStackTrace(this, CustomError);
        }
    
        this.name = "CustomError";
        // Custom debugging information
        this.foo = foo;
        this.date = new Date();
      }
    }
    
    try {
      throw new CustomError("baz", "bazMessage");
    } catch (e) {
      console.error(e.name); // CustomError
      console.error(e.foo); // baz
      console.error(e.message); // bazMessage
      console.error(e.stack); // stacktrace
    }

3. Loops and iteration

3.1. For

  • A for loop repeats until a specified condition evaluates to false. The JavaScript for loop is similar to the Java and C for loop.

    // similar to the Java and C for loop.
    for (initialization; condition; afterthought)
      statement
    for (let i = 0; i < 3; i++) {
      console.log(i);
    }
    // 0
    // 1
    // 2
  • The for…​in statement iterates a specified variable over all the enumerable properties of an object. For each distinct property, JavaScript executes the specified statements.

    for (variable in object)
      statement
    const car = { make: "Ford", model: "Mustang" };
    for (const p in car) {
      console.log(`car.${p} = ${car[p]}`);
    }
    // car.make = Ford
    // car.model = Mustang

    Although it may be tempting to use this as a way to iterate over Array elements, the for…​in statement will return the name of the user-defined properties in addition to the numeric indexes.

    const nums = [3, 4, 5];
    nums.foo = 'bar';
    for (const idx in nums) {
      console.log(`nums[${idx}] = ${nums[idx]}`);
    }
    // nums[0] = 3
    // nums[1] = 4
    // nums[2] = 5
    // nums[foo] = bar
  • The for…​of statement creates a loop Iterating over iterable objects (including Array, Map, Set, arguments object and so on), invoking a custom iteration hook with statements to be executed for the value of each distinct property.

    for (variable of object)
      statement
    const nums = [3, 4, 5];
    nums.foo = 'bar';
    for (const num of nums) {
      console.log(num);
    }
    // 3
    // 4
    // 5
  • The for…​of and for…​in statements can also be used with destructuring.

    const obj = { foo: 1, bar: 2 };
    
    for (const [key, val] of Object.entries(obj)) {
      console.log(key, val);
    }
    // "foo" 1
    // "bar" 2

3.2. While

  • The while statement executes its statements as long as a specified condition evaluates to true.

    while (condition)
      statement
    let i = 0;
    while (i < 3) {
      console.log(i);
      i++;
    }
    // 0
    // 1
    // 2
  • The do…​while statement repeats until a specified condition evaluates to false.

    do
      // statement is always executed once before the condition is checked.
      statement
    while (condition);
    let i = 0;
    do {
      console.log(i);
      i++;
    } while(i < 3)
    // 0
    // 1
    // 2

3.3. Label, break, continue

  • A label provides a statement with an identifier that lets you refer to it elsewhere in your program.

    label:
      statement
  • Use the break statement to terminate a loop, switch, or in conjunction with a labeled statement.

    • When you use break without a label, it terminates the innermost enclosing while, do-while, for, or switch immediately and transfers control to the following statement.

    • When you use break with a label, it terminates the specified labeled statement.

    break;
    break label;
    let x = 0;
    let z = 0;
    labelCancelLoops: while (true) {
      console.log("Outer loops:", x);
      x += 1;
      z = 1;
      while (true) {
        console.log("Inner loops:", z);
        z += 1;
        if (z === 10 && x === 10) {
          break labelCancelLoops;
        } else if (z === 10) {
          break;
        }
      }
    }
  • The continue statement can be used to restart a while, do-while, for, or label statement.

    • When you use continue without a label, it terminates the current iteration of the innermost enclosing while, do-while, or for statement and continues execution of the loop with the next iteration.

      In contrast to the break statement, continue does not terminate the execution of the loop entirely.

      In a while loop, it jumps back to the condition.

      In a for loop, it jumps to the increment-expression.

    • When you use continue with a label, it applies to the looping statement identified with that label.

    continue;
    continue label;
    let i = 0;
    let j = 10;
    checkiandj: while (i < 4) {
      console.log(i);
      i += 1;
      checkj: while (j > 4) {
        console.log(j);
        j -= 1;
        if (j % 2 === 0) {
          continue checkj;
        }
        console.log(j, "is odd.");
      }
      console.log("i =", i);
      console.log("j =", j);
    }

4. Functions

In JavaScript, functions are first-class objects, because they can be passed to other functions, returned from functions, and assigned to variables and properties, and can also have properties and methods just like any other object.

4.1. Function definition

  • A function definition (also called a function declaration, or function statement) consists of the function keyword, followed by:

    • The name of the function.

    • A list of parameters to the function, enclosed in parentheses and separated by commas.

      • Parameters are essentially passed to functions by value.

      • When pass an object as a parameter, if the function changes the object’s properties, that change is visible outside the function.

    • The JavaScript statements that define the function, enclosed in curly braces, { /* … */ }.

    function square(number) {
      return number * number;
    }

4.2. Function expression

  • The function keyword can be used to define a function inside an expression.

    • Such a function can be anonymous; it does not have to have a name.

      const square = function (number) {
        return number * number;
      };
      
      console.log(square(4)); // 16
    • Providing a name allows the function to refer to itself, and also makes it easier to identify the function in a debugger’s stack traces:

      const factorial = function fac(n) {
        return n < 2 ? 1 : n * fac(n - 1);
      };
      
      console.log(factorial(3)); // 6
    • Function expressions are convenient when passing a function as an argument to another function.

      const nums = [1, 3, 5];
      const square = nums.map(function(num) { return num * num});
      console.log(square.join()); // 1,9,25

4.3. Function object

The Function object provides methods for functions. In JavaScript, every function is actually a Function object.

  • Use the Function constructor to create functions from a string at runtime, much like eval().

    const sum = new Function('a', 'b', 'console.log(a + b)');
    sum(2, 6); // 8

    The call() and apply() methods of the Function object can also be used to call functions.

    sum.call(null, 1, 1); // 2
    sum.apply(null, [1, 1]); // 2
  • A method is a function that is a property of an object.

    const car = {
      make: "Ford",
      model: "Mustang",
      greet() { console.log(`${this.make}, ${this.model}`) }
    };
    car.greet(); // Ford, Mustang
  • JavaScript interpreter hoists the entire function declaration — not with function expressions to the top of the current scope.

    console.log(square(5)); // 25
    
    function square(n) {
      return n * n;
    }
    console.log(square(5)); // ReferenceError: Cannot access 'square' before initialization
    const square = function (n) {
      return n * n;
    };

4.4. Recursion and closures

  • A function that calls itself is called a recursive function. There are three ways for a function to refer to itself:

    • The function’s name

    • arguments.callee

    • An in-scope variable that refers to the function

    const foo = function bar() {
      // statements go here
    
      // bar()
      // arguments.callee()
      // foo()
    };
  • A function can be nested within another function, which forms a closure. The nested (inner) function is private to its containing (outer) function.

    function outside(x) {
      function inside(y) {
        return x + y;
      }
      return inside;
    }
    
    const fnInside = outside(3); // Think of it like: give me a function that adds 3 to whatever you give it
    console.log(fnInside(5)); // 8
    console.log(outside(3)(5)); // 8
    A closure is an expression (most commonly, a function) that can have free variables together with an environment that binds those variables (that "closes" the expression).
    A closure must preserve the arguments and variables in all scopes it references. Since each call provides potentially different arguments, a new closure is created for each call to outside. The memory can be freed only when the returned inside is no longer accessible.
  • When two arguments or variables in the scopes (scope chaning) of a closure have the same name, the more nested scopes take precedence.

    function outside() {
      const x = 5;
      function inside(x) {
        return x * 2;
      }
      return inside;
    }
    
    console.log(outside()(10)); // 20 (instead of 10)
  • Creating closures in loops: a common mistake

    const funcs = [];
    for (var i = 0; i < 3; i++) { // var-based index
      funcs.push(function () { console.log(i); });
      // solution: using the scope chaining to override the outer variable.
      // funcs.push(function (i) { return function () { console.log(i); } }(i));
    }
    for(const func of funcs) {
      func();
    }
    // 3
    // 3
    // 3
    const funcs = [];
    for (let i = 0; i < 3; i++) { // let-based index
      funcs.push(function () { console.log(i); });
    }
    for(const func of funcs) {
      func();
    }
    // 0
    // 1
    // 2

4.5. Arguments object

  • The arguments of a function are maintained in an array-like object, but not an array.

  • It is array-like in that it has a numbered index and a length property. However, it does not possess all of the array-manipulation methods.

  • Using the arguments object, a function can be called with more arguments than it is formally declared to accept.

    function seq() {
      console.log(arguments.length);
      for (const arg of arguments) {
        console.log(arg);
      }
    }
    seq(0, 1, 2);
    // 3
    // 0
    // 1
    // 2

4.6. Default parameters and rest parameters

  • In JavaScript, parameters of functions default to undefined. However, in some situations it might be useful to set a different default value. This is exactly what default parameters do.

    // function multiply(a, b) {
    //   b = typeof b !== "undefined" ? b : 1;
    //   return a * b;
    // }
    // With default parameters, a manual check in the function body is no longer necessary.
    function multiply(a, b = 1) {
      return a * b;
    }
    console.log(multiply(5)); // 5
  • The rest parameter (i.e., variadic) syntax allows us to represent an indefinite number of arguments as an array.

    function multiply(multiplier, ...theArgs) {
      return theArgs.map((x) => multiplier * x);
    }
    const arr = multiply(2, 1, 2, 3);
    console.log(arr); // [2, 4, 6]

4.7. Arrow functions

An arrow function expression (also called a fat arrow to distinguish from a hypothetical -> syntax in future JavaScript) has a shorter syntax compared to function expressions and does not have its own this, arguments, super, or new.target.

  • Arrow functions are always anonymous.

  • Two factors influenced the introduction of arrow functions: shorter functions and non-binding of this.

const a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];

const a2 = a.map(function (s) {
  return s.length;
});

console.log(a2); // [8, 6, 7, 9]

const a3 = a.map((s) => s.length); // shorter functions

console.log(a3); // [8, 6, 7, 9]

Until arrow functions, every new function defined its own this value (a new object in the case of a constructor, undefined in strict mode function calls, the base object if the function is called as an "object method", etc.).

function Person() {
  // The Person() constructor defines `this` as itself.
  this.age = 0;

  setInterval(function growUp() {
    // In nonstrict mode, the growUp() function defines `this`
    // as the global object, which is different from the `this`
    // defined by the Person() constructor.
    this.age++;
  }, 1000);
}

In ECMAScript 3/5, this issue was fixed by assigning the value in this to a variable that could be closed over.

// ECMAScript 3/5 closures
function Person() {
  // Some choose `that` instead of `self`.
  // Choose one and be consistent.
  const self = this;
  self.age = 0;

  setInterval(function growUp() {
    // The callback refers to the `self` variable of which
    // the value is the expected object.
    self.age++;
  }, 1000);
}

Alternatively, a bound function could be created so that the proper this value would be passed to the growUp() function.

function Person() {
  this.age = 0;

  setInterval(function growUp() {
    this.age++;
  }.bind(this), 1000);
}

An arrow function does not have its own this; the this value of the enclosing execution context is used.

function Person() {
  this.age = 0;

  setInterval(() => {
    this.age++; // `this` properly refers to the person object
  }, 1000);
}

5. Expressions and operators

operand1 operator operand2 // infix binary operator, e.g., 3 + 4 or x * y
operator operand           // prefix unary operator, e.g., ++x
operand operator           // postfix unary operator, e.g., x++

5.1. Assignment operators

An assignment operator assigns a value to its left operand based on the value of its right operand. The simple assignment operator is equal (=), which assigns the value of its right operand to its left operand. There are also compound assignment operators that are shorthand for the operations.

x = f()      // x = f()
x += f()     // x = x + f()
x -= f()     // x = x - f()
x *= f()     // x = x * f()
x /= f()     // x = x / f()
x %= f()     // x = x % f()
x **= f()    // x = x ** f()
x <<= f()    // x = x << f()
x >>= f()    // x = x >> f()
x >>>= f()   // x = x >>> f()
x &= f()     // x = x & f()
x ^= f()     // x = x ^ f()
x |= f()     // x = x | f()
x &&= f()    // x && (x = f())
x ||= f()    // x || (x = f())
x ??= f()    // x ?? (x = f())

5.1.1. Assigning to properties

  • If an expression evaluates to an object, then the left-hand side of an assignment expression may make assignments to properties of that expression.

    const obj = {};
    
    obj.x = 3;
    console.log(obj.x); // Prints 3.
    console.log(obj); // Prints { x: 3 }.
    
    const key = "y";
    obj[key] = 5;
    console.log(obj[key]); // Prints 5.
    console.log(obj); // Prints { x: 3, y: 5 }.
  • If an expression does not evaluate to an object, then assignments to properties of that expression do not assign:

    const val = 0;
    val.x = 3;
    
    console.log(val.x); // Prints undefined.
    console.log(val); // Prints 0.

    In strict mode, the code above throws, because one cannot assign properties to primitives.

    "use strict"
    const val = 0;
    val.x = 3; // Uncaught TypeError: can't assign to property "x" on 0: not an object

5.1.2. Destructuring

The destructuring assignment syntax is a JavaScript expression that makes it possible to extract data from arrays or objects using a syntax that mirrors the construction of array and object literals.

  • Without destructuring, it takes multiple statements to extract values from arrays and objects:

    const foo = ["one", "two", "three"];
    
    const one = foo[0];
    const two = foo[1];
    const three = foo[2];
  • With destructuring, you can extract multiple values into distinct variables using a single statement:

    const [one, two, three] = foo;

5.1.3. Evaluation and nesting

In general, assignments are used within a variable declaration (i.e., with const, let, or var) or as standalone statements.

// Declares a variable x and initializes it to the result of f().
// The result of the x = f() assignment expression is discarded.
let x = f();

x = g(); // Reassigns the variable x to the result of g().

However, like other expressions, assignment expressions like x = f() evaluate into a result value. Although this result value is usually not used, it can then be used by another expression.

By chaining or nesting an assignment expression, its result can itself be assigned to another variable. It can be logged, it can be put inside an array literal or function call, and so on.

let x;
const y = (x = f()); // Or equivalently: const y = x = f();
console.log(y); // Logs the return value of the assignment x = f().

console.log(x = f()); // Logs the return value directly.

// An assignment expression can be nested in any place
// where expressions are generally allowed,
// such as array literals' elements or as function calls' arguments.
console.log([0, x = f(), 0]);
console.log(f(0, x = f(), 0));

Avoid assignment chains

Chaining assignments or nesting assignments in other expressions can result in surprising behavior. For this reason, chaining assignments in the same statement is discouraged.

In particular, putting a variable chain in a const, let, or var statement often does not work. Only the outermost/leftmost variable would get declared; other variables within the assignment chain are not declared by the const/let/var statement.

const z = y = x = f();

This statement seemingly declares the variables x, y, and z. However, it only actually declares the variable z. y and x are either invalid references to nonexistent variables (in strict mode) or, worse, would implicitly create global variables for x and y in sloppy mode.

// "use strict"
{ const z = y = x = Math.PI; }
console.log(x, y); // 3.141592653589793 3.141592653589793
console.log(z);    // Uncaught ReferenceError: z is not defined
"use strict"
{ const z = y = x = Math.PI; } // Uncaught ReferenceError: assignment to undeclared variable x

5.2. Comparison operators

  • The strict equality (===) operator checks whether its two operands are equal, returning a Boolean result. Unlike the equality (==) operator, the strict equality operator always considers operands of different types to be different. See also Object.is and sameness in JS.

    console.log(1 === 1);
    // Expected output: true
    
    console.log('hello' === 'hello');
    // Expected output: true
    
    console.log('1' === 1);
    // Expected output: false
    
    console.log(0 === false);
    // Expected output: false
    console.log(1 == 1);
    // Expected output: true
    
    console.log('hello' == 'hello');
    // Expected output: true
    
    console.log('1' == 1);
    // Expected output: true
    
    console.log(0 == false);
    // Expected output: true
  • The strict inequality (!==) operator checks whether its two operands are not equal, returning a Boolean result. Unlike the inequality (!=) operator, the strict inequality operator always considers operands of different types to be different.

    console.log(1 !== 1);
    // Expected output: false
    
    console.log('hello' !== 'hello');
    // Expected output: false
    
    console.log('1' !== 1);
    // Expected output: true
    
    console.log(0 !== false);
    // Expected output: true
    console.log(1 != 1);
    // Expected output: false
    
    console.log('hello' != 'hello');
    // Expected output: false
    
    console.log('1' != 1);
    // Expected output: false
    
    console.log(0 != false);
    // Expected output: false

5.3. Arithmetic operators

In addition to the standard arithmetic operations (+, -, *, /), JavaScript provides also the arithmetic operators: %, ++, --, -, +, **.

division by zero produces Infinity.

5.4. Bitwise operators

A bitwise operator treats their operands as a set of 32 bits (zeros and ones), rather than as decimal, hexadecimal, or octal numbers.

  • &, |, ^, ~, <<, >>, >>>

  • The operands are converted to thirty-two-bit integers and expressed by a series of bits (zeros and ones). Numbers with more than 32 bits get their most significant bits discarded. For example, the following integer with more than 32 bits will be converted to a 32-bit integer:

    Before: 1110 0110 1111 1010 0000 0000 0000 0110 0000 0000 0001
    After:                 1010 0000 0000 0000 0110 0000 0000 0001
  • The bitwise shift operators take two operands: the first is a quantity to be shifted, and the second specifies the number of bit positions by which the first operand is to be shifted. The direction of the shift operation is controlled by the operator used.

  • Shift operators convert their operands to thirty-two-bit integers and return a result of either type Number or BigInt: specifically, if the type of the left operand is BigInt, they return BigInt; otherwise, they return Number.

5.5. Logical operators

  • Logical operators are typically used with Boolean (logical) values; when they are, they return a Boolean value.

  • The && and || operators actually return the value of one of the specified operands, so if these operators are used with non-Boolean values, they may return a non-Boolean value.

    const a1 = true && true; // t && t returns true
    const a2 = true && false; // t && f returns false
    const a3 = false && true; // f && t returns false
    const a4 = false && 3 === 4; // f && f returns false
    const a5 = "Cat" && "Dog"; // t && t returns Dog
    const a6 = false && "Cat"; // f && t returns false
    const a7 = "Cat" && false; // t && f returns false
    const o1 = true || true; // t || t returns true
    const o2 = false || true; // f || t returns true
    const o3 = true || false; // t || f returns true
    const o4 = false || 3 === 4; // f || f returns false
    const o5 = "Cat" || "Dog"; // t || t returns Cat
    const o6 = false || "Cat"; // f || t returns Cat
    const o7 = "Cat" || false; // t || f returns Cat
  • As logical expressions are evaluated left to right, they are tested for possible "short-circuit" evaluation using the following rules:

    • false && anything is short-circuit evaluated to false.

    • true || anything is short-circuit evaluated to true.

  • The nullish coalescing (??) operator is a logical operator that returns its right-hand side operand when its left-hand side operand is null or undefined, and otherwise returns its left-hand side operand.

    const foo = null ?? 'default string';
    console.log(foo);
    // Expected output: "default string"
    
    const baz = 0 ?? 42;
    console.log(baz);
    // Expected output: 0

5.6. Conditional (ternary) operator

The conditional operator is the only JavaScript operator that takes three operands. The operator can have one of two values based on a condition. The syntax is:

condition ? val1 : val2

5.7. Comma operator

The comma operator (,) evaluates both of its operands and returns the value of the last operand.

  • This operator is primarily used inside a for loop, to allow multiple variables to be updated each time through the loop.

  • It is regarded bad style to use it elsewhere, when it is not necessary. Often two separate statements can and should be used instead.

const x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const a = [x, x, x, x, x];

for (let i = 0, j = 9; i <= j; i++, j--) {
  //                              ^
  console.log(`a[${i}][${j}]= ${a[i][j]}`);
}

5.8. Unary operators

  • The delete operator removes a property from an object. If the property’s value is an object and there are no more references to the object, the object held by that property is eventually released automatically.

    delete object.property
    delete object[property]
    const car = { make: "Ford", model: "Mustang" };
    delete car.make;
    console.log(car); // { model: "Mustang" }
    const nums = [0, 1, 2, 3];
    delete nums[1];
    console.log(nums); // [ 0, <1 empty slot>, 2, 3 ]
  • The typeof operator returns a string indicating the type of the unevaluated operand. operand is the string, variable, keyword, or object for which the type is to be returned. The parentheses are optional.

    typeof new Function("5 + 2"); // "function"
    typeof "round"; // "string"
    typeof 1; // "number"
    typeof ["Apple", "Mango", "Orange"]; // "object"
    typeof new Date(); // "object"
    typeof true; // "boolean"
    typeof {}; // "boolean"
    typeof /ab+c/; // "object"
    typeof undefined; // "undefined"
    typeof null; // "object"
  • The void operator specifies an expression to be evaluated without returning a value. expression is a JavaScript expression to evaluate. The parentheses surrounding the expression are optional, but it is good style to use them to avoid precedence issues.

    const output = void 1;
    console.log(output);
    // Expected output: undefined
    
    void console.log('expression evaluated');
    // Expected output: "expression evaluated"
    
    void (function iife() {
      console.log('iife is executed');
    })();
    // Expected output: "iife is executed"
    
    void function test() {
      console.log('test function executed');
    };
    try {
      test();
    } catch (e) {
      console.log('test function is not defined');
      // Expected output: "test function is not defined"
    }

5.9. Relational operators

  • The in operator returns true if the specified property is in the specified object or its prototype chain. The in operator cannot be used to search for values in other collections. To test if a certain value exists in an array, use Array.prototype.includes(). For sets, use Set.prototype.has().

    // Arrays
    const trees = ["redwood", "bay", "cedar", "oak", "maple"];
    0 in trees; // returns true
    3 in trees; // returns true
    6 in trees; // returns false
    "bay" in trees; // returns false
    // (you must specify the index number, not the value at that index)
    "length" in trees; // returns true (length is an Array property)
    
    // built-in objects
    "PI" in Math; // returns true
    const myString = new String("coral");
    "length" in myString; // returns true
    
    // Custom objects
    const mycar = { make: "Honda", model: "Accord", year: 1998 };
    "make" in mycar; // returns true
    "model" in mycar; // returns true
  • The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. The return value is a boolean value. Its behavior can be customized with Symbol.hasInstance.

    function Car(make, model, year) {
      this.make = make;
      this.model = model;
      this.year = year;
    }
    const auto = new Car('Honda', 'Accord', 1998);
    
    console.log(auto instanceof Car);
    // Expected output: true
    
    console.log(auto instanceof Object);
    // Expected output: true

5.10. Grouping operator

The grouping ( ) operator controls the precedence of evaluation in expressions. It also acts as a container for arbitrary expressions in certain syntactic constructs, where ambiguity or syntax errors would otherwise occur.

  • Evaluating addition and subtraction before multiplication and division.

    const a = 1;
    const b = 2;
    const c = 3;
    
    // default precedence
    a + b * c; // 7
    // evaluated by default like this
    a + (b * c); // 7
    
    // now overriding precedence
    // addition before multiplication
    (a + b) * c; // 9
    
    // which is equivalent to
    a * c + b * c; // 9
  • Using the grouping operator to eliminate parsing ambiguity

    // An IIFE (Immediately Invoked Function Expression)
    (function () {
      // code
    })();
    // an arrow function expression body
    const f = () => ({ a: 1 });
    // a property accessor dot `.` may be ambiguous with a decimal point
    (1).toString(); // "1"

5.11. Optional chaining operator

The optional chaining (?.) operator accesses an object’s property or calls a function. If the object accessed or function called using this operator is undefined or null, the expression short circuits and evaluates to undefined instead of throwing an error.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};

const dogName = adventurer.dog?.name;
console.log(dogName);
// Expected output: undefined

console.log(adventurer.someNonExistentMethod?.());
// Expected output: undefined

6. Regular expressions

  • Regular expression literals (/pattern/flags) provide compilation of the regular expression when the script is loaded. If the regular expression remains constant, using this can improve performance.

    const re = /ab+c/i; // literal notation
  • Using the RegExp constructor function provides runtime compilation of the regular expression.

    // OR
    const re = new RegExp("ab+c", "i"); // constructor with string pattern as first argument
    // OR
    const re = new RegExp(/ab+c/, "i"); // constructor with regular expression literal as first argument
  • Regular expressions are used with the RegExp methods test() and exec() and with the String methods match(), matchAll(), replace(), replaceAll(), search(), and split().

7. Indexed and keyed Collections

Indexed collections (data which are ordered by an index value) includes arrays and array-like constructs such as Array objects and TypedArray objects.

7.1. Arrays

At the implementation level, JavaScript’s arrays actually store their elements as standard object properties, using the array index as the property name.

  • The length property is special. Its value is always a positive integer greater than the index of the last element if one exists.

  • Writing a value that is shorter than the number of stored items truncates the array.

  • Arrays can also be used like objects, to store related information.

    const nums = []; // same as: const nums = new Array(); OR const nums = new Array(0);
    nums[0] = 0;
    nums[2] = 2;
    nums.size = function () { return this.length; }; // a user-defined extension method
    console.log(nums); // Array(3) [ 0, <1 empty slot>, 2 ]
    console.log(nums.length, nums.size()); // 3 3
    nums.length = 2;
    console.log(nums); // Array [ 0, <1 empty slot> ]
  • The forEach() method executes callback on every array item and returns undefined.

    const colors = ["red", /* empty */, "green", "blue"];
    // Unassigned values are not iterated in a forEach loop.
    colors.forEach((color) => console.log(color));
    // red
    // green
    // blue
  • The concat() method joins two or more arrays and returns a new array.

    let myArray = ["1", "2", "3"];
    myArray = myArray.concat("a", "b", "c");
    // myArray is now ["1", "2", "3", "a", "b", "c"]
  • The flat() method returns a new array with all sub-array elements concatenated into it recursively up to the specified depth.

    const arr1 = [0, 1, 2, [3, 4]];
    
    console.log(arr1.flat());
    // expected output: Array [0, 1, 2, 3, 4]
    
    const arr2 = [0, 1, [2, [3, [4, 5]]]];
    
    console.log(arr2.flat());
    // expected output: Array [0, 1, 2, Array [3, Array [4, 5]]]
    
    console.log(arr2.flat(2));
    // expected output: Array [0, 1, 2, 3, Array [4, 5]]
    
    console.log(arr2.flat(Infinity));
    // expected output: Array [0, 1, 2, 3, 4, 5]
  • The map() method returns a new array of the return value from executing callback on every array item.

    const a1 = ["a", "b", "c"];
    const a2 = a1.map((item) => item.toUpperCase());
    console.log(a2); // ['A', 'B', 'C']
  • The flatMap() method runs map() followed by a flat() of depth 1.

    const a1 = ["a", "b", "c"];
    const a2 = a1.flatMap((item) => [item.toUpperCase(), item.toLowerCase()]);
    console.log(a2); // ['A', 'a', 'B', 'b', 'C', 'c']
  • The reduce() method of Array instances executes a user-supplied "reducer" callback function on each element of the array, in order, passing in the return value from the calculation on the preceding element.

    • The final result of running the reducer across all elements of the array is a single value.

    • The first time that the callback is run there is no "return value of the previous calculation".

      • If supplied, an initial value may be used in its place.

      • Otherwise the array element at index 0 is used as the initial value and iteration starts from the next element (index 1 instead of index 0).

    const array1 = [1, 2, 3, 4];
    
    // 0 + 1 + 2 + 3 + 4
    const initialValue = 0;
    const sumWithInitial = array1.reduce(
      (accumulator, currentValue) => accumulator + currentValue,
      initialValue,
    );
    
    console.log(sumWithInitial);
    // Expected output: 10
  • The Array.isArray() static method determines whether the passed value is an Array.

    console.log(Array.isArray([1, 3, 5]));
    // Expected output: true
    
    console.log(Array.isArray('[]'));
    // Expected output: false
    
    console.log(Array.isArray(new Array(5)));
    // Expected output: true
    
    console.log(Array.isArray(new Int16Array([15, 33])));
    // Expected output: false

7.2. Typed arrays

JavaScript typed arrays are array-like objects that provide a mechanism for reading and writing raw binary data in memory buffers.

To achieve maximum flexibility and efficiency, JavaScript typed arrays split the implementation into buffers and views.

  • A buffer is an object representing a chunk of data; it has no format to speak of, and offers no mechanism for accessing its contents.

  • In order to access the memory contained in a buffer, it’s needed to use a view which provides a context — that is, a data type, starting offset, and number of elements.

A diagram showing how different typed arrays may be views of the same underlying buffer. Each one has a different element number and width.

7.3. Maps and sets

Maps and sets are keyed collections (data which are indexed by a key), and both contain elements which are iterable in the order of insertion.

  • A Map object is a simple key/value map and can iterate its elements in insertion order.

    const sayings = new Map();
    sayings.set("dog", "woof");
    sayings.set("cat", "meow");
    sayings.set("elephant", "toot");
    sayings.size; // 3
    sayings.get("dog"); // woof
    sayings.get("fox"); // undefined
    sayings.has("bird"); // false
    sayings.delete("dog");
    sayings.has("dog"); // false
    
    for (const [key, value] of sayings) {
      console.log(`${key} goes ${value}`);
    }
    // "cat goes meow"
    // "elephant goes toot"
    
    sayings.clear();
    sayings.size; // 0
  • A Set object is a collection of unique values.

    • Its elements can be iterated in insertion order.

    • A value in a Set may only occur once; it is unique in the Set's collection.

    const mySet = new Set();
    mySet.add(1);
    mySet.add("some text");
    mySet.add("foo");
    
    mySet.has(1); // true
    mySet.delete("foo");
    mySet.size; // 2
    
    for (const item of mySet) {
      console.log(item);
    }
    // 1
    // "some text"
  • Both the key equality of Map objects and the value equality of Set objects are based on the SameValueZero algorithm:

    • Equality works like the identity comparison operator ===.

    • -0 and +0 are considered equal.

    • NaN is considered equal to itself (contrary to ===).

8. Prototypes, objects and classes

In object-oriented programming, inheritance is the mechanism of basing an object or class upon another object (prototype-based inheritance) or class (class-based inheritance), retaining similar implementation.

— Inheritance (object-oriented programming) - Wikipedia

8.1. Prototype

JavaScript is a prototype-based, object-oriented scripting language, which implements inheritance by using objects.

  • Each object has an internal link to another object called its prototype.

  • That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype.

  • By definition, null has no prototype and acts as the final link in this prototype chain.

  • It is possible to mutate any member of the prototype chain or even swap out the prototype at runtime, so concepts like static dispatching do not exist in JavaScript.

8.2. Properties

JavaScript objects are dynamic "bags" of properties (referred to as own properties) and have a link to a prototype object. When trying to access a property of an object,

  • the property will not only be sought on the object but on the prototype of the object, the prototype of the prototype,

  • and so on until either a property with a matching name is found or the end of the prototype chain is reached.

Following the ECMAScript standard, the notation someObject.[[Prototype]] is used to designate the prototype of someObject.

The [[Prototype]] internal slot can be accessed and modified with the Object.getPrototypeOf() and Object.setPrototypeOf() functions respectively.

It is equivalent to the JavaScript accessor __proto__ which is non-standard but de-facto implemented by many JavaScript engines.

It’s worth noting that the { __proto__: ... } syntax is different from the obj.__proto__ accessor: the former is standard and not deprecated, and the later is non-standard and deprecated. .

It should not be confused with the func.prototype property of functions, which instead specifies the to be assigned to all instances of objects created by the given function when used as a constructor.

In an object literal like { a: 1, b: 2, __proto__: c }, the value c (which has to be either null or another object) will become the [[Prototype]] of the object represented by the literal, while the other keys like a and b will become the own properties of the object.

const o = {
  a: 1,
  b: 2,
  // __proto__ sets the [[Prototype]]. It's specified here as another object literal.
  __proto__: {
    b: 3,
    c: 4,
    // a longer prototype chain
    __proto__: {
      // Object literals (without the `__proto__` key) automatically
      // have `Object.prototype` as their `[[Prototype]]`
      d: 5,
    },
  },
};

// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> { d: 5 } ---> Object.prototype ---> null
  • In JavaScript, any function can be added to an object in the form of a property, aka "method". When the function is executed, the value of this points to the inheriting object, not to the prototype object where the function is an own property.

    const parent = {
      value: 2,
      method() {
        return this.value + 1;
      },
    };
    
    console.log(parent.method()); // 3
    // When calling parent.method in this case, 'this' refers to parent
    
    // child is an object that inherits from parent
    const child = {
      __proto__: parent,
    };
    console.log(child.method()); // 3
    // When child.method is called, 'this' refers to child.
    // So when child inherits the method of parent,
    // The property 'value' is sought on child. However, since child
    // doesn't have an own property called 'value', the property is
    // found on the [[Prototype]], which is parent.value.
    
    child.value = 4; // assign the value 4 to the property 'value' on child.
    // This shadows the 'value' property on parent.
    // The child object now looks like:
    // { value: 4, __proto__: { value: 2, method: [Function] } }
    console.log(child.method()); // 5
    // Since child now has the 'value' property, 'this.value' means
    // child.value instead
  • To check whether an object has a property defined on itself, it is necessary to use the Object.hasOwn or Object.prototype.hasOwnProperty methods.

    All objects, except those with null as [[Prototype]], inherit hasOwnProperty from Object.prototype — unless it has been overridden further down the prototype chain.
    Object.hasOwn() is intended as a replacement for Object.prototype.hasOwnProperty().
    const example = {};
    example.prop = "exists";
    
    // `hasOwn` will only return true for direct properties:
    Object.hasOwn(example, "prop"); // true
    Object.hasOwn(example, "toString"); // false
    Object.hasOwn(example, "hasOwnProperty"); // false

8.3. Constructors

  • A constructor is a function with a special property called prototype, which works with the new operator.

    // A constructor function, with good reason, to use a capital initial letter
    function Box(value) {
      this.value = value;
    }
    
    // Properties all boxes created from the Box() constructor
    // will have
    Box.prototype.getValue = function () {
      return this.value;
    };
    
    const boxes = [new Box(1), new Box(2), new Box(3)];
    // class are syntax sugar over constructor functions.
    class Box {
      constructor(value) {
        this.value = value;
      }
    
      // Methods are created on Box.prototype
      getValue() {
        return this.value;
      }
    }
    // without constructor
    const boxPrototype = {
      getValue() {
        return this.value;
      },
    };
    
    const boxes = [
      { value: 1, __proto__: boxPrototype },
      { value: 2, __proto__: boxPrototype },
      { value: 3, __proto__: boxPrototype },
    ];
  • To build longer prototype chains, set the [[Prototype]] of Constructor.prototype via the Object.setPrototypeOf() function.

    function Base() {}
    function Derived() {}
    // Set the `[[Prototype]]` of `Derived.prototype`
    // to `Base.prototype`
    Object.setPrototypeOf(Derived.prototype, Base.prototype);
    
    const obj = new Derived();
    // obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
    // It is equivalent to using the `extends` syntax in class terms.
    class Base {}
    class Derived extends Base {}
    
    const obj = new Derived();
    // obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null

8.4. Objects

JavaScript is designed on a simple object-based paradigm.

  • An object is a collection of properties, and a property is an association between a name (or key) and a value.

  • A property’s value can be a function, in which case the property is known as a method.

  • A property can be accessed in two syntaxes: dot notation (.) and bracket notation ([ ]).

  • A non-inherited property can be removed using the delete operator.

  • A getter is a function associated with a property that gets the value of a specific property.

    { get prop() { /* … */ } }
    { get [expression]() { /* … */ } }
  • A setter is a function associated with a property that sets the value of a specific property.

    { set prop(val) { /* … */ } }
    { set [expression](val) { /* … */ } }
  • An object can be created using an object initializer, a constructor function, a class, and the Object.create() method.

    const myHonda = {
      color: "red",
      wheels: 4,
      engine: { cylinders: 4, size: 2.2 },
    };
    function Car(make, model, year) {
      this.make = make;
      this.model = model;
      this.year = year;
    }
    
    const myCar = new Car("Eagle", "Talon TSi", 1993);
    class Car {
      constructor (make, model, year) {
        this.make = make;
        this.model = model;
        this.year = year;
      }
    }
    
    const myCar = new Car("Eagle", "Talon TSi", 1993);
    // Animal properties and method encapsulation
    const Animal = {
      type: "Invertebrates", // Default value of properties
      displayType() {
        // Method which will display type of Animal
        console.log(this.type);
      },
    };
    
    // Create new animal type called animal1
    const animal1 = Object.create(Animal);
    animal1.displayType(); // Logs: Invertebrates
    
    // Create new animal type called fish
    const fish = Object.create(Animal);
    fish.type = "Fishes";
    fish.displayType(); // Logs: Fishes

8.5. Classes

In JavaScript, classes are mainly an abstraction over the existing prototypical inheritance mechanism — all patterns are convertible to prototype-based inheritance.

  • Classes themselves are normal JavaScript values as well, which are syntax sugar over constructor functions, and have their own prototype chains.

  • Classes are in fact "special functions", and just as defining function expressions and function declarations, a class can be defined in two ways: a class expression or a class declaration.

  • Unlike function declarations, class declarations have the same temporal dead zone restrictions as let or const and behave as if they are not hoisted.

  • The body of a class is executed in strict mode even without the "use strict" directive.

  • A class element can be characterized by three aspects:

    • Kind: Getter, setter, method, or field

    • Location: Static or instance

    • Visibility: Public or private

  • A class can have any number of static {} initialization blocks in its class body, which are evaluated, along with any interleaved static field initializers, in the order they are declared. Any static initialization of a super class is performed first, before that of its sub classes.

  • A derived class is declared with an extends clause, which indicates the class it extends from.

// same as implicityly: class MyClass extends Object { ... }
class MyClass {
  // Constructor
  constructor() {
    // Constructor body
  }
  // Instance field
  myField = "foo";
  // Instance method
  myMethod() {
    // myMethod body
  }
  // Static field
  static myStaticField = "bar";
  // Static method
  static myStaticMethod() {
    // myStaticMethod body
  }
  // Static block
  static {
    // Static initialization code
  }
  // Fields, methods, static fields, and static methods all have
  // "private" forms
  #myPrivateField = "bar";
  // Instance getter
  get myPrivateField() {
    return this.#myPrivateField;
  }
  // Instance setter
  set myPrivateField(value) {
    this.#myPrivateField = value;
  }
}

9. Promises and asynchronous programming

JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.

A diagram showing how stacks are comprised of frames, heaps are comprised of objects, and queues are comprised of messages.
  • Function calls form a stack of frames.

  • Objects are allocated in a heap which is just a name to denote a large (mostly unstructured) region of memory.

  • A JavaScript runtime uses a message queue, which is a list of messages to be processed one by one by an associated function.

    // waits synchronously for a message to arrive
    while (queue.waitForMessage()) {
      // Each message is processed completely before any other message is processed.
      queue.processNextMessage();
    }
  • Handling I/O is typically performed via events and callbacks, so when the application is waiting for an IndexedDB query to return or a fetch() request to return, it can still process other things like user input.

    const xhr = new XMLHttpRequest();
    
    xhr.addEventListener("loadend", () => {
      console.log(`Finished with status: ${xhr.status}`);
    });
    
    xhr.open(
      "GET",
      "https://httpbin.org/headers",
    );
    xhr.send();
    const fetchPromise = fetch(
      "https://httpbin.org/headers",
    );
    
    fetchPromise.then((response) => {
      console.log(`Received response: ${response.status}`);
    });

9.1. Promises

A Promise is an object representing the eventual completion or failure of an asynchronous operation.

Flowchart showing how the Promise state transitions between pending, fulfilled, and rejected via then/catch handlers. A pending promise can become either fulfilled or rejected. If fulfilled, the "on fulfillment" handler, or first parameter of the then() method, is executed and carries out further asynchronous actions. If rejected, the error handler, either passed as the second parameter of the then() method or as the sole parameter of the catch() method, gets executed.
  • With a promise-based API, the asynchronous function starts the operation and returns a Promise object.

    // callback hell
    fetch("https://httpbin.org/headers")
      .then(response => {
        response.json()
          .then(data => {
            console.log(data['headers']['User-Agent']);
          });
      })
      .catch(error => console.log(error))
      .finally(() => console.log("finally"));
    // promise chaining
    fetch("https://httpbin.org/headers")
      .then(response => response.json())
      .then(headers => console.log(headers['headers']['User-Agent']))
      .catch(error => console.log(error))
      .finally(() => console.log("finally"));
    // async and await
    async function fetchRquestHeaders() {
      try {
        const response = await fetch("https://httpbin.org/headers");
        const headers = await response.json();
        console.log(headers['headers']['User-Agent']);
      } catch (error) {
        console.log(error);
      } finally {
        console.log("finally");
      }
    }
  • The Promise() constructor creates Promise objects. It is primarily used to wrap callback-based APIs that do not already support promises.

    new Promise(executor)
    • The executor is a function to be executed by the constructor. Its signature is expected to be:

      function executor(resolveFunc, rejectFunc) {
        // Typically, some asynchronous operation that accepts a callback,
        // like the `readFile` function above
      }
    • It receives two functions as parameters: resolveFunc and rejectFunc.

      resolveFunc(value); // call on resolved
      rejectFunc(reason); // call on rejected
      • The value parameter passed to resolveFunc can be another promise object, in which case the newly constructed promise’s state will be "locked in" to the promise passed.

      • The rejectFunc has semantics close to the throw statement, so reason is typically an Error instance. If the executor function throws an error, reject is called automatically.

      • If either value or reason is omitted, the promise is fulfilled/rejected with undefined.

      const readFilePromise = (path) =>
        new Promise((resolve, reject) => {
          readFile(path, (error, result) => {
            if (error) {
              reject(error);
            } else {
              resolve(result);
            }
          });
        });
      
      readFilePromise("./data.txt")
        .then((result) => console.log(result))
        .catch((error) => console.error("Failed to read data"));
    • Turning a callback-based API into a promise-based one

      function myGetAsync(url) {
        return new Promise((resolve, reject) => {
          const xhr = new XMLHttpRequest();
          xhr.open("GET", url);
          xhr.onload = () => resolve(xhr.responseText);
          xhr.onerror = () => reject(xhr.statusText);
          xhr.send();
        });
      }
      
      myGetAsync('https://httpbin.org/headers').then(txt => console.log(txt));
    • If the executor function throws an error, reject is called automatically.

      function alarm(person, delay) {
        return new Promise((resolve, reject) => {
          if (delay < 0) {
            reject(new Error("Alarm delay must not be negative"));
          } else {
            setTimeout(() => {
              resolve(`Wake up, ${person}!`);
            }, delay);
          }
        });
      }
      
      alarm("Jon", 500).then(m => console.log(m)); // Wake up, Jon!

      Same as:

      function alarm(person, delay) {
        return new Promise((resolve) => {
          if (delay < 0) {
            throw new Error("Alarm delay must not be negative");
          }
          setTimeout(() => {
            resolve(`Wake up, ${person}!`);
          }, delay);
        });
      }
  • async and await

    The async keyword can be used to define an define an async function to a given name, and an async function inside an expression. The await operator is used to wait for a Promise and get its fulfillment value inside an async function or at the top level of a module, enabling asynchronous, promise-based behavior to be written in a cleaner style and avoiding the need to explicitly configure promise chains.

    async function setAlarm(person, delay) {
      try {
        const message = await alarm(person, delay);
        console.log(message);
      }
      catch (error) {
        console.log(error.message);
      }
    }
    
    setAlarm("Jon", 500); // Wake up, Jon!
    setAlarm("Jon", -50); // Alarm delay must not be negative

9.2. Workers

// TODO

10. Iterators and generators

In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination, and an object is iterable if it defines its iteration behavior, such as what values are looped over in a for…​of construct. A generator is an object returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.

  • When called, generator functions do not initially execute their code, instead, return a special type of iterator, called a Generator.

  • When a value is consumed by calling the generator’s next method, the Generator function executes until it encounters the yield keyword.

  • The function can be called as many times as desired, and returns a new Generator each time. Each Generator may only be iterated once.

  • In order to be iterable, an object must implement the @@iterator, a zero-argument method, meaning that the object (or one of the objects up its prototype chain) must have a property with a @@iterator key which is available via constant Symbol.iterator.

  • Whenever an object needs to be iterated (such as at the beginning of a for…​of loop), its @@iterator method is called with no arguments, and the returned iterator is used to obtain the values to be iterated.

  • Iterables which can iterate only once (such as Generators) customarily return this from their @@iterator method, whereas iterables which can be iterated many times must return a new iterator on each invocation of @@iterator.

  • A simple iterator that encapsulates the state in a closure

    function makeRangeIterator(start, count, step = 1) {
      let nextValue = start;
      let iterationCount = count;
      const iterator = {
        next() {
          while (iterationCount > 0) {
            const result = { value: nextValue, done: false };
            iterationCount--;
            nextValue += step;
            return result;
          }
          return { done: true };
        },
      };
      return iterator;
    }
    
    const iter = makeRangeIterator(0, 3, 2);
    let result = iter.next();
    while (!result.done) {
      console.log(result.value);
      result = iter.next();
    }
    // 0
    // 2
    // 4
  • A simple iterator that encapsulates the state in a constructor function

    function Range(start, count, step = 1) {
      this.nextValue = start;
      this.iterationCount = count;
      this.step = step;
      this.next = function () {
        while (this.iterationCount > 0) {
          const result = { value: this.nextValue, done: false };
          this.iterationCount--;
          this.nextValue += step;
          return result;
        }
        return { done: true };
      }
    }
    
    const iter = new Range(0, 3, 2);
    let result = iter.next();
    while (!result.done) {
      console.log(result.value);
      result = iter.next();
    }
    // 0
    // 2
    // 4
  • An iterator defined with a generator function that NOT need to explicitly maintain the internal state

    function* makeRangeIterator(start, count, step = 1) {
      let nextValue = start;
      let iterationCount = count;
      while (iterationCount > 0) {
        yield nextValue;
        iterationCount--;
        nextValue += step;
      }
    }
    
    const iter = makeRangeIterator(0, 3, 2);
    let result = iter.next();
    while (!result.done) {
      console.log(result.value);
      result = iter.next();
    }
    // 0
    // 2
    // 4
  • A generator is an iterable object

    function* makeIterator() {
      yield 1;
      yield 2;
    }
    
    const iter = makeIterator();
    
    console.log(Symbol.iterator in iter);
    // true
    
    console.log(iter[Symbol.iterator]() === iter);
    // true
    
    for (const num of iter) {
      console.log(num);
    }
    // 1
    // 2
    
    // If we change the @@iterator method of `iter` to a function/generator
    // which returns a new iterator/generator object, `iter`
    // can iterate many times
    iter[Symbol.iterator] = function* () {
      yield 2;
      yield 1;
    };
    
    for (const num of iter) {
      console.log(num);
    }
    // 2
    // 1
    
    for (const num of iter) {
      console.log(num);
    }
    // 2
    // 1
  • User-defined iterables can be used in for…​of loops or the spread syntax as usual

    const myIterable = {
      *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
      },
    };
    
    for (const value of myIterable) {
      console.log(value);
    }
    // 1
    // 2
    // 3
    
    [...myIterable]; // [1, 2, 3]

11. Events

Events are things that happen in a system that produces (or "fires") a signal of some kind when an event occurs, and provides a mechanism by which an action (event handler) can be automatically taken (that is, some code running) when the event occurs.

Event handlers are sometimes called event listeners — they are pretty much interchangeable, although strictly speaking, they work together. The listener listens out for the event happening, and the handler is the code that is run in response to it happening.
  • Handling a click event

    <button>Change color</button>
    const btn = document.querySelector("button");
    
    function random(number) {
      return Math.floor(Math.random() * (number + 1));
    }
    
    btn.addEventListener("click", () => {
      const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
      const btn = document.querySelector("button");
      btn.style.backgroundColor = rndCol;
    });
    Details
  • Event handler properties

    With event handler properties, ONLY one handler can be added for a single event.
    btn.onclick = () => {
      const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
      document.body.style.backgroundColor = rndCol;
    };
  • Using addEventListener() to add/register listeners

    function changeBackground() {
      const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
      const btn = document.querySelector(".eg-addEventListener");
      btn.style.backgroundColor = rndCol;
    }
    
    // an event object specified with a name such as event, evt, or e.
    function changeColor(e) {
      e.preventDefault();  // preventing default behavior
      const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
      e.target.style.color = rndCol;
    }
    
    // adding multiple listeners for a single event
    btn.addEventListener("click", changeBackground);
    btn.addEventListener("click", changeColor);
    
    btn.addEventListener("click", (e) => {
      e.stopPropagation(); // preventing event bubbling
      e.target.style.fontSize = `${ Math.random() + 1 }em`;
    });
    
    btn.addEventListener("click", (e) => {
      const sign = Math.random() < 0.5 ? 1 : -1;
      e.target.style.fontWeight = 500 + sign * random(200);
    }, { capture: true }); // enable event capture
    Details
  • Using the removeEventListener() to remove listeners

    btn.removeEventListener("click", changeBackground);

    Event handlers can also be removed by passing an AbortSignal to addEventListener() and then later calling abort() on the controller owning the AbortSignal.

    const controller = new AbortController();
    
    btn.addEventListener("click",
      () => {
        const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
        document.body.style.backgroundColor = rndCol;
      },
      { signal: controller.signal } // pass an AbortSignal to this handler
    );
    controller.abort(); // removes any/all event handlers associated with this controller

12. Modules

JavaScript modules (also known as “JS modules”, “ES modules” or “ECMAScript modules”) are a major new feature, or rather a collection of new features.

  • CommonJS modules are the original way to package JavaScript code for Node.js. Node.js also supports the ECMAScript modules standard used by browsers and other JavaScript runtimes.

  • ECMAScript modules are the official standard format to package JavaScript code for reuse, and are defined using a variety of import and export statements.

  • V8’s documentation recommends using the .mjs extension for modules, but it’s also recommended to use x.mjs.js, because it is a non-standard file extension, some operating systems might not recognize it.

  • In Node.js, each file is treated as a separate module. For example, consider a file named foo.cjs:

    const circle = require('./circle.cjs');
    console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
  • Every module can have two different types of export, named export and default export, but only one default export.

    // File: lib.mjs
    
    // export feature declared elsewhere as default
    export { myFunction as default };
    // This is equivalent to:
    export default myFunction;
    
    // export individual features as default
    export default function () { /* … */ }
    export default class { /* … */ }
    import myFunction from "./lib.mjs"; // Note the lack of curly braces
    
    // This is equivalent to:
    import { default as myFunction } from "./lib.mjs";
  • A more convenient way of exporting all the items is to use a single export statement at the end of the module file, followed by a comma-separated list of the features wrapped in curly braces.

    export { name, draw, reportArea, reportPerimeter };
  • Use the import keyword to import the module from another module.

    // File: lib.mjs
    export const repeat = (string) => `${string} ${string}`;
    export function shout(string) {
      return `${string.toUpperCase()}!`;
    }
    // File: main.mjs
    import {repeat, shout} from './lib.mjs';
    repeat('hello');
    // → 'hello hello'
    shout('Modules in action');
    // → 'MODULES IN ACTION!'
  • Renaming imports and exports with as to avoid naming conflicts

    // inside module.js
    export { function1 as newFunctionName, function2 as anotherNewFunctionName };
    
    // inside main.js
    import { newFunctionName, anotherNewFunctionName } from "./modules/module.js";
    // inside module.js
    export { function1, function2 };
    
    // inside main.js
    import {
      function1 as newFunctionName,
      function2 as anotherNewFunctionName,
    } from "./modules/module.js";
    // import module's features inside a module object
    import * as Module from "./modules/module.js";
    Module.function1();
    Module.function2();
  • When importing modules, the string that specifies the location of the module is called the “module specifier” or the “import specifier”, that the JavaScript environment can resolve to a path to the module file.

    // For now, module specifiers must be full URLs, or relative URLs starting with `/`, `./`, or `../`.
    import { name as squareName, draw } from "./shapes/square.js";
    import { name as circleName } from "https://example.com/shapes/circle.js";
    • Import maps allow developers to instead specify almost any text they want in the module specifier when importing a module; the map provides a corresponding value that will replace the text when the module URL is resolved.

      • The import map is defined using a JSON object inside a <script> element with the type attribute set to importmap.

        <script type="importmap">
          {
            "imports": {
              "shapes": "./shapes/square.js",
              "shapes/square": "./modules/shapes/square.js",
              "https://example.com/shapes/square.js": "./shapes/square.js",
              "https://example.com/shapes/": "/shapes/square/",
              "../shapes/square": "./shapes/square.js"
            }
          }
        </script>
      • There can only be one import map in the document, and because it is used to resolve which modules are loaded in both static and dynamic imports, it must be declared before any <script> elements that import modules.

      • Note that the import map only applies to the document — the specification does not cover how to apply an import map in a worker or worklet context.

      • If there is no trailing forward slash on the module specifier key then the whole module specifier key is matched and substituted.

        // Bare module names as module specifiers
        import { name as squareNameOne } from "shapes";
        import { name as squareNameTwo } from "shapes/square";
        
        // Remap a URL to another URL
        import { name as squareNameThree } from "https://example.com/shapes/square.js";
      • If the module specifier has a trailing forward slash then the value must have one as well, and the key is matched as a "path prefix".

        // Remap a URL as a prefix ( https://example.com/shapes/)
        import { name as squareNameFour } from "https://example.com/shapes/moduleshapes/square.js";
  • Using JS modules in the browser.

    <!-- Use a `<script>` element as a module by setting the `type` attribute to `module`. -->
    <script type="module" src="main.mjs"></script>
    <!-- Browsers that understand type="module" ignore scripts with a `nomodule` attribute. -->
    <script nomodule defer src="fallback.js"></script>
    <!-- embed the module's script directly into the HTML file -->
    <script type="module">
      /* JavaScript module code here */
    </script>
  • Modules are a little different from classic scripts:

    • Modules have strict mode enabled by default.

    • HTML-style comment syntax is not supported in modules, although it works in classic scripts.

      // Don’t use HTML-style comment syntax in JavaScript!
      const x = 42; <!-- TODO: Rename x to y.
      // Use a regular single-line comment instead:
      const x = 42; // TODO: Rename x to y.
    • Modules have a lexical top-level scope. This means that for example, running var foo = 42; within a module does NOT create a global variable named foo, accessible through window.foo in a browser, although that would be the case in a classic script.

    • Similarly, the this within modules does not refer to the global this, and instead is undefined. (Use globalThis if you need access to the global this.)

    • The new static import and export syntax is only available within modules — it doesn’t work in classic scripts.

    • Top-level await is available in modules, but not in classic scripts. Relatedly, await cannot be used as a variable name anywhere in a module, although variables in classic scripts can be named await outside of async functions.

    • Also, module scripts and their dependencies are fetched with CORS. This means that any cross-origin module scripts must be served with the proper headers, such as Access-Control-Allow-Origin: *. This is not true for classic scripts. For example, the file:// URL will run into CORS errors.

    • There is no need to use the defer attribute when loading a module script; modules are deferred automatically.

      Modules are deferred by default
    • The async attribute does not work for inline classic scripts, but it does work for inline <script type="module">.

    • Modules are only executed once, even if they have been referenced in multiple <script> tags.

      <script src="classic.js"></script>
      <script src="classic.js"></script>
      <!-- classic.js executes multiple times. -->
      
      <script type="module" src="module.mjs"></script>
      <script type="module" src="module.mjs"></script>
      <script type="module">import './module.mjs';</script>
      <!-- module.mjs executes only once. -->
  • The function import() with a path to the module as a parameter returns a Promise which fulfills with a module object, and can be used for dynamic module loading.

    // dynamic module loading
    import("./modules/myModule.js").then((module) => {
      // Do something with the module.
    });
    <script type="module">
      (async () => {
        const moduleSpecifier = './lib.mjs';
        const {repeat, shout} = await import(moduleSpecifier);
        repeat('hello');
        // → 'hello hello'
        shout('Dynamic import in action');
        // → 'DYNAMIC IMPORT IN ACTION!'
      })();
    </script>
    Dynamic import is permitted in the browser main thread, and in shared and dedicated workers. However import() will throw if called in a service worker or worklet.
  • Import declarations are hoisted

    // …
    const myCanvas = new Canvas("myCanvas", document.body, 480, 320);
    myCanvas.create();
    import { Canvas } from "./modules/canvas.js";
    myCanvas.createReportList();
    // …

13. npm

  • npm is a website used to discover packages, set up profiles, and manage other aspects of the npm experience.

  • npm is a registry that provides a large public database of JavaScript software and the meta-information surrounding it.

  • npm is a CLI that runs from a terminal as the standard package manager for Node.js.

  • A package is a folder tree described by a package.json file. The package consists of the folder containing the package.json file and all subfolders until the next folder containing another package.json file, or a folder named node_modules.

  • Packages can be unscoped or scoped to a user or organization, and scoped packages can be private or public.

    • A scoped package is listed as a dependent in a package.json file preceded by a scope name which is everything between the @ and the /.

    • Unscoped packages are always public.

  • A module is any file or directory in the node_modules directory that can be loaded by the Node.js require() or import() function.

  • A package.json file must contain "name" and "version" fields.

13.1. Volta: The Hassle-Free JavaScript Tool Manager

  • install Volta

    curl https://get.volta.sh | bash
    source ~/.bashrc
  • (Optional) generate Volta completions

    volta completions bash >> ~/.bashrc
    source ~/.bashrc
  • install Node

    volta install node
    
    # start using Node, Npm, Npx
    npm help help

13.2. Manage the npm configuration files

npm gets its config settings from the command line, environment variables, and npmrc files. For a list of available configuration options, see config.

npm config set <key>=<value> [<key>=<value> ...]
npm config get [<key> [<key> ...]]
npm config delete <key> [<key> ...]
npm config list [--json]
npm config edit
npm config fix

alias: c
  • Echo the config value(s) to stdout

    $ npm config get cache prefix global registry
    cache=/home/x/.npm
    prefix=/home/x/.volta/tools/image/node/20.12.2
    global=false
    registry=https://registry.npmjs.org/
  • config a custom registry

    npm config get registry # https://registry.npmjs.org/
    npm config set registry https://registry.npmmirror.com
    cat ~/.npmrc # registry=https://registry.npmmirror.com

13.3. Creating a package.json file

  • npm init

    npm init <package-spec> (same as `npx <package-spec>`)
    npm init <@scope> (same as `npx <@scope>/create`)
    
    aliases: create, innit
  • Creating a scoped public package

    npm init --scope=@foo --yes
    {
      "name": "@foo/hello",
      "version": "1.0.0",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "description": ""
    }
  • To publish a scoped package with public visibility, use npm publish --access public

    npm publish --access public

13.4. Specifying dependencies and devDependencies in a package.json file

  • npm install

    npm install [<package-spec> ...]
    
    aliases: add, i, in, ins, inst, insta, instal, isnt, isnta, isntal, isntall
  • To add an entry to the "dependencies" attribute of a package.json file, on the command line, run the following command:

    npm install <package-name>[@<version>] [--save-prod]
    $ npm install @azure/msal-browser
    
    added 2 packages, and audited 3 packages in 4s
    
    found 0 vulnerabilities
    $ cat package.json
    {
    . . .
      "dependencies": {
        "@azure/msal-browser": "^3.13.0"
      }
    }
  • To add an entry to the "devDependencies" attribute of a package.json file, on the command line, run the following command:

    npm install <package-name>[@<version>] --save-dev
    $ npm install vite@^5 --save-dev
    
    added 10 packages, and audited 13 packages in 2s
    
    3 packages are looking for funding
      run `npm fund` for details
    
    found 0 vulnerabilities
    $ cat package.json
    {
    . . .
      "devDependencies": {
        "vite": "^5.2.9"
      }
    }
  • Using semantic versioning, see also the npm semver calculator

    $ npm install vite --save-dev
    
    added 1 package in 2s
    
    3 packages are looking for funding
      run `npm fund` for details
    $ npm ls vite
    @foo/hello@1.0.0 /path/to/package/hello
    └── vite@5.2.9
    • Use the caret (aka hat) symbol, ^, to include everything that does not increment the first non-zero portion of semver

      ^2.2.1
      ^0.1.0
      ^0.0.3
      $ npm i vite@^5.0.0
      
      up to date in 1s
      
      3 packages are looking for funding
        run `npm fund` for details
      $ npm ls vite
      @foo/hello@1.0.0 /path/to/package/hello
      └── vite@5.2.9
    • Use the tilde symbol, ~, to include everything greater than a particular version in the same minor range

      ~2.2.0
      $ npm i vite@~5.0.0
      
      changed 3 packages in 4s
      
      3 packages are looking for funding
        run `npm fund` for details
      $ npm ls vite
      @foo/hello@1.0.0 /path/to/package/hello
      └── vite@5.0.13
    • Use >, <, =, >= or <= for comparisons, or - to specify an inclusive range to specify a range of stable versions

      >2.1
      1.0.0 - 1.2.0
      $ npm i "vite@3.0 - 5.0"
      
      added 2 packages, and changed 4 packages in 3s
      
      3 packages are looking for funding
        run `npm fund` for details
      $ npm ls vite
      @foo/hello@1.0.0 /path/to/package/hello
      └── vite@5.0.13
    • Use || to combine multiple sets of versions

      ^2 <2.2 || > 2.3
      $ npm i "vite@^2 <2.2 || > 2.3"
      
      changed 3 packages in 2s
      
      3 packages are looking for funding
        run `npm fund` for details
      $ npm ls vite
      @foo/hello@1.0.0 /path/to/package/hello
      └── vite@5.2.9
    • Use the pre-release tag to include pre-release versions like alpha and beta

      1.0.0-rc.1
      >1.0.0-alpha
      >=1.0.0-rc.0 <1.0.1
      $ npm i vite@6.0.0-alpha.2
      
      added 1 package, and changed 3 packages in 2s
      
      3 packages are looking for funding
        run `npm fund` for details
      $ npm ls vite
      @foo/hello@1.0.0 /path/to/package/hello
      └── vite@6.0.0-alpha.2

13.5. Run arbitrary package scripts

  • npm run

    npm run-script <command> [-- <args>]
    
    aliases: run, rum, urn
    • run[-script] is used by the test, start, restart, and stop commands from a package’s "scripts" object.

    • If no "command" is provided, it will list the available scripts.

    • The env script is a special built-in command that can be used to list environment variables that will be available to the script at runtime.

    • When the scripts in the package are printed out, they’re separated into lifecycle (test, start, restart) and directly-run scripts.

  • Run a predefined command specified in the "test", "start", "restart", "stop" of a package’s "scripts" object:

    npm test [-- <args>]
    npm start [-- <args>]
    npm stop [-- <args>]
    npm restart [-- <args>]
  • Run a command from a local or remote npm package

    npx -- <pkg>[@<version>] [args...]
    npx --package=<pkg>[@<version>] -- <cmd> [args...]
    npx -c '<cmd> [args...]'
    npx --package=foo -c '<cmd> [args...]'
    $ npx create-react-app my-app
    $ cd my-app
    $ npm run
    Lifecycle scripts included in my-app@0.1.0:
      start
        react-scripts start
      test
        react-scripts test
    
    available via `npm run-script`:
      build
        react-scripts build
      eject
        react-scripts eject
    $ cat package.json
    . . .
      "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject"
      },
    . . .

13.6. pnpm: Fast, disk space efficient package manager

volta install pnpm
# (optional) set aliases
alias pn='pnpm' pnx='pnpx'