There is this idea that animals don’t experience pain or unhappiness the way humans do. Well, evolution made us feel those things because they maximize our reproductive fitness. And guess what? Animals face the same evolutionary pressures.

They have similar nervous systems. They respond to injury and deprivation in the same ways. What makes you think their suffering is less real than yours?

JavaScript Notes

Programming language notes — ECMAScript 2021

JavaScript Notes

These are my JavaScript notes, covering ES2021, 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

ASI

It is possible to end some lines without semicolons, and, following a process called automatic semicolon insertion or ASI, the interpreter will treat such lines as if semicolons had been added. 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}`;
}

It is acceptable to place a line break 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
  );
}

Identifiers

Identifiers must begin with letters, underscores, or dollar signs. After the first letter, digits can be added. Identifiers are sometimes prefixed with underscores to suggest that they represent 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

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 are typically used for explicit conversions:

const oNum = Number("10.0");

JavaScript does allow primitive constructors to be invoked within new expressions. Doing so creates an object, however, not a primitive instance. The object has some qualities of the primitive, but it is not strictly equivalent:

const oObjNum = new Number(10.0);
uAssert(oNum !== oObjNum);

There is generally no reason to create these objects.

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 represented with 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 values and undefined numeric results. 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. 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 literal, including the integer and fractional parts, or the exponent:

  const oMolsPerMole = 6.022_140_76e23;

Strings can be converted to number instances with these global functions:

parseFloat(text)

Trims text of leading whitespace and trailing non-numeric characters, then converts it to a number. Returns NaN if the text is not a valid number. Unlike parseInt, this function does interpret scientific notation.

The Number constructor, by contrast, trims leading and trailing whitespace, but not other non-numeric characters.

parseInt(text, [base])

Trims text of leading whitespace and trailing non-numeric characters, discards any fractional component, then converts it to a number. Accepts hexadecimal input if the string begins with 0x or 0X, or decimal input if it begins with a non-zero number, or another base between two and 36, if specified. When no base is provided, some implementations interpret strings beginning with zero as octal, while others treat them as decimal. Returns NaN if the text is not a valid number.

Because they mark the exponent with a letter, strings containing scientific notation are not converted properly.

Number class

Number primitives are wrapped by the Number class. Its static members include:

MIN_VALUE
MAX_VALUE
The least and greatest values that can be represented by a double-precision float.
MIN_SAFE_INTEGER
MAX_SAFE_INTEGER

The least and greatest integers that can be represented unambiguously by a double-precision float, without losing precision.

Some numbers outside this range can be represented without precision loss, but nearby numbers will be rounded to these same values, making them ambiguous.

isInteger(num)
Returns true if num is an integer.
isSafeInteger(num)
Returns true if num is an integer and if it can be represented unambiguously by a double-precision float and without losing precision.

Non-static members include:

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:

floor(num)
ceil(num)
Returns num rounded toward negative or positive infinity.
trunc(num)
Returns num rounded toward zero.
round(num)
fround(num)
Returns num rounded toward the nearest integer or the nearest single-precision float.

Exponents and roots:

E
Euler’s number e.
SQRT1_2
SQRT2
The square roots of one-half and two.
exp(exp)
pow(base, exp)
Returns e or base to the power of exp.
sqrt(num)
cbrt(num)
Returns the square or cube root of num.
log(num)
log2(num)
log10(num)
Returns the natural, base-2, or base-10 logarithm of num.

Trigonometry:

PI
cos(rad)
sin(rad)
tan(rad)
Returns the cosine, sine, or tangent of an angle, measured in radians.
acos(num)
asin(num)
atan(num)
Returns the inverse cosine, sine, or tangent of num, in radians, or NaN if num is out of range.
atan2(y, x)
Returns the inverse tangent, in radians, of the slope specified by y and x.

and other functions:

sign(num)
Returns negative one if num is negative, zero if it is zero, and one if it is positive.
abs(num)
Returns the absolute value of num.
min(num, ...)
max(num, ...)
Returns the minimum or maximum of one or more numbers.
random()
Returns a random number greater than or equal to zero, and less than one.

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 constructors:

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.

Strings

Every JavaScript string is a sequence of UTF-16 code points. There is no character type.

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 by ending each line 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 +=.

Array syntax can be used to read characters, just like charAt. String instances are immutable, however, so the characters cannot be modified:

const oCh = Warn[0];

Other types define toString methods that are used during string conversions. Overridding this method allows custom output to be produced when the String constructor 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 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
\u XXXX A Unicode code point

If \0 is followed by a digit, the digit sequence will be interpreted as an octal number, producing an error if strict mode is enabled, or a possibly unexpected result if it is not. It is safer to specify the null character with \x00.

Template literals

Introduced in ES6, template literals are strings surrounded by backquotes. They function much like interpolated strings in C#:

const oCt = uRead_Int();
const oText = `Max value: ${(1 << oCt) - 1}`;

Within the literal, a placeholder is defined by surrounding a JavaScript expression with curly braces, and prefixing these with a dollar sign.

Backquote characters can be included by prefixing each with a single backslash. Template literals can also include tabs and line breaks, which are then stored within the string:

const oQuote = `So little pains
do the vulgar take
in the investigation
of truth`;

Unlike C#’s verbatim strings, 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 does not allow a trailing backslash to be included, 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 containing 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. Following the array, the function receives n arguments representing the placeholder results before they are converted to strings. The tag function can use this data to generate its result, which need not be a string.

The tag function’s first argument also contains a raw property that shows the string content before escape sequences are processed.

String class

String primitives are wrapped by the String class. Its members include:

length
Returns the number of code units in the string. Most characters are represented with a single code unit, but some require more.
charAt(index)
Returns a string containing the character at the specified index.
substr(start, len)

Returns the substring that begins at start and has length len. If start is negative, it wraps back once around the end of the string.

Note that this method specifies the substring length, unlike the similarly-named substring.

substring(start, [next])

Returns the substring that begins at start and stops at the end of the string, or just before next. If either argument is negative, it is considered to equal zero. If either argument is greater than the length, it is considered to equal the length. If start is greater than next, substring acts as if the arguments were reversed.

Note that these methods specify the substring ‘next’ point, 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 substring that begins at start and stops at the end of the string, or just before next. If either argument is negative, it wraps back once around the end of the string. The extracted string never wraps forward past the end, so arguments greater than the length are treated as if they were equal to the length.
includes(sub, [start])
Returns true if sub is found within the string, searching from the start of the string, or from index start.
startsWith(sub)
endsWith(sub)
Returns true if the string starts or ends with sub.
indexOf(sub, [start])
Returns the index where sub first occurs, searching from the start of the string, or from index start. Returns -1 if it is not found.
lastIndexOf(sub, [start])
Returns the index where sub last occurs, searching from the end of the string, or from index start. Returns -1 if it is not found.
search(regex)
Returns the index of the first regular expression match within the string, or -1 if no match is found. The global search flag in the expression is ignored.
match(regex)

Returns an array containing one or more matches produced by a regular expression, or null if no match is found.

If the global search flag is set, the returned array contains every matching substring. If the flag is not set, the array contains the data returned by RegExp.exec, including the first matching substring, any substrings matched by capturing parentheses, plus metadata properties.

matchAll(regex)
Returns an iterable containing 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. Added in ES2020.
split(delim, [max])
Returns an array containing all substrings delimited by a substring or regular expression. If max is specified, no more than that number of elements are returned.
replace(orig, new)

Returns a new string that replaces string or regular expression orig with substring new. If orig is a string, only the first match is replaced. If it is a regular expression, and if the expression’s global flag is set, all matches are replaced.

It is also possible to specify a replacement function for new. The function should accept the matching substring, the substrings matched by capturing parentheses (if any), the match position, and the original string as arguments, and return the replacement substring.

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. Added in ES2021.

repeat(ct)
Returns a new string that repeats the original 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’s length to len by adding spaces or iterations of string pad to the beginning or end. If pad is more than one character in length, the last iteration may be truncated to fit.

Symbols

ES6 provides the new symbol primitive type. A symbol is created by invoking the Symbol function:

const FlagRun = Symbol("Flag");

Note that this is not a constructor, and it cannot be called with new. A description string can be passed to the function, and if it is, that string will be included in the output 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(FlagRun !== FlagCache);

Whereas the Symbol function always returns a new symbol, the static Symbol.for method fetches one 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, that symbol 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.

The Symbol class also statically defines a number of well-known symbols that are used by JavaScript itself.

Unlike most types, symbols are not converted automatically to strings. Such conversions must be performed explicitly, with the String constructor, or with the Symbol.prototype.toString method. Nor can symbols be converted to numbers, even explicitly. They can be converted to booleans, but their values are always true.

Symbol-keyed properties

Symbols can be used to add symbol-keyed properties to an object. Like a computed property name, each symbol is placed within square braces:

const oMsg = {
  [FlagRun]: true,
  [FlagCache]: false
};

The property is also dereferenced with the array syntax:

const oCk = oMsg[FlagCache];

Because the symbol is unique in value, the resulting property is unique within the object. Assuming the symbol is not shared, this ensures that the symbol-keyed property will never collide with another property, even if the object is handled by a second party. If the symbol is reused in the same object, the second value will take precedence, as would happen if any property name were reused.

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.

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 been declared, the runtime will produce a ReferenceError that describes the variable as ‘not defined’. This should not be taken to mean that the variable is undefined.

null represents the absence of a value, as it does in other languages.

Objects

An object is a set of zero or more 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, including 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, constructors like String, and globals defined in the 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 exception if the source data 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.

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, if it is quoted, any string can serve as the name, even the empty string. Names that do not work as identifiers must be quoted:

const oLook = {
  "Num": 10,
  "Site E": 108,
  "Site F": 90,
  "": 0
};

If a particular name is not a valid identifier, it must be dereferenced with the array syntax:

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 array syntax, nested objects:

const Prefs = {
  Def: { Name: "New", LenMax: 10 }
};

are accessed by concatenating dereference operators, just as a nested array element would be:

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 array syntax, 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 very top 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.

Accessor properties

JavaScript can define getters and setters that are read and written like ordinary class variables, but backed by functions. Properties defined this way are known as accessor properties.

Accessors are declared in object literals by prefixing the backing functions with get and set. Although overloading is normally not supported in JavaScript, 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.substring(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, thus hiding the parent value.

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.substring(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.substring(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 implementation.
set The accessor’s setter implementation.
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.

Testing properties

The existence of a property can be tested in several ways. The property can be strictly compared with undefined:

const oCkCd = (aParams.Cd !== undefined);

but this fails to distinguish undeclared properties from those that have been explicitly set to 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 method returns true if its 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 indicated 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 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:

with (oData) {
  St = "ACT";
  uAlloc(++xAct);
  ...
}

Using with is generally discouraged, and it is disallowed in strict mode.

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 not usually 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 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.substring(1); },
    enumerable: true,
    configurable: true
  },
  Rank: {
    get: function () { return (this.Num <= 3 ? "A" : "B") }
  }
});

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 static properties and methods such as:

prototype
The prototype for the Object class. Adding members to this prototype makes them available to all instances that derive from Object.
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.
fromEntries(props)
Uses the properties in iterable props to create a new object, which it then returns. props must generate two-element arrays that provide the key and value for each property.
getPrototypeOf(obj)
setPrototypeOf(obj, proto)
Gets or sets the prototype of obj. Setting the prototype after construction is discouraged for performance reasons.
getOwnPropertyDescriptor(obj, name)
Returns a property descriptor for the own property with name or symbol name, or undefined if that property is not found.
getOwnPropertyDescriptors(obj)
Returns property descriptors for all own properties in obj.
defineProperty(obj, name, desc)
defineProperty(obj, props)
Uses property descriptors to modify or create one or more properties in obj.
isExtensible(obj)
isSealed(obj)
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.
preventExtensions(obj)
seal(obj)
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 sealed objects also prevent extensions, and frozen objects are also sealed.
assign(dest, ...srcs)
Copies enumerable own properties from all the srcs objects to dest, then returns dest. Ignores srcs references that are null or undefined.
entries(obj)
Returns an array containing the enumerable properties in obj that are keyed with strings. Each property is represented by a two-element array that stores the key and value.
is(valL, valR)
Returns true if the arguments have the same value. Note that this is not an equality check; in particular, is returns false if the values are positive and negative zero, and true if they are both NaN.

The Object class also includes methods such as:

isPrototypeOf(obj)
Returns true if this object is found anywhere within the prototype chain of obj.
hasOwnProperty(name)
Returns true if name is an own property of this object.
propertyIsEnumerable(name)
Returns true if name is an own property of this object, and if it is enumerable.
toString()
Returns a string representation of this object. This method can also be invoked on the null and undefined objects.

Weak references

ES2021 adds the WeakRef class, which weakly references a single object. A weak reference allows the garbage collector to delete the target object if it is referenced nowhere else.

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.

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.

Constructors

A constructor is a function that initializes new objects. It can be explicitly defined as a function:

function tPt(aX, aY) {
  this.X = aX;
  this.Y = aY;
}

or it can be implicitly defined with 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.

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 shared default class 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 function. Most functions make no use of prototype or the referenced object. When a function is used as a constructor, however, 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, this derives instead 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. The links between the various prototypes define the inheritance hierarchy that would be found in a traditional language.

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 members of no class, and do not inherit.

No standard property within the object references the prototype. However:

  • It can be retrieved or replaced by passing the object to Object.getPrototypeOf or Object.setPrototypeOf;
  • Most runtimes support the non-standard __proto__ accessor property, which allows it to be read or written;
  • It is referenced by the prototype property of the class constructor.

Properties can be added to prototypes at any time, but replacing an object’s prototype after instantiation is discouraged for performance reasons.

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 all 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 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. This means that class variables in a superclass prototype act like shared, default values that can be replaced in subclass instances with ‘own’ properties specific to those instances. Variables assigned in superclass constructors start as ‘own’ properties, and are not shared.

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;
  • Non-static methods and shared default variables are added to the object prototype, which is referenced by the constructor’s prototype property;
  • Static variables and methods are added to the constructor instance.

Manual class setup

In versions before ES6, class setup is a manual process. Constructors are defined as functions and invoked with new. This assigns the prototype and constructor property for 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 the subclass constructor, which uses call to invoke the superclass constructor. Because the default prototype is overwritten by Object.create, the constructor property must be restored manually.

Class declarations and expressions

Starting with ES6, classes can be defined with a syntax that resembles other OOP languages. This is called a class declaration:

class tRg {
  static suFromUnord(aL, aR) {
    if (aL < aR) return new tRg(aL, aR);
    return new tRg(aR, aL);
  }

  constructor(aMin, aMax) {
    this.Min = aMin;
    this.Max = aMax;
  }

  uLen() {
    return this.Max - this.Min;
  }

  * uVals() {
    yield this.Min;
    yield this.Max;
  }

  get Ck() {
    return !isNaN(this.Min) && !isNaN(this.Max);
  }
}
tRg.sTol = 0.001;

Methods are defined with ordinary function declarations, but the function keyword is not used. The constructor keyword replaces the class name that would be found in an ES5 constructor. The constructor can be omitted from the class if it is not needed.

Generator methods are prefixed by an asterisk. Accessor properties are defined by prefixing their definitions with get or set. Static methods and accessor properties are prefixed with static. Static properties must be defined outside the class declaration, like they were in ES5. As expected, non-static methods are implicitly added to the class prototype, while static methods are added to the constructor.

Class declaration methods are not enumerable, unlike methods inherited the traditional way. Also, method implementations are executed in strict mode, along with everything else in the declaration.

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.

A subclass is defined 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, rather than Function.prototype, as most functions do. This produces a constructor prototype chain that parallels the chain used to implement the class functionality. This constructor inheritance relationship is not typically implemented when subclasses are defined manually.

The new class syntax can also be used to derive from classes that were defined manually:

function tStore(aName) {
  this.Name = aName;
}

class tStoreFile extends tStore {
  ...

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 from the object referenced by the constructor’s prototype property, whether directly or indirectly. Adding non-class properties to the tested object does not change this result. By extension, the operator 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.

A prototype’s isPrototypeOf method can also be used to determine whether some object is an instance of that class:

const oCkStore = tStore.isPrototypeOf(aStore);

Type conversion

JavaScript is permissive about type conversions, and most values are automatically converted when a different type is needed:

  • undefined produces NaN when converted to a number, while null produces zero. Both produce false when converted to a boolean;
  • true produces one and false produces zero when converted to a number;
  • Numbers are automatically converted to strings. Zero and NaN values produce false when converted to booleans, while other numbers produce true;
  • The empty string is converted to zero or false, as required. Strings that contain valid decimal numbers are converted to those numbers automatically, while other strings produce NaN. The empty string produces false when converted to a boolean, while non-empty strings produce true;
  • Empty arrays produce the empty string when converted to strings, while non-empty arrays produce comma-delimited lists of their elements. Nested arrays are not represented correctly, however. Empty arrays produce zero when converted to a number. Arrays containing a single number or numeric string produce that number when converted to a number, while multi-element arrays and those containing non-numeric elements produce NaN. All arrays produce true when converted to booleans, even empty ones;
  • Objects produce their toString result when converted to strings, or their valueOf results when converted to numbers. Overriding toString allows custom string output to be produced when the conversion is performed with the String constructor. Objects always produce true when converted to booleans.

These conversions are used when comparing values with the == operator, so undefined is equal to null, "1" is equal to one, and "0" is equal to false. When a string is added to any other type with the addition operator, the non-string type is converted to a string. In some cases, string conversion occurs even when neither operand is a string. Adding two arrays with the addition operator causes both to be converted to strings and then concatenated.

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 error will result. In sloppy mode, assigning an undeclared variable automatically declares it within the global object, even if the assignment occurs in a function. Reading from an undeclared variable always produces an error. Undeclared variables can also be deleted from the global object, while declared variables cannot.

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. If the new declaration also initializes the variable, it is simply assigned with the new value.

When declared outside of a function, a var variable is created as a property of the global object.

When declared inside a function, or within a block in the function, such a variable is hoisted, giving it function scope. This makes it accessible outside the containing block, 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 also applies when var is used within a for loop:

for (var oj = 0; oj < oCt; ++oj) {
  ...
}
const ojMatch = oj;

let index variables, by contrast, cannot be accessed before or after the loop.

var should be avoided in modern JavaScript.

let and const

Variables declared with let behave like those in other languages. When declared in a function, they have block scope, so they are inaccessible outside the containing block, and they cannot be accessed before the declaration. They have global scope when declared outside a function, but they do not add properties to the global object.

When using let, redeclaring a variable in the same scope produces a SyntaxError. Nothing prevents the same variable from being declared in a contained block, however, and when this is done, the inner declaration hides the outer one.

const behaves as let, but it creates read-only variables that produce errors when reassigned. Objects referenced by const variables can be modified as usual. const variables must be initialized where they are declared.

Destructuring

Starting with ES6, destructuring allows one or more values to be extracted simultaneously from an iterable or an object. This is accomplished 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 [oCd1, oCd2] = oCds;

When the source is an object, the pattern resembles an object literal:

const oTag = { Name: "BASE", Ct: 10 };
const { 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:

const { "Name": oName, "Ct": oCt } = oTag;

Property names can also specified indirectly, with the array syntax:

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:

const oShelf = { Loc: "C10" };
const { 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. If the source is assigned to an object pattern, however, the entire statement must be placed within parentheses. This prevents the pattern from being interpreted as an ordinary object literal:

let oCity = null;
if (oCkUpd) {
  ({ City: oCity } = oPlace);
  ...

Destructuring can also be used to extract function parameters:

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];

Control structures

if

The if statement automatically converts its argument to a boolean. Because undefined and null are both falsy, this allows the validity of an object reference to be checked very simply:

if (aObj) ...

switch

switch statements can branch on conditional values of any type. case values can be defined with run-time expressions, and the two are compared using strict equality.

As in C++, case blocks that do not break or return ‘fall through’ to the next block.

for loops

Basic for loops work as they do in other languages:

for (let oj = 0; oj < oCt; ++oj) {
  ...

The index variable should 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. The loop variable is copied from the container, so assigning to it leaves the container element unchanged.

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 those that were inherited. 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 does work 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, consisting of 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 the usual logical operators:

Operator Effect
! Logical complement
&& Logical AND
|| Logical OR

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 value from a set of identifiers:

function uExec(aCt) {
  const oCt = aCt || this.CtDef || 0;
  ...

Because the ! operator always returns a boolean, !! can be used to convert truthy or falsy values to boolean equivalents.

The ternary conditional operator ?: works as in other languages, though it is also capable of returning two different types:

return oCkReady ? "GO" : 0;

ES2020 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 falsy.

ES2021 adds logical assignment operators that assign the right operand to the left if the corresponding logical operator would return the right value:

Operator Condition
&&= left is truthy
||= left is falsy
??= 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 Effect
~ 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 treated as 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
** **= Exponentiation (ES2016)

However:

  • Because of JavaScript’s aggressive approach to type conversion, the unary plus operator + can be used to convert non-numeric types to numbers;
  • 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 also offers the increment ++ and decrement -- operators found in other languages. 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 otherwise identical instances are not considered equal. There is no operator that tells whether distinct objects or arrays contain the same properties and values.

Like the loose equality operators, the comparison operators automatically convert their operands.

Other operators

void

The void operator accepts a single operand, which it evaluates. It then discards the result and returns undefined:

const o = void 10;

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);

typeof

The typeof operator returns a string that gives the general type of its operand, whether "undefined", "boolean", "number", "bigint", "string", "symbol", "object", or "function". Note that the values are always lowercase. null variables are considered to have the "object" type.

Functions

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) {
  ...

Defining the function within an expression produces a function expression. Though a function name can be provided, that name will be inaccessible outside the function itself, so these definitions are typically anonymous:

const uReset = function (aPos, aCkSync) {
  ...

As of ES6, JavaScript supports lambda functions, which it calls arrow functions. They are defined much like function expressions, but the function keyword is not used, 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 only one parameter, the parameter list parentheses can be omitted:

const ouSq = a => a * a;

If there are no parameters, however, 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 property of its own, so the property in the containing scope will referenced instead.

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 also 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 is executed as if it were part of a function defined in the global scope, so closures cannot be created this way. It also uses sloppy mode.

JavaScript does not allow functions to be overloaded. If a second function is declared with the same name, the first function will be 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. Function expressions can appear anywhere. Strict mode also gives block scope to nested functions, rather than function scope. Like var instances, nested function declarations are hoisted, allowing them to be called before they are declared, if they are accessible. Unlike hoisted variables, which are undefined before their initializations, hoisted functions are usable immediately.

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}`;
  }
};

Functions assigned to array elements are also treated as methods. In JavaScript, functions are themselves objects that can contain their own properties, including additional methods.

Like global var variables, global functions are created as properties of the global object.

Parameters

JavaScript allows functions to be called without specifying all or 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. 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. arguments can be used to implement variadic functions, and it 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 it is explicitly undefined. In earlier versions, parameters would be checked for undefined values and then set within the function body.

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 that have been destructured from an array:

function uSet_Orig([aX, aY, aZ] = [0.0, 0.0, 0.0]) {
  ...

or from an object:

function uExec({aCt = 1, aOptRenew = true} = {}) {
  ...

In this example, the object itself has been given a default so that the function can be called with no parameters.

Rest parameters and spread syntax

ES6 supports the rest parameter, which is defined by prefixing the last parameter with an ellipsis:

function uRecalc(aBin, ...aWgts) {
  ...

When the function is invoked, any arguments following the non-rest parameters are passed as an array through the rest parameter.

ES6 also introduces the spread syntax, which is invoked by prefixing an iterable argument 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 object or array:

const oArgsEx = [...oArgs, "POST"];

Because strings are iterable, this can be used to split a string into characters:

const oChs = [..."0123456789ABCDEF"];

Because objects are iterable, it can also be used to produce a shallow copy of an object:

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.

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 directly to a local variable and invoked, its this value will no longer reference the object that contained it. This can be remedied with the bind method, which is inherited by all functions:

const oRg = new tRg(100, 102);
const ouLen = oRg.uLen.bind(oRg);
const oLen = ouLen();

This creates a bound function that wraps the original. Within the new function, this returns the value of the first bind argument. If additional arguments are passed to bind, those will be automatically and invisibly forwarded to the original function every time the bound function is invoked. If arguments are passed to the bound function, those will also be forwarded to the original, after the permanently bound arguments, if any.

A similar fix can be produced with an arrow function:

const ouLen = () => oRg.uLen();

Every function also inherits call and apply methods that invoke the function directly. If an argument is passed to either method, that value is referenced by this when the function is executed. 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 specified, its elements are passed as arguments to the function.

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. The containing function can also return an object that itself defines a number of closures, allowing the data to be manipulated by different operations. This is another way that function scope can be used to limit access to functions and data. All functions within a closure share the same function-scope data, but each invocation of the containing function creates a new context with a distinct copy of that 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:

function uFuncPt3(aX) {
  return aY => aZ => new tPt3(aX, aY, aZ);
}

The operation as a whole is then performed by chaining function invocations:

const oPt = uFuncPt3(0.0)(0.0)(1.0);

Partial application is similar, but more general. Instead of decomposing a multi-parameter function into a series of one-parameter functions that (except for last) each returns another, this operation accepts a subset of the parameters, and returns a new function that accepts the rest of them to produce the result:

const ouPtPlane0 = (aX, aY) => new tPt3(aX, aY, 0.0);
const oPt = ouPtPlane0(1.0, 1.0);

Generators

ES6 supports generator functions, which resemble iterator methods in C#. These are defined like other functions, but an asterisk is appended to the function keyword. They can be structured as declarations:

function* uEls() {
  ...
}

or expressions:

const uEls = function* () {
  ...
}

When defined as methods, function is omitted:

class tMgrEl {
  * uEls() {
    ...
  }
  ...

Generator functions return generator objects that implement the iterable and iterator protocols. No generator code is executed when the object is created. Instead (from the function’s standpoint) the program counter pauses before the first line, and it 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:

function* uCds() {
  yield "A";
  yield "B";
  return 100;
}

If the function stops at a yield, its state is conserved until next is called again. Because they are iterable, generators can be used in for/of loops, their values can be spread into arguments, and they can be destructured:

const oiCds = uCds();
for (const oCd of oiCds)
  uExec(oCd);

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:

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! If a generator explicitly returns a value instead of yielding it, that value will be ignored by for/of loops, and when spreading arguments or destructuring.

The yield* statement yields a sequence of values from an 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);

As always, when first created, the generator waits at the very start of the function. Because there is no yield there, it is impossible for it to 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, so 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 that 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 necessitates 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 implements 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, the method also returns an object containing value and done properties, with value set to the argument that was passed to the return method (if any) and done set to true. In like manner, the throw method throws its argument from the current yield.

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. The Error function can be used as a constructor:

throw new Error("uExec: Invalid name");

but it also creates and returns an instance without new:

throw Error("uExec: Invalid name");

A try block is followed by a catch block, a finally block, or both. If an exception is thrown, and both are provided, the catch will be executed before the finally. The finally is always executed, even if the catch returns from the containing function.

A given catch collects all exceptions in the try block, regardless of type. The catch must define an exception variable, even if it is not needed:

try {
  ...
}
catch (oErr) {
  ...
}
finally {
  ...
}

Containers

Iterables

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 must provide a function keyed with the well-known iterator symbol:

class tNums {
  constructor(aMax) {
    this.Max = aMax;
  }

  [Symbol.iterator]() {
    return new tNum(this.Max);
  }
}

This function should return an object that implements another protocol named iterator. This object must provide a function next that returns iteration result objects:

class tNum {
  constructor(aMax) {
    this.Next = 0;
    this.Max = aMax;
  }

  next() {
    if (this.Next > this.Max)
      return { done: true };
    return { value: this.Next++ };
  }
}

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 within 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);

The iterable is a factory for the iterator. Iterators can also be created directly, by returning a closure from some function:

function uMake_Nums(aMax) {
  let oNext = 0;
  return {
    next: function () {
      if (oNext > aMax) return { done: true };
      return { value: oNext++, };
    }
  }
}
const oiNums = uMake_Nums(3);

However, this does not implement iterable itself, so the function cannot be used where an iterable is expected.

An iterable can also be created by defining the iterator function 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 fact, the returned generator object is 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);

However, a single generator object can be iterated only once. Therefore, it is necessary to obtain a new instance to iterate the generator again.

Arrays

JavaScript arrays inherit from Array.prototype. In many respects, they resemble JavaScript objects more than they resemble the typed arrays found in other languages. They can be instantiated with array literals, which are comma-delimited sequences inside square braces:

const oInsPend = [ "A11", "B04", "CXX" ];

When commas are given 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 sometimes called holes. Though they are counted in the array length, they do not produce real elements. In particular, those indices will not be iterated by for/in loops. They can be dereferenced to produce undefined, but that is true for any invalid index.

Arrays can also be created with the Array constructor. Calling Array without an argument produces an empty array. Passing a single numeric argument creates an array of the specified length, but it does not add real elements. Passing multiple arguments, or a single non-numeric argument to Array assigns those values as elements, much like an array literal:

const oInsPend = new Array("A11", "B04", "CXX");

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 array syntax 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. Elements can be added or removed, and an array can be truncated or extended by writing to the length property. Assigning to an element with an index that is greater than or equal to the current length also extends the array. In both these cases, however, the length is increased without adding enumerable elements.

Array class

The Array class includes static methods such as:

of(el, ...)
Returns an array containing one element for each of the specified arguments. Added in ES6.
from(els, [map], [this])
Returns an array containing the elements referenced by iterable or array-like els. If a map function is specified, the elements are instead passed to that function, and its output is used to populate the new array. map receives the same arguments it would receive if it were used in the map method. If this is specified, its value is used for this within the map function. Added in ES6.

The Array class also includes methods like:

join([delim])
Returns a new string that combines the string representation of every element, delimited by commas, or by delim.
keys()

Returns an iterator that generates all indices in the array.

Note that Object.keys is a static method that returns an array, while this is a non-static method that returns an iterator.

entries()

Returns an iterator that generates all keys and values in the array. Each pair is stored as a two-element array within the value property of the iteration result:

const oEls = [1, 2, 3];
const oiEls = oEls.entries();
for (const [oKey, oVal] of oiEls)
  uLog(`${oKey}: ${oVal}`);

Added in ES2016.

slice(start, [next])
Returns a new array containing elements that begin at start and stop at the array’s end, or just before next. If either argument is negative, it wraps back once around the end of the array. The extracted array never wraps forward past the end, so arguments greater than the length are treated as if they were equal to the length.
indexOf(val, [start])
Returns the index of the first element that equals val, or negative one if no match is found. If start is specified, the search begins at that index. If start is negative, it wraps back once around the end of the string. If start is greater than the last index, the search fails.
lastIndexOf(val, [start])
Returns the index of the last element that is equal to val, or negative one if no match is found. If start is specified, the search begins at that index. If start is negative, it wraps back once around the end of the string. If start is greater than the last index, the search begins at the last element.
includes(val, [start])
Returns true if any element is equal to val. If start is specified, the search begins at that index. If start is negative, it wraps back once around the end of the string. If start is greater than the last index, the search fails. Added in ES2016.
unshift(el, ...)
Adds one or more elements to the beginning of the array, adjusts the element indices to account for their new positions, and then returns the array’s new length.
shift()
Removes one element from the beginning of the array, adjusts the indices of the surviving elements to account for their new positions, and returns the removed element.
push(el, ...)
Adds one or more elements to the end of the array, then returns its new length.
pop()
Removes one element from the end of the array and returns it.
concat(add, ...)
Returns a new array containing the original elements, plus any arguments passed to the function. Unlike splice, if one or more arguments are themselves arrays, their elements are added, rather than the arrays 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 a len is specified, that number of elements are removed. If more arguments are provided, those values are also inserted at start. Unlike concat, array arguments are inserted as arrays.

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. Added in ES6.
reverse()
Reverses the element order in place, then returns the array.
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, zero if they are equal, and a negative number if the first should be sorted after the second.

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.
find(call, [this])
Returns the value of the first element for which call returns true, or undefined if no match is found. Added in ES6.
findIndex(call, [this])
Returns the index of the first element for which call returns true, or negative one if no match is found. Added in ES6.
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

All JavaScript objects can serve as associative arrays, but object keys are always strings or symbols. ES6 introduces the Map class, which allows keys to have any type.

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 will be 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 multiple map elements to updated with an array or another map:

const oNumZonesEx = [[20, "D"], [40, "E"]];
const oZonesByNumEx = new Map([
  ...oZonesByNum,
  ...oNumZonesEx
]);

Object instances are always considered unique when used as keys. There is no way to define a custom comparer.

Iterating a map returns arrays containing the key/value pairs in the order that the keys were added:

for (const [oNum, oZone] of oZonesByNum)
  uLog(`${oNum}: ${oZone}`);

Note that map instances are also objects. This means that properties can be added to the map, but these will not be recognized by Map methods like has:

oZonesByNum[100] = "F";

Map class

The Map class includes methods such as:

size()
Returns the number of elements in the map.
has(key)
Returns true if the map contains the specified key.
entries()
Returns an iterator that generates all key/value pairs in the map. Each pair is stored as a two-element array within the value property of the iteration result.
get(key)

Returns the value with the specified key, or undefined if no such key exists:

const oZone12 = oZonesByNum.get(12);

Although NaN is unequal to itself, a pair with an NaN key can be read from the map.

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 the function call, which itself accepts up to three arguments: the element value, the element key, and the map as a whole. If this is provided, it is referenced wherever this is used within call.

Weak maps

ES6 also introduces weak maps, which are simple associative arrays with keys that weakly reference object instances. Any map can associate data with some object without the need to create new properties. Weak maps do this while also allowing the garbage collector to delete the object, if it is referenced nowhere else.

Like ordinary maps, they can be initialized with an iterable of key/value arrays:

const oCtsFromBuff = new WeakMap([
  [oBuffFront, 0],
  [oBuffBack, 0]
]);

However, only objects can be used as keys. A TypeError will be thrown if a primitive is used instead.

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.

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. Set values are iterated in the same order 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 a member of the set.
values()
keys()
entries()

values returns the same value iterator that is used when the set is treated as an iterable:

const oiCds = oCds.values();
for (const oCd of oiCds) {
  ...

For consistency with Map, the keys method also returns this iterator, while entries returns an iterator that generates value/value pairs.

add(val)

Adds val to the set, then returns the set instance. Has no effect if val is already in the set:

oCds.add("A").add("Z");
delete(val)

Removes the specified value and returns true if it was part of the set:

const oCkDel = oCds.delete("B");
clear()
Removes all values from the set.
forEach(call, [this])
Iterates the set and passes each value to the 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.

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:

const oTagBase = { Name: "BASE", Ct: 10 };
const oTagOff = { Name: "OFF", Ct: 2 };
const oTags = new WeakSet([oTagBase, oTagOff]);

However, only objects can be used as values. A TypeError will be thrown if a primitive is used instead.

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 wrapped by instances of the RegExp class. Instances can be created with the RegExp constructor:

const oRegCd = new RegExp("A[1-3]");

or by assigning regular expression literals, which surround the expression with forward slashes:

const oRegCd = /A[1-3]/;

It is also possible to pass another RegExp instance to the constructor:

const oRegCd = 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.

Tabs and other non-printing characters are specified with the same escape sequences used in string literals, with the exception of the backspace character, which is matched by [\b]. ctrl-X is matched with \c X.

The trailing slash in the expression literal can be followed by one or more letters that set flags:

const oRegCmds = /F\d\d/ig;

These letters can also be passed as a second parameter to the RegExp constructor. Flags are used to configure the search:

Flag Property Effect
i ignoreCase Produces a case-insensitive search. If the u flag is set, also uses Unicode case folding to ignore differences in case.
u unicode

Allows Unicode escape sequences to be used within patterns. Superfluous escape sequences (like \;, which would otherwise produce a semicolon) generate syntax errors in this mode.

Also causes strings encoding binary data to be interpreted as Unicode.

Added in ES6.

m multiline Enables multi-line mode, which causes ^ and $ to match the beginnings and ends of lines.
s dotAll Causes . to match newline characters as well as others. Added in ES2018.
g global Produces a global search, allowing some functions to process matches beyond the first.
y sticky Causes the match to succeed if it starts exactly at the position indicated by lastIndex. Matches beyond this point are not identified. Added in ES6.

Each flag sets the associated property, which cannot be changed later.

The search is started by invoking a RegExp method like exec or test.

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 oRegNumLot = /[123]/;

Prefixing the characters with a caret negates the set, causing it to match any one character that is not within the braces:

const oRegCdLot = /[^123]/;

A range of characters is specified by joining the lower and upper limits with a hyphen:

const oRegDigOct = /[0-7]/;

Neither periods nor asterisks are treated as special characters within the set, so there is no need to escape them.

Other classes include:

Class Match
. Any character that is not a newline
\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

Quantifiers

Characters and sub-expressions can be followed by quantifiers that allow them to repeat in 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.

Capturing parentheses

Surrounding characters with parentheses produces a sub-expression that can be modified as a whole by a quantifier or another function:

const oReg = / (XO)+ /;

Within a sub-expression, entire sequences can be matched against a set of alternatives by delimiting the alternatives with pipe characters:

const oRegRt = /(MAIN\d|AUX\d\d|OFF) /;

Sub-expressions are checked from left to right, and the first to produce a match is used, even if another would match more completely.

Capturing parentheses also store the target substring matched by a sub-expression. The substring can be recalled in another part of the expression by prefixing the sub-expression number with a backslash:

const oRegChQuot = /(["']).\1/;

The recalled substring is matched only if the target text contains an exact repetition of the substring that matched the referenced sub-expression.

Non-capturing parentheses do not store the matching substring. These are created by prefixing the inside characters with ?:.

const oReg = / (?:XO)+ /;

Anchors

Normally, expressions are matched wherever possible within the target text. Matches can be constrained to certain positions within the text by anchors. These 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 multi-line flag is set.
$ The end of the text, or the end of any line, if the multi-line 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 multi-line flag.
\B Any point that is not the beginning or end of a word, as defined by \b.

Enclosing characters within parentheses and prefixing with ?= or ?! creates a lookahead. Prefixing with ?<= or ?<! creates a lookbehind. These also constrains the match relative to its surroundings:

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 Matches patt if it is immediately preceded by pre. Added in ES2018.
(?<!pre)patt Matches patt if it is not immediately preceded by pre. Added in ES2018.

RegExp class

The RegExp class includes properties and methods such as:

source
Returns a string containing the expression itself. Starting with ES5, returns "(?:)" if the expression is empty.
flags
Returns a string containing the flags set in this instance.
lastIndex
The index in the target string where the next search should start.
exec(text)

Returns an array containing a substring matched by the regular expression, plus substrings matched by capturing parentheses in the expression, if such are defined. The array also defines an index property that gives the position of the match within text, and an input property that returns text itself.

If the global search flag is set within the expression, the method 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 no match is found, exec returns null.

test(text)
Returns true if a substring is matched by the regular expression. If the global search flag is set, the method also sets the lastIndex property within the expression instance to the position just after the match. This position becomes the starting point for the next search, if test is called again. Eventually, in this case, test will return false.

A number of String methods also use regular expressions, including search, match, split, and replace.

Asynchronous programming

JavaScript programs are cooperatively multitasked. User input adds elements to a message queue, much the way messages are processed in a Win32 program. The queue is serviced by an event loop, which invokes the callback or task associated with each event. There are no other threads; unless it cedes control, every task runs to completion before returning to the loop. This ensures that no task is ever interrupted by another, but it also allows long-running tasks to block the 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 uwReq(aKey) {
  if (!aKey)
    return Promise.reject(new Error("Key not set"));

  return new Promise((auResolve, auReject) => {
    const oURL = "http://localhost:8080/svc/" + aKey;

    const oReq = new XMLHttpRequest();
    oReq.open("GET", oURL);
    oReq.onload = () => {
      if (this.status === 200)
        auResolve(oReq.responseText);
      else {
        const oErr = new Error("STAT " + this.status);
        auReject(oErr);
      }
    };
    oReq.onerror = () => {
      const oErr = new Error("Cannot connect");
      auReject(oErr);
    };
    oReq.send();
  });
}

Though JavaScript does not allow programs to create threads, the runtime can create one to support the asynchronous work. The function 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, and the promise has been resolved;
  • Rejected: The asynchronous operation failed or could not be started, and the promise has been explicitly rejected.

A promise that has been resolved or rejected is said to be settled. Neither its state nor its value can change after it is settled.

The work function accepts two functions from the Promise implementation as parameters. The first resolves 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 current task, or from handlers assigned by the work function to the asynchronous system. Though the promise and its work function are defined in the current task, the work is not started until after that task (including the code that follows the promise-returning function) is complete.

Both functions accept one argument. The resolving 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. Therefore, throwing and explicitly rejecting the same type (most likely Error) allows reason data to be handled consistently.

then, catch, and finally

As will be seen, promises can be chained so that the result of one task can be passed to another:

uwReq("F10")
  .then(aVal => {
    ...
    uExec(aVal);
  })
  .finally(() =>
    uTerm()
  );

Much as the Promise constructor allows promise result functions to be associated with asynchronous events, 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 ancestor promises:

then([handResolve], [handReject])

Accepts up to two handlers, one of which may be invoked when the promise is settled. handResolve is called if the promise is resolved. 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 this 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 if the promise is resolved 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 resolution handler was probably registered. If so, the handler will be invoked if the parent was resolved. 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 resolved when there is no resolution handler, the child will be resolved. 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 resolved with that value as its result;
  • If it finishes without returning a value, the child promise is resolved 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 resolved, 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 resolution handler argument to its child. The chain itself is instantiated without blocking. As a whole, it is represented by the promise at its end.

A set of asynchronous processes can be made to run in parallel by defining them separately, and then joining their results with then:

const owReq10 = uwUpd(10)
const owReq12 = uwUpd(12)
const owReq14 = uwUpd(14)
const oReqsAll =
  owReq10.then(aIdx10 =>
    owReq12.then(aIdx12 =>
      owReq14.then(aIdx14 =>
        [aIdx10, aIdx12, aIdx14]
      )
    )
  );

Because throwing from any handler causes the associated promise to be rejected, placing catch at the end of a sequence allows exceptions thrown by then handlers to be managed in the same place with rejections generated by the original promise:

uwUpd(80)
  .then(aIdx => {
    if (aIdx < 0)
      throw Error("Invalid index");
    uCache(aIdx);
  })
  .catch(aErr =>
    uLog(aErr);
  );

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 containing asynchronous setup function) allows the exception to leave the function, which 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 happen to be 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((auResolve, auReject) => {
    const oURL = gURLUpdBase + oIDPrep;
    const oReq = new XMLHttpRequest();
    ...
  });
}

If it does not throw or return a rejected promise of its own, a catch handler resolves its promise. When this happens, a then handler that was added after the catch will be invoked whether the parent promise was rejected or not:

uwUpd(90)
  .then(aIdx =>
    uCache(aIdx);
  )
  .catch(aErr =>
    uLog(aErr);
  )
  .then(aErr =>
    uTerm();
  );

Promise class

The Promise class includes static methods such as:

resolve(result)
Returns a new promise that is resolved 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.
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 static methods that allow promises to be executed in parallel:

all(proms)
Causes all promises in iterable proms to run concurrently, then returns a new promise that is resolved when all the promises are resolved, or rejected when any is rejected. If the new promise is resolved, 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.
allSettled(proms)
Causes all promises in iterable proms to run concurrently, then returns a new promise that is resolved 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 completed promises.
any(proms)
Causes all promises in iterable proms to run concurrently, then returns a new promise that is resolved when any promise is resolved, or rejected when all are rejected. If the new promise is resolved, its result will be that of the first resolved promise. If the new promise is rejected, its result will be an AggregateError object containing all the rejection reasons. Added in ES2021.
race(proms)
Causes all promises in iterable proms to run concurrently, then returns a new promise that is resolved 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 in proms.

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) {
    ...
  }
  ...

As will be seen, async functions are often paused at one or more points by await. The first time the function pauses, it returns a new promise that represents the function’s work as a whole. Later, when the code inside the function returns, that promise may be settled, with the outcome depending on what was returned:

  • If a non-promise value was returned, the promise will be resolved with that value as its result;
  • If the function finished without returning, the promise will be resolved with an undefined result;
  • If the function threw an object, the promise will be rejected with that object as its reason. The throw 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 are the same rules that determine the state of a chained promise when the associated then, catch, or finally handler returns.

async functions are usable within promise chains, just like other promise-returning functions. Unlike those functions, however, an async function ensures that a promise is returned. A non-async function could throw from outside the work function, for instance, or return a non-promise value. Those same actions would settle the promise returned by an async function.

Awaiting promises

The await keyword can be used only within async functions. It cannot be used in a nested function unless that function is also async.

await accepts one argument. If that argument is a promise, the function containing await pauses:

async function uwUpd(aIDRec) {
  oRec = await uwRecFromID(aIDRec);
  ...

The promise is returned to the code that called the containing function, and the current task continues its work there. At some point in a later task, when the awaited promise has settled, the program resumes the function where it paused. If the awaited promise was resolved, 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(aErr => {
      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 can now be stored in local variables:

async function uwUpdByName(aName) {
  let oBatch;
  try {
    uLog(`Fetching batch '${aName}'...`);
    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 some 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.

If there is a need to await a promise in the global scope, the asynchronous code can be wrapped in an async IIFE:

(async () => {
  const oBatch = await uwBatchDef();
  uLog(oBatch);
})();

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 = "";
  }

  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 from an asynchronous iterable, awaits it, and then executes the loop code if the promise was resolved. 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);
}

This sequence repeats until a promise is rejected. The loop can also accept an iterable of promises, or an ordinary synchronous iterable. In all cases, successive iterations are processed within 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

Modules are JavaScript files that share data and functionality with other files. A number of non-standard module formats have seen use, including CommonJS (used in Node.js) and AMD (implemented by the RequireJS library).

ECMAScript modules

Modern JavaScript uses ECMAScript modules, also known as ES modules, ESM modules, or ES6 modules. These were introduced in ES6; they are supported by modern browsers, plus recent versions of Node.js.

It is recommended that ECMAScript modules use the mjs file extension. By default, Node.js interprets js files as CommonJS modules. These can be interpreted as ECMAScript modules instead 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.

export

export shares elements defined in the top level of a module, including variables, functions, and classes. Nested objects cannot be exported.

Specific elements can be exported by prefixing their definitions with export:

export const Dim = 2;

export function uDist(aPtL, aPtR) {
  ...

export class tPt {
  ...

export can also be used to share elements defined elsewhere in the file. These are listed within curly braces:

export { Dim, uDist, tPt };

When exporting this way, as can be used to change the exported names:

export { Dim, uDist as Dist, tPt as Pt };

This statement can be placed anywhere in the file, even before the elements are defined. Multiple statements of this type can be used, as long as no export name is repeated.

It is also possible to export elements defined in another module. The syntax resembles import/from, with import replaced by export:

export { Ready } from "./MgrLot.mjs";

The elements are not imported into the current module; they are merely exported as if they had been defined there.

When exporting this way, elements can be renamed with as:

export { Rnd as Mersenne } from "./Mersenne.mjs";
export { Rnd as Xorshift } from "./Xorshift.mjs";

They can also exported as module objects:

export * from "./Face.mjs";

Starting in ES2020, all exports from one module can be packaged and re-exported in a new object named with as:

export * as Util from "./Util.mjs";

Like other re-exported elements, that object can be imported as if it were defined in the re-exporting module.

import

import brings exported elements into scope within the current module. Code in the exported module is executed before it is imported for the first time. It is not executed again, even if the module is imported more than once, 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 elements are imported by listing their names in curly braces. Any order can be used. The imported elements are implicitly const:

import { Pt, Dim, Dist } from "./Pt.mjs";

The module specifier identifies the module to be loaded. In most import statements, the specifier follows from. It can contain a URL, an absolute file path, or a file path that is relative to the importing file. The file extension is required in these cases. In Node.js, specifiers without paths or file extensions are interpreted as package names.

When an element 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 exported elements together, in a module object that is named with as:

import * as Pt from "./Pt.mjs";

Omitting the element list, the module object, and from causes the module code to be executed without importing anything. This is useful for modules that produce side effects:

import "./Init.mjs";

When modules are imported within a 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.

Default exports

A single exported element can be marked as the default for the module. This is done by following the export keyword with default:

export default Ready() {
  ...
}

If the export is performed away from the element definition, the element name is listed without curly braces, since only one default can be specified:

export default Ready;

The element name can be used to identify the element within the module, but it will not be used in the importing module. In fact, by specifying it as the default, it is possible to export an anonymous function:

export default function () {
  ...

or an anonymous value:

export default 60 * 60 * 24;

Like module objects, default exports are named as they are imported. In this case, the import statement does not require an asterisk or as:

import uReady from "./Cache.mjs";

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();
  ...
});

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 or 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 is required to specify 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:

var Avg = (function () {
  function uAlg(aVals) {
    ...
  }

  function uGeom(aVals) {
    ...
  }

  function ouCkValid(aVals) {
    ...
  }

  return {
    uAlg,
    uGeom
  };
}());

function uExec(aVals) {
  var oAvg = Avg.uGeom(aVals);
  ...
}

Elements that are not exported remain inaccessible outside the IIFE. This avoids the name collisions that can result when objects are placed in the global scope.

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.

Place the directive in the first non-comment line of the script to enable strict mode globally. Place it in the first line of a function to enable 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 unhandled exceptions that were thrown within its code. In particular, if the code cannot be parsed, eval produces a SyntaxError.

debugger

The debugger statement pauses script execution and shows the debugger, like a breakpoint.

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.

MDN Web Docs: Arithmetic operators, async function, Asynchronous JavaScript, await, BigInt type, Binary strings, Classes, Concurrency model and the event loop, const, Default parameters, Destructuring assignment, Function.prototype.bind, globalThis, Iteration protocols, let, Map, Math, Modules, Object, Object initializer, Object.defineProperty, Optional chaining (?.), Promise, Promise.prototype.then(), RegExp, Regular Expressions, Rest parameters, Set, Spread syntax, String, Symbol, Symbol (glossary entry), Symbol.for, Template literals, Using Promises, var, void, WeakMap, WeakRef
Retrieved January 2018 - April 2023

BigInt: Arbitrary precision integers in JavaScript
Ecma TC39
Retrieved October 2019

Exploring ES6: Generators
Axel Rauschmayer
Retrieved November 2019

Stack Overflow: __proto__ VS. prototype in JavaScript, Explain the encapsulated anonymous function syntax, How to understand JS realms, JSON left out Infinity and NaN..., Using Node.js require vs. ES6 import/export, What are the rules for JavaScript’s automatic semicolon insertion...
Retrieved February 2018 - January 2022

V8 JavaScript Engine: JavaScript modules
Retrieved December 2021