• Ogle
  • Magister
  • Syntheogen
  • nthClock
  • TypeScript
  • JavaScript
  • C++
  • C#
  • React
  • WPF
  • Git
  • Split Notation
  • Physics (PDF)
  • DSP (PDF)
  • Math (PDF)
  • Mathematica
  • Play Time
  • Music
  • Politics
  • “How can anyone view the trashing of our founding tradition as evidence of patriotism? Because some have adopted a very different political philosophy than the Founders held. This approach to government promises the recovery of a mythical past. It feeds a sense of White victimhood. It emphasizes emotion over reason. It denigrates experts and expertise. It slanders outsiders and blames them for social and economic ills. It warns of global plots by Jews and shadowy elites. It accepts the lies of a leader as a deeper form of political truth. It revels in anger and dehumanization. It praises law and order while reserving the right to disobey the law and overturn the political order through violence.

    ...[Trump] and a significant portion of his supporters have embraced American fascism.”

    — Michael Gerson in The Washington Post

    JavaScript Notes

    Programming language notes — ECMAScript 2023

    JavaScript Notes

    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

    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:

    • abstract
    • arguments
    • boolean
    • byte
    • char
    • double
    • enum
    • final
    • float
    • implements
    • int
    • interface
    • long
    • native
    • package
    • private
    • protected
    • public
    • short
    • synchronized
    • throws
    • transient
    • void
    • volatile

    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 comments

    Primitive 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 true if num is NaN, or something other than a number or boolean. However, type conversion causes isNaN("") to return false.
    isFinite(num)
    Returns true if num is neither positive nor negative infinity, and if it is not NaN.

    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:

    • undefined is converted to NaN;
    • null is converted to zero;
    • A boolean is converted to zero or one;
    • A BigInt is 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 NaN if 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 the Number range;
    • A symbol produces the TypeError exception;
    • An object is converted to a primitive with [Symbol.toPrimitive], valueOf, or toString, 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 that NaN is 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 with parseFloat, 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, or NaN will be returned.

    Sources

    Number coercion parseFloat

    Number 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 true if num is any integer, or one between MIN_SAFE_INTEGER and MAX_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 NaN if 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 -1 if num is negative, 0 if it is zero, or 1 if 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

    Math

    BigInt

    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 literals

    Template 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 literals

    String 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, at wraps back once from the end of the string if index is negative. Like that notation, it returns undefined if index is out of range.
    charAt(index)
    Returns a string containing the code unit at the specified index. Unlike the bracket notation, charAt returns an empty string if the index is out of range.
    codePointAt(index)
    static fromCodePoint(...)
    codePointAt returns an integer containing the code point at the specified code unit index, or a trailing surrogate, if index is inside a point, or undefined if it is out of range. fromCodePoint accepts 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 from slice in that arguments do not wrap around the end of the string, and may be reversed. slice is 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 is undefined, or if it equals or exceeds the length, the length is used instead.
    includes(sub, [start])
    Returns true if string sub is found anywhere within the string, searching from the start, or from index start.
    startsWith(sub, [start])
    Returns true if the string begins with sub, or if it is found at index start.
    endsWith(sub, [end])
    Returns true if 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 -1 if 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 -1 if sub is not found.
    search(regex)
    Returns the index of the first regular expression match within the string, or -1 if 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. Returns null if 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. matchAll throws 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 that split("") 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
    $num The capturing group with the specified num between one and 100
    $name The 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 RegExp instance. 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 replace

    Symbols

    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.for

    Special 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.defineProperty

    Testing 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.keys Array of names No No
    Object.values Array of values No No
    Object.getOwnPropertyNames 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 initializer

    Object 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 Object class. Adding members to this prototype makes them available to all instances that derive from Object.
    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.entries ignores 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 undefined if 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 true if 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 null or undefined. 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 true if the arguments have the exact same value, as opposed to equivalent values, as determined by an equality check. In particular, is returns false if the values are -0 and +0, and true if they are both NaN.
    isPrototypeOf(obj)
    Returns true if this is found anywhere within the prototype chain of obj. This resembles instanceof, though that operator compares an object with a constructor, the prototype property of which defines the start of the chain.
    hasOwnProperty(key)
    ES2022 static hasOwn(obj, key)
    Returns true if key is an own property of the object, even if it is null or undefined.
    propertyIsEnumerable(key)
    Returns true if key is an enumerable own property of this object, even if it is null or undefined.
    toString()
    Returns a string representation of this object. This method can also be invoked on the null and undefined objects. It is often overridden to customize an object’s string conversion behavior.

    Sources

    fromEntries

    Weak 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

    WeakRef

    Classes

    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

    Classes

    Constructors

    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:

    Prototypal inheritance

    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:

    Subclassing with prototypal inheritance

    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 prototype property of the class constructor;
    • It can be retrieved or replaced by passing the object to Object.getPrototypeOf or Object.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__ setPrototypeOf

    Class 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 prototype property. For this reason, non-static methods are identified in documentation with class.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 fields
    Private 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 a TypeError instead.

    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 properties

    Static 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 blocks

    Subclass 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 realms

    Type conversion

    JavaScript is permissive about type conversions, and many operands or parameters are automatically converted to expected types:

    • undefined and null both produce false when converted to a boolean. undefined produces NaN when converted to a number, while null produces zero. They produce the text “undefined” or “null” when converted to a string;
    • true and false produce one or zero when converted to a number. They produce text “true” or “false” when converted to a string;
    • Zero and NaN produce false when converted to booleans, while other numbers produce true. Numbers produce decimal text when converted to strings;
    • The empty string produces false when converted to a boolean, while non-empty strings produce true. When converted to numbers, strings are processed as if they had been passed to the Number function they are trimmed of whitespace, then converted to the number that remains, or to zero if no text is left, or to NaN if the text is invalid;
    • All arrays produce true when 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 produce NaN. 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 true when converted to booleans. They produce their valueOf or toString results 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

    var

    let 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 scopes

    Destructuring

    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 assignment

    Control 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 to BigInt produces a TypeError exception, 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 void

    Functions

    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 parameters

    Rest 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 syntax

    this

    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.bind

    Closures

    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 protocols

    Exceptions

    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 value to reference that element. If done is defined, it should be set to false;
    • If the iterator is not able to return an element, it should set done to true. The value property 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 arrays

    Array 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 map method. If this is specified, its value is used for this within 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.keys is 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 -1 if 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 true if any element is equal to val. Elements are compared using strict equality, except that NaN is matched with NaN elements. 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 is undefined, 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 true if call returns true for any element.
    every(call, [this])
    Iterates the array and returns true if call returns true for every element.
    ES6 find(call, [this])
    ES2023 findLast(call, [this])
    Returns the value of the first or last element for which call returns true, or undefined if no match is found.
    ES6 findIndex(call, [this])
    Returns the index of the first element for which call returns true, or -1 if 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 length property;
    • 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 Object prototype. 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 true if the map contains the specified key.
    get(key)

    Returns the value associated with the specified key, or undefined if 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 true if 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 this is used within call.

    Sources

    Map

    Weak 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 true if val is part of the set.
    values()
    keys()
    entries()
    values returns the same value iterator that is used when the set is treated as an iterable. For consistency with Map, the keys method also returns this iterator, while entries returns 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 true if 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 this is used within call.

    Sources

    Set

    Weak 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 sheet

    Search 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 \u{X} plus Unicode character class escape sequences \p{prop} and \P{prop} to be used within patterns. Causes surrogate pairs to be matched as a whole, rather than as separate code units, and improves Unicode handling in general.

    Superfluous escape sequences (like \;, which would otherwise produce a semicolon) generate SyntaxError in this mode.

    v unicodeSets

    ES2024 Provides the u flag functionality, plus more confusing and esoteric Unicode features.

    Combining this flag with u produces a SyntaxError.

    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.unicodeSets

    Character 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 escape

    Quantifiers

    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

    Quantifiers

    Groups

    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 Disjunction

    Anchors

    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

    Assertions

    RegExp 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 index property that gives the position of the match within text, plus an input property that stores text itself. exec returns null if no match is found.

    If the global search flag is set within the expression, exec also sets the lastIndex property of the expression instance to the position just after the match. This position becomes the starting point for the next search, if exec is called again. If the next invocation produces no match, null is returned, and lastIndex is set to zero.

    test(text)
    Returns true if a text substring is matched by the regular expression. If the global search flag is set, test updates lastIndex the same way exec does.

    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 RegExp

    Asynchronous 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 loop

    Promises

    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 Fates

    then, 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, then stores 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 then parameter, and catch in fact forwards its argument to that method. Like then, catch stores 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, finally stores 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 undefined result;
    • 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 AggregateError object 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

    Promise

    async 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 undefined result;
    • 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 function

    Awaiting 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

    Modules

    export

    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/export

    IIFE

    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 syntax

    Miscellanea

    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, this is undefined within non-class functions;
    • The with statement 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 arguments nor eval are allowed to be assigned to another object or function;
    • Variables and functions declared within code passed to eval are 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 eval code uses strict mode, then the eval code will be executed within a temporary scope, causing variables declared within eval to go out of scope when it returns. In sloppy mode, such variables will persist;
    • If the eval function is assigned to and called from another variable, if it is invoked through its call method, if it is called through globalThis.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.