JavaScript Learning Notes
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-definedError
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
- 2. Control flow and error handling
- 3. Loops and iteration
- 4. Functions
- 5. Expressions and operators
- 6. Regular expressions
- 7. Indexed and keyed Collections
- 8. Prototypes, objects and classes
- 9. Promises and asynchronous programming
- 10. Iterators and generators
- 11. Events
- 12. Modules
- 13. npm
- 14. References
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:
-
Boolean.
true
andfalse
. -
null. A special keyword denoting a null value. (Because JavaScript is case-sensitive,
null
is not the same asNull
,NULL
, or any other variant.) -
undefined. A top-level property whose value is not defined.
-
Number. An integer or floating point number. For example:
42
or3.14159
. -
BigInt. An integer with arbitrary precision. For example:
9007199254740992n
. -
String. A sequence of characters that represent a text value. For example:
"Howdy"
. -
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 likeArray.prototype.map
, empty slots are skipped. However, index-accessingfish[1]
still returnsundefined
.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 leading0o
(or0O
) indicates it is in octal. -
A leading
0x
(or0X
) indicates a hexadecimal integer literal. -
A leading
0b
(or0B
) 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 like0123n
is not allowed, but0o123n
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
orE
).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, makingsuper
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 For example:
|
-
Use the
if
statement to execute a statement if a logical condition istrue
. Use the optionalelse
clause to execute a statement if the condition isfalse
. Use the optionalelse 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 acase
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 thetry…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 entiretry…catch…finally
production, regardless of anyreturn
statements in thetry
andcatch
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 thecatch
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 Cfor
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 (includingArray
,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
andfor…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 totrue
.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 enclosingwhile
,do-while
,for
, orswitch
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 awhile
,do-while
,for
, orlabel
statement.-
When you use
continue
without a label, it terminates the current iteration of the innermost enclosingwhile
,do-while
, orfor
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 theincrement-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 likeeval()
.const sum = new Function('a', 'b', 'console.log(a + b)'); sum(2, 6); // 8
The
call()
andapply()
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
-
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 returnedinside
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
orBigInt
: specifically, if the type of the left operand isBigInt
, they returnBigInt
; otherwise, they returnNumber
.
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 isnull
orundefined
, 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 returnstrue
if the specified property is in the specified object or its prototype chain. Thein
operator cannot be used to search for values in other collections. To test if a certain value exists in an array, useArray.prototype.includes()
. For sets, useSet.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 withSymbol.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
methodstest()
andexec()
and with theString
methodsmatch()
,matchAll()
,replace()
,replaceAll()
,search()
, andsplit()
.
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 returnsundefined
.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 runsmap()
followed by aflat()
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.
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.
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 The It is equivalent to the JavaScript accessor It’s worth noting that the |
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
orObject.prototype.hasOwnProperty
methods.All objects, except those with null
as[[Prototype]]
, inherithasOwnProperty
fromObject.prototype
— unless it has been overridden further down the prototype chain.Object.hasOwn()
is intended as a replacement forObject.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 thenew
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]]
ofConstructor.prototype
via theObject.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
orconst
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.
-
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 afetch()
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.
-
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 createsPromise
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
andrejectFunc
.resolveFunc(value); // call on resolved rejectFunc(reason); // call on rejected
-
The
value
parameter passed toresolveFunc
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 thethrow
statement, so reason is typically anError
instance. If theexecutor
function throws an error,reject
is called automatically. -
If either
value
orreason
is omitted, the promise is fulfilled/rejected withundefined
.
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 theyield
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 constantSymbol.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 usualconst 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 listenersfunction 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 listenersbtn.removeEventListener("click", changeBackground);
Event handlers can also be removed by passing an AbortSignal to
addEventListener()
and then later callingabort()
on the controller owning theAbortSignal
.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 usex.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 thetype
attribute set toimportmap
.<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 namedfoo
, accessible throughwindow.foo
in a browser, although that would be the case in a classic script. -
Similarly, the
this
within modules does not refer to the globalthis
, and instead isundefined
. (Use globalThis if you need access to the globalthis
.) -
The new static
import
andexport
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 namedawait
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, thefile://
URL will run into CORS errors. -
There is no need to use the
defer
attribute when loading a module script; modules are deferred automatically. -
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 thepackage.json
file and all subfolders until the next folder containing anotherpackage.json
file, or a folder namednode_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.jsrequire()
orimport()
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 apackage.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 apackage.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
andbeta
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'