JavaScript Notes
Compiled by Jeremy Kelly
www.anthemion.org
Printed UNKNOWN
These are my JavaScript notes, covering ES2019, 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
- Identifiers
- Primitive types
- Booleans
- Numbers
- Number class
- Math class
- BigInt
- Strings
- Special characters
- Template literals
- Tagged literals
- String class
- Symbols
- Symbol-keyed properties
- Special primitive types
- Objects
- Properties
- Accessor properties
- Property attributes
- Testing properties
- Enumerating properties
- Deleting properties
- with
- Creating objects
- Object literals
- Constructors
- Object.create
- Object attributes
- Object class
- Classes
- Manual class setup
- Class declarations and expressions
- Testing inheritance
- Type conversion
- Variables
- var
- let and const
- Destructuring
- Control structures
- if
- switch
- for loops
- for/of
- for/in
- Labels
- Operators
- Logical operators
- Bitwise operators
- Arithmetic operators
- Equality and comparison operators
- Other operators
- void
- Sequence operator
- typeof
- Functions
- Parameters
- Default parameters
- Rest parameters and spread syntax
- this
- Closures
- Currying
- Generators
- Input to generators
- Exceptions
- Containers
- Iterables
- Arrays
- Array class
- Array-like objects
- Maps
- Map class
- Weak maps
- Sets
- Set class
- Weak sets
- Regular expressions
- Character classes
- Quantifiers
- Capturing parentheses
- Anchors
- RegExp class
- Asynchronous programming
- Promises
- Promise settlement
- then, catch, and finally
- Chaining promises
- Promise class
- async and await
- async functions
- Awaiting promises
- Asynchronous iterables
- for await/of
- Asynchronous generators
- Modules
- ECMAScript modules
- export
- import
- Default exports
- Dynamic imports
- IIFE
- Miscellanea
- Strict mode
- eval
- debugger
- Sources
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:
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 isNaN
, or something other than a number or boolean. Type conversion causesisNaN('')
to returnfalse
. -
isFinite(num)
-
Returns
true
if num is neither positive nor negative infinity, and if it is notNaN
.
Numbers can be specified with scientific notation. The mantissa and the exponent are separated by E
or e
:
const Tol = 1.1E-3;
A different base can be specified by prefixing the number literal:
Prefix | Base |
0b or 0B |
Binary |
0o or 0O |
Octal |
0x or 0X |
Hexadecimal |
However, non-decimal literals cannot have fractional components.
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. UnlikeparseInt
, 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
or0X
, 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. ReturnsNaN
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 ES2019, 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;
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 fromslice
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 substrings matched by a regular expression. If the global search flag is set, all matches are returned in the array. If the flag is not set, the first array element contains the first match, if any, and the following elements contain substrings matched by capturing parentheses in the expression. If no match is found, the method returns
null
. -
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.
-
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 ES2019, 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. |
Array of names | No | No |
Object. |
Array of values | No | No |
Object. |
Array of names | No | Yes |
None of these methods return symbol-keyed properties.
Deleting properties
The delete
operator removes a property from an object. The property can be 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 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.
Constructors
A constructor is a special function that initializes new objects. It is preceded by the new
keyword when invoked:
const oRg = new tRg(2, 8);
This causes an empty object to be created and passed to the constructor, where it is referenced with this
. If the constructor has no parameters, its parentheses can be omitted during the call.
Two more values are automatically assigned in the new object: its prototype, and its constructor
property:
-
Like other functions, the constructor has a
prototype
property, and the instance it references is used as the object’s prototype. Theprototype
property itself does not belong to the object, however; it belongs to the constructor, which makes it static with respect to the class. In fact, the object prototype is technically hidden, though it can be accessed withObject.getPrototypeOf
orObject.setPrototypeOf
, or with the non-standard__proto__
property; -
The prototype instance has (at least, by default) a
constructor
property that points back to the constructor function. That value is copied to the new object’sconstructor
property.
Note that the constructor’s prototype
property does not reference the constructor’s own prototype! Like other objects, functions have hidden prototypes that are accessible through the non-standard __proto__
property, and these directly or indirectly reference Function.prototype
. That __proto__
chain implements functionality specific to those constructors, however. The instance referenced by the constructor’s prototype
property stores (along with other instances in that prototype chain) functionality specific to the class that the constructor instantiates.
Constructors are not meant to return values. If an object is returned, the first constructed object will be replaced with the returned value. If a non-object value is returned, it will be ignored.
Object.create
Objects can also be instantiated with the Object.create
method. This method accepts an argument that specifies the object’s prototype:
const oData = Object.create(tData.prototype);
The new object’s constructor
property will be set to the constructor
of the specified prototype, but that function will not be called. As shown below, 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 as a whole, not for any particular instance. Adding members to this prototype makes them available to all instances that derive fromObject
. -
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
orundefined
. -
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
returnsfalse
if the values are positive and negative zero, andtrue
if they are bothNaN
.
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
andundefined
objects.
Classes
JavaScript uses prototypal inheritance, which resembles traditional OOP inheritance only vaguely. In JavaScript, an object’s class is determined by its prototype, which is simply another object that stores methods and class metadata. If the prototype is null
, then the object is not a member of any class, and does not inherit.
The prototype is not directly accessible. However:
-
It can be retrieved or replaced by passing the object to
Object.getPrototypeOf
orObject.setPrototypeOf
; -
Most browsers support the non-standard
__proto__
accessor property, which allows it to be read or written; -
It can be retrieved from the
prototype
property of the class constructor.
Prototype instances can be modified, but replacing them altogether is discouraged for performance reasons.
When a property is accessed, it is first sought in the object itself. Properties defined in a particular 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 finally 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, a method is shared among class instances by adding it 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 object. 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.
Every object starts with a constructor
property, and all objects in a given class will typically reference the same constructor function. This allows class-static variables and methods to be defined in and accessed through the constructor. Static values cannot be added to the prototype, as that would cause them to be inherited, and modifying them would produce different values in different instances.
To summarize:
- Non-static variables are added to the object, and this is typically done inside the constructor;
-
Non-static methods are added to the object prototype, which is referenced by the constructor’s
prototype
property; - Static variables and methods are added to the constructor.
A type subclasses another when its prototype inherits from the prototype of the superclass. The relationship between a non-function object and its prototype is akin to that between a concrete instance and its most-derived type in a traditional OOP language. The links between the various prototypes define the inheritance hierarchy that would be found in a traditional language.
Manual class setup
In versions before ES6, class setup is a largely manual process. An object’s prototype is assigned when the object is created. Every function has a prototype
property that references a default prototype object. When the function is used as a constructor:
function tRg(aMin, aMax) {
this.Min = aMin;
this.Max = aMax;
}
const oRg = new tRg(100, 105);
the new object is automatically made to reference that prototype. Because every function has this property, every function can theoretically be used as a constructor, though seldom to useful effect.
Along with its prototype, every object has a constructor
property that is meant to reference the constructor that created it. The default prototype for a given function contains a non-enumerable constructor
property that references the function itself, and this value is copied to the new object. Replacing the function’s prototype object can break this link:
tRg.prototype = {
uLen: function () {
return this.Max - this.Min;
},
...
};
The constructor
property can be restored manually, or new properties can be added to the original prototype instance:
tRg.prototype.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;
All code inside the class is executed in strict mode. 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.
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 child constructor must invoke the parent constructor with super
before using this
.
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
producesNaN
when converted to a number, whilenull
produces zero. Both producefalse
when converted to a boolean; -
true
produces one andfalse
produces zero when converted to a number; -
Numbers are automatically converted to strings. Zero and
NaN
values producefalse
when converted to booleans, while other numbers producetrue
; -
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 produceNaN
. The empty string producesfalse
when converted to a boolean, while non-empty strings producetrue
; -
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 producetrue
when converted to booleans, even empty ones; -
Objects produce their
toString
result when converted to strings, or theirvalueOf
results when converted to numbers. OverridingtoString
allows custom string output to be produced when the conversion is performed with theString
constructor. Objects always producetrue
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 enumerable properties of an object, including those that were inherited. Note that it iterates keys rather than values:
for (const oName in oData) {
const oLine = oName + ": " + oData[oName];
...
By extension, it can also be used to iterate array indices:
for (const oj in oEls) {
if (uCk(oEls[oj])) ...
for
/in
variables can be declared const
.
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.
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
Closures can be used to curry an operation that would otherwise require multiple arguments. The first parameter is accepted by an outer function, which returns an inner function that accepts the second argument, et cetera, until one function is defined for each parameter:
function ouPosAbs(aOrig) {
return (aOff) => aOrig + aOff;
}
The operation as a whole can then be performed by chaining the function invocations:
const oPos = ouPosAbs(1.0)(-3.0);
More importantly, a subset of the arguments can be provided to create a new function that implicitly applies them when it is invoked with the remaining arguments:
const ouSysOne = ouPosAbs(1.0);
const oPos = ouSysOne(-3.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 forthis
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 returnstrue
for any element. -
every(call, [this])
-
Iterates the array and returns
true
if call returnstrue
for every element. -
find(call, [this])
-
Returns the value of the first element for which call returns
true
, orundefined
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 anNaN
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
, thekeys
method also returns this iterator, whileentries
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 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 aninput
property that returns text itself. If the global search flag is set within the expression, the method also sets thelastIndex
property of the expression instance to the position just after the match. This position becomes the starting point for the next search, ifexec
is called again. If no match is found,exec
returnsnull
. -
test(text)
-
Returns
true
if a substring is matched by the regular expression. If the global search flag is set, the method also sets thelastIndex
property within the expression instance to the position just after the match. This position becomes the starting point for the next search, iftest
is called again. Eventually, in this case,test
will returnfalse
.
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, andcatch
in fact forwards its argument to that method. Likethen
,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 promises in proms.
-
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";
In ES2020, exported module objects can also be renamed with as
.
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 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
isundefined
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
noreval
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 theeval
code will be executed within a temporary scope, causing variables declared withineval
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 itscall
method, if it is called throughglobalThis.eval
, or if it is called in any similarly indirect manner, its code will be executed within the global scope.
The code can always access and modify existing variables in the calling scope.
eval
returns the value of its last statement, or undefined
if that statement has no value. It also throws 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
Retrieved January 2018 - January 2022
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