JavaScript Notes
Compiled by Jeremy Kelly
www.anthemion.org
Printed UNKNOWN
These are my JavaScript notes, covering ES2023, plus a few features from later versions. If you find a mistake, please let me know.
The example code uses a new notation I am developing. More on that soon.
This page includes a two-column print style. For best results, print in landscape, apply narrow margins, change the Scale setting in your browser’s print options to 70%, and enable background graphics. Firefox and Chrome each have their own set of printing bugs, but one of them usually works.
Contents
- Syntax
- Identifiers
- ASI
- Hashbang comments
- Primitive types
- Booleans
- Numbers
- Number class
- Math class
- BigInt
- Strings
- Special characters
- Template literals
- Tagged literals
- String class
- Symbols
- Symbol-keyed properties
- Well-known symbols
- Special primitive types
- Objects
- Properties
- Accessor properties
- Property attributes
- Testing properties
- Enumerating properties
- Deleting properties
- with
- Creating objects
- Object literals
- new
- Object.create
- Object attributes
- Object class
- Weak references
- Classes
- Constructors
- Prototypes
- Class properties
- Manual class setup
- Class declarations
- Class fields
- Private properties
- Static class members
- Subclass declarations
- Class expressions
- Testing inheritance
- Type conversion
- Variables
- var
- let and const
- Destructuring
- Control structures
- if
- switch
- for loops
- for/of
- for/in
- Labels
- Operators
- Logical operators
- Bitwise operators
- Arithmetic operators
- Equality and comparison operators
- Other operators
- typeof
- Sequence operator
- void
- Functions
- Function declarations
- Function expressions
- Arrow functions
- Function constructor
- Methods
- Parameters
- Default parameters
- Rest parameters and spread syntax
- this
- Closures
- Currying
- Generators
- Input to generators
- Exceptions
- Containers
- Iterables and iterators
- Arrays
- Array class
- Array-like objects
- Maps
- Map class
- Weak maps
- Sets
- Set class
- Weak sets
- Regular expressions
- Search flags
- Character classes
- Quantifiers
- Groups
- Anchors
- RegExp class
- Asynchronous programming
- Promises
- Promise settlement
- then, catch, and finally
- Chaining promises
- Promise class
- async and await
- async functions
- Awaiting promises
- Asynchronous iterables
- for await/of
- Asynchronous generators
- Modules
- ECMAScript modules
- export
- Default exports
- import
- Dynamic imports
- IIFE
- Miscellanea
- Strict mode
- eval
- debugger
- General sources
Syntax
Identifiers
Identifiers must begin with letters, underscores, or dollar signs. After the first letter, digits can be added. Underscore prefixes are sometimes used to suggest private data.
When strict mode is enabled, JavaScript keywords are forbidden for use as identifiers. The following reserved words are also forbidden:
ASI
Automatic semicolon insertion (ASI) allows some lines to end without semicolons. If an attempt is made to return an expression, and if that expression begins on the line following the return, ASI will be applied to the return line, and the expression will be ignored:
function uBase() {
return
`EDIT:
A->{TargA}
B->{TargB}`;
}
Line breaks can be placed within the expression, as long as the expression starts on the return line:
function uMSFromDays(aDays) {
return aDays
* 24 * 60 * 60 * 1000;
}
ASI can also be avoided by parenthesizing the expression:
function uMSFromDays(aDays) {
return (
aDays * 24 * 60 * 60 * 1000
);
}
Sources
What are the rules for [ASI]...Hashbang comments
JavaScript supports the usual line // and block /* */ comments.
ES2023 also treats the first line of a file as a comment if it begins with a shebang #!. There can be no whitespace or BOM before the pound sign.
Sources
Hashbang commentsPrimitive types
Primitive types include boolean, number, BigInt, string, and symbol, plus the special types that implement undefined and null. These are copied and compared by value.
Most primitives contain properties, but (unlike objects) they do not allow these to be modified, nor can new properties be added. Primitives also have constructors, and these can be called without new for explicit conversions:
const oNum = Number("10.0");
JavaScript also allows primitive constructors to be invoked with new, but doing so creates objects, not primitive instances. These objects have some qualities of the corresponding primitives, but they are not strictly equivalent, and there is generally no reason to create them:
const oObjNum = new Number(10.0);
uAssert(oObjNum !== oNum);
Booleans
Boolean values are represented with true and false. When evaluated as booleans, falsy values like these are also considered to be false:
undefined- Zero
NaN- The empty string
null
All other values are considered truthy, including empty objects and empty arrays.
Numbers
Number values are double precision IEEE 754 floats. Until ES2019, JavaScript did not provide an integer type, but all integer values between -253 and 253 retain full precision in the number type. JavaScript defines global variables Infinity and NaN to represent infinite and undefined values. As in other languages, NaN is usually unequal to every value, including itself. However, Object.is returns true if both arguments are NaN.
Values can be checked for Infinity or NaN with these global functions:
-
isNaN(num) -
Returns
trueif num isNaN, or something other than a number or boolean. However, type conversion causesisNaN("")to returnfalse. -
isFinite(num) -
Returns
trueif num is neither positive nor negative infinity, and if it is notNaN.
Numbers can be specified with scientific notation. The mantissa and the exponent are separated by E or e:
const Tol = 1.1E-3;
A different base can be specified by prefixing the number literal:
| Prefix | Base |
0b or 0B |
Binary |
0o or 0O |
Octal |
0x or 0X |
Hexadecimal |
However, non-decimal literals cannot have fractional components.
Starting with ES2021, underscores can be used as separators in number literals of any base. They can be placed between any two digits, within the integer, fraction, or exponent parts:
const oMolsPerMole = 6.022_140_76e23;
When the Number function is used to convert a value:
undefinedis converted toNaN;nullis converted to zero;- A boolean is converted to zero or one;
- A
BigIntis converted with precision loss, or to an infinite value, as necessary; - A string is trimmed of leading and trailing whitespace, then converted to the number that remains (with possible precision loss), or to zero if no text is left, or to
NaNif the text is not a number. Scientific notation is supported, and “0x”, “0o”, or “0b” prefixes can be used to specify the base.+or-signs are allowed in the mantissa or the exponent. Leading zeros are ignored. “Infinity” (or “-Infinity”) is converted to an infinite value, as are numbers outside theNumberrange; - A symbol produces the
TypeErrorexception; - An object is converted to a primitive with
[Symbol.toPrimitive],valueOf, ortoString, then that primitive is converted to a number.
The unary + operator performs a similar conversion, except that BigInt operands produce TypeError exceptions.
Strings can also be converted to number instances with these global functions:
-
parseFloat(text) -
Converts text as if it had been passed to
Number, except thatNaNis returned if text is null, the “0x”, “0o”, and “0b” prefixes are not supported, and non-numeric characters after the first number character are ignored. -
parseInt(text, [base]) -
Behaves as
parseFloat, except that any fractional component is ignored, along with any exponent. If base is zero or undefined, the number base is inferred from the text, however only “0x” and “0X” are widely recognized as prefixes. As withparseFloat, other prefixes cause zero to be returned, since the prefix letter and everything after it is ignored. Most implementations ignore leading zeros, but some interpret the following number as octal. If base is defined and non-zero, it must be between two and 36 inclusive, orNaNwill be returned.
Sources
Number coercion parseFloatNumber class
Number primitives are wrapped by the Number class. Its members include:
-
static
MIN_VALUE -
static
MAX_VALUE - The least and greatest values that can be represented by a double precision float.
-
static
MIN_SAFE_INTEGER -
static
MAX_SAFE_INTEGER - The least and greatest integers that can be represented unambiguously by a double precision float. Values outside this range are likely to lose precision.
-
static
isInteger(num) -
static
isSafeInteger(num) -
Returns
trueif num is any integer, or one betweenMIN_SAFE_INTEGERandMAX_SAFE_INTEGER. -
toString([base]) - Returns a string representation of the number, in decimal, or in the specified base.
-
toFixed(ct) - Returns a string representation that rounds the number to ct digits after the decimal point.
-
toPrecision(ct) - Returns a string representation that rounds the number to ct significant digits.
-
toExponential([ct]) - Returns a string representation of the number, in scientific notation. Optionally rounds the mantissa to ct digits after the decimal point.
Math class
The static Math class contains many constants and methods, including those that handle rounding:
-
static
floor(num) -
static
ceil(num) - Returns num rounded toward negative or positive infinity.
-
static
trunc(num) - Returns num rounded toward zero.
-
static
round(num) -
static
fround(num) - Returns num rounded toward the nearest integer, or the nearest single-precision float.
Exponents and roots:
-
static
E - Euler’s number e.
-
static
SQRT1_2 -
static
SQRT2 - The square roots of one-half and two.
-
static
exp(exp) -
static
pow(base, exp) - Returns e or base to the power of exp.
-
static
sqrt(num) -
static
cbrt(num) - Returns the square or cube root of num.
-
static
log(num) -
static
log2(num) -
static
log10(num) - Returns the natural, base-2, or base-10 logarithm of num.
Trigonometry:
-
static
PI -
static
cos(rad) -
static
sin(rad) -
static
tan(rad) - Returns the cosine, sine, or tangent of an angle measured in radians.
-
static
acos(num) -
static
asin(num) -
static
atan(num) -
Returns the inverse cosine, sine, or tangent of num, in radians, or
NaNif num is out of range. -
static
atan2(y, x) - Returns the inverse tangent, in radians, of the slope specified by y and x.
and other functions:
-
static
sign(num) -
Returns
-1if num is negative,0if it is zero, or1if it is positive. -
static
abs(num) - Returns the absolute value of num.
-
static
min(num, ...) -
static
max(num, ...) - Returns the minimum or maximum of one or more numbers.
-
static
random() - Returns a random number greater than or equal to zero, and less than one.
Sources
MathBigInt
In ES2020, true integer values can be represented with BigInt, which has arbitrary length. A BigInt literal is created by appending n to the value:
const oGoogol = 10n ** 100n;
Underscores can be used as separators in these literals, starting in ES2021.
BigInt values cannot be mixed with numbers without explicit conversion, using the Number and BigInt functions:
const oVal = Number(oGoogol) / 100.0;
return BigInt(oVal);
A BigInt value may lose precision when converted. If it is outside the number range, an infinite value will be produced.
Sources
BigInt type BigInt: Arbitrary precision integers... (Ecma TC39)Strings
JavaScript has no character type. Every string is a sequence of Unicode characters or code points. Many code points contain a single UTF-16 code unit, while others are composed of two code points, called a surrogate pair. Many string functions operate on code units, not code points, and these can accidentally produce lone surrogates, which are not valid characters. A string with no lone surrogates is considered well-formed. Some symbols (including many emoji) that appear to be a single character are actually composed of several characters; these are called grapheme clusters.
String literals can be surrounded by single or double quotes. Quote characters are escaped with a single backslash. Long strings can be split across lines with a backslash inside the string:
const Warn = "No people ever recognize \
their dictator in advance";
Single backslashes in other positions have no effect, and are excluded from the string. String instances can be concatenated with the addition operators + and +=.
The bracket notation can be used to extract individual code points. This works much like charAt, except that no attempt is made to convert the operand to a number, and undefined is returned if the index is out of range. String instances are immutable, so characters cannot be modified this way:
const oCh = Warn[0];
Other types define toString methods that are used during string conversions. Overriding this method allows custom output to be produced when the String function is used to convert the type:
tPt.prototype.toString = function () {
return `[${this.X}, ${this.Y}]`;
};
const oPt = new tPt(1, 2);
const oText = String(oPt);
Special characters
The usual escape sequences are supported:
| Sequence | Character |
\0 |
Null |
\b |
Backspace |
\f |
Form feed |
\n |
Newline |
\f |
Form feed |
\r |
Carriage return |
\t |
Tab |
\v |
Vertical tab |
\\ |
Backslash |
\' |
Single quote |
\" |
Double quote |
An arbitrary character or code unit can be specified by combining \x or \u with a number of hex digits:
| Sequence | Character |
\x XX |
An element from the Latin-1 character set. XX must be exactly two digits. |
\u XXXX |
A Unicode code unit. XXXX must be exactly four digits. |
\u{X} |
ES6 A Unicode code point. X can be one to six digits. |
In sloppy mode, a Latin-1 character can also be specified with a single backslash followed by up to three octal digits. In strict mode, this sequence throws SyntaxError instead. For this reason, the \0 escape sequence must not be immediately followed by a number. It is generally safer to specify the null character with \x00.
Sources
String literals Lexical grammar / String literalsTemplate literals
Introduced in ES6, template literals are strings surrounded by backquotes. They resemble interpolated strings in C#. Within the literal, a placeholder is defined by surrounding a JavaScript expression with curly braces, prefixed with a dollar sign:
const oText = `Max value: ${(1 << oCt) - 1}`;
A backquote can be added by prefixing it with a single backslash. Line breaks and tabs inside the literal are stored within the string:
const oQuote = `So little pains
do the vulgar take
in the investigation
of truth`;
Escape sequences are processed as usual. This processing can be partially avoided by tagging the string with String.raw():
const oPath = String.raw`C:\Temp`;
This makes it impossible to specify a single trailing backslash, however, as that would be interpreted as an attempt to escape the closing backquote.
Tagged literals
A template literal is tagged by prefixing it with the name of a function:
function ouAdd_Dist(aStrs, ...aVals) {
let oSqs = 0.0;
let oText = aStrs[0];
for (let o = 0; o < aVals.length; ++o) {
oText += (aVals[o] + aStrs[o + 1]);
oSqs += Math.pow(aVals[o], 2.0);
}
oText += ` DIST: ${Math.sqrt(oSqs)}`;
return oText;
}
const oj = 3, oY = 4;
const oLine = ouAdd_Dist`X: ${oj} Y: ${oY}`;
This function is invoked when the template literal is evaluated. The function’s first argument is an array that contains the static portions of the literal, before and after each placeholder. If there are n placeholders, this array will contain n+1 elements, some of them possibly empty strings. The array also contains a raw property that shows the string content before escape sequences are processed. After the array, the function receives n arguments representing the placeholder results before they are converted to strings. The function’s result (which need not be a string) becomes the value of the template expression.
Sources
Template literalsString class
String primitives are wrapped by the String class. Its members include:
-
length - Returns the number of code units in the string.
-
ES2022
at(index) -
Returns a string containing the code unit at the specified index. Unlike the bracket notation,
atwraps back once from the end of the string if index is negative. Like that notation, it returnsundefinedif index is out of range. -
charAt(index) -
Returns a string containing the code unit at the specified index. Unlike the bracket notation,
charAtreturns an empty string if the index is out of range. -
codePointAt(index) -
static
fromCodePoint(...) -
codePointAtreturns an integer containing the code point at the specified code unit index, or a trailing surrogate, if index is inside a point, orundefinedif it is out of range.fromCodePointaccepts zero or more numbers representing code points, and converts them to a string. -
deprecated
substr(start, len) -
Returns the len characters beginning at start. If start is negative, it wraps back once from the end of the string. If len is negative, zero is used instead.
Note that this method specifies the substring length, unlike the similarly-named
substring. -
substring(start, [next]) -
Returns the characters that begin at start, and run through the end of the string, or end just before next. If either argument is negative or
NaN, zero is used instead. If either argument is greater than the length, the length is used instead. If next is equal to start, an empty string is returned. If next is less than start, the arguments are swapped.Note that this method specifies the substring ‘next’ index, unlike the similarly-named
substr. They differ fromslicein that arguments do not wrap around the end of the string, and may be reversed.sliceis generally preferred. -
slice(start, [next]) -
Returns the characters that begin at start, and run through the end of the string, or end just before next. If either argument is negative, it wraps back once from the end of the string. If start is
undefined, zero is used instead. If start equals or exceeds the length, or if it equals or exceeds next, an empty string is returned. If next isundefined, or if it equals or exceeds the length, the length is used instead. -
includes(sub, [start]) -
Returns
trueif string sub is found anywhere within the string, searching from the start, or from index start. -
startsWith(sub, [start]) -
Returns
trueif the string begins with sub, or if it is found at index start. -
endsWith(sub, [end]) -
Returns
trueif the string ends with sub, or if it ends just before index end. -
indexOf(sub, [start]) -
Returns the index where sub first occurs, searching from the start of the string, or from index start. If start is negative, zero is used instead. Returns
-1if sub is not found. -
lastIndexOf(sub, [start]) -
Returns the index where sub last occurs, searching from the end of the string, or from index start. Though start prevents substrings that begin after that index from being found, it does not prevent substrings that run past start from being found. If start is negative, zero is used instead. Returns
-1if sub is not found. -
search(regex) -
Returns the index of the first regular expression match within the string, or
-1if no match is found. Any global search flag in the expression is ignored. -
match(regex) -
Returns an array containing one or more matches produced by a regular expression. If the global search flag is set, this array contains every matching substring. If the flag is not set, it contains the data returned by
RegExp.exec, including the first matching substring, any substrings matched by capturing parentheses, plus metadata properties. Returnsnullif no match is found. -
ES2020
matchAll(regex) -
Returns an iterable that produces all matches produced by a regular expression. Each element contains the data returned by
RegExp.exec, including the first matching substring, any substrings matched by capturing parentheses, plus metadata properties. The iterable will generate no elements if no matches were found.matchAllthrows if the global search flag is not set within the expression. -
split(delim, [max]) -
Returns an array containing all substrings delimited by a substring or regular expression. If delim is
undefined, a single element will be produced, containing the entire string. If max is specified, no more than that number of elements will be returned. Note thatsplit("")divides the string into code units, possibly producing lone surrogates. -
replace(orig, new) -
Returns a new string that replaces one or more matches of string or regular expression orig with string or replacement function new. If orig is a string, only the first match is replaced. If orig is an empty string, this adds new to the start of the source string. If orig is a regular expression, and if the expression’s global flag is set, all matches are replaced.
If a string is passed as orig, it can contain replacement patterns, which begin with dollar signs:
Pattern Result $$A dollar sign $&The substring match $`The string that precedes the substring match $'The string that follows the substring match $numThe capturing group with the specified num between one and 100 $nameThe capturing group with the specified name If a replacement function is used, it may accept arguments that provide the substring match, the substrings matched by any capturing parentheses, the match position, the original string, and an object associating capturing group names with matches. The exact argument list will depend on whether orig is a
RegExpinstance. The function should return the replacement substring. -
ES2021
replaceAll(orig, new) -
Returns a new string that replaces all matches of string or regular expression orig with substring new. As with
replace, new can be set to a replacement function. Throws if orig is a regular expression without the global flag set. -
repeat(ct) - Returns a new string that repeats the source ct times.
-
trim() -
trimStart() -
trimEnd() - Returns a new string with whitespace removed from one or both ends.
-
toLowerCase() -
toUpperCase() - Converts to a new string containing all lowercase or uppercase characters.
-
padStart(len, [pad]) -
padEnd(len, [pad]) - Returns a new string that extends the original to len code units, by adding spaces or iterations of string pad to the beginning or end. If pad is more than one code unit in length, the last iteration may be truncated to fit len, possibly producing a lone surrogate.
Sources
String charAt codePointAt slice replaceSymbols
ES6 provides the new symbol primitive type. A symbol is created by invoking the Symbol function. Note that this is not a constructor, and it cannot be called with new:
const FlagRun = Symbol("Flag");
If a description string is passed to the function, that string will be included when the symbol is converted to a string. The description has no other effect.
A symbol created this way is unequal to every other symbol, even one with the same description:
const FlagCache = Symbol("Flag");
uAssert(FlagCache !== FlagRun);
The static Symbol.for method fetches an existing symbol from the global symbol registry. This method accepts a string parameter to be used as a key within the registry. If a symbol with that key is found, it is returned. If not, a new symbol is created for the key, it is added to the registry, and then returned:
const oCkA = Symbol.for("Ck");
const oCkB = Symbol.for("Ck");
uAssert(oCkA === oCkB);
The static Symbol.keyFor method returns the key that was used to create a registry symbol, or undefined if the symbol is not in the registry.
Unlike most types, symbols are not converted automatically to strings. Such conversions must be performed explicitly, with the String function, or with the Symbol.prototype.toString method. Nor can symbols be converted to numbers, even explicitly. They can be converted to booleans, but they are always truthy.
Symbol-keyed properties
Symbols can be used to add symbol-keyed properties to an object. Like a computed property name, the symbol is placed within square braces:
const oMsg = {
[FlagRun]: true,
[FlagCache]: false
};
The property is also dereferenced with the bracket notation:
const oCk = oMsg[FlagCache];
Because the symbol is unique, the resulting property is unique within the object. If the symbol is not shared, the symbol-keyed property will never collide with another property.
Symbol-keyed properties are not enumerated by for/in loops, or by methods like Object.keys. The static Object.getOwnPropertySymbols method returns an array containing the symbols that have been used to define properties in a particular object.
Well-known symbols
The Symbol class also statically defines a number of well-known symbols, which can be used to add symbol-keyed properties to an object. Symbol.toPrimitive, for instance, references a function that the runtime will use when coercing an object to a primitive:
class tSwitch {
constructor(aAlt) {
this.Alt = aAlt;
}
[Symbol.toPrimitive](hint) {
if (hint !== "number") return null;
return this.Alt ? Number(this.Alt) : null;
}
...
Sources
Symbol MDN Glossary: Symbol Symbol.forSpecial primitive types
undefined and null are unique instances of their own, dedicated types.
undefined is assigned to variables that have been declared but not initialized, among other things. If an attempt is made to read a variable that has not even been declared, the runtime will produce a ReferenceError that describes the variable as ‘not defined’. This does not mean that the variable is undefined.
null represents the explicit absence of a value, unlike many languages, where its meaning resembles undefined.
Objects
An object is a set of zero or more key/value pairs called properties. Object variables are references, and objects are copied and compared by reference. Technically, every instance that is not a primitive type is an object. Even functions are objects, and these can define their own properties, which can even reference other functions.
Many global values are members of the global object, which is created when the runtime starts. This includes global properties like undefined and NaN, functions like isNaN and String, plus globals defined in a script with var. Within a browser, the Window instance is the global object. In ES2020, globalThis can be used from any scope to reference the global this value, which often points to the global object.
An object can be serialized by passing it to JSON.stringify, which returns a JSON string representation of its data, including contained objects and arrays. Methods are ignored. The object can be deserialized with JSON.parse. If the same contained object is referenced more than once in the source object, it will be serialized more than once in the stringify output; as a result, the source object will not have the same referential structure when deserialized. stringify throws a TypeError if the source contains a reference cycle.
JSON represents data with a subset of the JavaScript object literal syntax, but it cannot represent Infinite or NaN values, so JSON.stringify writes these as null.
Sources
Object globalThis JSON left out Infinity and NaN...Properties
A property is a key/value pair. Each key can be defined as a string or a symbol. String keys are known as names, and these are typically specified without quotes:
const oRack = {
Num: oNumNext,
Cap: 8
};
...
const oCtAvail = oRack.Cap - oCtUsed;
Superficially, properties resemble class or structure members in other languages, but they are more like associative array elements. Entirely new properties can be added simply by assigning to the key:
oRack.Name = "WEST";
A property name need not qualify as an identifier. In fact, any string can serve as a name, even the empty string, if it is quoted:
const oLook = {
"Num": 10,
"Site E": 108,
"Site F": 90,
"": 0
};
If the name is not a valid identifier, it must be dereferenced with the bracket notation:
const oCd = oCdsFromName["Site E"];
This syntax also allows the name to be specified with a variable:
const oKey = "Num";
const oNum = oRack[oKey];
When using the bracket notation, nested objects are accessed by concatenating dereference operators, just as a nested array element would be:
const Prefs = {
Def: {Name: "New", LenMax: 10}
};
...
Prefs["Def"]["Name"] = "OBSOLETE";
Because arrays and functions are themselves objects, properties can be added to them in the same way. Adding a property to an array does not change its length unless the name is a valid index that is outside the current index range.
If a property is defined with a variable, the name can be omitted. When this is done, the property assumes the variable’s name, along with its value:
const oPos = {X, Y};
uLog(oPos.X);
In ES2020, properties can be read with optional chaining, which implicitly checks the reference to the left of the operator for undefined or null values. To use optional chaining with the dot operator, prefix it with a question mark:
const oj = oPos?.X;
To use it with the bracket notation, prefix the opening brace with a question mark and a dot:
const oData = DataNext?.[oMode];
In both cases, if the parent reference has an undefined or null value, the expression as a whole will be undefined. This allows long property chains to be safely dereferenced in a single line:
const ojNext = oPack?.Data?.jNext;
The reference at the start of the chain can be undefined or null, but it must be declared; otherwise, a ReferenceError will be thrown. References within the chain can be implicitly undefined.
Optional chaining cannot be used when writing to a property.
Sources
Optional chaining (?.)Accessor properties
Objects can define getters and setters that are read and written like ordinary properties, but are backed by accessor functions. A property defined this way is known as an accessor property.
Accessors are declared in object literals by prefixing the accessor functions with get or set. Both functions are named after the property they define:
const oRef = {
Num: 0,
get Cd() { return "F" + this.Num; },
set Cd(a) { this.Num = a.slice(1); }
}
The setter accepts a single parameter that represents the r-value in a property assignment:
oRef.Cd = "F10";
Omitting the getter produces a read-only property, and omitting the setter produces one that is write-only. Accessors are inherited like other methods. If a setter is called from a child object, any property it sets will be added to the child, hiding the parent value as usual.
Accessors can be added to existing objects with Object.defineProperty or Object.defineProperties.
Property attributes
Properties have attributes that determine whether they are enumerable, whether they can be reconfigured or deleted, and whether their values can be changed. Attributes are also used to add accessor properties to existing objects.
The attributes for a single property can be set with Object.defineProperty, which accepts the object, the name of the property, and a property descriptor:
Object.defineProperty(oRef, "Cd", {
get: function () { return "F" + this.Num; },
set: function (a) { this.Num = a.slice(1); },
enumerable: true,
configurable: true
});
If the property already exists, and if it is configurable, it will be modified. If it does not exist, it will be created.
Multiple properties can be configured by replacing the property name and descriptor with a second object that associates names with descriptors:
Object.defineProperties(oRef, {
Cd: {
get: function () { return "F" + this.Num; },
set: function (a) { this.Num = a.slice(1); },
enumerable: true,
configurable: true
},
Rank: {
get: function () {
return (this.Num <= 3 ? "A" : "B")
}
}
});
A descriptor can be retrieved by passing the object and property name to Object.getOwnPropertyDescriptor. The object must be the one that originally defined the property, not a descendent.
A descriptor is an object with up to four properties, each defining a specific attribute. A data descriptor configures an ordinary, non-accessor property:
| Attribute | Value |
value |
The property’s starting value. |
writable |
Set to true if the value can be changed. When false, the only way to change the value is to reconfigure it with Object.defineProperty or Object.defineProperties. Even setters in the same object cannot change read-only values. Normally, writing to an inherited property creates a new property in the child, leaving the parent unchanged, but even this is disallowed for read-only inherited properties.
|
enumerable |
Set to true if the property can be enumerated by for/in loops or functions like Object.keys.
|
configurable |
Set to true if the property can be configured by another call to Object.defineProperty or Object.defineProperties, or if it can be deleted. Attempting to reconfigure a non-configurable property produces a TypeError.
|
An accessor descriptor configures an accessor property:
| Attribute | Value |
get |
The accessor’s getter function. |
set |
The accessor’s setter function. |
enumerable |
Controls enumerability, as above. |
configurable |
Controls configurability, as above. |
When creating new properties, value, get, and set default to undefined, while writable, enumerable, and configurable default to false. If neither value, writable, get, nor set are specified, the object is assumed to be a data descriptor.
When reconfiguring properties, unspecified attributes are left unchanged.
Sources
Object.definePropertyTesting properties
A property’s existence can be tested in several ways. The property can be strictly compared with undefined, though this fails to distinguish undeclared properties from those that have been explicitly set to undefined:
const oCkCd = (aParams.Cd !== undefined);
The in operator accepts a string operand on the left and an object on the right. It returns true if the string is the name of a property, inherited or otherwise, whether its value is undefined or not:
const oCkCd = "Cd" in aParams;
The Object.hasOwnProperty and Object.prototype.hasOwn methods return true if a string argument names an own property, which is one that is not inherited. Object.propertyIsEnumerable returns true for own properties that are also enumerable.
Enumerating properties
The for/in loop iterates the names of enumerable properties within some object, including those of inherited properties. The static Object.keys method also identifies enumerable properties, but it returns an array of names, and it excludes inherited properties, while Object.values returns the corresponding values. The static Object.getOwnPropertyNames method returns a similar array, but non-enumerable properties are included:
| Method | Result | Inherited | Non-enumerable |
for/in |
Names | Yes | No |
Object. |
Array of names | No | No |
Object. |
Array of values | No | No |
Object. |
Array of names | No | Yes |
None of these methods return symbol-keyed properties.
Deleting properties
The delete operator removes a property from an object. The property can be specified with the dot notation:
delete aParams.Cd;
or the array notation:
delete aParams["Cd"];
The operator can also target an array element:
delete oEls[10];
Deleting an element does not change the array size, at least not as reported by length; it merely sets the element to undefined. However, if the array is iterated with a for/in loop, the deleted element will be skipped, so for/in cannot generally be assumed to produce length iterations.
If the targeted property or element does not exist, delete will fail silently. Inherited properties cannot be deleted through the child; they must be deleted directly from the parent.
with
Passing an object to with causes identifiers in the statement or block that follows it to be interpreted as properties of that object, if possible. Using with is generally discouraged, and it is disallowed in strict mode:
with (oData) {
St = "ACT";
uAlloc(++xAct);
...
}
Creating objects
Object literals
Objects can be created and initialized with object literals, which are comma-separated name/value pairs within curly braces. A colon is placed between each name and its value:
const oRt = {
Region: RegionDef,
Zone: 0
};
A property name can be read from the content of a variable by surrounding that variable with square braces. This is called a computed property name:
function uUpd(aName, aVal) {
const oData = {
[aName]: aVal
};
...
Omitting all properties produces an empty object:
const oRack = {};
Creating an object with a literal causes Object.prototype to be assigned as the object’s prototype, and Object as its constructor.
new
An object can also be created with the new operator and a constructor, which is a function that initializes objects:
function tPt(aX, aY) {
this.X = aX;
this.Y = aY;
}
const oPt = new tPt(3, 4);
The new object is created automatically, it is referenced in the constructor with this, and it is returned automatically from new. It is seldom appropriate to return from the constructor.
As will be seen, every constructor also defines a class. Objects created this way become instances of the class.
Object.create
Class instances can also be created with the static Object.create method, which accepts an argument specifying the object’s prototype. As will be seen, this is another object that defines members (particularly methods) to be inherited by class instances:
const oData = Object.create(tData.prototype);
The new object’s constructor property is set to the constructor of the specified prototype, but that function is not called, so the instance is not initialized. This is useful when manually defining subclasses.
An optional second argument can be used to define one or more properties. Like Object.defineProperties, this parameter accepts an object that maps property names to property descriptors:
oRef = Object.create(Object.prototype, {
Cd: {
get: function () { return "F" + this.Num; },
set: function (a) { this.Num = a.slice(1); },
enumerable: true,
configurable: true
},
Rank: {
get: function () { return (this.Num <= 3 ? "A" : "B") }
}
});
Sources
Object initializerObject attributes
A non-extensible object is one that does not allow new properties to be added. A sealed object is one that is non-extensible, with properties that are non-configurable as well. A frozen object is sealed and contains only read-only properties:
| Add properties | Configure properties | Write to properties | |
| (default) | Yes | Yes | Yes |
| Non-extensible | No | Yes | Yes |
| Sealed | No | No | Yes |
| Frozen | No | No | No |
These qualities are checked with functions like Object.isExtensible, Object.isSealed, and Object.isFrozen. They are applied with Object.preventExtensions, Object.seal, and Object.freeze. They can also be applied by manually configuring property attributes. A non-extensible object cannot be made extensible again. Neither can sealed or frozen objects be unsealed or unfrozen.
Object class
The Object class includes members such as:
-
static
prototype -
The prototype for the
Objectclass. Adding members to this prototype makes them available to all instances that derive fromObject. -
static
create(proto, [descs]) - Creates and returns a new object with the specified prototype. If descs is defined, property descriptors in that object will be used to define properties in the new object.
-
static
fromEntries(props) -
Uses the properties in iterable props to create a new object, which it then returns. props should generate array-like objects, each with two elements that provide the key and value for a single property. Though
Object.entriesignores symbol-keyed properties, this method does allow them to be added. -
static
getPrototypeOf(obj) -
static
setPrototypeOf(obj, proto) - Gets or sets the prototype of obj. Setting the prototype after construction is discouraged for performance reasons.
-
static
getOwnPropertyDescriptor(obj, key) -
Returns a property descriptor for the own property with name or symbol key, or
undefinedif that property is not found. -
static
getOwnPropertyDescriptors(obj) - Returns property descriptors for all own properties in obj.
-
static
defineProperty(obj, key, desc) -
static
defineProperty(obj, props) - Uses property descriptors to create or modify one or more properties in obj, then returns the object.
-
static
isExtensible(obj) -
static
isSealed(obj) -
static
isFrozen(obj) -
Returns
trueif properties can be added to obj, or if existing properties cannot be configured, or if existing properties are read-only. -
static
preventExtensions(obj) -
static
seal(obj) -
static
freeze(obj) - Configures obj so that new properties cannot be added to it, so that existing properties cannot be configured, or so that existing properties become read-only. Note that frozen objects are also sealed, and sealed objects also prevent extensions.
-
static
assign(dest, ...srcs) -
Copies enumerable own properties from all the srcs objects to the dest object, then returns dest. Ignores srcs that are
nullorundefined. If multiple srcs use the same key, the last value takes precedence. -
static
entries(obj) - Returns an array containing the enumerable properties in obj that are keyed with strings. Symbol-keyed properties are ignored. Each property is represented by a two-element array that stores the key and value.
-
static
is(valL, valR) -
Returns
trueif the arguments have the exact same value, as opposed to equivalent values, as determined by an equality check. In particular,isreturnsfalseif the values are-0and+0, andtrueif they are bothNaN. -
isPrototypeOf(obj) -
Returns
trueifthisis found anywhere within the prototype chain of obj. This resemblesinstanceof, though that operator compares an object with a constructor, theprototypeproperty of which defines the start of the chain. -
hasOwnProperty(key) -
ES2022 static
hasOwn(obj, key) -
Returns
trueif key is an own property of the object, even if it isnullorundefined. -
propertyIsEnumerable(key) -
Returns
trueif key is an enumerable own property of this object, even if it isnullorundefined. -
toString() -
Returns a string representation of this object. This method can also be invoked on the
nullandundefinedobjects. It is often overridden to customize an object’s string conversion behavior.
Sources
fromEntriesWeak references
ES2021 adds the WeakRef class, which weakly references a single object. This allows the garbage collector to delete the object if no strong reference targets it.
The target is passed to the WeakRef constructor. The class defines a single method, deref, which returns the target reference, or undefined if it has been deallocated.
Sources
WeakRefClasses
JavaScript classes use prototypal inheritance, which resembles traditional OOP inheritance only superficially. In JavaScript, an object’s class is determined by its constructor and its prototype.
Sources
ClassesConstructors
A constructor is a function that initializes new objects. It can be defined as a function that is named after the class:
function tPt(aX, aY) {
this.X = aX;
this.Y = aY;
}
or it can be defined with the constructor keyword inside a class declaration. In either case, the function is invoked with the new operator:
const oPt = new tPt(1, 0);
This causes an empty object to be created and passed to the constructor, where it is referenced and initialized with this. If no arguments are provided, the constructor argument parentheses can be omitted from the new expression.
Technically, any function can be used as a constructor, though seldom to useful effect. Note that constructors usually do not return values. If an object is returned, the constructed object will be replaced with the returned object. If a non-object value is returned, it will be ignored. Neither outcome is typically useful.
Every object — excepting one produced with Object.create(null) — is created with a constructor property. All instances of a given class are meant to reference the same constructor instance, so class-static variables and methods are defined in and accessed through the constructor. As will be seen, static variables cannot be added to the prototype, because modifying them in class instances would produce different own values in different instances.
Prototypes
A prototype is an object that stores properties common to an entire class, particularly methods and default values for shared variables.
Every function is given a prototype property when it is created, and this property is made to reference a default prototype object. Though otherwise empty, this default object contains a non-enumerable constructor property that points back to the constructor. Most functions make no use of prototype or the referenced object, but when a function is used as a constructor, its prototype object is assigned as the prototype of the new class instance. The relationship between a class instance and its prototype resembles that between a concrete instance and its most-derived type in a traditional OOP language:
Note that the constructor’s prototype property does not reference the constructor’s own prototype! Like other functions, the constructor instead derives from Function.prototype.
A type subclasses another when its prototype inherits from the prototype of the superclass. Though it is not used like a regular class instance, this makes the subclass prototype an instance of the superclass.
Most plain objects inherit from the Object prototype. Prototype objects that do not derive from custom classes are typically plain objects, so most classes implicitly subclass Object:
It is also possible to create instances with null prototypes. These are not members of any class, and they inherit nothing.
No standard object property references the prototype. However:
-
It is referenced by the
prototypeproperty of the class constructor; -
It can be retrieved or replaced by passing the object to
Object.getPrototypeOforObject.setPrototypeOf; -
Many runtimes support the deprecated
__proto__accessor property, which allows it to be read or written.
Properties can be added to prototypes at any time, but replacing an object’s prototype after instantiation is discouraged for performance reasons. To avoid prototype pollution attacks, special objects like Object.prototype and window also disallow prototype replacement.
Sources
__proto__ VS. prototype in JavaScript __proto__ setPrototypeOfClass properties
When a property is read from some object, it is first sought in the object itself. Properties defined directly within an object are called own properties. If the property is not found, it is sought within the object’s prototype. If it is not found there, then the prototype’s prototype is checked, and so on, until the property is found, or until a null prototype is encountered. In this way, every object inherits the properties of its ancestors throughout the prototype chain. In particular, methods are shared among class instances by adding them to the prototype.
Note that the object does not inherit copies of the prototype properties; it links in the most literal sense to the prototypes and their current state. If a property changes in some prototype, the same change will be observed in every class instance. However, assigning to that property in the derived object creates a new property that hides the original. Similarly, if the property is an inherited accessor with a setter, the inherited setter will be called, but it will produce a new property in the object that hides the prototype value. Variables in a superclass prototype are effectively shared default values. Alternatively, class variables can be assigned in superclass constructors, which are then called from subclass constructors. This allows logic in the superclass constructor to be shared, but each instance receives its own copies of the class variables, which are added directly to this.
By assigning to the instance, the prototype, or the constructor, various kinds of class-static and non-static data can be stored:
- Non-static variables are added to the object, and this is typically done inside the constructor;
-
Shared default values and non-static methods are added to the object prototype, which is referenced by the constructor’s
prototypeproperty. For this reason, non-static methods are identified in documentation withclass.prototype.name; -
Static variables and methods are added to the constructor instance. For this reason, static methods are identified with
class.name.
Manual class setup
In versions before ES6, class setup is a manual process. Constructors are defined as functions. When these are invoked with new, the prototype and constructor property are assigned to the new instance:
function tRg(aMin, aMax) {
this.Min = aMin;
this.Max = aMax;
}
const oRg = new tRg(100, 105);
Methods are assigned directly to the prototype object, which could be the original prototype:
tRg.prototype.uLen = function () {
return this.Max - this.Min;
};
or a new instance that overwrites the constructor’s prototype property. When this is done, the constructor property must be recreated in the new prototype:
tRg.prototype = {
constructor: tRg,
uLen: function () {
return this.Max - this.Min;
},
...
};
Static members are added directly to the constructor:
tRg.suFromUnord = function (aL, aR) {
if (aL < aR) return new tRg(aL, aR);
return new tRg(aR, aL);
};
Subclasses require additional setup. A new subclass prototype stores properties to be shared by the subclass instances:
function tStore(aName) {
this.Name = aName;
}
function tStoreFile(aName, aPath) {
tStore.call(this, aName);
this.Path = aPath;
}
tStoreFile.prototype = Object.create(tStore.prototype);
tStoreFile.prototype.constructor = tStoreFile;
tStoreFile.prototype.uWrite = function (aData) {
...
};
Instantiating the prototype with Object.create assigns the superclass prototype without invoking its constructor, which would add unwanted properties to the subclass prototype. Instead, superclass properties are added to subclass instances by invoking the superclass constructor with call. Because the default prototype is overwritten by Object.create, the constructor property must be restored manually.
Class declarations
In ES6, classes can be defined with a syntax that resembles other OOP languages. This is called a class declaration:
class tRg {
static sTol = 0.001;
static suFromUnord(aL, aR) {
if (aL > aR) return new tRg(aR, aL);
return new tRg(aL, aR);
}
Tag = "NONE";
#Extra;
constructor(aMin, aMax, aExtra) {
this.Min = aMin;
this.Max = aMax;
this.#Extra = aExtra;
}
get Ck() {
return !isNaN(this.Min) && !isNaN(this.Max);
}
uLen() {
return this.Max - this.Min;
}
* uVals() {
yield this.Min;
yield this.Max;
}
#uReset_CtExtra() {
this.#Extra.Ct = 0;
}
}
Method implementations and everything else in the declaration are executed in strict mode.
The constructor keyword replaces the class name that would be found in an ES5 constructor. The constructor can be omitted from the declaration if it is not needed.
Methods are defined like ordinary function declarations, but without the function keyword. Note that these are not enumerable, unlike methods defined the traditional way. Generator methods are prefixed with an asterisk. Accessor properties are defined the same way they are in objects.
Class fields
ES2022 supports fields, which are class properties that are defined outside the constructor. The syntax resembles a local variable definition, but without let or const. Fields need not be initialized:
class tRg {
Tag = "NONE";
#Extra;
...
If the class has no parent, its fields are added to the new instance before the constructor runs. However, if the class has a parent, its fields are added after super() is invoked. Fields are added in the order in which they were declared, so later fields can reference earlier fields in their initializers.
Sources
Public class fieldsPrivate properties
Fields can be public or private. Public fields produce ordinary properties that are inherited, and can be enumerated, configured, or deleted.
In older code, private data might be associated with an object by adding it to a WeakMap or a closure. In ES2022, this can be accomplished with private properties, which are inaccessible outside the class. These can be class variables or methods, and static or non-static. Their names are prefixed with a pound sign when defined or used; these are called hash names.
Private properties differ from other properties in many ways:
-
Private variables must be defined as fields, even if they are assigned in the constructor:
class tRg { #Extra = null; constructor(aMin, aMax, aExtra) { this.#Extra = aExtra; ...Because of this, they cannot be added to existing instances. Also, unlike other fields, they cannot be deleted, and they are unaffected by
Object.freeze; - The hash names used for private properties do not define string keys, so private properties cannot be dereferenced with bracket notation;
-
Normally, reading an unknown property from an object produces
undefined. Reading an unknown private property produces aTypeErrorinstead.
Private properties are not accessible in subclasses. If a subclass defines a like-named private property, that property exists in parallel with the parent property.
Getters, setters, and ordinary methods can be private, but constructors cannot. Private methods cannot be accessed through the class’s prototype.
Sources
Private propertiesStatic class members
Static members are prefixed with static. As expected, non-static methods are implicitly added to the class prototype, while static methods are added to the constructor. Before ES2022, static properties had to be added outside the declaration:
class tRg {
...
}
tRg.sTol = 0.001;
In ES2022, they can be defined as static fields:
class tRg {
static sTol = 0.001;
...
ES2022 also supports static initialization blocks, which are defined like static methods, but without names:
class tRg {
static {
tRg.sTol = tPrefs.Read("Tol");
}
...
A single class can define any number of these blocks. Together, static properties are added to the class, and static initialization blocks are executed in the order in which they were declared. This means that initializers and blocks can reference static properties declared above, but not below. These blocks cannot await or yield.
Sources
static Static initialization blocksSubclass declarations
A subclass is declared by specifying a parent class with the extends keyword:
class tStore {
constructor(aName) {
this.Name = aName;
}
}
class tStoreFile extends tStore {
constructor(aName, aPath) {
super(aName);
this.Path = aPath;
}
uWrite() {
...
}
}
The subclass constructor must invoke the parent constructor with super before using this. If the subclass defines no constructor, super will be called automatically.
When this syntax is used to define a subclass, the subclass constructor prototype is made to reference the superclass constructor prototype, rather than Function.prototype, as most functions do. This produces a constructor prototype chain that parallels the instance prototype chain, which allows static members to be inherited in subclasses. This constructor inheritance relationship is not typically implemented when subclasses are defined manually.
extends can also be used to derive from classes that were defined manually:
function tStore(aName) {
this.Name = aName;
}
class tStoreFile extends tStore {
...
Class expressions
Classes can also be defined with class expressions, which work something like function expressions:
const tPt = class {
constructor(aX, aY) {
this.X = aX;
this.Y = aY;
}
uLen() {
const oSqs = (this.X * this.X) + (this.Y * this.Y);
return Math.sqrt(oSqs);
}
};
const oPt = new tPt(3, 4);
The constructor function is returned by the expression and assigned to a variable. That variable then serves as the constructor, so its name becomes the name of the class. It is also possible to specify a name after the class keyword:
const tPt = class PtClass {
...
};
When this is done, the second name is assigned to the constructor’s name property, rather than the variable name.
Testing inheritance
An object’s class is determined by its prototype. The instanceof operator accepts an instance on the left and a constructor on the right:
const oCkStore = aStore instanceof tStore;
It returns true if the instance is an object, and if it inherits directly or indirectly from the object referenced by the constructor’s prototype property. This is equivalent to:
const oCkStore = tStore.prototype.isPrototypeOf(aStore);
Adding non-class properties to the tested object does not change this result. By extension, instanceof typically returns true if the right operand is Object, since all objects inherit from Object.prototype by default. Similarly, arrays can be identified by setting the right operand to Array. However, instances occasionally originate in a different realm, this being a frame or other context within which JavaScript code executes. If the instance is from another realm, instanceof will not identify its class correctly. This seems to include imported classes in Node.js.
Sources
How to understand JS realmsType conversion
JavaScript is permissive about type conversions, and many operands or parameters are automatically converted to expected types:
-
undefinedandnullboth producefalsewhen converted to a boolean.undefinedproducesNaNwhen converted to a number, whilenullproduces zero. They produce the text “undefined” or “null” when converted to a string; -
trueandfalseproduce one or zero when converted to a number. They produce text “true” or “false” when converted to a string; -
Zero and
NaNproducefalsewhen converted to booleans, while other numbers producetrue. Numbers produce decimal text when converted to strings; -
The empty string produces
falsewhen converted to a boolean, while non-empty strings producetrue. When converted to numbers, strings are processed as if they had been passed to theNumberfunction they are trimmed of whitespace, then converted to the number that remains, or to zero if no text is left, or toNaNif the text is invalid; -
All arrays produce
truewhen converted to booleans, even empty ones. Empty arrays produce zero when converted to a number. Arrays containing a single number or numeric string produce that value when converted to a number, while multi-element arrays and those containing non-numeric elements produceNaN. Empty arrays produce the empty string when converted to strings. Non-empty arrays produce comma-delimited element sequences, with nested arrays first being flattened to linear sequences; -
All objects produce
truewhen converted to booleans. They produce theirvalueOfortoStringresults when converted to numbers or strings.
These conversions are often applied at unexpected times. For example, when a string is added to any other type with the addition operator +, the non-string type is converted to a string. But in some cases, string conversion occurs even when neither operand is a string: adding two arrays causes both to be converted to strings, and these are then concatenated (though without the comma between that might have made this useful). One or more conversions may be applied when values are compared with the loose equality operator ==, so that "0" == false produces true, even though non-empty strings are generally truthy.
Because they typically convert their arguments to expected types, many library functions produce unexpected results when a different type is passed, or when arguments are omitted. String.prototype.charAt, for instance, converts its argument to a number. A missing argument will be undefined, which converts to zero, which causes the first character to be returned. String.prototype.indexOf converts its first argument to a string. If no arguments are provided, the text “undefined” will be sought.
Explicit conversions are performed by passing values to the Boolean, Number, BigInt, String, or Object constructors, or with functions like parseFloat and parseInt. For values other than undefined and null, a string can also be produced by invoking the value’s toString method.
Variables
JavaScript variables have no type, so a value of one type can be overwritten with another type at any time.
In strict mode, a variable must be declared before it is assigned, or an exception will result. In sloppy mode, assigning to an undeclared variable automatically defines it, with global scope, even if the assignment occurs in a function. If the assignment is made within a script, it is also defined as a property of the global object, which allows it to be deleted. Reading from an undeclared variable always produces an exception.
Variables can be declared with var, let, or const.
var
In ES5, variables are declared with the var keyword. Multiple variables can be declared and optionally initialized in the same line by separating them with commas:
var oj = 0.0, oY;
Uninitialized variables have the undefined value. Redeclaring a variable that was created with var has no effect, other than to assign a new value, if an initializer is present.
When used at the top level of a script, var adds a property to the global object, but this property cannot be deleted or otherwise configured. At the top of a module, a variable is defined, rather than a property, and it has module scope.
When used inside a function, the variable is hoisted, giving it function scope. This makes it accessible outside the containing block (if there is one) and even before the variable is declared, at which point its value is undefined:
function uExec(aCkCalc) {
Wgt = oWgt;
if (aCkCalc) {
var oWgt = 0.0;
...
}
}
This is also true when var is used within a for loop:
for (var oj = 0; oj < oCt; ++oj) {
...
}
const ojMatch = oj;
By contrast, let variables declared within for cannot be accessed before or after the loop.
var should be avoided in modern JavaScript.
Sources
varlet and const
Variables declared with let behave like those in other languages. When declared in a function or other block, they have block scope, so they are inaccessible outside that block, and cannot be accessed before the declaration. When declared at the top level of a script, they have global scope, but they do not add properties to the global object. All the script elements in an HTML document share the same global scope. When declared at the top level of a module, they have module scope.
Redeclaring a let variable in the same scope produces a SyntaxError. Nothing prevents the same variable from being declared in a contained block. When this is done, the inner declaration hides the outer one.
const behaves as let, but it creates read-only variables that must be initialized where they are declared, because they produce exceptions when assigned. Objects referenced by const variables can be modified as usual.
Sources
let const Script scope and module scopesDestructuring
Starting with ES6, destructuring allows one or more values to be extracted simultaneously from an iterable or an object. This is done by defining a pattern that matches some or all of the source instance’s structure. When the source is an array or other iterable, the pattern resembles an array literal:
const oCds = ["AA", "BB"];
const [oCd0, oCd1] = oCds;
When the source is an object, the pattern resembles an object literal:
const oTag = {Name: "BASE", Ct: 10};
let {Name: oName, Ct: oCt} = oTag;
In both cases, destination variables are placed where values would be found within the source. Property names can be quoted in the pattern, just as they are sometimes quoted in object literals:
let {"Name": oName, "Ct": oCt} = oTag;
Property names can also specified indirectly, with the bracket notation:
const oKeyName = "Name";
const oKeyCt = "Ct";
const {[oKeyName]: oName, [oKeyCt]: oCt} = oTag;
If a variable name matches the property name in a source object, that property name can be omitted:
var oShelf = {Loc: "C10"};
var {Loc} = oShelf;
As shown above, placing let, const, or var before the pattern causes the contained variables to be defined as a group. Destructuring can also be used to assign existing variables, but when this is done, the entire statement must be parenthesized. This prevents the pattern from being interpreted as an ordinary object literal:
({"Name": oName, "Ct": oCt} = oTagUpd);
Destructuring can also be used to extract parameters from an object passed to a function:
function uUpd({Name: aName, Ct: aCt}) {
...
}
const oTag = {Name: "BASE", Ct: 10};
uUpd(oTag);
The pattern can ignore properties or elements that are not needed. In particular, holes can be left in an array pattern to extract certain elements while ignoring others:
const oIDs = ["01", "22", "30", "31", "33"];
const [oID0, , oID2] = oIDs;
Alternatively, because an array is itself an object, it can be matched with an object pattern that uses array indices as property names:
const {0: oID0, 2: oID2} = oIDs;
If the pattern attempts to extract a property or element that does not exist, the corresponding variable will be undefined, unless a default value is assigned in the pattern:
oAct = {};
const {Path: oPath = "/", Exec: oExec = null} = oAct;
Much like a rest parameter, the last pattern variable can be prefixed with an ellipsis to extract trailing values. If the source is an iterable, the extra values will be returned as an array:
const oIts = [1, 1000, 1010, 1011];
const [oItPref, ...oItsEx] = oIts;
const oCtItsEx = oItsEx.length;
If the source is an object, the extra values will be returned as an object:
const oRt = {Zone: "F80", Drive: 1008, CtOrd: 12};
const {Zone: oZone, ...oDtl} = oRt;
uUpd(oDtl.Drive, oDtl.CtOrd);
Patterns can be nested to extract values from complex types:
const oFig = {
ID: 102,
Pts: [
{X: 0, Y: 0},
{X: 10, Y: 12}
]
};
const {Pts: [, {X: oj1}]} = oFig;
Among other things, destructuring can be used to exchange values without temporary variables:
let ojBase = 0, ojAux1 = 8, ojAux2 = 10;
[ojBase, ojAux1, ojAux2] = [ojAux1, ojAux2, null];
Sources
Destructuring assignmentControl structures
if
The if statement automatically converts its argument to a boolean. Because undefined and null are both falsy, this allows an object’s existence to be checked very simply:
if (aObj) ...
switch
switch statements can branch on values of any type. case values can reference run-time expressions, and the two are compared using strict equality.
As in other languages, case blocks that do not break or return ‘fall through’ to the next block.
for loops
Basic for loops resemble those in other languages:
for (let oj = 0; oj < oCt; ++oj) {
...
The index variable must not be declared const, or the loop will fail at run time, after the first iteration.
for/of
Introduced in ES6, the for/of loop iterates any iterable object. This includes strings, arrays, maps, and sets, among others:
const oMsg = "EX23A";
for (const oCh of oMsg) {
...
Unlike basic for loop variables, for/of variables can be declared const.
Destructuring can be used to extract several loop variables with each iteration:
for (const [oj, oEl] of oEls.entries()) {
const oLine = `${oj}: ${oEl}`;
...
for/in
The for/in loop iterates the keys of the enumerable properties within an object, including inherited properties. Its loop variable can be declared const:
for (const oName in oData) {
const oLine = oName + ": " + oData[oName];
...
for/in can also be used to iterate arrays. However, because it iterates keys, the loop variable is actually a string. This is compatible with the array index operator, but it may not work elsewhere:
for (const oj in oEls) {
if (uCk(oEls[oj])) ...
Labels
Normally, the break statement causes the innermost loop or switch to end, while continue causes the innermost loop to iterate. An outer loop can be targeted by prefixing the loop statement with a label, defined with a label name followed by a colon:
Main: while (true) {
Bls: for (let ojBl in oBls) {
for (let ojMsg in oMsgs)
switch (oMsgs[ojMsg]) {
case "HOLD": continue Bls;
case "DONE": break Main;
...
If break is followed by a label name, the specified loop will end. If continue is followed by a name, that loop will iterate.
Operators
Logical operators
JavaScript offers familiar-looking logical operators:
| Operator | Effect |
! |
Logical complement |
&& |
Logical AND |
|| |
Logical OR |
but neither && nor || necessarily returns a boolean value. If the left && operand is falsy, the operator immediately returns that value. If the operand is truthy, it returns the right operand, whether that happens to be truthy or falsy. Conversely, if the first || operand is truthy, it returns that value, otherwise it returns the second operand.
Because undefined is falsy, the || operator can be used to select the first defined result from a set of expressions:
function uExec(aCt) {
const oCt = aCt || this.CtDef || 0;
...
ES2020 also offers the nullish coalescing operator ??, which returns the left operand if it is neither undefined nor null, or the right operand otherwise:
const oIDLot = aID ?? oIDLotDef;
Unlike logical OR ||, this can return left operands that are defined but falsy.
Because the ! operator always returns a boolean, !! is sometimes used to convert truthy or falsy values to boolean equivalents.
The ternary conditional operator ?: works as in other languages, except that it can return two different types:
return oCkReady ? "GO" : 0;
ES2021 adds logical assignment operators that assign the right operand to the left if the corresponding logical operator would return the right value:
| Operator | Effect |
&&= |
Assign if left is truthy |
||= |
Assign if left is falsy |
??= |
Assign if left is undefined or null |
Bitwise operators
The bitwise operators work as they do in other languages, but the operands are treated as 32-bit integers. Integer bits outside this range are discarded, as are fractional components:
| Operator | Result | |
~ |
Bitwise complement | |
& |
&= |
Bitwise AND |
| |
|= |
Bitwise OR |
^ |
^= |
Bitwise XOR |
<< |
<<= |
Left shift |
>> |
>>= |
Right shift with sign |
>>> |
>>>= |
Right shift with zeros |
Right shift with sign conserves the high-order bit, so it effectively divides by powers of two, even if the left operand is negative. Right shift with zeros inserts zeros instead. The right operand of all shift operations must be between zero and 31. Negative right operands cause the operator to return zero, while operands greater than 31 are replaced with operand % 32.
JavaScript supports the compound assignment operators found in other languages. These include bitwise operators &=, |=, ^=, <<=, >>=, and >>>=,
Arithmetic operators
The arithmetic operators function mostly as expected:
| Operator | Effect | |
+ |
+= |
Addition |
- |
-= |
Subtraction |
* |
*= |
Multiplication |
\ |
\= |
Division |
% |
%= |
Modulus |
** |
**= |
ES2016 Exponentiation |
However:
-
Because of JavaScript’s aggressive type conversion, the unary plus operator
+can be used to convert most non-numeric types to numbers. Applying the operator toBigIntproduces aTypeErrorexception, however; -
The modulus operator
%also works with float values. The sign of the remainder, if any, matches that of the first operand; -
Unary expressions cannot be used on the left side of the exponentiation operator. For this reason, negative literals must be parenthesized:
const oMask = (-2) ** oExp;
JavaScript offers the usual increment ++ and decrement -- operators. They can be used as prefix or postfix operators.
Equality and comparison operators
The loose equality operators == and != check for general equivalence, so various type conversions are allowed. The strict equality operators === and !== return false if the operands have different types. When applied to arrays, functions, or other objects, both varieties compare references, so distinct but functionally identical instances are not considered equal. There is no operator that tells whether distinct objects or arrays contain the same properties and values. Strings and other primitives are compared by value.
Like the loose equality operators, the comparison operators, <, >, <=, and >= automatically convert their operands.
Other operators
typeof
The typeof operator returns a lowercase string that gives the general type of its operand, whether “undefined”, “boolean”, “number”, “bigint”, “string”, “symbol”, “object”, or “function”. null variables are considered to have the “object” type.
Sequence operator
As in other languages, the sequence operator evaluates both its operands and returns the value on the right. Because this operator has the lowest possible precedence, the sequence expression must be parenthesized if its result is to be assigned:
const oYOrig = (++oj, ++oY);
void
The void operator accepts a single operand, which it evaluates. It then discards the result and returns undefined:
const o = void 10;
Sources
Expressions and operators voidFunctions
Function declarations
A function declaration consists of the function keyword, the function name, and a list of untyped parameters. Any type can be returned by any function, so no return type is specified:
function uReset(aPos, aCkSync) {
...
JavaScript does not allow functions to be overloaded. In strict mode, if a second function is declared with the same name, an exception results. In sloppy mode, the first function is replaced.
Function declarations can be nested within other functions, but, in strict mode, they can be placed only at the top level of the containing function, or within another block. Strict mode also gives block scope to nested function declarations, rather than function scope. Like var instances, nested function declarations are hoisted. Unlike hoisted variables (which are accessible but undefined before their initializations), hoisted functions are defined throughout their scope.
When they are defined within a script (rather than a module), global functions are created as properties of the global object, but they cannot be deleted.
Function expressions
Assigning the function definition to a variable produces a function expression. Though a name can be provided, it will be inaccessible outside the function itself, so these are typically anonymous:
const uReset = function (aPos, aCkSync) {
...
};
Arrow functions
As of ES6, JavaScript also supports lambda functions, which it calls arrow functions. They are defined much like function expressions, but the function keyword is omitted, and the arrow token is placed between the parameter list and the body:
const ouAdd = (aL, aR) => {
return aL + aR
};
Arrow functions can also be expression-bodied:
const ouAdd = (aL, aR) => aL + aR;
If there is exactly one parameter, the parameter list parentheses can be omitted:
const ouSq = a => a * a;
but if there are no parameters, they must be included:
const ouRnd10 = () => Math.ceil(Math.random() * 10);
Unlike other functions, this in an arrow function matches the value in the containing scope. An arrow function also has no arguments variable of its own, so the variable in the containing scope is referenced instead.
Function constructor
Function instances can also be created with the Function constructor:
const ouPow2 = new Function("aExp", "return 1 << aExp;");
The last argument is a string that gives the implemention of the new function. The preceding arguments, if any, are strings containing the names of the function’s parameters. Multiple parameters can be specified in a single string by delimiting them with commas:
const ouExp = new Function(
"aBase, aExp",
"return aBase ** aExp"
);
const oVal = ouExp(2, 3);
The code uses sloppy mode. It is executed as if it were part of a function defined in the global scope, so closures cannot be created this way.
Methods
A method is a function that has been assigned to a property within an object. Function expressions can be assigned to properties like other values, but methods can also be defined with a syntax like the class method declaration, even within a plain object:
const oArea = {
Reg: "West",
Auth: "NET",
uTag(aTick) {
return `${this.Reg}/${this.Auth}: ${aTick}`;
}
};
Within a method, this references the containing object.
Functions are themselves objects, so these can contain their own properties, including methods. Functions assigned to array elements are also treated as methods.
Parameters
JavaScript allows functions to be called without passing all or even any of their arguments. When this is done, the parameters are undefined within the function. Similarly, reading a result from a function that returns no value produces undefined.
If a function is called with extra arguments, they are ignored. Within the function, the array-like arguments object can be used to access these and other arguments, so that variadic functions can be created. This object was originally presented as a property of the Function prototype, but that has been deprecated; it is now a local variable within the function. Because it is array-like, arguments has a length property that gives the actual argument count. The function also has a length property, and this gives the parameter count.
Default parameters
Starting with ES6, default parameters can be defined by assigning default values in the parameter list. Unlike many languages, JavaScript does allow non-default parameters to follow default parameters:
function uWait(aMilli = 1000, aCkSpin) {
...
A given default is applied if its argument is missing when the function is called, or if undefined is passed as the argument.
Default value expressions can reference parameters defined earlier in the parameter list:
function uReady(aID, aName = "User " + aID) {
...
Defaults can also be assigned to parameters as they are destructured from an array:
function uSet_Orig([aX = 0.0, aY = 0.0, aZ = 0.0]) {
...
or from an object:
function uExec({aCt = 1, aOptRenew = true} = {}) {
...
In this example, the object as a whole is also given a default, so that the function can be called with no parameters. Because the default object is empty, the parameter-level defaults will be applied when this happens.
Sources
Default parametersRest parameters and spread syntax
ES6 introduces the rest parameter, which is defined by prefixing a function’s last parameter with an ellipsis. When the function is invoked, any arguments following the non-rest parameters are passed as an array through the rest parameter:
function uRecalc(aBin, ...aWgts) {
...
If non-rest arguments are not provided, the array will be empty.
ES6 also adds the spread syntax, which is invoked by prefixing an iterable with an ellipsis. When this is done, the elements in the iterable are transformed into discrete arguments. This can be used to call a function:
function uUpd(aID, aName, aCt) {
...
}
const oArgs = ["01B", "Northgate", 6];
uUpd(...oArgs);
or to populate an array:
const oArgsEx = [...oArgs, "POST"];
or an object. When an object is spread, its properties are iterated and copied, producing a shallow copy of the whole:
const oPosStart = {X: 0.0, Y: 0.0};
const oPosCurr = {...oPosStart};
If the syntax is applied to a non-iterable, no arguments will be produced.
Rest parameters and spread syntax look similar, but they work in opposite directions. Prefixing a parameter with an ellipsis converts a number of discrete arguments into an array. Prefixing an argument with an ellipsis converts an iterable (such as an array) into a number of discrete arguments.
Sources
Rest parameters Spread syntaxthis
this is a keyword, not a variable, and its meaning changes in different contexts. Within the global scope, it always references the global object. Its meaning within a function depends on the general type of that function:
| Function type | Mode | Referent |
| Constructor | any | The new object |
| Method | any | The containing object |
| Arrow function | any | this from the containing scope |
| All others | Strict | undefined |
| Sloppy | The global object |
As a result, if a method is copied to a local variable and invoked, its this will no longer reference the object that contained it. This can be fixed with an arrow function, which captures the object reference in a closure:
const ouLen = () => oRg.uLen();
It can also be fixed with the bind method, which is inherited by all functions. This creates a bound function that wraps the original:
const oRg = new tRg(100, 102);
const ouLen = oRg.uLen.bind(oRg);
const oLen = ouLen();
Within the new function, this returns the value of the first bind argument. If additional arguments are passed to bind, those will be forwarded to the original function as parameters every time the new function is invoked. If arguments are passed to the new function, those will also be forwarded to the original, after any permanently bound arguments.
Every function also inherits call and apply methods that invoke the function immediately. If an argument is passed to either method, that value is referenced by this during the invocation. When strict mode is enabled, this can be made to reference a primitive type, null, or undefined. Before ES5, or in sloppy mode, primitive types are replaced with wrapper objects, while null and undefined cause this to reference the global object.
When call is invoked with more than one argument, the additional arguments are forwarded to the function. The second apply argument is expected to be an array. If that argument is provided, its elements are passed to the function as arguments.
Sources
Function.prototype.bindClosures
A closure binds one or more functions to a persistent copy of the context in which they were defined. Returning a nested function produces a closure that can access variables or call functions in the containing scope, even after the program has left that scope. Returning an object that contains functions allows data in the containing scope to be manipulated by multiple operations. The functions in this object are part of the same closure, so they share the same data. Later invocations of the containing function produce new closures for the new objects, each with a distinct copy of the contained data.
Currying
A function that accepts multiple parameters can be curried to produce a chain of functions that each accept a single parameter. In JavaScript, this is accomplished with closures. An outer function accepts the first parameter and returns a new function that accepts the second, et cetera, until all parameters have been captured by the innermost function, which returns the result. Arrow functions allow this to implemented as a single expression:
const uCurryPt3 = aX => aY => aZ => new tPt3(aX, aY, aZ);
The operation as a whole is then performed by chaining function invocations:
const oPt = uCurryPt3(0.0)(0.0)(1.0);
Partial application passes only some of the arguments to the curried function. The result is a new function that accepts the remaining arguments:
const ouPlaneYZ = uCurryPt3(0.0);
const oPt = ouPlaneYZ(1)(2);
Generators
ES6 supports generator functions, which resemble iterator methods in C#. These are defined like other functions, but an asterisk follows the function keyword. They can be structured as declarations:
function* uEls() {
...
}
or expressions:
const uEls = function* () {
...
}
or methods. Because these don't use the function keyword, the asterisk appears on its own, before the method name:
class tMgrEl {
* uEls() {
...
}
...
Generator functions implicitly return generator objects that implement the iterable and iterator protocols. Note that the generator must be invoked to produce this object. Because they are iterable, generator objects can be used in for/of loops:
for (const oEl of uEls())
uCalc(oEl);
their values can be spread into arguments:
uQue(...uEls());
and they can be destructured:
const [, oEl1] = uEls();
No generator code is executed when the generator object is first created. Instead, function execution pauses before the first line, and waits there until the object’s next method is called. This runs the generator until it reaches a yield statement, a return statement, or the end of the function. If the function stops at a yield, its state is conserved until next is called again:
function* uCds() {
yield "INIT";
yield "SYNC";
yield "WAIT";
}
As with any iterator, the next method returns an object that contains a value property, a done property, or both. If the generator stops at a yield, that statement’s argument will be assigned to value, and done will be set to false. If the generator reaches the end of its function, or if it executes a return statement with no argument, value will be undefined, and done will be set to true:
const oiCds = uCds();
for (let o = oiCds.next(); !o.done; o = oiCds.next())
uExec(o.value);
If the generator executes a return statement with an argument, value will be set to that argument, and done will be set to true. This is not the pattern followed by other iterators! When this is done, the explicitly returned value is ignored by for/of loops, and when spreading arguments or destructuring.
The yield* statement yields a sequence of values from an argument that is itself iterable:
function* uVowels() {
yield* ["a", "e", "i", "o", "u"];
}
With each call to next, one element is extracted. If an ordinary yield were used, the iterable itself would be returned.
Because generator objects are themselves iterable, yield* can be used to chain generator output:
class tNode {
constructor (aVal, aChildren = []) {
this.Val = aVal;
this.Children = aChildren;
}
* [Symbol.iterator]() {
yield this.Val;
for (const oChild of this.Children)
yield* oChild;
}
}
This allows complex data structures to be iterated recursively:
const oNodeRoot = new tNode("R", [
new tNode("A", [
new tNode("A1"),
new tNode("A2")
]),
new tNode("B", [
new tNode("B1"),
])
]);
for (const oNode of oNodeRoot)
uExec(oNode);
Input to generators
Generators can also accept data from their callers. If a value is passed to the next method, that value will be returned from the yield statement at which the generator has paused:
function* uAdd() {
const oL = yield;
const oR = yield;
uExec(oL + oR);
}
const oiAdd = uAdd();
oiAdd.next();
oiAdd.next(2);
oiAdd.next(3);
The principles that govern output generators apply to this function as well. When first created, the generator waits before the first line of the function. Because there is no yield there, it cannot receive a value from the first next. Each next causes the generator to run until yield is encountered (where it pauses) or until the function returns. After waiting at the last yield, the function runs to completion before returning to next, and that next result sets done to true.
It is possible to send input to and receive output from the generator with the same next invocation. When this is done, the input is sent to the yield at which next starts, while the output is drawn from the yield at which it ends. In these situations, the yield keyword represents an input value, yet it also accepts an output argument. This unusual combination requires that yield be parenthesized (along with its arguments, if any) if it is part of an expression that uses the yield input:
function* uWork() {
...
const oText = "In: " + (yield oOut);
...
The generator object also provides methods named return and throw. The return method causes the generator to act as if a return statement had been found at the current yield. Like next, that method also returns an object containing value and done properties, with value set to the argument that was passed to the return method, and done set to true. The throw method throws its argument from the current yield.
Sources
Exploring ES6: Generators Iteration protocolsExceptions
Any type can be thrown, but the JavaScript interpreter throws only Error and its subclasses. Error describes the exception with its name and message properties. ES2022 adds a cause property that can be used to reference another exception that caused the current one. The Error function can be used as a constructor:
throw new Error("uExec: Invalid name");
but it also returns an instance without new:
throw Error("uExec: Invalid name");
A try block is followed by a catch block, a finally block, or both:
try {
...
}
catch (oErr) {
...
}
finally {
...
}
If both are provided, the catch will be executed before the finally when an exception is thrown. No type is specified by the catch, so it collects all exceptions in the preceding try. The catch must define an exception variable, even if the instance is not needed.
The finally is always executed, even if the catch includes an explicit return.
Containers
Iterables and iterators
In C# and Java, interfaces are defined in code, and their requirements are enforced by the compiler. JavaScript does not support interfaces per se, but it does document a number of protocols, these being informal requirements defined outside the code and enforced only by the developer.
One such protocol is iterable, which ES6 uses to implement for/of and argument spreading. To implement iterable, an object provides a method keyed with the well-known iterator symbol:
class tNums {
constructor(aMax) {
this.Max = aMax;
}
[Symbol.iterator]() {
return new tiNums(this.Max);
}
}
This method is a factory that produces objects implementing another protocol, named iterator. The iterator provides a next method that returns result objects:
class tiNums {
constructor(aMax) {
this.Next = 0;
this.Max = aMax;
}
next() {
return (this.Next <= this.Max)
? {value: this.Next++}
: {done: true};
}
}
Each result should contain a value property, a done property, or both:
-
If the iterator is able to return an element, it should set
valueto reference that element. Ifdoneis defined, it should be set tofalse; -
If the iterator is not able to return an element, it should set
donetotrue. Thevalueproperty need not be set if the iterator is done.
An iterator can be manually iterated by calling its next method in a loop:
const oNums = new tNums(3);
const oiNums = oNums[Symbol.iterator]();
for (let o = oiNums.next(); !o.done; o = oiNums.next())
uExec(o.value);
An iterable can also be created by defining the iterator method as a generator:
class tServs {
constructor(aCkMain) {
this.CkMain = aCkMain;
}
* [Symbol.iterator]() {
if (this.CkMain) yield "MAIN";
else {
yield "ALPHA";
yield "BETA";
yield "GAMMA";
}
}
}
In this case, the returned generator object is itself both an iterator:
const oServsBase = new tServs(false);
const oiServs = oServsBase[Symbol.iterator]();
for (let o = oiServs.next(); !o.done; o = oiServs.next())
uExec(o.value);
and an iterable:
const oiServs = oServsBase[Symbol.iterator]();
for (const oServ of oiServs)
uExec(oServ);
Unlike most iterables, however, a generator object can be iterated only once. It is necessary to create a new instance to iterate a second time.
An iterator can also be created by returning a closure from a function:
function uNumsFromMax(aMax) {
let oNext = 0;
return {
next: function () {
if (oNext > aMax) return {done: true};
return {value: oNext++};
}
}
}
const oiNums = uNumsFromMax(3);
However, the containing function does not implement iterable, so it cannot be used where an iterable is expected.
Arrays
JavaScript arrays inherit from Array.prototype. In some respects, they resemble objects more than they resemble the typed arrays found in other languages.
Arrays can be instantiated with array literals, which are comma-delimited sequences inside square braces:
const oInsPend = ["A11", "B04", "CXX"];
When commas are added without intervening values, elements are indexed (and the array length set) as though values had been provided. The last trailing comma before the closing brace is ignored, however:
const oInsMark = [, "NUL", ,];
uAssert(oInsMark[1] === "NUL");
uAssert(oInsMark.length === 3);
The missing values are called empty slots or holes. Though they are counted in the array length, they do not produce real elements. In particular, those indices are not iterated by for/in loops. They can be dereferenced to produce undefined, but that is true for any invalid index. Arrays that contain empty slots are called sparse arrays.
Arrays can also be created with the static Array.of method, which accepts zero or more array elements as arguments. They can also be created with the Array constructor, which behaves as Array.of when zero arguments are provided, or more than one argument, or exactly one non-number argument. However, when a single number argument is provided, that value specifies the length of the new array, which consists solely of empty slots:
const oInsPend = new Array(10);
JavaScript arrays allow different types to be mixed in the same instance. Because of this, they are sometimes used as tuples. Multidimensional arrays are structured as arrays of arrays.
Arrays are indexed with 32-bit unsigned integers, allowing over four billion elements to be stored. The element count is returned by the length property. Because arrays are objects, and because the bracket notation can also be used to reference ordinary object properties, negative numbers, non-integer numbers, and other invalid indices can be used to read or write values, and the resulting properties are enumerable, but they do not change the array length. Dereferencing an index that is out of range produces undefined, like any attempt to read an undeclared property.
JavaScript arrays are resizable. Arrays can be truncated or extended by writing to the length property. When an array is extended this way, empty slots are created.
Elements outside the current length can be added by assigning them. If the new index exceeds the original length, one or more empty slots will also be added:
oEls[oEls.length + 4] = 0;
Existing elements can be converted to empty slots with delete:
delete oEls[0];
An array can be copied by calling slice without parameters:
const oEls = aEls.slice();
or by spreading the source array inside brackets:
const oEls = [...aEls];
Sources
Array Array() constructor Sparse arraysArray class
The Array class includes methods such as:
-
ES6 static
of(...) - Returns an array containing one element for each of the specified arguments.
-
ES6 static
from(els, [map], [this]) -
Returns a new array containing the elements referenced by iterable or array-like els. If a map function is specified, each element is first passed to that function, and the result is used to populate the new array. map receives the same arguments it would receive if it were passed to the
mapmethod. If this is specified, its value is used forthiswithin the map function. -
join([delim]) - Returns a string that concatenates the string representation of every element, delimited by commas, or by delim.
-
keys() -
Returns an iterator that produces the array’s indices, including those of empty slots.
Note that
Object.keysis a static method that returns an array, while this is a non-static method that returns an iterator. -
ES2016
entries() -
Returns an iterator that produces two-element arrays containing the array’s keys and values:
for (const [oKey, oVal] of oEls.entries()) uLog(`${oKey}: ${oVal}`); -
ES2022
at(index) - Returns the element at the specified index, wrapping back once from the end of the array (unlike the bracket notation) if index is negative.
-
indexOf(val, [start]) -
lastIndexOf(val, [start]) -
Returns the index of the first or last element that strictly equals val, or
-1if no match is found. If start is specified, the search begins at that index. If start is negative, it wraps back once from the end of the array. If start is greater than the last index, the search fails. -
ES2016
includes(val, [start]) -
Returns
trueif any element is equal to val. Elements are compared using strict equality, except thatNaNis matched withNaNelements. If start is specified, the search begins at that index. If start is negative, it wraps back once from the end of the array. If start is greater than the last index, the search fails. -
ES2023
with(index, value) -
Copies the array, assigns a new value to the specified element within the copy, then returns the new array. Empty slots in the source are replaced with
undefined. If index is negative, it wraps back once from the end of the array. -
shift() - Removes one element from the beginning of the array, adjusts the indices of the remaining elements to account for their new positions, and returns the removed element.
-
pop() - Removes one element from the end of the array and returns it.
-
unshift(el, ...) - Adds one or more elements to the beginning of the array, adjusts all element indices to account for their new positions, then returns the array’s new length.
-
push(el, ...) - Adds one or more elements to the end of the array, then returns its new length.
-
concat(add, ...) - Returns a new array containing the source elements, plus one or more elements specified as arguments. However, if any argument is itself an array, its elements are added, rather than the array as a whole.
-
splice(start, [len], [...add]) -
Modifies the array in place by removing elements, or inserting them, or doing both, then returns any removed elements in a new array.
The operation begins at index start. If no other arguments are provided, this element and those that follow it are removed and returned. If len is specified, that number of elements are removed and returned. If more arguments are provided, those values are inserted at start. Unlike
concat, array arguments are inserted as arrays. -
ES2023
toSpliced(start, [len], [...add]) -
Functions as
splice, except that a copy is modified and returned. -
slice(start, [next]) -
Returns a new array containing the elements that begin at start, and run through the end of the array, or end just before next. If either argument is negative, it wraps back once from the end of the array. If start is
undefined, zero is used instead. If start equals or exceeds the length, or if it equals or exceeds next, an empty array is returned. If next isundefined, or if it equals or exceeds the length, the length is used instead. -
copyWithin(destStart, srcStart, [srcNext]) -
Modifies the array in place by overwriting elements in one section with copies of elements from another, then returns the array.
Copies are drawn from the sequence that begins at index srcStart, and ends just before srcNext, or at the end of the string if that parameter is
undefined. The sequence is copied to the elements at destStart. All indices wrap back once from the end of the array if they are negative. This method never changes the array length. -
ES6
fill(val, [start], [next]) - Modifies the array in place by setting elements to val, then returns it. If start is specified, elements before that index are left unmodified. If next is specified, that element and those following it are unmodified.
-
reverse() - Reverses the element order in place, then returns the array.
-
ES2023
toReversed() -
Functions as
reverse, except that a copy is reversed and returned. -
sort([compare]) - Sorts the elements in place, then returns the array. By default, elements are sorted by their string representations, so numbers are not sorted in increasing order. To customize the sort, pass a compare function that accepts two values, and returns a positive number if the second should be sorted after the first, a negative number if it should be sorted before, or zero if the values are equal.
-
ES2023
toSorted([compare]) -
Functions as
sort, except that a copy is sorted and returned.
The following Array methods pass elements to a function call, which itself accepts up to three arguments: an element, its array index, and the array as a whole. These array methods also accept an optional this parameter. When this is provided, it is referenced wherever this is used within call:
-
forEach(call, [this]) - Iterates the array and passes each element to call.
-
some(call, [this]) -
Iterates the array and returns
trueif call returnstruefor any element. -
every(call, [this]) -
Iterates the array and returns
trueif call returnstruefor every element. -
ES6
find(call, [this]) -
ES2023
findLast(call, [this]) -
Returns the value of the first or last element for which call returns
true, orundefinedif no match is found. -
ES6
findIndex(call, [this]) -
Returns the index of the first element for which call returns
true, or-1if no match is found. -
filter(call, [this]) -
Iterates the array, passes each element to call, and returns a new array containing the elements for which call returned
true. -
map(call, [this]) - Iterates the array, passes each element to call, and returns a new array containing the values returned by call.
The following Array methods use a callAccum function that accepts up to four values: an accumulator, which stores an ongoing calculation, an element, its array index, and the array as a whole:
-
reduce(callAccum, [init]) - Iterates the array, passes each element to callAccum, and returns the last value produced by that function. If init is provided, iteration begins at the first element, and init is used as the first accumulator value. If it is not provided, iteration begins at the second element, and the first is used as the accumulator.
-
reduceRight(callAccum, [init]) - Iterates the array in reverse, passes each element to callAccum, and returns the last value produced by that function. If init is provided, iteration begins at the last element, and init is used as the first accumulator value. If it is not provided, iteration begins at the element before the last, and the last element is used as the accumulator.
Array-like objects
Some objects (like the arguments instance defined within functions) are known as array-like objects. These are not true arrays, but they can sometimes be used as if they were. Every such object:
-
Provides a
lengthproperty; - Associates a number of property values with integer indices.
Though they are not Array instances, many Array.prototype methods can be applied to these objects with Function.call or Function.apply.
Maps
Though any JavaScript object can serve as an associative array, the Map class, introduced in ES6, is often a better choice:
-
Objects inherit properties from the
Objectprototype. Maps contain no elements except those explicitly added to them; - Storing user-generated keys and values in objects can produce object injection attacks;
- Object keys are always strings or symbols, while map keys can have any type;
- Maps offer better performance when adding or removing elements.
A map can be created and initialized by passing an iterable to the Map constructor:
const oNumZones = [[10, "A"], [12, "B"], [20, "C"]];
const oZonesByNum = new Map(oNumZones);
The iterable is expected to return zero or more arrays, each containing one key/value pair. If any array contains fewer than two elements, one or both of the key and value will be considered undefined. Array elements beyond two are ignored. If the iterable produces any result that is not an array, TypeError will be thrown. Omitting the iterable altogether produces an empty map.
If the same key is specified more than once, the last value takes precedence. This allows map elements to be updated with an array or another map:
const oNumZonesEx = [[20, "D"], [40, "E"]];
const oZonesByNumEx = new Map([
...oZonesByNum,
...oNumZonesEx
]);
Keys are compared using strict equality, except that NaN is matched with NaN keys during lookup. There is no way to define a custom comparer.
Iterating a map returns arrays containing the key/value pairs, in the order the keys were added:
for (const [oNum, oZone] of oZonesByNum)
uLog(`${oNum}: ${oZone}`);
Like arrays, maps are also objects. This means that properties can be added to the map, but these will not be recognized by Map methods like has and get:
oZonesByNum[100] = "F";
uAssert(oZonesByNum.get(100) === undefined);
Map class
The Map class includes methods such as:
-
size() - Returns the number of elements in the map.
-
entries() - Returns an iterator that produces two-element arrays containing the map’s keys and values.
-
has(key) -
Returns
trueif the map contains the specified key. -
get(key) -
Returns the value associated with the specified key, or
undefinedif no such key exists:const oZone12 = oZonesByNum.get(12); -
set(key, val) -
Creates an element with the specified key, or overwrites the element with that key if one already exists, then returns the map itself:
oZonesByNum.set(10, "Z") .set(12, "Y") .set(14, "X"); -
delete(key) -
Deletes the element with the specified key, then returns
trueif key was found:const oCkDel = oZonesByNum.delete(20); -
clear() - Removes all elements from the map.
-
forEach(call, [this]) -
Iterates the map and passes each pair to function call, which itself accepts up to three arguments: the value, the key, and the map as a whole. If this is provided, it is referenced wherever
thisis used within call.
Sources
MapWeak maps
ES6 also introduces the weak map, which is a simple associative array with object or (in ES2023) non-registered symbol keys that are weakly referenced by the map. This allows the garbage collector to delete both keys and values, when these are referenced nowhere else.
Like an ordinary map, it can be initialized with an iterable of key/value arrays. However, only objects can be used as keys:
const oBuffFront = {...};
const oBuffBack = {...};
const oCtsFromBuff = new WeakMap([
[oBuffFront, 0],
[oBuffBack, 0]
]);
A TypeError will be thrown if a primitive is used as a key.
WeakMap is not iterable, and it provides only a few of the methods found in Map. These include set, has, get, and delete. Note that size and clear are not implemented.
Sources
WeakMap ...actual uses of ES6 WeakMap?Sets
ES6 also provides the Set class, which stores unique values. A set can be initialized with an iterable of values:
const oCds = new Set(["A", "B", "F"]);
If no iterable is specified, an empty set will be created.
Sets themselves are iterable. Their values are iterated in the order in which they were added.
Set class
The Set class includes methods such as:
-
size() - Returns the number of values in the set.
-
has(val) -
Returns
trueif val is part of the set. -
values() -
keys() -
entries() -
valuesreturns the same value iterator that is used when the set is treated as an iterable. For consistency withMap, thekeysmethod also returns this iterator, whileentriesreturns an iterator that produces two-element value/value arrays. -
add(val) -
Adds val to the set, then returns the set itself. Does nothing if val is already a member:
oCds.add("A").add("Z"); -
delete(val) -
Removes the specified value and returns
trueif it was part of the set. -
clear() - Removes all values from the set.
-
forEach(call, [this]) -
Iterates the set and passes each value to function call, which itself accepts up to three arguments: the value, the same value again, and the set as a whole. If this is provided, it is referenced wherever
thisis used within call.
Sources
SetWeak sets
ES6 also provides weak sets, which weakly reference unique object instances. Weak sets allow the garbage collector to delete a contained object if it is referenced nowhere else.
Like ordinary sets, they can be initialized with an iterable of values. However, only objects can be added:
const oTagBase = {Name: "BASE", Ct: 10};
const oTagOff = {Name: "OFF", Ct: 2};
const oTags = new WeakSet([oTagBase, oTagOff]);
A TypeError will be thrown if a primitive is added to the set.
WeakSet is not iterable, and the only methods it provides are has, add, and delete. In particular, size and clear are not implemented.
Regular expressions
Regular expressions are implemented with the RegExp class. An instance can be created with a regular expression literal, which surrounds the expression with forward slashes:
const oRegexCd = /A[1-3]/;
or with the RegExp constructor, which can accept a literal (actually another RegExp instance):
const oRegexCd = new RegExp(/A[1-3]/);
or an expression string:
const oRegexCd = new RegExp("A[1-3]");
Most expression characters are expected to match exactly within the target text. Others have special meanings. To match one of these, it is often necessary to escape the character with a backslash:
\ / | . * + ^ $ ? : = ! [ ] { } ( )
Using a regular expression literal makes it unnecessary to escape backslashes, but it does require that forward slashes be escaped.
Tabs and other non-printing characters are mostly specified with the same escape sequences used in string literals. The backspace character is matched by [\b], however, to avoid confusion with the \b anchor. ctrl-X is matched with \c X.
The search is started by invoking a RegExp method like exec or test.
Sources
Regular expressions [Regex] cheat sheetSearch flags
The trailing slash in the expression literal can be followed by one or more flags that configure the search:
const oRegexCmds = /F\d\d/ig;
A string containing these letters can also be passed as the second parameter in the RegExp constructor. Each flag is represented by a read-only boolean property in the RegExp instance:
| Flag | Property | Effect |
g |
global |
Produces a global search, allowing some functions to process matches beyond the first. |
y |
sticky |
ES6
Causes the match to succeed if it starts exactly at the position indicated by lastIndex. Matches beyond this point are not identified.
|
u |
unicode |
ES6
Allows Unicode code point escape sequences
Superfluous escape sequences (like |
v |
unicodeSets |
ES2024
Provides the
Combining this flag with |
m |
multiline |
Enables multi-line mode, which causes ^ and $ to match the beginnings and ends of lines.
|
s |
dotAll |
ES2018
Causes . to match line feeds and carriage returns as well as other characters.
|
d |
hasIndices |
ES2022
Adds an indices property to the array returned by methods like exec. This property references another array that contains two-element arrays storing the start and end indices of substrings matched by capturing parentheses.
|
Sources
RegExp.prototype.unicode RegExp.prototype.unicodeSetsCharacter classes
Expressions can also include character classes, each of which matches one of a number of characters.
Enclosing characters within square braces produces a character set, which matches any one of the contained characters:
const oRegexNumLot = /[123]/;
Prefixing the characters with a caret negates the set, causing it to match any one character that is not within the braces:
const oRegexCdLot = /[^123]/;
A range of allowable characters is specified by joining the lower and upper limits with a hyphen:
const oRegexDigOct = /[0-7]/;
Neither periods nor asterisks are treated as special characters inside the set, so there is no need to escape them.
Other classes include:
| Class | Match |
. |
Any character that is not a line feed or carriage return. The s flag causes those to be matched as well.
|
\s |
Any ASCII or Unicode whitespace character |
\S |
Any character that is not matched by \s |
\d |
Any ASCII number character |
\D |
Any character that is not matched by \d |
\w |
Any ASCII letter, number, or underscore character. Note that accented or non-roman characters are not included. |
\W |
Any character that is not matched by \w |
\p{prop} |
ES2018
Any character with the specified Unicode property, such as Script=Latin. Requires the u or v flag.
|
\P{prop} |
ES2018 Any character that is not matched by \p |
These can be included in character sets.
Sources
Character classes Unicode character class escapeQuantifiers
Characters and groups can be followed by quantifiers that allow them to repeat within the target text:
| Quantifier | Effect |
? |
Match once or not at all |
* |
Match zero or more times |
+ |
Match one or more times |
{ct} |
Match exactly ct times |
{min,} |
Match at least min times |
{min, max} |
Match anywhere from min to max times |
Because they allow characters to be matched zero times, quantifiers like ? and * can produce expressions that match all strings, since every string contains zero or more instances of a given character.
By default, quantifiers implement greedy matches that consume as much of the target text as possible before the remainder is matched with the rest of the expression. Although ? is itself a quantifier, it can also be added to the end of a quantifier to specify a lazy match that consumes as little of the text as needed to produce a match.
Sources
QuantifiersGroups
Surrounding a character sequence with parentheses produces a group, which can be modified as a whole by a quantifier or another function:
const oRegex = / (XO)+ /;
Within a group, pipe characters called disjunctions can separate two or more sequences. Any one of these, when found within the target text, will be counted as a match for the group as a whole:
const oRegexRt = /(MAIN\d|AUX\d\d|OFF) /;
These alternatives are checked from left to right. The first matching sequence will be used, even if another would match more completely. If there is a pattern after the group, and if that fails to match after the selected alternative is matched, the search will try again with the next alternative. This is called backtracking.
Disjunctions can also be used outside of groups, but they have the lowest operator precedence, so only simple expressions can use them this way:
const oRegexSide = /LEFT|RIGHT/g;
When the parenthesized sequence does not start with ?:, the parentheses are called capturing parentheses, and the whole is called a capturing group. These also store the target substring that was matched by the group. This allows the substring to be recalled in another part of the expression by prefixing the one-indexed group number with a backslash. This is called a backreference:
const oRegexChQuot = /(["']).\1/;
The original group likely matched a number of possible substrings, but the recalled substring is matched with another part of the text only if that text repeats the specific substring that produced the original match.
Non-capturing parentheses prefix the inside sequence with ?:. These create non-capturing groups, which do not store the matching substring:
const oRegex = / (?:XO)+ /;
Sources
Groups and backreferences DisjunctionAnchors
Normally, expressions are matched wherever possible within the target text. Matches can be constrained to certain positions within the text by anchors. Note that anchors are not matched to characters, but to positions between characters:
| Anchor | Position |
^ |
The beginning of the text, or the beginning of any line, if the m flag is set |
$ |
The end of the text, or the end of any line, if the m flag is set |
\b |
The beginning or end of a word, which is any point between a \w character and a \W, or between a \w and the beginning or end of the text. Line breaks are already non-word characters, so there is no need to set the m flag.
|
\B |
Any point that is not matched by \b |
Parenthesizing a character sequence and prefixing with ?= or ?! creates a lookahead. Prefixing with ?<= or ?<! creates a lookbehind:
| Expression | Effect |
patt(?=post)
|
Matches patt if it is immediately followed by post, without consuming or matching post. |
patt(?!post)
|
Matches patt if it is not immediately followed by post. |
(?<=pre)patt
|
ES2018 Matches patt if it is immediately preceded by pre. |
(?<!pre)patt
|
ES2018 Matches patt if it is not immediately preceded by pre. |
Sources
AssertionsRegExp class
The RegExp class includes members such as:
-
source -
Returns a string containing the expression itself. Starting with ES5, the string contains
(?:)if the expression is empty. This property is read-only. -
flags - Returns a string containing the flags set in this instance. This property is read-only.
-
lastIndex -
Returns the index in the target string where the next search should start. This property is updated by methods like
exec, and it can also be assigned. -
exec(text) -
Returns an array containing a text substring matched by the regular expression, plus substrings matched by any capturing parentheses. The array has an
indexproperty that gives the position of the match within text, plus aninputproperty that stores text itself.execreturnsnullif no match is found.If the global search flag is set within the expression,
execalso sets thelastIndexproperty of the expression instance to the position just after the match. This position becomes the starting point for the next search, ifexecis called again. If the next invocation produces no match,nullis returned, andlastIndexis set to zero. -
test(text) -
Returns
trueif a text substring is matched by the regular expression. If the global search flag is set,testupdateslastIndexthe same wayexecdoes.
Note that String methods like search, match, and matchAll also implement regular expression searches, and these are often easier to use than exec or test. String also provides the split and replace methods, which can use regular expressions to modify strings.
Sources
Regular Expressions RegExpAsynchronous programming
JavaScript programs are cooperatively multitasked. The runtime adds elements to a message queue, much like a Win32 program. The queue is serviced by an event loop, which invokes the callback or task associated with each event. Unless it cedes control, the task runs to completion before returning to the loop. This ensures that no task ever interrupts another, but it also allows long-running tasks to block the loop. The processing of a single message and its task is sometimes known as a tick.
Sources
Asynchronous JavaScript The event loopPromises
ES6 introduces promises, which allow tasks to be performed asynchronously. A promise is created by passing a work function to the Promise constructor:
function uwStSvc(aKey) {
if (!aKey)
return Promise.reject(new Error("Key not set"));
return new Promise((auFulfill, auReject) => {
const oURL = "http://localhost:8080/svc/" + aKey;
const oReq = new XMLHttpRequest();
oReq.open("GET", oURL);
oReq.onload = () => {
if (this.status === 200)
auFulfill(oReq.responseText);
else
auReject(new Error("STAT " + this.status));
};
oReq.onerror = () => {
auReject(new Error("Cannot connect"));
};
oReq.send();
});
}
Though JavaScript does not allow programs to create threads, the runtime can create one to support the asynchronous work. The factory that creates the promise performs no long-running operations, so it does not block the current task.
Promise settlement
Every promise has one of three states:
- Pending: The asynchronous operation is in progress. The promise starts in this state;
- Fulfilled: The asynchronous operation completed successfully;
- Rejected: The asynchronous operation failed or could not be started.
A promise that has been fulfilled or rejected is said to be settled. Neither its state nor its value can change after it is settled. The word resolved is often used as a synonym for fulfilled, but other times it describes a promise that has been settled (possibly even rejected) or that wraps another promise, ensuring that it will settle in the same state when that promise settles.
The work function accepts two functions from the Promise implementation as parameters. The first fulfills the promise, while the second rejects it. These are meant to be called when the outcome of the operation is known. They can be called directly, from the work function, or from handlers assigned by the work function to the asynchronous operation. Though the promise and its work function are defined in the current task, the work is not started until after that task (including any code that follows the promise-returning factory) is complete.
Both functions accept one argument. The fulfilling function accepts the result of the operation. The rejecting function accepts a reason instance of any type. Throwing from the work function automatically rejects the promise with the thrown object as the reason.
Sources
States and Fatesthen, catch, and finally
As will be seen, promises can be chained so that the result of one operation is passed to another:
uwStSvc("RAD1")
.then(aSt => uLoad(aSt))
.finally(() => uTerm());
To that end, the Promise class provides then, catch, and finally methods that associate promise outcomes with settlement handlers. then and catch also extract results and reasons from the parent promise, and all three return new promises:
-
then([handFulfill], [handReject]) -
Accepts up to two handlers. handFulfill is called if the promise is fulfilled. That function accepts a single argument that gives the result of the parent operation. handReject is called if the promise is rejected, and its argument gives the reason that was thrown or otherwise provided.
A non-function value can be provided in place of either handler. When that is done, the value is replaced with a simple function that returns the value.
No promise is ever settled in the task in which it was created, so any handler will be invoked after the current task. In the meantime,
thenstores the handlers and returns a new promise. If no handlers are provided, the promise will settle when the parent settles, with the same state and value. -
catch(handReject) -
Accepts a rejection handler. This is the same functionality provided by the second
thenparameter, andcatchin fact forwards its argument to that method. Likethen,catchstores the handler and returns a new promise. -
finally(handFinal) -
Accepts a handler that will be invoked when the promise is fulfilled or rejected. Like the other methods,
finallystores the handler and returns a new promise. The handler does not receive an argument, however.
Chaining promises
Each then, catch, or finally adds a new promise to the chain. When a parent promise is settled, one of its settlement handlers may be invoked. Whether this happens depends upon the nature of the settlement:
-
If the child promise was created with
then, a fulfillment handler was probably registered. If so, the handler will be invoked if the parent was fulfilled. If a rejection handler was registered, it will be invoked if the parent was rejected; -
If the child was created with
catch, the handler will be invoked if the parent was rejected; -
If the child was created with
finally, the handler will be invoked regardless of the parent outcome.
If the parent is fulfilled when there is no fulfillment handler, the child will be fulfilled. This allows the program to continue past a catch promise when the preceding promise is a success.
If the parent is rejected when there is no rejection handler, the child will be rejected. This causes successive promises to be rejected until a catch, finally, or rejection-handling then promise is encountered.
When a handler is invoked, another asynchronous process is started that eventually causes the child to be settled. The way in which it settles depends upon the output of the handler:
- If it returns a non-promise value, the child promise is fulfilled with that value as its result;
-
If it finishes without returning a value, the child promise is fulfilled with an
undefinedresult; - If it throws an object, the child promise is rejected with that object as its reason;
- If it returns a settled promise, the child promise is settled the same way, with the same result or reason;
- If it returns a pending promise, the child promise retains its pending state. It will be settled automatically when the returned promise is settled.
Note that these rules apply to catch and finally, just as they apply to then. This means that the promise returned from a catch is fulfilled, not rejected, unless the catch handler throws or returns a rejected promise.
Within the chain, promises are settled sequentially and asynchronously. The result of each successful promise is passed through the fulfillment handler argument to its child. The chain itself is instantiated without blocking. As a whole, it is represented by the promise at its end.
The Promise class offers static methods like all and allSettled that run promises in parallel. This can also be accomplished by defining promises as a group, and joining their results after, with then:
const owReq10 = uwUpd(10)
const owReq12 = uwUpd(12)
const owReq14 = uwUpd(14)
const owReqsAll =
owReq10.then(aSt10 =>
owReq12.then(aSt12 =>
owReq14.then(aSt14 =>
[aSt10, aSt12, aSt14]
)
)
);
Because child promises without rejection handlers are automatically rejected when their parents are rejected, a single catch can handle rejections produced anywhere within a sequence of then calls:
uwUpd(80)
.then(aIdx => {
if (aIdx < 0) throw Error("Invalid index");
return uwCache(aIdx);
})
.then(aID => uwReady(aID))
.catch(oErr => uLog(oErr));
If it does not throw or return a rejected promise of its own, a catch handler fulfills its promise. This allows then handlers that follow the catch to proceed as usual:
uwUpd(90)
.then(aIdx => uwCache(aIdx))
.catch(oErr => uLog(oErr))
.then(oErr => uTerm());
Despite its name, catch handles rejected promises, not exceptions per se. Throwing from the work function automatically rejects the promise, but throwing from outside the work function (while still within the promise factory) allows the exception to leave the factory. This prevents it from being converted to a promise, and therefore prevents it from reaching the catch handler. For this reason, it is often better to catch exceptions that are thrown outside the work function, and return a rejected promise instead:
function uwUpd(aBatch) {
let oIDPrep;
try {
oIDPrep = uPrep_Upd(aBatch);
}
catch (oErr) {
return Promise.reject(oErr);
}
return new Promise((auFulfill, auReject) => {
const oURL = gURLUpdBase + oIDPrep;
const oReq = new XMLHttpRequest();
...
});
}
Promise class
Along with then, catch, and finally, the Promise class includes methods such as:
-
static
resolve(result) - Returns a new promise that is fulfilled with the specified result, if result is a non-promise value. If result is a promise, the new promise will be settled when result is settled, and in the same way.
-
static
reject(reason) - Returns a new promise that is rejected with the specified reason.
Chaining promises causes them to be settled sequentially. The Promise class also includes methods that allow promises to be executed in parallel:
-
static
all(proms) - Runs all promises in iterable proms concurrently, then returns a new promise that is fulfilled when all the promises are fulfilled, or rejected when any is rejected. If the new promise is fulfilled, its result will be an array containing the results of the other promises. If the new promise is rejected, its reason will be that of the first promise that was rejected.
-
static
allSettled(proms) - Runs all promises in iterable proms concurrently, then returns a new promise that is fulfilled when all the promises are settled, successfully or not. The result of the new promise is an array containing the results or reasons produced by the promises.
-
ES2021 static
any(proms) -
Runs all promises in iterable proms concurrently, then returns a new promise that is fulfilled when any promise is fulfilled, or rejected when all are rejected. If the new promise is fulfilled, its result will be that of the first fulfilled promise. If the new promise is rejected, its result will be an
AggregateErrorobject containing all the rejection reasons. -
static
race(proms) - Runs all promises in iterable proms concurrently, then returns a new promise that is fulfilled or rejected when any of the promises is settled, successfully or not. The result of the new promise is the result or reason produced by the first settled promise.
Sources
Promiseasync and await
ES2017 adds the async and await keywords, which simplify promise syntax. They work much as they do in C#.
async functions
An async function wraps asynchronous code. It is defined by prefixing a function declaration with the async keyword:
async function uwRecFromID(aID) {
...
}
async can also be applied to function expressions:
const uwSvcDef = async function () {
...
}
to arrow functions:
const ouw = async (aNum) => uwReq("F" + aNum);
and to methods:
class tMgrConn {
async uwOpen(aURL) {
...
}
...
async functions always return promises. As will be seen, they may be paused at one or more points by await. The first time the function does this, it implicitly returns a promise that represents the function’s remaining work, including its explicit return value, if any. In a later task, when the function explicitly returns or throws, the promise may be settled, with the outcome depending on what was explicitly returned or thrown. The same rules apply if the async function explicitly returns or throws before awaiting:
- If a non-promise value was returned, the promise will be fulfilled with that value as its result;
-
If the function finished without explicitly returning, the promise will be fulfilled with an
undefinedresult; - If the function threw an object, the promise will be rejected with that object as its reason. The thrown object will not leave the function;
- If a settled promise was returned, the outer promise will be settled in the same way, with the same result or reason;
- If a pending promise was returned, the outer promise will retain its pending state. It will be settled automatically when the returned promise is settled.
These rules also determine the state of a chained promise when the associated then, catch, or finally handler returns, so async functions can be used within promise chains, just like other promise factory functions. Unlike many functions, however, an async function ensures that a promise is returned. Other factories could throw from outside the work function, for instance, or return a non-promise value. Those actions would automatically settle the promise returned by an async function.
Sources
async functionAwaiting promises
The await operator can be used only within async functions. It accepts one argument. If that argument is a promise, the containing function pauses:
async function uwUpd(aIDRec) {
oRec = await uwRecFromID(aIDRec);
...
A new promise is returned to the code that called the containing function, and (if it does not itself await) that code continues to run within the current task. During some later task, when the awaited promise has settled, the program resumes the containing function where it paused. If the awaited promise was fulfilled, await returns its result. If the promise was rejected, await throws the reason for the rejection. This allows a promise chain:
function uUpdByName(aName) {
uLog(`Fetching batch '${aName}'...`);
uwBatchFromName(aName)
.catch(oErr => {
uLog("Reverting to default...");
return uwBatchDef();
})
.then(aBatch => {
uLog(`Updating batch ${aBatch}...`);
return uwUpd(aBatch);
})
.then (aIdx => uCache(aIdx));
}
to be replaced with something resembling synchronous code. In particular, where asynchronous results were forwarded as settlement handler arguments, they now can be stored in local variables:
async function uwUpdByName(aName) {
uLog(`Fetching batch '${aName}'...`);
let oBatch;
try {
oBatch = await uwBatchFromName(aName);
}
catch (oErr) {
uLog("Reverting to default...");
oBatch = await uwBatchDef();
}
uLog(`Updating batch ${oBatch}...`);
const oIdx = await uwUpd(oBatch);
uCache(oIdx);
}
Non-promise arguments can also be passed to await. When that is done, the containing function does not pause; await simply returns the argument, and the function continues.
There may be no need to await the last promise in a given function; the promise code will run on its own, even though nothing is waiting for it. Awaiting such a promise allows its rejection to be handled locally, however.
Originally, there was no way to await in the global scope without wrapping the asynchronous code in an async IIFE. However, the promise returned by the IIFE could not itself be awaited, so the wrapped code finished some time after the containing program returned:
(async () => {
const oBatch = await uwBatchDef();
uLog(oBatch);
})();
Starting in ES2022, top level await can be used in the global scope, but only within modules. This allows the containing module to wait on asynchronous code before returning:
const oBatch = await uwBatchDef();
uLog(oBatch);
Sources
Using Promises await Promise.prototype.then()Asynchronous iterables
ES2018 adds features that generate iteration results asynchronously.
The asynchronous iterable protocol is similar to the synchronous version, but the iterator method is keyed with the well-known asyncIterator symbol:
class tAccts {
constructor(aType) {
this.Type = aType;
}
[Symbol.asyncIterator]() {
return new tAcct(this.Type);
}
...
The iterator is expected to return a promise that wraps the iteration result:
class tAcct {
constructor(aType) {
this.Type = aType;
this.IDPrev = null;
}
async next() {
try {
const oAcct = await uwAcctNext(
this.Type, this.IDPrev
);
this.IDPrev = oAcct.ID;
return {value: oAcct};
}
catch (oErr) {
return {done: true};
}
}
}
This ensures that iteration results can be awaited:
async function uExecArch() {
const oAccts = new tAccts("ARCH");
const oiAccts = oAccts[Symbol.asyncIterator]();
const o = await oiAccts.next();
if (o) {
uExec(o.value.ID);
...
for await/of
ES2018 also adds the for await/of loop, which extracts a promise in each iteration from an asynchronous iterable, awaits it, and then executes the loop code if the promise was fulfilled. Like await, it can be used only within async functions:
async function uwExec_Accts(aType) {
const oAccts = new tAccts(aType);
for await (const oAcct of oAccts)
uExec(oAcct.ID);
}
The loop repeats until a promise is rejected. It can also accept an iterable of promises, or an ordinary synchronous iterable. In all cases, successive iterations are processed in different tasks.
Asynchronous generators
Generators can also be declared async, and these allow await and yield to be used together:
async function* uwPrepRead_Svc() {
const oSvc = await uwSvc();
yield oSvc.uRead("PREP");
yield oSvc.uRead("READ");
}
As expected, they implement the asynchronous versions of the iterable and iterator protocols:
async function uwRead() {
const owReads = uwPrepRead_Svc();
for await (const oRead of owReads)
uLog(oRead);
}
Modules
A module is a JavaScript file that can share functionality and data with other files. Every module defines a separate module scope, which prevents name collisions between elements defined at the top levels of different files. A number of non-standard module formats have been widely used, including CommonJS (used by Node.js), AMD (implemented by the RequireJS library), and UMD.
Non-module files are called scripts. When combined in a single HTML document, these share a common global scope, even when placed in separate script elements.
ECMAScript modules
ES6 introduces ECMAScript modules, also known as ES modules, ESM modules, or ES6 modules.
It is recommended that ECMAScript module files use the mjs file extension. By default, Node.js interprets js files as CommonJS modules, but these can be identified as ECMAScript modules by adding:
"type": "module"
to the top level of the package.json file. When served to a browser, a module’s Content-Type header should be set to text/javascript. Some servers set this MIME type automatically for mjs files, but not some do not.
ECMAScript modules use strict mode implicitly.
Sources
Modulesexport
export shares elements defined in the top level of a module, including variables, functions, and classes. Elements can be defined and exported simultaneously by prefixing their definitions with export:
export const Dim = 2;
export function uDist(aPtL, aPtR) {
...
export class tPt {
...
By listing them within curly braces, export can also share elements defined elsewhere in the file:
export {Dim, uDist, tPt};
In either case, named exports are produced. These names must be specified when the elements are imported (though they can also be renamed at that time).
exports statements can be placed anywhere in the top level of the file, even before the elements are defined. Any number of them can be used, so long as no export name is repeated. When exporting this way, as can be used to change the exported names:
export {Dim, uDist as Dist, tPt as Pt};
It is also possible to re-export elements defined in another module. The syntax resembles import/from, with import replaced by export:
export {Ready} from "./MgrLot.mjs";
The module that imports from this one requests the elements as if they had been defined here. export/from does not bring elements into the current module, however.
Re-exported elements can be renamed with as:
export {Rnd as Mersenne} from "./Mersenne.mjs";
export {Rnd as Xorshift} from "./Xorshift.mjs";
It is also possible to re-export all named exports by replacing the name list and braces with an asterisk:
export * from "./Face.mjs";
Starting in ES2020, adding as to this statement allows these exports to be packaged within a named object:
export * as Util from "./Util.mjs";
Default exports
A single element can be marked as the default export for the module. This is done by following the export keyword with default:
export default Ready() {
...
}
Like named exports, the default can also be specified before or after the element’s definition. When this is done, the element is listed without curly braces, since only one default is allowed:
export default Ready;
The importing module provides its own name for the default element during the import. This makes it possible to export an anonymous function:
export default function () {
...
or an anonymous value:
export default 60 * 60 * 24;
so long as it is the default.
import
import brings exported elements into scope within the current module. Code in the exporting module is executed before elements are imported for the first time. It is not re-executed — even if the module is imported again — and it is never executed without an import. All imports are processed before code in the importing module is executed, even if the import statements are placed at the end of the importing file.
Specific named exports are imported by listing them within curly braces. Any order can be used. The imported elements are implicitly const:
import {Pt, Dim, Dist} from "./Pt.mjs";
When a name list is provided, as can be used to change the imported names:
import {Pt as tPt, Dim, Dist as uDist} from "./Pt.mjs";
Replacing the names and braces with an asterisk imports all named exports together, in a module object that is named with as:
import * as Pt from "./Pt.mjs";
A default export is imported by specifying a single name, without braces or as:
import uReady from "./Cache.mjs";
Including no name list, name, asterisk, or from causes the module code to be executed without importing anything. This is useful for modules that produce side effects, and these are sometimes called side effect imports:
import "./Init.mjs";
The module specifier identifies the module to be loaded; it appears at the end of the statement. It can contain a URL, an absolute file path, a path that is relative to the importing file, or a package name. A file extension is required for URLs and paths. In Node.js, specifiers without paths or file extensions are interpreted as package names.
When modules are imported within a web page, the containing script element must set its type attribute to module:
<script type="module">
import * as Pt from "./Pt.mjs";
const oPtOrig = new Pt.tPt(0, 0);
...
</script>
This implicitly sets the defer attribute as well, causing the script to be executed after the HTML has been parsed.
Dynamic imports
Normally, import statements must be placed at the top level of the importing module, and these are processed before that module is executed. ES2020 allows imports to be nested and performed conditionally.
The dynamic import syntax resembles a function; the module specifier is passed as an argument, and a promise is returned. This promise resolves to a module object:
import("./Mod.mjs").then(aMod => {
aMod.uStart();
...
});
Sources
Modules V8: JavaScript modules Using Node.js require vs. ES6 import/exportIIFE
In older JavaScript, functions are often used as private namespaces. A global function is created, functions and variables are defined and used within, and the containing function is invoked immediately after. This pattern is known as the Immediately-Invoked Function Expression (IIFE), and it provides some of the functionality offered by modules:
(function () {
...
}());
Note that the IIFE is surrounded by parentheses. Without them, the interpreter would read it as a function declaration, which requires a name. Only expressions are allowed within parentheses.
An IIFE effectively exports certain functions and variables by returning an object. These act as an interface to the module. Elements that are not exported remain inaccessible outside the IIFE:
var Avg = (function () {
function uAlg(aVals) {
...
}
function uGeom(aVals) {
...
}
function ouCkValid(aVals) {
...
}
return {uAlg, uGeom};
}());
function uExec(aVals) {
var oAvg = Avg.uGeom(aVals);
...
}
Sources
Explain the encapsulated anonymous function syntaxMiscellanea
Strict mode
The "use strict" directive is an ordinary string that enables strict mode. This mode improves the language in many ways, including:
-
Instead of creating a new global variable, writing to an undeclared variable produces a
ReferenceError; -
Instead of referencing the global object,
thisisundefinedwithin non-class functions; -
The
withstatement is disallowed; - Instead of failing silently, an error is produced when an attempt is made to change a read-only property, or to add or delete a property from a read-only object;
-
Instead of being interpreted as octal values, integer literals that begin with zero produce
SyntaxError; -
Neither
argumentsnorevalare allowed to be assigned to another object or function; -
Variables and functions declared within code passed to
evalare not added to the containing scope.
Placing the directive in the first non-comment line of the file enables strict mode globally. Placing it in the first line of a function enables it locally:
function uExec() {
"use strict";
...
}
The default state is sometimes known as sloppy mode. The directive is ignored altogether in versions before ES5.
eval
The global eval function accepts a single string and executes it as JavaScript code. Generally, the code behaves as if it were run from the calling scope. However:
-
If either the calling code or the
evalcode uses strict mode, then theevalcode will be executed within a temporary scope, causing variables declared withinevalto go out of scope when it returns. In sloppy mode, such variables will persist; -
If the
evalfunction is assigned to and called from another variable, if it is invoked through itscallmethod, if it is called throughglobalThis.eval, or if it is called in any similarly indirect manner, its code will be executed within the global scope.
The code can always access and modify existing variables in the calling scope.
eval returns the value of its last statement, or undefined if that statement has no value. It also throws exceptions that were unhandled within its code. In particular, if the code cannot be parsed, eval produces a SyntaxError.
debugger
The debugger statement pauses execution and shows the debugger, like a breakpoint.
General sources
JavaScript for Impatient Programmers
Axel Rauschmayer
2019, Axel Rauschmayer
Eloquent JavaScript, 3rd Edition
Marijn Haverbeke
2019, No Starch Press, Inc.
JavaScript Pocket Reference, 3rd Edition
David Flanagan
2012, O’Reilly Media, Inc.
JavaScript: The Definitive Guide, 6rd Edition
David Flanagan
2011, O’Reilly Media, Inc.