Deep JavaScript Theory and Techniques
Deep JavaScript Theory and Techniques
Deep JavaScript
2020
Copyright © 2020 by Dr. Axel Rauschmayer
Cover photo by Jakob Boman on Unsplash
All rights reserved. This book or any portion thereof may not be reproduced or used in
any manner whatsoever without the express written permission of the publisher except
for the use of brief quotations in a book review or scholarly journal.
exploringjs.com
Contents
I Frontmatter 7
1 About this book 9
1.1 Where is the homepage of this book? . . . . . . . . . . . . . . . . . . . . 9
1.2 What is in this book? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 What do I get for my money? . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4 How can I preview the content? . . . . . . . . . . . . . . . . . . . . . . . 10
1.5 How do I report errors? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.6 Tips for reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.7 Notations and conventions . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.8 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3
4 CONTENTS
Frontmatter
7
Chapter 1
Contents
1.1 Where is the homepage of this book? . . . . . . . . . . . . . . . . . 9
1.2 What is in this book? . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 What do I get for my money? . . . . . . . . . . . . . . . . . . . . . . 10
1.4 How can I preview the content? . . . . . . . . . . . . . . . . . . . . . 10
1.5 How do I report errors? . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.6 Tips for reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.7 Notations and conventions . . . . . . . . . . . . . . . . . . . . . . . 10
1.7.1 What is a type signature? Why am I seeing static types in this
book? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.7.2 What do the notes with icons mean? . . . . . . . . . . . . . . . 11
1.8 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
9
10 1 About this book
That is called the type signature of Number.isFinite(). This notation, especially the static
types number of num and boolean of the result, are not real JavaScript. The notation
is borrowed from the compile-to-JavaScript language TypeScript (which is mostly just
JavaScript plus static typing).
Why is this notation being used? It helps give you a quick idea of how a function works.
The notation is explained in detail in a 2ality blog post, but is usually relatively intuitive.
1.8 Acknowledgements 11
Reading instructions
Explains how to best read the content.
External content
Points to additional, external, content.
Tip
Gives a tip related to the current content.
Question
Asks and answers a question pertinent to the current content (think FAQ).
Warning
Warns about pitfalls, etc.
Details
Provides additional details, complementing the current content. It is similar to a
footnote.
1.8 Acknowledgements
• Thanks to Allen Wirfs-Brock for his advice via Twitter and blog post comments. It
helped make this book better.
• More people who contributed are acknowledged in the chapters.
12 1 About this book
Part II
13
Chapter 2
Contents
2.1 What is type coercion? . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.1 Dealing with type coercion . . . . . . . . . . . . . . . . . . . . 17
2.2 Operations that help implement coercion in the ECMAScript speci-
fication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2.1 Converting to primitive types and objects . . . . . . . . . . . . 18
2.2.2 Converting to numeric types . . . . . . . . . . . . . . . . . . . 18
2.2.3 Converting to property keys . . . . . . . . . . . . . . . . . . . 19
2.2.4 Converting to Array indices . . . . . . . . . . . . . . . . . . . 19
2.2.5 Converting to Typed Array elements . . . . . . . . . . . . . . 20
2.3 Intermission: expressing specification algorithms in JavaScript . . . 20
2.4 Example coercion algorithms . . . . . . . . . . . . . . . . . . . . . . 21
2.4.1 ToPrimitive() . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4.2 ToString() and related operations . . . . . . . . . . . . . . . 24
2.4.3 ToPropertyKey() . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.4.4 ToNumeric() and related operations . . . . . . . . . . . . . . . 28
2.5 Operations that coerce . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.5.1 Addition operator (+) . . . . . . . . . . . . . . . . . . . . . . . 29
2.5.2 Abstract Equality Comparison (==) . . . . . . . . . . . . . . . 30
2.6 Glossary: terms related to type conversion . . . . . . . . . . . . . . 32
In this chapter, we examine the role of type coercion in JavaScript. We will go relatively
deeply into this subject and, e.g., look into how the ECMAScript specification handles
coercion.
15
16 2 Type coercion in JavaScript
function multiply(x, y) {
if (typeof x !== 'number' || typeof y !== 'number') {
throw new TypeError();
}
// ···
}
function multiply(x, y) {
if (typeof x !== 'number' || typeof y !== 'number') {
return NaN;
}
// ···
}
function multiply(x, y) {
if (typeof x !== 'number') {
x = Number(x);
}
if (typeof y !== 'number') {
y = Number(y);
}
// ···
}
In (3), the operation performs an implicit type conversion. That is called type coercion.
JavaScript initially didn’t have exceptions, which is why it uses coercion and error values
for most of its operations:
// Coercion
assert.equal(3 * true, 3);
// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);
However, there are also cases (especially when it comes to newer features) where it
throws exceptions if an argument doesn’t have the right type:
> undefined.prop
TypeError: Cannot read property 'prop' of undefined
> null.prop
TypeError: Cannot read property 'prop' of null
> 'prop' in null
TypeError: Cannot use 'in' operator to search for 'prop' in null
2.1 What is type coercion? 17
• Using symbols:
> 6 / Symbol()
TypeError: Cannot convert a Symbol value to a number
> 6 / 3n
TypeError: Cannot mix BigInt and other types
> 123()
TypeError: 123 is not a function
> (class {})()
TypeError: Class constructor cannot be invoked without 'new'
> 'abc'.length = 1
TypeError: Cannot assign to read only property 'length'
> Object.freeze({prop:3}).prop = 1
TypeError: Cannot assign to read only property 'prop'
• A caller can explicitly convert values so that they have the right types. For example,
in the following interaction, we want to multiply two numbers encoded as strings:
let x = '3';
let y = '2';
assert.equal(Number(x) * Number(y), 6);
• A caller can let the operation make the conversion for them:
let x = '3';
let y = '2';
assert.equal(x * y, 6);
I usually prefer the former, because it clarifies my intention: I expect x and y not to be
numbers, but want to multiply two numbers.
18 2 Type coercion in JavaScript
function isNaN(number) {
let num = ToNumber(number);
// ···
}
• ToBoolean()
• ToNumber()
• ToBigInt()
• ToString()
• ToObject()
These internal functions have analogs in JavaScript that are very similar:
> Boolean(0)
false
> Boolean(1)
true
> Number('123')
123
After the introduction of bigints, which exists alongside numbers, the specification often
uses ToNumeric() where it previously used ToNumber(). Read on for more information.
• ToNumeric() returns a numeric value num. Its callers usually invoke a method mthd
of the specification type of num:
Type(num)::mthd(···)
2.2 Operations that help implement coercion in the ECMAScript specification 19
• ToInt8()
• ToUint8()
• ToUint8Clamp()
• ToInt16()
• ToUint16()
• ToInt32()
• ToUint32()
• ToBigInt64()
• ToBigUint64()
Some things are omitted – for example, the ReturnIfAbrupt shorthands ? and !.
/**
* An improved version of typeof
*/
function TypeOf(value) {
const result = typeof value;
switch (result) {
case 'function':
return 'object';
case 'object':
if (value === null) {
return 'null';
} else {
return 'object';
}
default:
return result;
}
}
function IsCallable(x) {
return typeof x === 'function';
}
– ToNumeric()
– ToNumber()
– ToBigInt(), BigInt()
–Abstract Relational Comparison (<)
• hint === 'string'. The following operations prefer strings:
– ToString()
– ToPropertyKey()
• hint === 'default'. The following operations are neutral w.r.t. the type of the
returned primitive value:
– Abstract Equality Comparison (==)
– Addition Operator (+)
– new Date(value) (value can be either a number or a string)
As we have seen, the default behavior is for 'default' being handled as if it were 'num-
ber'. Only instances of Symbol and Date override this behavior (shown later).
• 'toString' is called first if hint indicates that we’d like the primitive value to be
a string.
• 'valueOf' is called first if hint indicates that we’d like the primitive value to be a
number.
const obj = {
toString() { return 'a' },
valueOf() { return 1 },
};
A method with the property key Symbol.toPrimitive overrides the normal conversion
to primitive. That is only done twice in the standard library:
• Symbol.prototype[Symbol.toPrimitive](hint)
– If the receiver is an instance of Symbol, this method always returns the
wrapped symbol.
– The rationale is that instances of Symbol have a .toString() method that
returns strings. But even if hint is 'string', .toString() should not be
called so that we don’t accidentally convert instances of Symbol to strings
(which are a completely different kind of property key).
• Date.prototype[Symbol.toPrimitive](hint)
– Explained in more detail next.
24 2 Type coercion in JavaScript
2.4.1.3 Date.prototype[Symbol.toPrimitive]()
Date.prototype[Symbol.toPrimitive] = function (
hint: 'default' | 'string' | 'number') {
let O = this;
if (TypeOf(O) !== 'object') {
throw new TypeError();
}
let tryFirst;
if (hint === 'string' || hint === 'default') {
tryFirst = 'string';
} else if (hint === 'number') {
tryFirst = 'number';
} else {
throw new TypeError();
}
return OrdinaryToPrimitive(O, tryFirst);
};
The only difference with the default algorithm is that 'default' becomes 'string' (and
not 'number'). This can be observed if we use operations that set hint to 'default':
• The == operator coerces objects to primitives (with a default hint) if the other
operand is a primitive value other than undefined, null, and boolean. In the fol-
lowing interaction, we can see that the result of coercing the date is a string:
• The + operator coerces both operands to primitives (with a default hint). If one of
the results is a string, it performs string concatenation (otherwise it performs nu-
meric addition). In the following interaction, we can see that the result of coercing
the date is a string because the operator returns a string.
function ToString(argument) {
if (argument === undefined) {
return 'undefined';
2.4 Example coercion algorithms 25
Note how this function uses ToPrimitive() as an intermediate step for objects, before
converting the primitive result to a string (line A).
ToString() deviates in an interesting way from how String() works: If argument is a
symbol, the former throws a TypeError while the latter doesn’t. Why is that? The default
for symbols is that converting them to strings throws exceptions:
> const sym = Symbol('sym');
> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a string
2.4.2.1 String()
function String(value) {
let s;
if (value === undefined) {
s = '';
} else {
if (new.target === undefined && TypeOf(value) === 'symbol') {
26 2 Type coercion in JavaScript
String() works differently, depending on whether it is invoked via a function call or via
new. It uses new.target to distinguish the two.
/**
* Creates a String instance that wraps `value`
* and has the given protoype.
*/
function StringCreate(value, prototype) {
// ···
}
function SymbolDescriptiveString(sym) {
assert.equal(TypeOf(sym), 'symbol');
let desc = sym.description;
if (desc === undefined) {
desc = '';
}
assert.equal(TypeOf(desc), 'string');
return 'Symbol('+desc+')';
}
2.4.2.2 Symbol.prototype.toString()
Symbol.prototype.toString = function () {
let sym = thisSymbolValue(this);
return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
if (TypeOf(value) === 'symbol') {
return value;
}
if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
2.4 Example coercion algorithms 27
let s = value.__SymbolData__;
assert.equal(TypeOf(s), 'symbol');
return s;
}
}
2.4.2.3 Object.prototype.toString
Object.prototype.toString = function () {
if (this === undefined) {
return '[object Undefined]';
}
if (this === null) {
return '[object Null]';
}
let O = ToObject(this);
let isArray = Array.isArray(O);
let builtinTag;
if (isArray) {
builtinTag = 'Array';
} else if ('__ParameterMap__' in O) {
builtinTag = 'Arguments';
} else if ('__Call__' in O) {
builtinTag = 'Function';
} else if ('__ErrorData__' in O) {
builtinTag = 'Error';
} else if ('__BooleanData__' in O) {
builtinTag = 'Boolean';
} else if ('__NumberData__' in O) {
builtinTag = 'Number';
} else if ('__StringData__' in O) {
builtinTag = 'String';
} else if ('__DateValue__' in O) {
builtinTag = 'Date';
} else if ('__RegExpMatcher__' in O) {
builtinTag = 'RegExp';
} else {
builtinTag = 'Object';
}
let tag = O[Symbol.toStringTag];
if (TypeOf(tag) !== 'string') {
tag = builtinTag;
}
return '[object ' + tag + ']';
};
> String({})
'[object Object]'
It is interesting to compare the overriding versions of .toString() with the original ver-
sion in Object.prototype:
> ['a', 'b'].toString()
'a,b'
> Object.prototype.toString.call(['a', 'b'])
'[object Array]'
> /^abc$/.toString()
'/^abc$/'
> Object.prototype.toString.call(/^abc$/)
'[object RegExp]'
2.4.3 ToPropertyKey()
ToPropertyKey() is used by, among others, the bracket operator. This is how it works:
function ToPropertyKey(argument) {
let key = ToPrimitive(argument, 'string'); // (A)
if (TypeOf(key) === 'symbol') {
return key;
}
return ToString(key);
}
Once again, objects are converted to primitives before working with primitives.
return primValue;
}
return ToNumber(primValue);
}
2.4.4.1 ToNumber()
function ToNumber(argument) {
if (argument === undefined) {
return NaN;
} else if (argument === null) {
return +0;
} else if (argument === true) {
return 1;
} else if (argument === false) {
return +0;
} else if (TypeOf(argument) === 'number') {
return argument;
} else if (TypeOf(argument) === 'string') {
return parseTheString(argument); // not shown here
} else if (TypeOf(argument) === 'symbol') {
throw new TypeError();
} else if (TypeOf(argument) === 'bigint') {
throw new TypeError();
} else {
// argument is an object
let primValue = ToPrimitive(argument, 'number');
return ToNumber(primValue);
}
}
return false;
}
• strictEqualityComparison()
• StringToBigInt()
• isSameMathematicalValue()
32 2 Type coercion in JavaScript
Contents
3.1 Preparing for the pattern matching algorithm . . . . . . . . . . . . . 33
3.1.1 Using declarative rules for specifying the matching algorithm 34
3.1.2 Evaluating expressions based on the declarative rules . . . . . 35
3.2 The pattern matching algorithm . . . . . . . . . . . . . . . . . . . . 35
3.2.1 Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2.2 Rules for variable . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2.3 Rules for object patterns . . . . . . . . . . . . . . . . . . . . . 35
3.2.4 Rules for Array patterns . . . . . . . . . . . . . . . . . . . . . 36
3.3 Empty object patterns and Array patterns . . . . . . . . . . . . . . . 37
3.4 Applying the algorithm . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.4.1 Background: passing parameters via matching . . . . . . . . . 38
3.4.2 Using move2() . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.4.3 Using move1() . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.4.4 Conclusion: Default values are a feature of pattern parts . . . 40
The algorithm will give us a better understanding of default values. That will be useful
at the end, where we’ll try to figure out how the following two functions differ:
«pattern» = «value»
33
34 3 The destructuring algorithm
We will now look at an algorithm for performing this kind of assignment. This algorithm
is known in functional programming as pattern matching (short: matching). It specifies
the operator ← (“match against”) that matches a pattern against a value and assigns to
variables while doing so:
«pattern» ← «value»
The specification for the match operator consists of declarative rules that descend into
the structures of both operands. The declarative notation may take some getting used to,
but it makes the specification more concise.
«pattern» ← obj.key
{«properties»} ← obj
• (2c) is the number of the rule. The number is used to refer to the rule.
• The head (first line) describes what the input must look like so that this rule can be
applied.
• The body (remaining lines) describes what happens if the rule is applied.
In rule (2c), the head means that this rule can be applied if there is an object pattern
with at least one property (whose key is key) and zero or more remaining properties.
The effect of this rule is that execution continues with the property value pattern being
matched against obj.key and the remaining properties being matched against obj.
// We are finished
In rule (2e), the head means that this rule is executed if the empty object pattern {} is
matched against a value obj. The body means that, in this case, we are done.
Together, rule (2c) and rule (2e) form a declarative loop that iterates over the properties
of the pattern on the left-hand side of the arrow.
3.2 The pattern matching algorithm 35
To apply a sequence of rules, we go over them from top to bottom and execute the first
applicable rule. If there is a matching expression in the body of that rule, the rules are
applied again. And so on.
Sometimes the head includes a condition that also determines if a rule is applicable – for
example:
• A variable: x
• An object pattern: {«properties»}
• An Array pattern: [«elements»]
The next three sections specify rules for handling these three cases in matching expres-
sions.
x = value
«pattern» ← obj.key
{«properties»} ← obj
// We are finished
Rules 2a and 2b deal with illegal values. Rules 2c–2e loop over the properties of the
pattern. In rule 2d, we can see that a default value provides an alternative to match
against if there is no matching property in obj.
Helper function:
function isIterable(value) {
return (value !== null
&& typeof value === 'object'
&& typeof value[Symbol.iterator] === 'function');
}
«pattern» ← tmp
} else {
«pattern» ← default_value
}
«elements» ← iterator
Helper function:
function getNext(iterator) {
const {done,value} = iterator.next();
return (done ? undefined : value);
}
Given an empty Array pattern []: If the value to be destructured is iterable, then nothing
happens. Otherwise, a TypeError is thrown.
const [] = 'abc'; // OK, iterable
assert.throws(
() => {
const [] = 123; // not iterable
38 3 The destructuring algorithm
},
/^TypeError: 123 is not iterable$/)
In other words: Empty destructuring patterns force values to have certain characteristics,
but have no other effects.
But why would we define the parameters as in the previous code snippet? Why not as
follows?
function move2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
To see why move1() is correct, we are going to use both functions in two examples. Before
we do that, let’s see how the passing of parameters can be explained via matching.
The single Array element on the left-hand side does not have a match on the right-hand
side, which is why {x,y} is matched against the default value and not against data from
the right-hand side (rules 3b, 3d):
{x, y} ← { x: 0, y: 0 }
This destructuring leads to the following two assignments (rules 2c, 1):
x = 0;
y = 0;
This is what we wanted. However, in the next example, we are not as lucky.
Example 2. Let’s examine the function call move2({z: 3}) which leads to the following
destructuring:
[{x, y} = { x: 0, y: 0 }] ← [{z: 3}]
There is an Array element at index 0 on the right-hand side. Therefore, the default value
is ignored and the next step is (rule 3d):
{x, y} ← { z: 3 }
That leads to both x and y being set to undefined, which is not what we want. The
problem is that {x,y} is not matched against the default value, anymore, but against
{z:3}.
We don’t have an Array element at index 0 on the right-hand side and use the default
value (rule 3d):
{x=0, y=0} ← {}
The left-hand side contains property value shorthands, which means that this destruc-
turing is equivalent to:
{x: x=0, y: y=0} ← {}
40 3 The destructuring algorithm
Neither property x nor property y have a match on the right-hand side. Therefore, the
default values are used and the following destructurings are performed next (rule 2d):
x ← 0
y ← 0
Here, we get what we want. Let’s see if our luck holds with the next example.
Example 2: move1({z: 3})
[{x=0, y=0} = {}] ← [{z: 3}]
The first element of the Array pattern has a match on the right-hand side and that match
is used to continue destructuring (rule 3d):
{x=0, y=0} ← {z: 3}
Like in example 1, there are no properties x and y on the right-hand side and the default
values are used:
x = 0
y = 0
It works as desired! This time, the pattern with x and y being matched against {z:3} is
not a problem, because they have their own local default values.
Contents
4.1 Scopes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2 Lexical environments . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.3 The global object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.4 In browsers, globalThis does not point directly to the global object 42
4.5 The global environment . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.5.1 Script scope and module scopes . . . . . . . . . . . . . . . . . 44
4.5.2 Creating variables: declarative record vs. object record . . . . 44
4.5.3 Getting or setting variables . . . . . . . . . . . . . . . . . . . . 45
4.5.4 Global ECMAScript variables and global host variables . . . . 45
4.6 Conclusion: Why does JavaScript have both normal global variables
and the global object? . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.7 Further reading and sources of this chapter . . . . . . . . . . . . . . 47
In this chapter, we take a detailed look at how JavaScript’s global variables work. Several
interesting phenomena play a role: the scope of scripts, the so-called global object, and
more.
4.1 Scopes
The lexical scope (short: scope) of a variable is the region of a program where it can be
accessed. JavaScript’s scopes are static (they don’t change at runtime) and they can be
nested – for example:
function func() { // (A)
const aVariable = 1;
if (true) { // (B)
const anotherVariable = 2;
41
42 4 A detailed look at global variables
}
}
The scope introduced by the if statement (line B) is nested inside the scope of function
func() (line A).
The innermost surrounding scope of a scope S is called the outer scope of S. In the example,
func is the outer scope of if.
• An environment record that maps variable names to variable values (think dictio-
nary). This is the actual storage space for the variables of the scope. The name-
value entries in the record are called bindings.
• A reference to the outer environment – the environment for the outer scope.
• Available on all platforms: globalThis. The name is based on the fact that it has
the same value as this in global scope.
• Other variables for the global object are not available on all platforms:
– window is the classic way of referring to the global object. It works in normal
browser code, but not in Web Workers (processes running concurrently to the
normal browser process) and not on Node.js.
– self is available everywhere in browsers (including in Web Workers). But it
isn’t supported by Node.js.
– global is only available on Node.js.
• Whenever the src of the iframe changes, it gets a new global object.
• However, globalThis always has the same value. That value can be checked from
outside the iframe, as demonstrated below (inspired by an example in the glob-
alThis proposal).
4.5 The global environment 43
File parent.html:
<iframe src="iframe.html?first"></iframe>
<script>
const iframe = document.querySelector('iframe');
const icw = iframe.contentWindow; // `globalThis` of iframe
iframe.onload = () => {
// Access properties of global object of iframe
const firstGlobalThis = icw.globalThis;
const firstArray = icw.Array;
console.log(icw.iframeName); // 'first'
iframe.onload = () => {
const secondGlobalThis = icw.globalThis;
const secondArray = icw.Array;
File iframe.html:
<script>
globalThis.iframeName = location.search.slice(1);
</script>
How do browsers ensure that globalThis doesn’t change in this scenario? They inter-
nally distinguish two objects:
The global environment record uses two environment records to manage its variables:
• An object environment record has the same interface as a normal environment record,
but keeps its bindings in a JavaScript object. In this case, the object is the global
object.
• A normal (declarative) environment record that has its own storage for its bindings.
Which of these two records is used when will be explained soon.
// (Global variables)
{ // Scope of module 1
···
}
{ // Scope of module 2
···
}
// (More module scopes)
}
console.log(globalThis.one); // undefined
console.log(globalThis.two); // 2
</script>
<script>
let myGlobalVariable = 1; // declarative environment record
globalThis.myGlobalVariable = 2; // object environment record
Using const or let guarantees that global variable declarations aren’t influencing (or
influenced by) the built-in global variables of ECMAScript and host platform.
If a variable already exists (such as location in this case), then a var declaration with an
initializer behaves like an assignment. That’s why we get into trouble in this example.
Note that this is only an issue in global scope. In modules, we are never in global scope
(unless we use eval() or similar).
Figure 4.1: The environment for the global scope manages its bindings via a global envi-
ronment record which in turn is based on two environment records: an object environment
record whose bindings are stored in the global object and a declarative environment record
that uses internal storage for its bindings. Therefore, global variables can be created by
adding properties to the global object or via various declarations. The global object is
initialized with the built-in global variables of ECMAScript and the host platform. Each
ECMAScript module has its own environment whose outer environment is the global
environment.
4.7 Further reading and sources of this chapter 47
Thankfully, most of the code written in modern JavaScript, lives in ECMAScript mod-
ules and CommonJS modules. Each module has its own scope, which is why the rules
governing global variables rarely matter for module-based code.
Contents
5.1 Remainder operator rem vs. modulo operator mod . . . . . . . . . . . 49
5.2 An intuitive understanding of the remainder operation . . . . . . . 50
5.3 An intuitive understanding of the modulo operation . . . . . . . . . 50
5.4 Similarities and differences between rem and mod . . . . . . . . . . . 51
5.5 The equations behind remainder and modulo . . . . . . . . . . . . . 51
5.5.1 rem and mod perform integer division differently . . . . . . . . 52
5.5.2 Implementing rem . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.3 Implementing mod . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.6 Where are rem and mod used in programming languages? . . . . . . 53
5.6.1 JavaScript’s % operator computes the remainder . . . . . . . . 53
5.6.2 Python’s % operator computes the modulus . . . . . . . . . . . 53
5.6.3 Uses of the modulo operation in JavaScript . . . . . . . . . . . 53
5.7 Further reading and sources of this chapter . . . . . . . . . . . . . . 54
Remainder and modulo are two similar operations. This chapter explores how they work
and reveals that JavaScript’s % operator computes the remainder, not the modulus.
49
50 5 % is a remainder operator, not a modulo operator (bonus)
7 rem 3
We remove 3 from the dividend until we are left with a value that is smaller than 3:
-7 rem 3
This time, we add 3 to the dividend until we have a value that is smaller than -3:
x: -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7
x rem 3: -1 0 -2 -1 0 -2 -1 0 1 2 0 1 2 0 1
Among the results, we can see a symmetry: x and -x produce the same results, but with
opposite signs.
x mod 3
[0,3) = {0,1,2}
That is, zero is included (opening square bracket), 3 is excluded (closing parenthesis).
How does mod perform this mapping? It is easy if x is already inside the range:
> 0 mod 3
0
> 2 mod 3
2
If x is greater than or equal to the upper boundary of the range, then the upper boundary
is subtracted from x until it fits into the range:
5.4 Similarities and differences between rem and mod 51
> 4 mod 3
1
> 7 mod 3
1
That means we are getting the following mapping for non-negative integers:
x: 0 1 2 3 4 5 6 7
x mod 3: 0 1 2 0 1 2 0 1
x: -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7
x mod 3: 2 0 1 2 0 1 2 0 1 2 0 1 2 0 1
Note how the range [0,3) is repeated over and over again.
• With rem, the result has the same sign as the dividend (first operand):
> 5 rem 4
1
> -5 rem 4
-1
> 5 rem -4
1
> -5 rem -4
-1
• With mod, the result has the same sign as the divisor (second operand):
> 5 mod 4
1
> -5 mod 4
3
> 5 mod -4
-3
> -5 mod -4
-1
How often do we need to multiply the divisor to get as close to the dividend
as possible?
For example, that gives us two different results for dividend −2 and divisor 3:
• -2 rem 3 = -2
3 × 0 gets us close to −2. The difference between 0 and the dividend −2 is −2.
• -2 mod 3 = 1
3 × −1 also gets us close to −2. The difference between −3 and the dividend −2 is
1.
It also gives us two different results for dividend 2 and divisor −3:
• 2 rem -3 = 2
-3 × 0 gets us close to 2.
• 2 mod -3 = -1
-3 × -1 gets us close to 2.
The results differ depending on how the integer division div is implemented.
Note that other ways of doing integer division are possible (e.g. based on Math.ceil()
or Math.round()). That means that there are more operations that are similar to rem and
mod.
> 7 % 6
1
> -7 % 6
-1
>>> 7 % 6
1
>>> -7 % 6
5
• To convert the operands of the >>> operator to unsigned 32-bit integers (x mod
2**32):
• To convert arbitrary numbers so that they fit into Typed Arrays. For example, x
mod 2**8 is used to convert numbers to unsigned 8-bit integers (after first convert-
ing them to integers):
tarr[0] = 256;
assert.equal(tarr[0], 0);
tarr[0] = 257;
assert.equal(tarr[0], 1);
54 5 % is a remainder operator, not a modulo operator (bonus)
55
Chapter 6
Contents
6.1 Shallow copying vs. deep copying . . . . . . . . . . . . . . . . . . . 57
6.2 Shallow copying in JavaScript . . . . . . . . . . . . . . . . . . . . . 58
6.2.1 Copying plain objects and Arrays via spreading . . . . . . . . 58
6.2.2 Shallow copying via Object.assign() (optional) . . . . . . . . 61
6.2.3 Shallow copying via Object.getOwnPropertyDescriptors()
and Object.defineProperties() (optional) . . . . . . . . . . 61
6.3 Deep copying in JavaScript . . . . . . . . . . . . . . . . . . . . . . . 62
6.3.1 Manual deep copying via nested spreading . . . . . . . . . . . 62
6.3.2 Hack: generic deep copying via JSON . . . . . . . . . . . . . . 62
6.3.3 Implementing generic deep copying . . . . . . . . . . . . . . . 63
6.4 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
In this chapter, we will learn how to copy objects and Arrays in JavaScript.
• Shallow copying only copies the top-level entries of objects and Arrays. The entry
values are still the same in original and copy.
• Deep copying also copies the entries of the values of the entries, etc. That is, it tra-
verses the complete tree whose root is the value to be copied and makes copies of
all nodes.
The next sections cover both kinds of copying. Unfortunately, JavaScript only has built-
in support for shallow copying. If we need deep copying, we need to implement it our-
selves.
57
58 6 Copying objects and Arrays
Alas, spreading has several issues. Those will be covered in the next subsections. Among
those, some are real limitations, others mere pecularities.
For example:
class MyClass {}
Therefore, we can fix this by giving the copy the same prototype as the original:
class MyClass {}
const copy = {
__proto__: Object.getPrototypeOf(original),
...original,
};
assert.equal(copy instanceof MyClass, true);
Alternatively, we can set the prototype of the copy after its creation, via Ob-
ject.setPrototypeOf().
6.2.1.2 Many built-in objects have special “internal slots” that aren’t copied by object
spreading
Examples of such built-in objects include regular expressions and dates. If we make a
copy of them, we lose most of the data stored in them.
6.2 Shallow copying in JavaScript 59
Given how prototype chains work, this is usually the right approach. But we still need
to be aware of it. In the following example, the inherited property .inheritedProp of
original is not available in copy because we only copy own properties and don’t keep
the prototype.
For example, the own property .length of Array instances is not enumerable and not
copied. In the following example, we are copying the Array arr via object spreading
(line A):
This is also rarely a limitation because most properties are enumerable. If we need to copy
non-enumerable properties, we can use Object.getOwnPropertyDescriptors() and Ob-
ject.defineProperties() to copy objects (how to do that is explained later):
• They consider all attributes (not just value) and therefore correctly copy getters,
setters, read-only properties, etc.
• Object.getOwnPropertyDescriptors() retrieves both enumerable and non-
enumerable properties.
Independently of the attributes of a property, its copy will always be a data property that
is writable and configurable.
For example, here we create the property original.prop whose attributes writable and
configurable are false:
writable: false,
configurable: false,
enumerable: true,
},
});
assert.deepEqual(original, {prop: 1});
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual({...original}, {
myGetter: 123, // not a getter anymore!
mySetter: undefined,
});
The copy has fresh versions of each key-value entry in the original, but the values of the
original are not copied themselves. For example:
Using a method instead of syntax has the benefit that it can be polyfilled on older
JavaScript engines via a library.
Among other things, assignment invokes own and inherited setters, while definition
doesn’t (more information on assignment vs. definition). This difference is rarely no-
ticeable. The following code is an example, but it’s contrived:
function copyAllOwnProperties(original) {
return Object.defineProperties(
{}, Object.getOwnPropertyDescriptors(original));
}
First, all attributes of own properties are copied correctly. Therefore, we can now copy
own getters and own setters:
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);
// We copied successfully:
assert.deepEqual(original, copy);
// The copy is deep:
assert.ok(original.work !== copy.work);
function jsonDeepCopy(original) {
return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
The significant downside of this approach is that we can only copy properties with keys
and values that are supported by JSON.
assert.deepEqual(
jsonDeepCopy({
// Symbols are not supported as keys
[Symbol('a')]: 'abc',
// Unsupported value
b: function () {},
// Unsupported value
c: undefined,
}),
{} // empty object
);
assert.throws(
() => jsonDeepCopy({a: 123n}),
/^TypeError: Do not know how to serialize a BigInt$/);
function deepCopy(original) {
if (Array.isArray(original)) {
const copy = [];
for (const [index, value] of original.entries()) {
copy[index] = deepCopy(value);
}
return copy;
} else if (typeof original === 'object' && original !== null) {
const copy = {};
for (const [key, value] of Object.entries(original)) {
copy[key] = deepCopy(value);
}
return copy;
} else {
// Primitive value: atomic, no need to copy
return original;
64 6 Copying objects and Arrays
}
}
Note that deepCopy() only fixes one issue of spreading: shallow copying. All oth-
ers remain: prototypes are not copied, special objects are only partially copied,
non-enumerable properties are ignored, most property attributes are ignored.
We can make our previous implementation of deepCopy() more concise if we use .map()
and Object.fromEntries():
function deepCopy(original) {
if (Array.isArray(original)) {
return original.map(elem => deepCopy(elem));
} else if (typeof original === 'object' && original !== null) {
return Object.fromEntries(
Object.entries(original)
.map(([k, v]) => [k, deepCopy(v)]));
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
6.4 Further reading 65
Contents
7.1 Examples: updating an object destructively and non-destructively . 67
7.2 Examples: updating an Array destructively and non-destructively . 68
7.3 Manual deep updating . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.4 Implementing generic deep updating . . . . . . . . . . . . . . . . . 69
• A destructive update of data mutates the data so that it has the desired form.
• A non-destructive update of data creates a copy of the data that has the desired form.
The latter way is similar to first making a copy and then changing it destructively, but it
does both at the same time.
67
68 7 Updating data destructively and non-destructively
assert.deepEqual(
original, {name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(
updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
If we see value as the root of a tree that we are updating, then deepUpdate() only deeply
changes a single branch (line A and C). All other branches are copied shallowly (line B
and D).
This is what using deepUpdate() looks like:
const original = {name: 'Jane', work: {employer: 'Acme'}};
Contents
8.1 What is shared mutable state and why is it problematic? . . . . . . . 71
8.2 Avoiding sharing by copying data . . . . . . . . . . . . . . . . . . . 73
8.2.1 How does copying help with shared mutable state? . . . . . . 73
8.3 Avoiding mutations by updating non-destructively . . . . . . . . . 75
8.3.1 How does non-destructive updating help with shared mutable
state? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
8.4 Preventing mutations by making data immutable . . . . . . . . . . 76
8.4.1 How does immutability help with shared mutable state? . . . 76
8.5 Libraries for avoiding shared mutable state . . . . . . . . . . . . . . 76
8.5.1 Immutable.js . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
8.5.2 Immer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
• If two or more parties can change the same data (variables, objects, etc.).
• And if their lifetimes overlap.
• Then there is a risk of one party’s modifications preventing other parties from
working correctly.
71
72 8 The problems of shared mutable state and how to avoid them
Note that this definition applies to function calls, cooperative multitasking (e.g., async
functions in JavaScript), etc. The risks are similar in each case.
The following code is an example. The example is not realistic, but it demonstrates the
risks and is easy to understand:
function logElements(arr) {
while (arr.length > 0) {
console.log(arr.shift());
}
}
function main() {
const arr = ['banana', 'orange', 'apple'];
console.log('Before sorting:');
logElements(arr);
console.log('After sorting:');
logElements(arr); // (A)
}
main();
// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
• Function main() wants to log an Array before and after sorting it.
• Function logElements() logs the elements of its parameter arr, but removes them
while doing so.
In the remainder of this chapter, we look at three ways of avoiding the problems of shared
mutable state:
In particular, we will come back to the example that we’ve just seen and fix it.
8.2 Avoiding sharing by copying data 73
Background
For background on copying data in JavaScript, please refer to the following two
chapters in this book:
• §6 “Copying objects and Arrays”
• §14 “Copying instances of classes: .clone() vs. copy constructors”
Defensive copying is a technique to always copy when issues might arise. Its objective is to
keep the current entity (function, class, etc.) safe:
• Input: Copying (potentially) shared data passed to us, lets us use that data without
being disturbed by an external entity.
• Output: Copying internal data before exposing it to an outside party, means that
that party can’t disrupt our internal activity.
Note that these measures protect us from other parties, but they also protect other parties
from us.
Remember that in the motivating example at the beginning of this chapter, we got into
trouble because logElements() modified its parameter arr:
function logElements(arr) {
while (arr.length > 0) {
console.log(arr.shift());
}
}
function logElements(arr) {
arr = [...arr]; // defensive copy
while (arr.length > 0) {
console.log(arr.shift());
}
}
function main() {
const arr = ['banana', 'orange', 'apple'];
console.log('Before sorting:');
logElements(arr);
console.log('After sorting:');
logElements(arr); // (A)
}
main();
// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
// 'apple'
// 'banana'
// 'orange'
Let’s start with a class StringBuilder that doesn’t copy internal data it exposes (line A):
class StringBuilder {
_data = [];
add(str) {
this._data.push(str);
}
getParts() {
// We expose internals without copying them:
return this._data; // (A)
}
toString() {
return this._data.join('');
}
}
If, however, the result of .getParts() is changed (line A), then the StringBuilder ceases
to work correctly:
8.3 Avoiding mutations by updating non-destructively 75
The solution is to copy the internal ._data defensively before it is exposed (line A):
class StringBuilder {
this._data = [];
add(str) {
this._data.push(str);
}
getParts() {
// Copy defensively
return [...this._data]; // (A)
}
toString() {
return this._data.join('');
}
}
Now changing the result of .getParts() doesn’t interfere with the operation of sb any-
more:
Background
For more information on updating data, see §7 “Updating data destructively and
non-destructively”.
This works, because we are only making non-destructive changes and are therefore copy-
ing the data on demand.
Background
For background on how to make data immutable in JavaScript, please refer to the
following two chapters in this book:
• §10 “Protecting objects from being changed”
• §15 “Immutable wrappers for collections”
• Immutable.js provides immutable data structures for lists, stacks, sets, maps, etc.
• Immer also supports immutability and non-destructive updating but for plain ob-
jects, Arrays, Sets, and Maps. That is, no new data structures are needed.
These libraries are described in more detail in the next two sections.
8.5.1 Immutable.js
In its repository, the library Immutable.js is described as:
• List
• Stack
8.5 Libraries for avoiding shared mutable state 77
Notes:
8.5.2 Immer
In its repository, the library Immer is described as:
Immer helps with non-destructively updating (potentially nested) plain objects, Arrays,
Sets, and Maps. That is, there are no custom data structures involved.
const people = [
{name: 'Jane', work: {employer: 'Acme'}},
];
draft[0].work.employer = 'Cyberdyne';
draft.push({name: 'John', work: {employer: 'Spectre'}});
});
assert.deepEqual(modifiedPeople, [
{name: 'Jane', work: {employer: 'Cyberdyne'}},
{name: 'John', work: {employer: 'Spectre'}},
]);
assert.deepEqual(people, [
{name: 'Jane', work: {employer: 'Acme'}},
]);
The original data is stored in people. produce() provides us with a variable draft. We
pretend that this variable is people and use operations with which we would normally
make destructive changes. Immer intercepts these operations. Instead of mutating draft,
it non-destructively changes people. The result is referenced by modifiedPeople. As a
bonus, it is deeply immutable.
assert.deepEqual() works because Immer returns plain objects and Arrays.
Part IV
79
Chapter 9
Property attributes: an
introduction
Contents
9.1 The structure of objects . . . . . . . . . . . . . . . . . . . . . . . . . 82
9.1.1 Internal slots . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
9.1.2 Property keys . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
9.1.3 Property attributes . . . . . . . . . . . . . . . . . . . . . . . . 83
9.2 Property descriptors . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
9.3 Retrieving descriptors for properties . . . . . . . . . . . . . . . . . . 85
9.3.1 Object.getOwnPropertyDescriptor(): retrieving a descriptor
for a single property . . . . . . . . . . . . . . . . . . . . . . . 85
9.3.2 Object.getOwnPropertyDescriptors(): retrieving descrip-
tors for all properties of an object . . . . . . . . . . . . . . . . 85
9.4 Defining properties via descriptors . . . . . . . . . . . . . . . . . . . 86
9.4.1 Object.defineProperty(): defining single properties via de-
scriptors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
9.4.2 Object.defineProperties(): defining multiple properties via
descriptors . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
9.5 Object.create(): Creating objects via descriptors . . . . . . . . . . 88
9.6 Use cases for Object.getOwnPropertyDescriptors() . . . . . . . . . 89
9.6.1 Use case: copying properties into an object . . . . . . . . . . . 89
9.6.2 Use case for Object.getOwnPropertyDescriptors(): cloning
objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
9.7 Omitting descriptor properties . . . . . . . . . . . . . . . . . . . . . 91
9.7.1 Omitting descriptor properties when creating properties . . . 91
9.7.2 Omitting descriptor properties when changing properties . . . 92
9.8 What property attributes do built-in constructs use? . . . . . . . . . 92
9.8.1 Own properties created via assignment . . . . . . . . . . . . . 92
9.8.2 Own properties created via object literals . . . . . . . . . . . . 93
9.8.3 The own property .length of Arrays . . . . . . . . . . . . . . 93
81
82 9 Property attributes: an introduction
In this chapter, we take a closer look at how the ECMAScript specification sees JavaScript
objects. In particular, properties are not atomic in the spec, but composed of multiple
attributes (think fields in a record). Even the value of a data property is stored in an
attribute!
• Internal slots, which are storage locations that are not accessible from JavaScript,
only from operations in the specification.
• A collection of properties. Each property associates a key with attributes (think fields
in a record).
• Internal slots correspond to internal state that is associated with objects and used
by various ECMAScript specification algorithms.
• Internal slots are not object properties and they are not inherited.
• Depending upon the specific internal slot specification, such state may consist of:
– values of any ECMAScript language type or
– of specific ECMAScript specification type values.
• Unless explicitly specified otherwise, internal slots are allocated as part of the pro-
cess of creating an object and may not be dynamically added to an object.
• Unless specified otherwise, the initial value of an internal slot is the value unde-
fined.
• Various algorithms within this specification create objects that have internal slots.
However, the ECMAScript language provides no direct way to associate internal
slots with an object.
• Internal methods and internal slots are identified within this specification using
names enclosed in double square brackets [[ ]].
• Method slots for manipulating objects (getting properties, setting properties, etc.)
• Data slots that store values.
We have already encountered the attributes value, get, and set. The other attributes
work as follows:
then those operations ignore the property. Most properties are enumerable
(e.g. those created via assignment or object literals), which is why you’ll rarely
notice this attribute in practice. If you are still interested in how it works, see §12
“Enumerability of properties”.
9.1.3.1 Pitfall: Inherited non-writable properties prevent creating own properties via
assignment
const proto = {
prop: 1,
};
// Make proto.prop non-writable:
Object.defineProperty(
proto, 'prop', {writable: false});
assert.throws(
() => obj.prop = 2,
/^TypeError: Cannot assign to read only property 'prop'/);
For more information, see §11.3.4 “Inherited read-only properties prevent creating own
properties via assignment”.
interface DataPropertyDescriptor {
value?: any;
writable?: boolean;
configurable?: boolean;
enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
get?: (this: any) => any;
set?: (this: any, v: any) => void;
configurable?: boolean;
enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;
The question marks indicate that all properties are optional. §9.7 “Omitting descriptor
properties” describes what happens if they are omitted.
9.3 Retrieving descriptors for properties 85
const legoBrick = {
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
};
assert.deepEqual(
Object.getOwnPropertyDescriptor(legoBrick, 'color'),
{
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
});
This is what the descriptor for the accessor property .description looks like:
Using the utility function desc() in line A ensures that .deepEqual() works.
Using the helper function desc() in line A ensures that .deepEqual() works.
• If there is no property with key k, a new own property is created that has the
attributes specified by propDesc.
• If there is a property with key k, defining changes the property’s attributes so that
they match propDesc.
Object.defineProperty(car, 'color', {
value: 'blue',
writable: true,
enumerable: true,
configurable: true,
});
9.4 Defining properties via descriptors 87
assert.deepEqual(
car,
{
color: 'blue',
});
Next, we change the kind of a property via a descriptor; we turn a data property into a
getter:
const car = {
color: 'blue',
};
let readCount = 0;
Object.defineProperty(car, 'color', {
get() {
readCount++;
return 'red';
},
});
assert.equal(car.color, 'red');
assert.equal(readCount, 1);
const car = {
color: 'blue',
};
assert.deepEqual(
car,
{
color: 'green',
});
assert.deepEqual(
legoBrick1,
{
kind: 'Plate 1x3',
color: 'yellow',
get description() {
return `${this.kind} (${this.color})`;
},
});
kind: {
value: 'Plate 1x3',
writable: true,
enumerable: true,
configurable: true,
},
color: {
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
},
description: {
get: function () {
return `${this.kind} (${this.color})`;
},
enumerable: true,
configurable: true,
},
});
target[key] = source[key];
• Its attribute writable is true and its attribute enumerable is true (because that’s
how assignment creates properties).
• It is a data property.
The following example illustrates this limitation. Object source has a setter whose key
is data.
const source = {
set data(value) {
this._data = value;
}
90 9 Property attributes: an introduction
};
If we use Object.assign() to copy property data, then the accessor property data is
converted to a data property:
assert.deepEqual(
Object.getOwnPropertyDescriptor(target1, 'data'),
{
value: undefined,
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(target2, 'data'),
{
get: undefined,
set: desc(source, 'data').set,
enumerable: true,
configurable: true,
});
9.7 Omitting descriptor properties 91
A method that uses super is firmly connected with its home object (the object it is stored
in). There is currently no way to copy or move such a method to a different object.
assert.deepEqual(original, clone);
For more information on this topic, see §6 “Copying objects and Arrays”.
const car = {
color: 'yellow',
};
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'yellow',
writable: true,
enumerable: true,
configurable: true,
});
Object.defineProperty(
car, 'color', {
value: 'pink',
});
assert.deepEqual(
Object.getOwnPropertyDescriptor(car, 'color'),
{
value: 'pink',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
9.8 What property attributes do built-in constructs use? 93
value: 3,
writable: true,
enumerable: true,
configurable: true,
}
});
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 'yes',
writable: true,
enumerable: true,
configurable: true
}
});
.length is a special data property, in that it is influenced by (and influences) other own
properties (specifically, index properties).
Note that all own properties of instances of DataContainer are writable, enumerable,
and configurable:
});
Creates or changes a property on obj whose key is key and whose attributes are
specified via propDesc. Returns the modified object.
First, creates an object whose prototype is proto. Then, if the optional parameter
properties has been provided, adds properties to it – in the same manner as Ob-
ject.defineProperties(). Finally, returns the result. For example, the following
code snippet produces the same result as the previous snippet:
});
assert.deepEqual(address1, address2);
Returns the descriptor of the own (non-inherited) property of obj whose key is
key. If there is no such property, undefined is returned.
assert.deepEqual(
Object.getOwnPropertyDescriptor(Object.prototype, 'toString'),
{
value: {}.toString,
writable: true,
enumerable: false,
configurable: true,
});
assert.equal(
Object.getOwnPropertyDescriptor({}, 'toString'),
undefined);
Returns an object where each property key 'k' of obj is mapped to the
property descriptor for obj.k. The result can be used as input for Ob-
ject.defineProperties() and Object.create().
Contents
10.1 Levels of protection: preventing extensions, sealing, freezing . . . . 99
10.2 Preventing extensions of objects . . . . . . . . . . . . . . . . . . . . 100
10.2.1 Checking whether an object is extensible . . . . . . . . . . . . 100
10.3 Sealing objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
10.3.1 Checking whether an object is sealed . . . . . . . . . . . . . . 102
10.4 Freezing objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
10.4.1 Checking whether an object is frozen . . . . . . . . . . . . . . 103
10.4.2 Freezing is shallow . . . . . . . . . . . . . . . . . . . . . . . . 103
10.4.3 Implementing deep freezing . . . . . . . . . . . . . . . . . . . 103
10.5 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
In this chapter, we’ll look at how objects can be protected from being changed. Examples
include: Preventing properties being added and preventing properties being changed.
99
100 10 Protecting objects from being changed
– Method: Object.preventExtensions(obj)
• Sealing prevents extensions and makes all properties unconfigurable (roughly: we
can’t change how a property works anymore).
– Method: Object.seal(obj)
• Freezing seals an object after making all of its properties non-writable. That is, the
object is not extensible, all properties are read-only and there is no way to change
that.
– Method: Object.freeze(obj)
assert.deepEquals(
Object.keys(obj), ['first']);
delete obj.first;
assert.deepEquals(
Object.keys(obj), []);
The following example demonstrates that sealing makes the object non-extensible and
its properties unconfigurable.
const obj = {
first: 'Jane',
last: 'Doe',
};
// Before sealing
assert.equal(Object.isExtensible(obj), true);
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
first: {
value: 'Jane',
writable: true,
enumerable: true,
configurable: true
},
last: {
value: 'Doe',
writable: true,
enumerable: true,
configurable: true
}
});
Object.seal(obj);
// After sealing
assert.equal(Object.isExtensible(obj), false);
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
first: {
value: 'Jane',
writable: true,
enumerable: true,
102 10 Protecting objects from being changed
configurable: false
},
last: {
value: 'Doe',
writable: true,
enumerable: true,
configurable: false
}
});
obj.first = 'John';
assert.deepEqual(
obj, {first: 'John', last: 'Doe'});
assert.throws(
() => Object.defineProperty(obj, 'first', { enumerable: false }),
/^TypeError: Cannot redefine property: first$/);
assert.throws(
() => point.x = 2,
/^TypeError: Cannot assign to read only property 'x'/);
10.4 Freezing objects 103
assert.throws(
() => Object.defineProperty(point, 'x', {enumerable: false}),
/^TypeError: Cannot redefine property: x$/);
assert.throws(
() => point.z = 4,
/^TypeError: Cannot add property z, object is not extensible$/);
function deepFreeze(value) {
if (Array.isArray(value)) {
for (const element of value) {
deepFreeze(element);
}
Object.freeze(value);
} else if (typeof value === 'object' && value !== null) {
for (const v of Object.values(value)) {
deepFreeze(v);
}
Object.freeze(value);
} else {
// Nothing to do: primitive values are already immutable
}
return value;
}
Revisiting the example from the previous section, we can check if deepFreeze() really
freezes deeply:
const teacher = {
name: 'Edna Krabappel',
students: ['Bart'],
};
deepFreeze(teacher);
assert.throws(
() => teacher.students.push('Lisa'),
/^TypeError: Cannot add property 1, object is not extensible$/);
Properties: assignment
vs. definition
Contents
11.1 Assignment vs. definition . . . . . . . . . . . . . . . . . . . . . . . . 106
11.1.1 Assignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
11.1.2 Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
11.2 Assignment and definition in theory (optional) . . . . . . . . . . . . 106
11.2.1 Assigning to a property . . . . . . . . . . . . . . . . . . . . . 107
11.2.2 Defining a property . . . . . . . . . . . . . . . . . . . . . . . . 109
11.3 Definition and assignment in practice . . . . . . . . . . . . . . . . . 110
11.3.1 Only definition allows us to create a property with arbitrary
attributes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
11.3.2 The assignment operator does not change properties in proto-
types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
11.3.3 Assignment calls setters, definition doesn’t . . . . . . . . . . . 111
11.3.4 Inherited read-only properties prevent creating own properties
via assignment . . . . . . . . . . . . . . . . . . . . . . . . . . 113
11.4 Which language constructs use definition, which assignment? . . . 114
11.4.1 The properties of an object literal are added via definition . . . 114
11.4.2 The assignment operator = always uses assignment . . . . . . 114
11.4.3 Public class fields are added via definition . . . . . . . . . . . 115
11.5 Further reading and sources of this chapter . . . . . . . . . . . . . . 115
There are two ways of creating or changing a property prop of an object obj:
• Assigning: obj.prop = true
• Defining: Object.defineProperty(obj, '', {value: true})
This chapter explains how they work.
105
106 11 Properties: assignment vs. definition
obj.prop = value
11.1.2 Definition
To define a property with the key propKey of an object obj, we use an operation such as
the following method:
Object.defineProperty(obj, propKey, propDesc)
This method works differently depending on what the property looks like:
• Changing properties: If an own property with key propKey exists, defining
changes its property attributes as specified by the property descriptor propDesc
(if possible).
• Creating properties: Otherwise, defining creates an own property with the at-
tributes specified by propDesc (if possible).
That is, the main purpose of definition is to create an own property (even if there is an
inherited setter, which it ignores) and to change property attributes.
The return value is a boolean that indicates whether or not the operation succeeded. As
explained later in this chapter, strict-mode assignment throws a TypeError if Ordinary-
SetWithOwnDescriptor() fails.
have:
* The current object O and the current property descriptor ownDesc on one
hand.
* The original object Receiver and the original property descriptor ex-
istingDescriptor on the other hand.
– If existingDescriptor is not undefined:
* (If we get here, then we are still at the beginning of the prototype chain
– we only recurse if Receiver does not have a property P.)
* The following two if conditions should never be true because ownDesc
and existingDesc should be equal:
· If existingDescriptor specifies an accessor, return false.
· If existingDescriptor.[[Writable]] is false, return false.
* Return Receiver.[[DefineOwnProperty]](P, { [[Value]]: V }). This
internal method performs definition, which we use to change the value
of property Receiver[P]. The definition algorithm is described in the
next subsection.
– Else:
* (If we get here, then Receiver does not have an own property with key
P.)
* Return CreateDataProperty(Receiver, P, V). (This operation creates
an own data property in its first argument.)
• (If we get here, then ownDesc describes an accessor property that is own or inher-
ited.)
• Let setter be ownDesc.[[Set]].
• If setter is undefined, return false.
• Perform Call(setter, Receiver, «V»). Call() invokes the function object set-
ter with this set to Receiver and the single parameter V (French quotes «» are
used for lists in the specification).
• Return true.
• In the spec, evaluation starts in the section on the runtime semantics of Assign-
mentExpression. This section handles providing names for anonymous functions,
destructuring, and more.
• If there is no destructuring pattern, then PutValue() is used to make the assign-
ment.
• For property assignments, PutValue() invokes the internal method .[[Set]]().
• For ordinary objects, .[[Set]]() calls OrdinarySet() (which calls Ordinary-
SetWithOwnDescriptor()) and returns the result.
The result of the operation is a boolean that indicates if it succeeded. Failure can
have different consequences. Some callers ignore the result. Others, such as Ob-
ject.defineProperty(), throw an exception if the result is false.
• If current is undefined, then property P does not currently exist and must be cre-
ated.
– If extensible is false, return false indicating that the property could not
be added.
– Otherwise, check Desc and create either a data property or an accessor prop-
erty.
– Return true.
• If Desc doesn’t have any fields, return true indicating that the operation succeeded
(because no changes had to be made).
• If current.[[Configurable]] is false:
• Next, we validate the property descriptor Desc: Can the attributes described by
current be changed to the values specified by Desc? If not, return false. If yes,
go on.
• Set the attributes of the property with key P to the values specified by Desc. Due
to validation, we can be sure that all of the changes are allowed.
• Return true.
obj.prop = 'b';
The rationale for this behavior is as follows: Prototypes can have properties whose values
are shared by all of their descendants. If we want to change such a property in only one
descendant, we must do so non-destructively, via overriding. Then the change does not
affect the other descendants.
example:
assert.equal(obj.prop, 'protoGetter');
// Defining obj.prop:
Object.defineProperty(
obj, 'prop', { value: 'objData' });
assert.equal(setterWasCalled, false);
If, instead, we assign to .prop, then our intention is often to change something that al-
ready exists and that change should be handled by the setter:
assert.equal(obj.prop, 'protoGetter');
// Assigning to obj.prop:
obj.prop = 'objData';
assert.equal(setterWasCalled, true);
In any object that inherits the read-only .prop from proto, we can’t use assignment to
create an own property with the same key – for example:
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 'objValue',
/^TypeError: Cannot assign to read only property 'prop'/);
Why can’t we assign? The rationale is that overriding an inherited property by creat-
ing an own property can be seen as non-destructively changing the inherited property.
Arguably, if a property is non-writable, we shouldn’t be able to do that.
However, defining .prop still works and lets us override:
Object.defineProperty(
obj, 'prop', { value: 'objValue' });
assert.equal(obj.prop, 'objValue');
Accessor properties that don’t have a setter are also considered to be read-only:
const proto = {
get prop() {
return 'protoValue';
}
};
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 'objValue',
/^TypeError: Cannot set property prop of #<Object> which has only a getter$/);
• There was an attempt to change the behavior, but that broke the library Lo-
dash and was abandoned (pull request on GitHub).
• Background knowledge:
– Pull request on GitHub
– Wiki page on ECMAScript.org (archived)
let lastSetterArgument;
const proto = {
set prop(x) {
lastSetterArgument = x;
},
};
const obj = {
__proto__: proto,
prop: 'abc',
};
assert.equal(lastSetterArgument, undefined);
let lastSetterArgument;
const proto = {
set prop(x) {
lastSetterArgument = x;
},
};
const obj = Object.create(proto);
// Normal assignment:
obj.prop = 'abc';
assert.equal(lastSetterArgument, 'abc');
[obj.prop] = ['def'];
assert.equal(lastSetterArgument, 'def');
Enumerability of properties
Contents
12.1 How enumerability affects property-iterating constructs . . . . . . . 117
12.1.1 Operations that only consider enumerable properties . . . . . 118
12.1.2 Operations that consider both enumerable and non-
enumerable properties . . . . . . . . . . . . . . . . . . . . . . 120
12.1.3 Naming rules for introspective operations . . . . . . . . . . . 121
12.2 The enumerability of pre-defined and created properties . . . . . . 122
12.3 Use cases for enumerability . . . . . . . . . . . . . . . . . . . . . . . 122
12.3.1 Use case: Hiding properties from the for-in loop . . . . . . . 123
12.3.2 Use case: Marking properties as not to be copied . . . . . . . . 124
12.3.3 Marking properties as private . . . . . . . . . . . . . . . . . . 127
12.3.4 Hiding own properties from JSON.stringify() . . . . . . . . 127
12.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
117
118 12 Enumerability of properties
The following operations (summarized in tbl. 12.1) only consider enumerable properties:
• Object.keys() [ES5] returns the keys of enumerable own string-keyed properties.
> Object.keys(obj)
[ 'objEnumStringKey' ]
• Spreading into object literals [ES2018] only considers own enumerable properties
(with string keys or symbol keys).
> const copy = {...obj};
> Reflect.ownKeys(copy)
[ 'objEnumStringKey', objEnumSymbolKey ]
• Object.assign() [ES6] only copies enumerable own properties (with either string
keys or symbol keys).
> const copy = Object.assign({}, obj);
> Reflect.ownKeys(copy)
[ 'objEnumStringKey', objEnumSymbolKey ]
[ES5]
• JSON.stringify() only stringifies enumerable own properties with string
keys.
> JSON.stringify(obj)
'{"objEnumStringKey":"objEnumStringKeyValue"}'
• for-in loop [ES1] traverses the keys of own and inherited enumerable string-keyed
properties.
120 12 Enumerability of properties
for-in is the only built-in operation where enumerability matters for inherited proper-
ties. All other operations only work with own properties.
The following operations (summarized in tbl. 12.2) consider both enumerable and non-
enumerable properties:
[ES5]
• Object.getOwnPropertyNames() lists the keys of all own string-keyed prop-
erties.
> Object.getOwnPropertyNames(obj)
[ 'objEnumStringKey', 'objNonEnumStringKey' ]
[ES6]
• Object.getOwnPropertySymbols() lists the keys of all own symbol-keyed
properties.
> Object.getOwnPropertySymbols(obj)
[ objEnumSymbolKey, objNonEnumSymbolKey ]
> Reflect.ownKeys(obj)
[
'objEnumStringKey',
'objNonEnumStringKey',
objEnumSymbolKey,
objNonEnumSymbolKey
]
> Object.getOwnPropertyDescriptors(obj)
{
objEnumStringKey: {
value: 'objEnumStringKeyValue',
writable: false,
enumerable: true,
configurable: false
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
writable: false,
enumerable: false,
configurable: false
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
writable: false,
enumerable: true,
configurable: false
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
writable: false,
enumerable: false,
configurable: false
}
}
In JavaScript, common introspective operations have short names, while rarely used op-
erations have long names. Ignoring non-enumerable properties is the norm, which is
why operations that do that have short names and operations that don’t, long names:
However, Reflect methods (such as Reflect.ownKeys()) deviate from this rule because
Reflect provides operations that are more “meta” and related to Proxies.
Additionally, the following distinction is made (since ES6, which introduced symbols):
{
writable: true,
enumerable: false,
configurable: true,
}
That includes:
• Assignment
• Object literals
• Public class fields
• Object.fromEntries()
• Property .length of strings (note that all properties of primitive values are read-
only):
We’ll look at the use cases for enumerability next, which will tell us why some properties
are enumerable and others aren’t.
In general, it is best to avoid for-in. The next two subsections explain why. The follow-
ing function will help us demonstrate how for-in works.
function listPropertiesViaForIn(obj) {
const result = [];
for (const key in obj) {
result.push(key);
}
return result;
}
With normal plain objects, for-in doesn’t see inherited methods such as Ob-
ject.prototype.toString() because they are all non-enumerable:
In user-defined classes, all inherited properties are also non-enumerable and therefore
ignored:
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
getName() {
return this.first + ' ' + this.last;
}
}
const jane = new Person('Jane', 'Doe');
assert.deepEqual(
124 12 Enumerability of properties
listPropertiesViaForIn(jane),
['first', 'last']);
The own property .length is non-enumerable in Arrays and strings and therefore ig-
nored by for-in:
> listPropertiesViaForIn(['a', 'b'])
[ '0', '1' ]
> listPropertiesViaForIn('ab')
[ '0', '1' ]
However, it is generally not safe to use for-in to iterate over the indices of an Array be-
cause it considers both inherited and own properties that are not indices. The following
example demonstrate what happens if an Array has an own non-index property:
const arr1 = ['a', 'b'];
assert.deepEqual(
listPropertiesViaForIn(arr1),
['0', '1']);
Conclusion: for-in should not be used for iterating over the indices of an Array because
it considers both index properties and non-index properties:
• If you are interested in the keys of an Array, use the Array method .keys():
> [...['a', 'b', 'c'].keys()]
[ 0, 1, 2 ]
• If you want to iterate over the elements of an Array, use a for-of loop, which has
the added benefit of also working with other iterable data structures.
If we use Object.extend() with an object, we can see that it copies inherited proper-
ties into own properties and ignores non-enumerable properties (it also ignores symbol-
keyed properties). All of this is due to how for-in works.
assert.deepEqual(
extend({}, obj),
{enumObjProp: 3, enumProtoProp: 1});
• It copies all enumerable own and inherited properties of source1 into own prop-
erties of target.
• Then it does the same with source2.
• Etc.
126 12 Enumerability of properties
Object.assign would pave the cowpath of all of the extend() APIs already
in circulation. We thought the precedent of not copying enumerable meth-
ods in those cases was enough reason for Object.assign to have this behav-
ior.
In other words: Object.assign() was created with an upgrade path from $.extend()
(and similar) in mind. Its approach is cleaner than $.extend’s because it ignores inherited
properties.
Cases where non-enumerability helps are few. One rare example is a recent issue that
the library fs-extra had:
• The built-in Node.js module fs has a property .promises that contains an ob-
ject with a Promise-based version of the fs API. At the time of the issue, reading
.promise led to the following warning being logged to the console:
class Point {
static fromJSON(json) {
return new Point(json[0], json[1]);
}
constructor(x, y) {
this.x = x;
this.y = y;
}
toJSON() {
return [this.x, this.y];
}
}
assert.equal(
JSON.stringify(new Point(8, -3)),
'[8,-3]'
);
128 12 Enumerability of properties
I find toJSON() cleaner than enumerability. It also gives us more freedom w.r.t. what the
storage format should look like.
12.4 Conclusion
We have seen that almost all applications for non-enumerability are work-arounds that
now have other and better solutions.
For our own code, we can usually pretend that enumerability doesn’t exist:
• Creating properties via object literals and assignment always creates enumerable
properties.
• Prototype properties created via classes are always non-enumerable.
That is, we automatically follow best practices.
Part V
OOP: techniques
129
Chapter 13
Contents
13.1 The problem: initializing a property asynchronously . . . . . . . . 131
13.2 Solution: Promise-based constructor . . . . . . . . . . . . . . . . . . 132
13.2.1 Using an immediately-invoked asynchronous arrow function . 133
13.3 Solution: static factory method . . . . . . . . . . . . . . . . . . . . . 133
13.3.1 Improvement: private constructor via secret token . . . . . . . 134
13.3.2 Improvement: constructor throws, factory method borrows the
class prototype . . . . . . . . . . . . . . . . . . . . . . . . . . 135
13.3.3 Improvement: instances are inactive by default, activated by
factory method . . . . . . . . . . . . . . . . . . . . . . . . . . 135
13.3.4 Variant: separate factory function . . . . . . . . . . . . . . . . 136
13.4 Subclassing a Promise-based constructor (optional) . . . . . . . . . 137
13.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
13.6 Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
In this chapter, we examine several approaches for creating instances of classes: Con-
structors, factory functions, etc. We do so by solving one concrete problem several times.
The focus of this chapter is on classes, which is why alternatives to classes are ignored.
131
132 13 Techniques for instantiating classes
In line A, we declare the private field .#data that we use in line B and line C.
class DataContainer {
#data;
constructor() {
return Promise.resolve('downloaded')
.then(data => {
this.#data = data;
return this; // (A)
});
}
getData() {
return 'DATA: '+this.#data;
}
}
new DataContainer()
.then(dc => assert.equal( // (B)
dc.getData(), 'DATA: downloaded'));
Now we have to wait until we can access our instance (line B). It is passed on to us after
the data is “downloaded” (line A). There are two possible sources of errors in this code:
In either case, the errors become rejections of the Promise that is returned from the con-
structor.
• A benefit of this approach is that we can only access the instance once it is fully
initialized. And there is no other way of creating instances of DataContainer.
• A disadvantage is that it may be surprising to have a constructor return a Promise
instead of an instance.
constructor() {
return (async () => {
this.#data = await Promise.resolve('downloaded');
return this;
})();
}
class DataContainer {
#data;
static async create() {
const data = await Promise.resolve('downloaded');
return new this(data);
}
constructor(data) {
this.#data = data;
}
getData() {
return 'DATA: '+this.#data;
}
}
134 13 Techniques for instantiating classes
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
This time, all asynchronous functionality is contained in .create(), which enables the
rest of the class to be completely synchronous and therefore simpler.
If secretToken and DataContainer reside in the same module and only the latter is ex-
ported, then outside parties don’t have access to secretToken and therefore can’t create
instances of DataContainer.
class DataContainer {
static async create() {
const data = await Promise.resolve('downloaded');
return Object.create(this.prototype)._init(data); // (A)
}
constructor() {
throw new Error('Constructor is private');
}
_init(data) {
this._data = data;
return this;
}
getData() {
return 'DATA: '+this._data;
}
}
DataContainer.create()
.then(dc => {
assert.equal(dc instanceof DataContainer, true); // (B)
assert.equal(
dc.getData(), 'DATA: downloaded');
});
class DataContainer {
#data;
static async create() {
const data = await Promise.resolve('downloaded');
return new this().#init(data);
}
#active = false;
constructor() {
}
#init(data) {
this.#active = true;
this.#data = data;
return this;
}
getData() {
this.#check();
return 'DATA: '+this.#data;
}
#check() {
if (!this.#active) {
throw new Error('Not created by factory');
}
}
}
DataContainer.create()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
The flag .#active is enforced via the private method .#check() which must be invoked
at the beginning of every method.
The major downside of this solution is its verbosity. There is also a risk of forgetting to
invoke .#check() in each method.
createDataContainer()
.then(dc => assert.equal(
dc.getData(), 'DATA: downloaded'));
Stand-alone functions as factories are occasionally useful, but in this case, I prefer a static
method:
Alas, extending the class with the Promise-based constructor leads to severe limitations.
In the following example, we subclass DataContainer. The subclass SubDataContainer
has its own private field .#moreData that it initializes asynchronously by hooking into
the Promise returned by the constructor of its superclass.
class DataContainer {
#data;
constructor() {
return Promise.resolve('downloaded')
.then(data => {
this.#data = data;
return this; // (A)
});
}
getData() {
return 'DATA: '+this.#data;
}
}
.then(_this => {
return Promise.resolve('more')
.then(moreData => {
_this.#moreData = moreData;
return _this;
});
});
}
getData() {
return super.getData() + ', ' + this.#moreData;
}
}
assert.rejects(
() => new SubDataContainer(),
{
name: 'TypeError',
message: 'Cannot write private member #moreData ' +
'to an object whose class did not declare it',
}
);
Why the failure? A constructor always adds its private fields to its this. However, here,
this in the subconstructor is the Promise returned by the superconstructor (and not the
instance of SubDataContainer delivered via the Promise).
However, this approach still works if SubDataContainer does not have any private fields.
13.5 Conclusion
For the scenario examined in this chapter, I prefer either a Promise-based constructor or
a static factory method plus a private constructor via a secret token.
However, the other techniques presented here can still be useful in other scenarios.
– Blog post “ES proposal: private methods and accessors in JavaScript classes”
140 13 Techniques for instantiating classes
Chapter 14
Contents
14.1 .clone() methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
14.2 Static factory methods . . . . . . . . . . . . . . . . . . . . . . . . . . 142
14.3 Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
In this chapter, we look at two techniques for implementing copying for class instances:
• .clone() methods
• So-called copy constructors, constructors that receive another instance of the current
class and use it to initialize the current instance.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
clone() {
return new Point(this.x, this.y);
}
}
class Color {
constructor(name) {
141
142 14 Copying instances of classes: .clone() vs. copy constructors
this.name = name;
}
clone() {
return new Color(this.name);
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
clone() {
return new ColorPoint(
this.x, this.y, this.color.clone()); // (A)
}
}
In JavaScript, we must make that decision at runtime and that leads to inelegant code:
class Point {
constructor(...args) {
if (args[0] instanceof Point) {
// Copy constructor
const [other] = args;
this.x = other.x;
this.y = other.y;
} else {
const [x, y] = args;
this.x = x;
this.y = y;
}
}
}
Static factory methods are an alternative to constructors and work better in this case be-
cause we can directly invoke the desired functionality. (Here, static means that these
factory methods are class methods.)
In the following example, the three classes Point, Color and ColorPoint each have a
static factory method .from():
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static from(other) {
return new Point(other.x, other.y);
}
}
class Color {
constructor(name) {
this.name = name;
}
static from(other) {
return new Color(other.name);
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
static from(other) {
return new ColorPoint(
other.x, other.y, Color.from(other.color)); // (A)
}
}
14.3 Acknowledgements
• Ron Korvig reminded me to use static factory methods and not overloaded con-
structors for deep-copying in JavaScript.
144 14 Copying instances of classes: .clone() vs. copy constructors
Chapter 15
Contents
15.1 Wrapping objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
15.1.1 Making collections immutable via wrapping . . . . . . . . . . 146
15.2 An immutable wrapper for Maps . . . . . . . . . . . . . . . . . . . . 146
15.3 An immutable wrapper for Arrays . . . . . . . . . . . . . . . . . . . 147
• Create a new object that stores the original in a private field. The new object is
said to wrap the original object. The new object is called wrapper, the original object
wrapped object.
• The wrapper only forwards some of the method calls it receives to the wrapped
object.
class Wrapper {
#wrapped;
constructor(wrapped) {
this.#wrapped = wrapped;
}
allowedMethod1(...args) {
return this.#wrapped.allowedMethod1(...args);
145
146 15 Immutable wrappers for collections
}
allowedMethod2(...args) {
return this.#wrapped.allowedMethod2(...args);
}
}
One important use case for this technique is an object that has an internal mutable data
structure that it wants to export safely without copying it. The export being “live” may
also be a goal. The object can achieve its goals by wrapping the internal data structure
and making it immutable.
The next two sections showcase immutable wrappers for Maps and Arrays. They both
have the following limitations:
• They are sketches. More work is needed to make them suitable for practical use:
Better checks, support for more methods, etc.
• They work shallowly: Each one makes the wrapped object immutable, but not
the data it returns. This could be fixed by wrapping some of the results that are
returned by methods.
class ImmutableMapWrapper {
static _setUpPrototype() {
// Only forward non-destructive methods to the wrapped Map:
for (const methodName of ['get', 'has', 'keys', 'size']) {
ImmutableMapWrapper.prototype[methodName] = function (...args) {
return this.#wrappedMap[methodName](...args);
}
}
}
#wrappedMap;
constructor(wrappedMap) {
this.#wrappedMap = wrappedMap;
}
15.3 An immutable wrapper for Arrays 147
}
ImmutableMapWrapper._setUpPrototype();
The setup of the prototype has to be performed by a static method, because we only have
access to the private field .#wrappedMap from inside the class.
function wrapArrayImmutably(arr) {
const handler = {
get(target, propKey, receiver) {
// We assume that propKey is a string (not a symbol)
if (RE_INDEX_PROP_KEY.test(propKey) // simplified check!
|| ALLOWED_PROPERTIES.has(propKey)) {
return Reflect.get(target, propKey, receiver);
}
throw new TypeError(`Property "${propKey}" can’t be accessed`);
},
set(target, propKey, value, receiver) {
throw new TypeError('Setting is not allowed');
148 15 Immutable wrappers for collections
},
deleteProperty(target, propKey) {
throw new TypeError('Deleting is not allowed');
},
};
return new Proxy(arr, handler);
}
Regular expressions
149
Chapter 16
Contents
16.1 Cheat sheet: lookaround assertions . . . . . . . . . . . . . . . . . . . 151
16.2 Warnings for this chapter . . . . . . . . . . . . . . . . . . . . . . . . 152
16.3 Example: Specifying what comes before or after a match (positive
lookaround) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
16.4 Example: Specifying what does not come before or after a match
(negative lookaround) . . . . . . . . . . . . . . . . . . . . . . . . . . 153
16.4.1 There are no simple alternatives to negative lookaround asser-
tions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
16.5 Interlude: pointing lookaround assertions inward . . . . . . . . . . 154
16.6 Example: match strings not starting with 'abc' . . . . . . . . . . . . 154
16.7 Example: match substrings that do not contain '.mjs' . . . . . . . . 155
16.8 Example: skipping lines with comments . . . . . . . . . . . . . . . . 155
16.9 Example: smart quotes . . . . . . . . . . . . . . . . . . . . . . . . . . 156
16.9.1 Supporting escaping via backslashes . . . . . . . . . . . . . . 156
16.10Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
16.11Further reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
151
152 16 Regular expressions: lookaround assertions by example
Pattern Name
(?=«pattern») Positive lookahead ES3
(?!«pattern») Negative lookahead ES3
(?<=«pattern») Positive lookbehind ES2018
(?<!«pattern») Negative lookbehind ES2018
• Lookbehind assertions are a relatively new feature that may not be supported by
all JavaScript engines you are targeting.
Lookaround assertions are especially convenient for .match() in /g mode, which re-
turns whole matches (capture group 0). Whatever the pattern of a lookaround assertion
16.4 Example: Specifying what does not come before or after a match (negative lookaround) 153
matches is not captured. Without lookaround assertions, the quotes show up in the re-
sult:
> 'how "are" "you" doing'.match(/"([a-z]+)"/g)
[ '"are"', '"you"' ]
The problem is that we extract sequences of characters that are not bracketed by quotes.
That means that in the string '"are"', the “r” in the middle is considered unquoted,
because it is preceded by an “a” and followed by an “e”.
We can fix this by stating that prefix and suffix must be neither quote nor letter:
> 'how "are" "you" doing'.match(/(?<!["a-z])[a-z]+(?!["a-z])/g)
[ 'how', 'doing' ]
Another solution is to demand via \b that the sequence of characters [a-z]+ start and
end at word boundaries:
> 'how "are" "you" doing'.match(/(?<!")\b[a-z]+\b(?!")/g)
[ 'how', 'doing' ]
One thing that is nice about negative lookbehind and negative lookahead is that they also
work at the beginning or end, respectively, of a string – as demonstrated in the example.
The regular expressions shown in the remainder of this chapter are different: Their
lookaround assertions point inward and restrict what’s inside the match.
> /^(?!abc)/.test('xyz')
true
> /^(?!abc)/.exec('xyz')
{ 0: '', index: 0, input: 'xyz', groups: undefined }
The problem is that assertions such as lookaround assertions don’t expand the matched
text. That is, they don’t capture input characters, they only make demands about the
current location in the input.
Therefore, the solution is to add a pattern that does capture input characters:
> /^(?!abc).*$/.exec('xyz')
{ 0: 'xyz', index: 0, input: 'xyz', groups: undefined }
As desired, this new regular expression rejects strings that are prefixed with 'abc':
> /^(?!abc).*$/.exec('abc')
null
> /^(?!abc).*$/.exec('abcd')
null
> /^(?!abc).*$/.exec('ab')
{ 0: 'ab', index: 0, input: 'ab', groups: undefined }
16.7 Example: match substrings that do not contain '.mjs' 155
const code = `
import {transform} from './util';
import {Person} from './person.mjs';
import {zip} from 'lodash';
`.trim();
assert.deepEqual(
code.match(/^import .*? from '[^']+(?<!\.mjs)';$/umg),
[
"import {transform} from './util';",
"import {zip} from 'lodash';",
]);
Here, the lookbehind assertion (?<!\.mjs) acts as a guard and prevents that the regular
expression matches strings that contain '.mjs’ at this location.
const lines = [
'indent: 2', // setting
'# Trim trailing whitespace:', // comment
'whitespace: trim', // setting
];
for (const line of lines) {
const match = RE_SETTING.exec(line);
if (match) {
const key = JSON.stringify(match[1]);
const value = JSON.stringify(match[2]);
console.log(`KEY: ${key} VALUE: ${value}`);
}
}
// Output:
// 'KEY: "indent" VALUE: " 2"'
// 'KEY: "whitespace" VALUE: " trim"'
/^([^:]*):(.*)$/
We can fix that by prefixing (?!#) as a guard. Intuitively, it means: ”The current location
in the input string must not be followed by the character #.”
The new regular expression works as desired:
> /^(?!#)([^:]*):(.*)$/.test('# Comment:')
false
Only the first quote and the last quote is curly. The problem here is that the * quantifier
matches greedily (as much as possible).
If we put a question mark after the *, it matches reluctantly:
> `The words "must" and "should".`.replace(/"(.*?)"/g, '“$1”')
'The words “must” and “should”.'
However, this regular expression can fail when there is a backslash-escaped backslash:
> String.raw`Backslash: "\\"`.replace(/(?<!\\)"(.*?)(?<!\\)"/g, '“$1”')
'Backslash: "\\\\"'
One issue remains. This guard prevents the first quote from being matched if it appears
at the beginning of a string:
> const regExp = /(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> `"abc"`.replace(regExp, '“$1”')
'"abc"'
16.10 Acknowledgements
• The first regular expression that handles escaped backslashes in front of quotes
was proposed by @jonasraoni on Twitter.
Contents
17.1 The basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
17.2 A tour of the features . . . . . . . . . . . . . . . . . . . . . . . . . . 160
17.2.1 Main features . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
17.2.2 Details and advanced features . . . . . . . . . . . . . . . . . . 160
17.3 Why is this useful? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
17.4 re and named capture groups . . . . . . . . . . . . . . . . . . . . . . 160
My small library re-template-tag provides a template tag function for composing regu-
lar expressions. This chapter explains how it works. The intent is to introduce the ideas
behind the library (more than the library itself).
Installation:
Importing:
Use: The following two expressions produce the same regular expression.
assert.deepEqual(
re`/abc/gu`,
/abc/gu);
159
160 17 Composing regular expressions via re-template-tag (bonus)
Flag-less mode:
const regexp = re`abc`;
assert.ok(regexp instanceof RegExp);
assert.equal(regexp.source, 'abc');
assert.equal(regexp.flags, '');
Regular expression flags (such as /u in the previous example) can also be computed:
const regexp = re`/abc/${'g'+'u'}`;
assert.ok(regexp instanceof RegExp);
assert.equal(regexp.source, 'abc');
assert.equal(regexp.flags, 'gu');
Miscellaneous topics
163
Chapter 18
Exploring Promises by
implementing them
Contents
18.1 Refresher: states of Promises . . . . . . . . . . . . . . . . . . . . . . 166
18.2 Version 1: Stand-alone Promise . . . . . . . . . . . . . . . . . . . . . 166
18.2.1 Method .then() . . . . . . . . . . . . . . . . . . . . . . . . . 167
18.2.2 Method .resolve() . . . . . . . . . . . . . . . . . . . . . . . 168
18.3 Version 2: Chaining .then() calls . . . . . . . . . . . . . . . . . . . 169
18.4 Convenience method .catch() . . . . . . . . . . . . . . . . . . . . . 170
18.5 Omitting reactions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
18.6 The implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
18.7 Version 3: Flattening Promises returned from .then() callbacks . . 172
18.7.1 Returning Promises from a callback of .then() . . . . . . . . . 172
18.7.2 Flattening makes Promise states more complicated . . . . . . . 173
18.7.3 Implementing Promise-flattening . . . . . . . . . . . . . . . . 173
18.8 Version 4: Exceptions thrown in reaction callbacks . . . . . . . . . . 175
18.9 Version 5: Revealing constructor pattern . . . . . . . . . . . . . . . . 177
In this chapter, we will approach Promises from a different angle: Instead of using this
API, we will create a simple implementation of it. This different angle once helped me
greatly with making sense of Promises.
165
166 18 Exploring Promises by implementing them
Figure 18.1: The states of a Promise (simplified version): A Promise is initially pending.
If we resolve it, it becomes fulfilled. If we reject it, it becomes rejected.
We start with a simplified version of how Promise states work (fig. 18.1):
• ToyPromise1.prototype.resolve(value)
• ToyPromise1.prototype.reject(reason)
• ToyPromise1.prototype.then(onFulfilled, onRejected)
That is, resolve and reject are methods (and not functions handed to a callback param-
eter of the constructor).
Promise
then
resolve(x1)
onFulfilled(x1)
reject(e1)
onRejected(e1)
then(onFulfilled, onRejected) {
const fulfillmentTask = () => {
if (typeof onFulfilled === 'function') {
onFulfilled(this._promiseResult);
168 18 Exploring Promises by implementing them
}
};
const rejectionTask = () => {
if (typeof onRejected === 'function') {
onRejected(this._promiseResult);
}
};
switch (this._promiseState) {
case 'pending':
this._fulfillmentTasks.push(fulfillmentTask);
this._rejectionTasks.push(rejectionTask);
break;
case 'fulfilled':
addToTaskQueue(fulfillmentTask);
break;
case 'rejected':
addToTaskQueue(rejectionTask);
break;
default:
throw new Error();
}
}
Promises must always settle asynchronously. That’s why we don’t directly execute tasks,
we add them to the task queue of the event loop (of browsers, Node.js, etc.). Note that the
real Promise API doesn’t use normal tasks (like setTimeout()), it uses microtasks, which
are tightly coupled with the current normal task and always execute directly after it.
_clearAndEnqueueTasks(tasks) {
this._fulfillmentTasks = undefined;
18.3 Version 2: Chaining .then() calls 169
this._rejectionTasks = undefined;
tasks.map(addToTaskQueue);
}
returns
return x2
Promise Promise
then return x2
resolve(x1) resolve(x2)
onFulfilled(x1)
reject(e1) reject(e2)
onRejected(e1)
Figure 18.3: ToyPromise2 chains .then() calls: .then() now returns a Promise that is re-
solved by whatever value is returned by the fulfillment reaction or the rejection reaction.
The next feature we implement is chaining (fig. 18.3): A value that we return from a
fulfillment reaction or a rejection reaction can be handled by a fulfilment reaction in a
following .then() call. (In the next version, chaining will become much more useful,
due to special support for returning Promises.)
new ToyPromise2()
.resolve('result1')
.then(x => {
assert.equal(x, 'result1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});
new ToyPromise2()
.reject('error1')
.then(null,
170 18 Exploring Promises by implementing them
x => {
assert.equal(x, 'error1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});
new ToyPromise2()
.reject('error1')
.catch(x => { // (A)
assert.equal(x, 'error1');
return 'result2';
})
.then(x => {
assert.equal(x, 'result2');
});
.catch(rejectionReaction)
.then(null, rejectionReaction)
catch(onRejected) { // [new]
return this.then(null, onRejected);
}
someAsyncFunction()
.then(fulfillmentReaction1)
.then(fulfillmentReaction2)
.catch(rejectionReaction);
someAsyncFunction()
.catch(rejectionReaction)
.then(fulfillmentReaction);
• .then() returns a Promise that is resolved with what either onFulfilled or onRe-
jected return.
• If onFulfilled or onRejected are missing, whatever they would have received is
passed on to the Promise returned by .then().
then(onFulfilled, onRejected) {
const resultPromise = new ToyPromise2(); // [new]
···
172 18 Exploring Promises by implementing them
.then() creates and returns a new Promise (first line and last line of the method). Addi-
tionally:
This approach becomes inconvenient if we return a Promise from a .then() callback. For
example, the result of a Promise-based function (line A):
asyncFunc1()
.then((result1) => {
assert.equal(result1, 'Result of asyncFunc1()');
return asyncFunc2(); // (A)
})
.then((result2Promise) => {
result2Promise
.then((result2) => { // (B)
assert.equal(
result2, 'Result of asyncFunc2()');
});
});
This time, putting the value returned in line A into the Promise returned by .then()
forces us to unwrap that Promise in line B. It would be nice if instead, the Promise re-
turned in line A replaced the Promise returned by .then(). How exactly that could be
done is not immediately clear, but if it worked, it would let us write our code like this:
asyncFunc1()
.then((result1) => {
18.7 Version 3: Flattening Promises returned from .then() callbacks 173
The Promise API has one additional feature: Q doesn’t have to be a Promise, only a
so-called thenable. A thenable is an object with a method .then(). The reason for this
added flexibility is to enable different Promise implementations to work together (which
mattered when Promises were first added to the language).
Note that the concept of resolving has also become more complicated. Resolving a
Promise now only means that it can’t be settled directly, anymore:
• Resolving may reject a Promise: We can resolve a Promise with a rejected Promise.
• Resolving may not even settle a Promise: We can resolve a Promise with another
one that is always pending.
The ECMAScript specification puts it this way: “An unresolved Promise is always in the
pending state. A resolved Promise may be pending, fulfilled, or rejected.”
Settled
resolve with
Pending non-thenable Fulfilled
rejec
t
resolve with Rejected
thenable Q
Locked-in on Q
Pending Fulfilled
Rejected
Figure 18.4: All states of a Promise: Promise-flattening introduces the invisible pseudo-
state “locked-in”. That state is reached if a Promise P is resolved with a thenable Q.
Afterwards, state and settlement value of P is always the same as those of Q.
settlement is x1 : Thenable
forwarded returns
x1 : NonThenable
return x2
Promise Promise
then return x2
resolve(x1) resolve(x2)
onFulfilled(x1)
reject(e1) reject(e2)
onRejected(e1)
Figure 18.5: ToyPromise3 flattens resolved Promises: If the first Promise is resolved with
a thenable x1, it locks in on x1 and is settled with the settlement value of x1. If the first
Promise is resolved with a non-thenable value, everything works as it did before.
18.8 Version 4: Exceptions thrown in reaction callbacks 175
resolve(value) { // [new]
if (this._alreadyResolved) return this;
this._alreadyResolved = true;
if (isThenable(value)) {
// Forward fulfillments and rejections from `value` to `this`.
// The callbacks are always executed asynchronously
value.then(
(result) => this._doFulfill(result),
(error) => this._doReject(error));
} else {
this._doFulfill(value);
}
• If value is fulfilled with a result, the current Promise is also fulfilled with that
result.
• If value is rejected with an error, the current Promise is also rejected with that error.
The settling is performed via the private methods ._doFulfill() and ._doReject(), to
get around the protection via ._alreadyResolved.
_doFulfill(value) { // [new]
assert.ok(!isThenable(value));
this._promiseState = 'fulfilled';
this._promiseResult = value;
this._clearAndEnqueueTasks(this._fulfillmentTasks);
}
.reject() is not shown here. Its only new functionality is that it now also obeys ._-
alreadyResolved.
settlement is x1 : Thenable
forwarded returns
x1 : NonThenable
return x2
Promise Promise
then return x2
resolve(x1) resolve(x2)
onFulfilled(x1) throw e2
reject(e1) reject(e2)
onRejected(e1)
throw e2
new ToyPromise4()
.resolve('a')
.then((value) => {
assert.equal(value, 'a');
throw 'b'; // triggers a rejection
})
.catch((error) => {
assert.equal(error, 'b');
})
.then() now runs the Promise reactions onFulfilled and onRejected safely, via the
helper method ._runReactionSafely() – for example:
Contents
19.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
19.2 Programming versus metaprogramming . . . . . . . . . . . . . . . . 181
19.2.1 Kinds of metaprogramming . . . . . . . . . . . . . . . . . . . 181
19.3 Proxies explained . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
19.3.1 An example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
19.3.2 Function-specific traps . . . . . . . . . . . . . . . . . . . . . . 183
19.3.3 Intercepting method calls . . . . . . . . . . . . . . . . . . . . . 184
19.3.4 Revocable Proxies . . . . . . . . . . . . . . . . . . . . . . . . . 185
19.3.5 Proxies as prototypes . . . . . . . . . . . . . . . . . . . . . . . 185
19.3.6 Forwarding intercepted operations . . . . . . . . . . . . . . . 186
19.3.7 Pitfall: not all objects can be wrapped transparently by Proxies 187
19.4 Use cases for Proxies . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
19.4.1 Tracing property accesses (get, set) . . . . . . . . . . . . . . . 191
19.4.2 Warning about unknown properties (get, set) . . . . . . . . . 193
19.4.3 Negative Array indices (get) . . . . . . . . . . . . . . . . . . . 194
19.4.4 Data binding (set) . . . . . . . . . . . . . . . . . . . . . . . . 195
19.4.5 Accessing a restful web service (method calls) . . . . . . . . . 196
19.4.6 Revocable references . . . . . . . . . . . . . . . . . . . . . . . 197
19.4.7 Implementing the DOM in JavaScript . . . . . . . . . . . . . . 199
19.4.8 More use cases . . . . . . . . . . . . . . . . . . . . . . . . . . 199
19.4.9 Libraries that are using Proxies . . . . . . . . . . . . . . . . . 200
19.5 The design of the Proxy API . . . . . . . . . . . . . . . . . . . . . . . 200
19.5.1 Stratification: keeping base level and meta level separate . . . 200
19.5.2 Virtual objects versus wrappers . . . . . . . . . . . . . . . . . 202
19.5.3 Transparent virtualization and handler encapsulation . . . . . 202
19.5.4 The meta object protocol and Proxy traps . . . . . . . . . . . . 203
179
180 19 Metaprogramming with Proxies (early access)
19.1 Overview
Proxies enable us to intercept and customize operations performed on objects (such as
getting properties). They are a metaprogramming feature.
When we get the property proxy.size, the handler intercepts that operation:
assert.equal(
proxy.size, 123);
assert.deepEqual(
logged, ['GET size']);
See the reference for the complete API for a list of operations that can be intercepted.
19.2 Programming versus metaprogramming 181
• At the base level (also called: application level), code processes user input.
• At the meta level, code processes base level code.
Base and meta level can be different languages. In the following meta program, the
metaprogramming language is JavaScript and the base programming language is Java.
Metaprogramming can take different forms. In the previous example, we have printed
Java code to the console. Let’s use JavaScript as both metaprogramming language and
base programming language. The classic example for this is the eval() function, which
lets us evaluate/compile JavaScript code on the fly. In the interaction below, we use it to
evaluate the expression 5 + 2.
Other JavaScript operations may not look like metaprogramming, but actually are, if we
look closer:
// Base level
const obj = {
hello() {
console.log('Hello!');
},
};
// Meta level
for (const key of Object.keys(obj)) {
console.log(key);
}
The program is examining its own structure while running. This doesn’t look like
metaprogramming, because the separation between programming constructs and
data structures is fuzzy in JavaScript. All of the Object.* methods can be considered
metaprogramming functionality.
assert.deepEqual(
obj1, {});
assert.deepEqual(
obj2, { color: 'blue' });
ECMAScript 5 doesn’t support intercession; Proxies were created to fill that gap.
Proxies are special objects that allow us to customize some of these operations. A Proxy
is created with two parameters:
19.3.1 An example
In the following example, the handler intercepts the operations get and has.
If we get a property (line A) or use the in operator (line B), the handler intercepts those
operations:
assert.deepEqual(
logged, [
'GET age',
'HAS hello',
]);
The handler doesn’t implement the trap set (setting properties). Therefore, setting
proxy.age is forwarded to target and leads to target.age being set:
proxy.age = 99;
assert.equal(target.age, 99);
The reason for only enabling these traps for function targets is simple: Otherwise, we
wouldn’t be able to forward the operations apply and construct.
function traceMethodCalls(obj) {
const handler = {
get(target, propKey, receiver) {
const origMethod = target[propKey];
return function (...args) { // implicit parameter `this`!
const result = origMethod.apply(this, args);
traced.push(propKey + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
}
};
return new Proxy(obj, handler);
}
We are not using a Proxy for the second interception; we are simply wrapping the original
method in a function.
const obj = {
multiply(x, y) {
return x * y;
},
squared(x) {
return this.multiply(x, x);
},
};
assert.deepEqual(
traced, [
'multiply[9,9] -> 81',
'squared[9] -> 81',
]);
Even the call this.multiply() inside obj.squared() is traced! That’s because this
keeps referring to the Proxy.
This is not the most efficient solution. One could, for example, cache methods. Further-
more, Proxies themselves have an impact on performance.
After we call the function revoke for the first time, any operation we apply to proxy
causes a TypeError. Subsequent calls of revoke have no further effect.
revoke();
assert.throws(
() => proxy.prop,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked$/
);
// Output:
// 'GET weight'
The property weight can’t be found in obj, which is why the search continues in proto
and the trap get is triggered there. There are more operations that affect prototypes; they
are listed at the end of this chapter.
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return delete target[propKey];
},
has(target, propKey) {
console.log('HAS ' + propKey);
return propKey in target;
},
// Other traps: similar
}
For each trap, we first log the name of the operation and then forward it by performing
it manually. JavaScript has the module-like object Reflect that helps with forwarding.
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return Reflect.deleteProperty(target, propKey);
},
has(target, propKey) {
console.log('HAS ' + propKey);
return Reflect.has(target, propKey);
},
19.3 Proxies explained 187
Now what each of the traps does is so similar that we can implement the handler via a
Proxy:
For each trap, the Proxy asks for a handler method via the get operation and we give it
one. That is, all of the handler methods can be implemented via the single meta-method
get. It was one of the goals for the Proxy API to make this kind of virtualization simple.
// Output:
// 'SET distance'
// 'GETOWNPROPERTYDESCRIPTOR distance'
// 'DEFINEPROPERTY distance'
// 'GET distance'
Therefore, if the handler is the empty object, the Proxy should transparently wrap the
target. Alas, that doesn’t always work.
188 19 Metaprogramming with Proxies (early access)
Before we dig deeper, let’s quickly review how wrapping a target affects this:
const target = {
myMethod() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
};
}
};
const handler = {};
const proxy = new Proxy(target, handler);
assert.deepEqual(
target.myMethod(), {
thisIsTarget: true,
thisIsProxy: false,
});
assert.deepEqual(
proxy.myMethod(), {
thisIsTarget: false,
thisIsProxy: true,
});
That is, if the Proxy forwards a method call to the target, this is not changed. As a
consequence, the Proxy continues to be in the loop if the target uses this, e.g., to make
a method call.
Normally, Proxies with empty handlers wrap targets transparently: we don’t notice that
they are there and they don’t change the behavior of the targets.
If, however, a target associates information with this via a mechanism that is not con-
trolled by Proxies, we have a problem: things fail, because different information is asso-
ciated depending on whether the target is wrapped or not.
For example, the following class Person stores private information in the WeakMap _-
name (more information on this technique is given in JavaScript for impatient programmers):
return _name.get(this);
}
}
class Person2 {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
Instances of most built-in constructors also use a mechanism that is not intercepted by
Proxies. They therefore can’t be wrapped transparently, either. We can see that if we use
an instance of Date:
assert.throws(
() => proxy.getFullYear(),
/^TypeError: this is not a Date object\.$/
);
The mechanism that is unaffected by Proxies is called internal slots. These slots are
property-like storage associated with instances. The specification handles these slots
as if they were properties with names in square brackets. For example, the following
method is internal and can be invoked on all objects O:
O.[[GetPrototypeOf]]()
190 19 Metaprogramming with Proxies (early access)
In contrast to properties, accessing internal slots is not done via normal “get” and “set”
operations. If .getFullYear() is invoked via a Proxy, it can’t find the internal slot it
needs on this and complains via a TypeError.
Unless explicitly defined otherwise, the methods of the Date prototype ob-
ject defined below are not generic and the this value passed to them must
be an object that has a [[DateValue]] internal slot that has been initialized
to a time value.
19.3.7.4 A work-around
As a work-around, we can change how the handler forwards method calls and selectively
set this to the target and not the Proxy:
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getFullYear') {
return target.getFullYear.bind(target);
}
return Reflect.get(target, propKey, receiver);
},
};
const proxy = new Proxy(new Date('2030-12-24'), handler);
assert.equal(proxy.getFullYear(), 2030);
The drawback of this approach is that none of the operations that the method performs
on this go through the Proxy.
p.push('a');
assert.equal(p.length, 1);
p.length = 0;
assert.equal(p.length, 0);
The reason for Arrays being wrappable is that, even though property access is cus-
tomized to make .length work, Array methods don’t rely on internal slots – they are
generic.
Getting and setting properties of the traced object p has the following effects:
assert.equal(tracedPoint.x, 5);
tracedPoint.x = 21;
// Output:
// 'GET x'
// 'SET x=21'
Intriguingly, tracing also works whenever Point accesses the properties because this
now refers to the traced object, not to an instance of Point:
assert.equal(
tracedPoint.toString(),
'Point(21, 7)');
// Output:
// 'GET x'
// 'GET y'
propKeys.forEach(function (propKey) {
propData[propKey] = obj[propKey];
Object.defineProperty(obj, propKey, {
get: function () {
log('GET '+propKey);
return propData[propKey];
},
set: function (value) {
log('SET '+propKey+'='+value);
propData[propKey] = value;
},
});
});
return obj;
}
obj.a = 1;
assert.equal(obj.a, 1);
obj.c = 3;
assert.equal(obj.c, 3);
assert.deepEqual(
logged, [
'SET a=1',
'GET a',
]);
Proxies give us a simpler solution. We intercept property getting and setting and don’t
have to change the implementation.
log('SET '+propKey+'='+value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
We can use Proxies to get an exception in such a case. This works as follows. We make
the Proxy a prototype of an object. If a property isn’t found in the object, the get trap of
the Proxy is triggered:
• If the property doesn’t even exist in the prototype chain after the Proxy, it really is
missing and we throw an exception.
• Otherwise, we return the value of the inherited property. We do so by forwarding
the get operation to the target (the Proxy gets its prototype from the target).
const propertyCheckerHandler = {
get(target, propKey, receiver) {
// Only check string property keys
if (typeof propKey === 'string' && !(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
};
const PropertyChecker = new Proxy({}, propertyCheckerHandler);
const jane = {
__proto__: PropertyChecker,
name: 'Jane',
};
// Own property:
assert.equal(
jane.name,
'Jane');
// Typo:
assert.throws(
() => jane.nmae,
/^ReferenceError: Unknown property: nmae$/);
194 19 Metaprogramming with Proxies (early access)
// Inherited property:
assert.equal(
jane.toString(),
'[object Object]');
If we turn PropertyChecker into a constructor, we can use it for classes via extends:
const p = Object.getPrototypeOf.bind(Object);
assert.equal(p(point), Point.prototype);
assert.equal(p(p(point)), PropertyChecker2.prototype);
assert.equal(p(p(p(point))), Object.prototype);
Alas, that doesn’t work when accessing elements via the bracket operator ([]). We can,
however, use Proxies to add that capability. The following function createArray() cre-
ates Arrays that support negative indices. It does so by wrapping Proxies around Array
instances. The Proxies intercept the get operation that is triggered by the bracket opera-
tor.
function createArray(...elements) {
const handler = {
get(target, propKey, receiver) {
if (typeof propKey === 'string') {
const index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
}
return Reflect.get(target, propKey, receiver);
}
};
// Wrap a proxy around the Array
return new Proxy(elements, handler);
}
const arr = createArray('a', 'b', 'c');
assert.equal(
arr[-1], 'c');
assert.equal(
arr[0], 'a');
assert.equal(
arr.length, 3);
observedArray.push('a');
// Output:
// '"0" = "a"'
// '"length" = 1'
Both implementations use the following function to make HTTP GET requests (how it
works is explained in JavaScript for impatient programmers).
function httpGet(url) {
return new Promise(
(resolve, reject) => {
19.4 Use cases for Proxies 197
In the following example, we create a revocable reference for a resource. We then read
one of the resource’s properties via the reference. That works, because the reference
grants us access. Next, we revoke the reference. Now the reference doesn’t let us read
the property, anymore.
// Access granted
assert.equal(reference.x, 11);
revoke();
// Access denied
assert.throws(
() => reference.x,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked/
);
Proxies are ideally suited for implementing revocable references, because they can inter-
cept and forward operations. This is a simple Proxy-based implementation of createRe-
vocableReference:
function createRevocableReference(target) {
198 19 Metaprogramming with Proxies (early access)
The code can be simplified via the Proxy-as-handler technique from the previous section.
This time, the handler basically is the Reflect object. Thus, the get trap normally re-
turns the appropriate Reflect method. If the reference has been revoked, a TypeError
is thrown, instead.
function createRevocableReference(target) {
let enabled = true;
const handler = new Proxy({}, {
get(_handlerTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform '${trapName}' on a proxy`
+ ` that has been revoked`);
}
return Reflect[trapName];
}
});
return {
reference: new Proxy(target, handler),
revoke: () => {
enabled = false;
},
};
}
19.4 Use cases for Proxies 199
function createRevocableReference(target) {
const handler = {}; // forward everything
const { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}
19.4.6.1 Membranes
Membranes build on the idea of revocable references: Libraries for safely running un-
trusted code wrap a membrane around that code to isolate it and to keep the rest of the
system safe. Objects pass the membrane in two directions:
• The untrusted code may receive objects (“dry objects”) from the outside.
• Or it may hand objects (“wet objects”) to the outside.
In both cases, revocable references are wrapped around the objects. Objects returned by
wrapped functions or methods are also wrapped. Additionally, if a wrapped wet object
is passed back into a membrane, it is unwrapped.
Once the untrusted code is done, all of the revocable references are revoked. As a result,
none of its code on the outside can be executed anymore and outside objects that it refer-
ences, cease to work as well. The Caja Compiler is “a tool for making third party HTML,
CSS and JavaScript safe to embed in your website”. It uses membranes to achieve this
goal.
Alas, the standard DOM can do things that are not easily replicated in JavaScript. For ex-
ample, most DOM collections are live views on the current state of the DOM that change
dynamically whenever the DOM changes. As a result, pure JavaScript implementations
of the DOM are not very efficient. One of the reasons for adding Proxies to JavaScript
was to enable more efficient DOM implementations.
• Data access objects for databases: Reading and writing to the object reads and
writes to the database. This use case is similar to the web service example.
• Profiling: Intercept method invocations to track how much time is spent in each
method. This use case is similar to the tracing example.
• MobX lets you observe changes to data structures such as objects, Arrays and class
instances. That is implemented via Proxies.
• Alpine.js (by Caleb Porzio) is a frontend library that implements data binding via
Proxies.
• on-change (by Sindre Sorhus) observes changes to an object (via Proxies) and re-
ports them.
• Env utility (by Nicholas C. Zakas) lets you access environment variables via prop-
erties and throws exceptions if they don’t exist. That is implemented via Proxies.
• LDflex (by Ruben Verborgh and Ruben Taelman) provides a query language for
Linked Data (think Semantic Web). The fluid query API is implemented via Prox-
ies.
const calc = {
__noSuchMethod__: function (methodName, args) {
switch (methodName) {
case 'plus':
return args.reduce((a, b) => a + b);
case 'times':
return args.reduce((a, b) => a * b);
default:
throw new TypeError('Unsupported: ' + methodName);
}
}
};
19.5 The design of the Proxy API 201
assert.equal(
calc.plus('Parts', ' of ', 'a', ' string'),
'Parts of a string');
Thus, __noSuchMethod__ works similarly to a Proxy trap. In contrast to Proxies, the trap
is an own or inherited method of the object whose operations we want to intercept. The
problem with that approach is that base level (normal methods) and meta level (__-
noSuchMethod__) are mixed. Base-level code may accidentally invoke or see a meta level
method and there is the possibility of accidentally defining a meta level method.
Even in standard ECMAScript, base level and meta level are sometimes mixed. For ex-
ample, the following metaprogramming mechanisms can fail, because they exist at the
base level:
assert.equal(
Object.prototype.hasOwnProperty.call(obj, 'width'), false);
// Abbreviated version:
assert.equal(
{}.hasOwnProperty.call(obj, 'width'), false);
• obj.__proto__: In plain objects, __proto__ is a special property that lets us get and
set the prototype of the receiver. Hence, when we use plain objects as dictionaries,
we must avoid __proto__ as a property key.
By now, it should be obvious that making (base level) property keys special is problem-
atic. Therefore, Proxies are stratified: Base level (the Proxy object) and meta level (the
handler object) are separate.
202 19 Metaprogramming with Proxies (early access)
• As wrappers, they wrap their targets, they control access to them. Examples of wrap-
pers are: revocable resources and tracing via Proxies.
• As virtual objects, they are simply objects with special behavior and their targets
don’t matter. An example is a Proxy that forwards method calls to a remote object.
An earlier design of the Proxy API conceived Proxies as purely virtual objects. However,
it turned out that even in that role, a target was useful, to enforce invariants (which are
explained later) and as a fallback for traps that the handler doesn’t implement.
Both principles give Proxies considerable power for impersonating other objects. One
reason for enforcing invariants (as explained later) is to keep that power in check.
// lib.mjs
const proxies = new WeakSet();
This module uses the data structure WeakSet for keeping track of Proxies. WeakSet is ide-
ally suited for this purpose, because it doesn’t prevent its elements from being garbage-
collected.
// main.mjs
import { createProxy, isProxy } from './lib.mjs';
assert.equal(isProxy(proxy), true);
assert.equal(isProxy({}), false);
In the context of programming languages and API design, a protocol is a set of inter-
faces plus rules for using them. The ECMAScript specification describes how to execute
JavaScript code. It includes a protocol for handling objects. This protocol operates at a
meta level and is sometimes called the meta object protocol (MOP). The JavaScript MOP
consists of own internal methods that all objects have. “Internal” means that they exist
only in the specification (JavaScript engines may or may not have them) and are not ac-
cessible from JavaScript. The names of internal methods are written in double square
brackets.
The internal method for getting properties is called .[[Get]](). If we use double under-
scores instead of double brackets, this method would roughly be implemented as follows
in JavaScript.
// Method definition
__Get__(propKey, receiver) {
const desc = this.__GetOwnProperty__(propKey);
if (desc === undefined) {
const parent = this.__GetPrototypeOf__();
if (parent === null) return undefined;
return parent.__Get__(propKey, receiver); // (A)
}
if ('value' in desc) {
return desc.value;
}
const getter = desc.get;
if (getter === undefined) return undefined;
return getter.__Call__(receiver, []);
}
In line A we can see why Proxies in a prototype chain find out about get if a property
isn’t found in an “earlier” object: If there is no own property whose key is propKey, the
search continues in the prototype parent of this.
Fundamental versus derived operations. We can see that .[[Get]]() calls other MOP
operations. Operations that do that are called derived. Operations that don’t depend on
other operations are called fundamental.
204 19 Metaprogramming with Proxies (early access)
The meta object protocol of Proxies is different from that of normal objects. For normal
objects, derived operations call other operations. For Proxies, each operation (regardless
of whether it is fundamental or derived) is either intercepted by a handler method or
forwarded to the target.
The upside of doing the latter is that it increases performance and is more convenient.
For example, if there weren’t a trap for get, we’d have to implement its functionality via
getOwnPropertyDescriptor.
A downside of including derived traps is that that can lead to Proxies behaving incon-
sistently. For example, get may return a value that is different from the value in the
descriptor returned by getOwnPropertyDescriptor.
First, stable operations are not well suited for interception. An operation is stable if it
always produces the same results for the same arguments. If a Proxy can trap a stable
operation, it can become unstable and thus unreliable. Strict equality (===) is one such
stable operation. It can’t be trapped and its result is computed by treating the Proxy itself
as just another object. Another way of maintaining stability is by applying an operation
to the target instead of the Proxy. As explained later, when we look at how invariants are
enfored for Proxies, this happens when Object.getPrototypeOf() is applied to a Proxy
whose target is non-extensible.
A second reason for not making more operations interceptable is that interception means
executing custom code in situations where that normally isn’t possible. The more this
interleaving of code happens, the harder it is to understand and debug a program. It
also affects performance negatively.
If we want to create virtual methods via Proxies, we have to return functions from a get
trap. That raises the question: why not introduce an extra trap for method invocations
(e.g. invoke)? That would enable us to distinguish between:
First, not all implementations distinguish between get and invoke. For example, Apple’s
JavaScriptCore doesn’t.
19.5 The design of the Proxy API 205
Second, extracting a method and invoking it later via .call() or .apply() should have
the same effect as invoking the method via dispatch. In other words, the following two
variants should work equivalently. If there were an extra trap invoke, then that equiva-
lence would be harder to maintain.
// Variant 1: call via dynamic dispatch
const result1 = obj.m();
Intercepting missing methods. invoke lets a Proxy emulate the previously mentioned
__noSuchMethod__ mechanism. The Proxy would again become the prototype of an ob-
ject obj. It would react differently depending on how an unknown property prop is
accessed:
• If we read that property via obj.prop, no interception happens and undefined is
returned.
• If we make the method call obj.prop() then the Proxy intercepts and, e.g., notifies
a callback.
For more information on this topic, see §10 “Protecting objects from being changed”.
These and other characteristics that remain unchanged in the face of language operations
are called invariants. It is easy to violate invariants via Proxies because they are not in-
trinsically bound by non-extensibility etc. The Proxy API prevents that from happening
by checking the target object and the results of handler methods.
The next two subsections describe four invariants. An exhaustive list of invariants is
given at the end of this chapter.
19.5.5.3 Two invariants that are enforced via the target object
• Proxies work like all other objects with regard to extensibility and configurability.
Therefore, universality is maintained. This is achieved without preventing Proxies
from virtualizing (impersonating) protected objects.
• A protected object can’t be misrepresented by wrapping a Proxy around it. Mis-
representation can be caused by bugs or by malicious code.
In response to the getPrototypeOf trap, the Proxy must return the target’s prototype if
the target is non-extensible.
To demonstrate this invariant, let’s create a handler that returns a prototype that is dif-
ferent from the target’s prototype:
assert.equal(
Object.getPrototypeOf(extProxy), fakeProto);
We do, however, get an error if we fake the prototype for a non-extensible object.
assert.throws(
() => Object.getPrototypeOf(nonExtProxy),
{
name: 'TypeError',
message: "'getPrototypeOf' on proxy: proxy target is"
+ " non-extensible but the trap did not return its"
+ " actual prototype",
});
208 19 Metaprogramming with Proxies (early access)
If the target has a non-writable non-configurable property, then the handler must return
that property’s value in response to a get trap. To demonstrate this invariant, let’s create
a handler that always returns the same value for properties.
const handler = {
get(target, propKey) {
return 'abc';
}
};
const target = Object.defineProperties(
{}, {
manufacturer: {
value: 'Iso Autoveicoli',
writable: true,
configurable: true
},
model: {
value: 'Isetta',
writable: false,
configurable: false
},
});
const proxy = new Proxy(target, handler);
assert.equal(
proxy.manufacturer, 'abc');
assert.throws(
() => proxy.model,
{
name: 'TypeError',
message: "'get' on proxy: property 'model' is a read-only and"
+ " non-configurable data property on the proxy target but"
+ " the proxy did not return its actual value (expected"
+ " 'Isetta' but got 'abc')",
});
19.6 FAQ: Proxies 209
The following operations are fundamental, they don’t use other operations to do their
work: apply, defineProperty, deleteProperty, getOwnPropertyDescriptor, getPro-
totypeOf, isExtensible, ownKeys, preventExtensions, setPrototypeOf
All other operations are derived, they can be implemented via fundamental operations.
For example, get can be implemented by iterating over the prototype chain via getPro-
totypeOf and calling getOwnPropertyDescriptor for each chain member until either an
own property is found or the chain ends.
• ownKeys(target): Array<PropertyKey>
– The handler must return an object, which treated as Array-like and converted
into an Array.
– The resulting Array must not contain duplicate entries.
– Each element of the result must be either a string or a symbol.
– The result must contain the keys of all non-configurable own properties of
the target.
– If the target is not extensible, then the result must contain exactly the keys of
the own properties of the target (and no other values).
• preventExtensions(target): boolean
– The handler must only return a truthy value (indicating a successful change)
if target.isExtensible() is false.
• set(target, propKey, value, receiver): boolean
– The property can’t be changed if the target has a non-writable, non-
configurable data property whose key is propKey. In that case, value must
be the value of that property or a TypeError is thrown.
– The property can’t be set in any way if the corresponding own target property
is a non-configurable accessor without a setter.
• setPrototypeOf(target, proto): boolean
– If the target is not extensible, the prototype can’t be changed. This is enforced
as follows: If the target is not extensible and the handler returns a truthy
value (indicating a successful change), then proto must be the same as the
prototype of the target. Otherwise, a TypeError is thrown.
• target.get(propertyKey, receiver)
If target has no own property with the given key, get is invoked on the prototype
of target.
• target.has(propertyKey)
Similarly to get, has is invoked on the prototype of target if target has no own
property with the given key.
• target.set(propertyKey, value, receiver)
Similarly to get, set is invoked on the prototype of target if target has no own
property with the given key.
19.7 Reference: the Proxy API 213
All other operations only affect own properties, they have no effect on the prototype
chain.
19.7.5 Reflect
The global object Reflect implements all interceptable operations of the JavaScript meta
object protocol as methods. The names of those methods are the same as those of the
handler methods, which, as we have seen, helps with forwarding operations from the
handler to the target.
• Reflect.apply(target, thisArgument, argumentsList): any
Similar to Function.prototype.apply().
• Reflect.construct(target, argumentsList, newTarget=target): object
The new operator as a function. target is the constructor to invoke, the optional
parameter newTarget points to the constructor that started the current chain of
constructor calls.
• Reflect.defineProperty(target, propertyKey, propDesc): boolean
Similar to Object.defineProperty().
• Reflect.deleteProperty(target, propertyKey): boolean
The delete operator as a function. It works slightly differently, though: It returns
true if it successfully deleted the property or if the property never existed. It re-
turns false if the property could not be deleted and still exists. The only way to
protect properties from deletion is by making them non-configurable. In sloppy
mode, the delete operator returns the same results. But in strict mode, it throws
a TypeError instead of returning false.
• Reflect.get(target, propertyKey, receiver=target): any
A function that gets properties. The optional parameter receiver points to the
object where the getting started. It is needed when get reaches a getter later in the
prototype chain. Then it provides the value for this.
• Reflect.getOwnPropertyDescriptor(target, propertyKey): undefined|PropDesc
Same as Object.getOwnPropertyDescriptor().
• Reflect.getPrototypeOf(target): null|object
Same as Object.getPrototypeOf().
• Reflect.has(target, propertyKey): boolean
The in operator as a function.
• Reflect.isExtensible(target): boolean
Same as Object.isExtensible().
• Reflect.ownKeys(target): Array<PropertyKey>
Returns all own property keys in an Array: the string keys and symbol keys of all
214 19 Metaprogramming with Proxies (early access)
• Reflect.preventExtensions(target): boolean
Similar to Object.preventExtensions().
Several methods have boolean results. For .has() and .isExtensible(), they are the
results of the operation. For the remaining methods, they indicate whether the operation
succeeded.
• Different return values: Reflect duplicates the following methods of Object, but
its methods return booleans indicating whether the operation succeeded (where
the Object methods return the object that was modified).
Going forward, Object will host operations that are of interest to normal applications,
while Reflect will host operations that are more low-level.
19.8 Conclusion
This concludes our in-depth look at the Proxy API. One thing to be aware of is that Proxies
slow down code. That may matter if performance is critical.
On the other hand, performance is often not crucial and it is nice to have the metapro-
gramming power that Proxies give us.
Acknowledgements:
• Allen Wirfs-Brock pointed out the pitfall explained in §19.3.7 “Pitfall: not all ob-
jects can be wrapped transparently by Proxies”.
• The idea for §19.4.3 “Negative Array indices (get)” comes from a blog post by
Hemanth.HM.
• André Jaenisch contributed to the list of libraries that use Proxies.
Contents
20.1 Names of functions . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
20.1.1 Function names are used in stack traces . . . . . . . . . . . . . 218
20.2 Constructs that provide names for functions . . . . . . . . . . . . . 219
20.2.1 Function declarations . . . . . . . . . . . . . . . . . . . . . . . 219
20.2.2 Variable declarations and anonymous functions . . . . . . . . 219
20.2.3 Assignments and anonymous functions . . . . . . . . . . . . . 219
20.2.4 Default values and anonymous functions . . . . . . . . . . . . 219
20.2.5 Named function expressions . . . . . . . . . . . . . . . . . . . 220
20.2.6 Methods in object literals . . . . . . . . . . . . . . . . . . . . . 220
20.2.7 Methods in class definitions . . . . . . . . . . . . . . . . . . . 221
20.2.8 Names of classes . . . . . . . . . . . . . . . . . . . . . . . . . 222
20.2.9 Default exports and anonymous constructs . . . . . . . . . . . 222
20.2.10 Other programming constructs . . . . . . . . . . . . . . . . . 222
20.3 Things to look out for with names of functions . . . . . . . . . . . . 223
20.3.1 The name of a function is always assigned at creation . . . . . 223
20.3.2 Caveat: minification . . . . . . . . . . . . . . . . . . . . . . . 223
20.4 Changing the names of functions . . . . . . . . . . . . . . . . . . . . 223
20.5 The function property .name in the ECMAScript specification . . . . 224
217
218 20 The property .name of functions (bonus)
function myBeautifulFunction() {}
assert.equal(myBeautifulFunction.name, 'myBeautifulFunction');
Prior to ECMAScript 6, this property was already supported by most engines. With ES6,
it became part of the language standard.
The language has become surprisingly good at finding names for functions, even when
they are initially anonymous (e.g., arrow functions).
1 function callFunc(func) {
2 return func();
3 }
4
5 function func1() {
6 throw new Error('Problem');
7 }
8
9 function func2() {
10 // The following arrow function doesn’t have a name
11 callFunc(() => func1());
12 }
13
14 function func3() {
15 func2();
16 }
17
18 try {
19 func3();
20 } catch (err) {
21 assert.equal(err.stack, `
22 Error: Problem
23 at func1 (ch_function-names.mjs:6:9)
24 at ch_function-names.mjs:11:18
25 at callFunc (ch_function-names.mjs:2:10)
26 at func2 (ch_function-names.mjs:11:3)
27 at func3 (ch_function-names.mjs:15:3)
28 at ch_function-names.mjs:19:3
29 `.trim());
30
31 }
20.2 Constructs that provide names for functions 219
From now on, whenever you see an anonymous function expression, you can as-
sume that an arrow function works the same way.
Because it comes first, the function expression’s name funcName takes precedence over
other names (in this case, the name varName of the variable). However, the name of a
function expression is still only a variable inside the function expression:
function shortHand() {}
const obj = {
methodDef() {}, // (A)
propDef: function () {}, // (B)
['computed' + 'Key']: function () {}, // (C)
shortHand, // (D)
};
assert.equal(obj.methodDef.name, 'methodDef');
assert.equal(obj.propDef.name, 'propDef');
assert.equal(obj.computedKey.name, 'computedKey');
assert.equal(obj.shortHand.name, 'shortHand');
The names of getters are prefixed with 'get', the names of setters are prefixed with
'set':
20.2 Constructs that provide names for functions 221
const obj = {
get myGetter() {},
set mySetter(value) {},
};
const getter = Object.getOwnPropertyDescriptor(obj, 'myGetter').get;
assert.equal(getter.name, 'get myGetter');
class ClassDecl {
methodDef() {}
['computed' + 'Key']() {} // computed property key
static staticMethod() {}
}
assert.equal(ClassDecl.prototype.methodDef.name, 'methodDef');
assert.equal(new ClassDecl().methodDef.name, 'methodDef');
assert.equal(ClassDecl.prototype.computedKey.name, 'computedKey');
assert.equal(ClassDecl.staticMethod.name, 'staticMethod');
Getters and setters again have the name prefixes 'get' and 'set', respectively:
class ClassDecl {
get myGetter() {}
set mySetter(value) {}
}
const getter = Object.getOwnPropertyDescriptor(
ClassDecl.prototype, 'myGetter').get;
assert.equal(getter.name, 'get myGetter');
The key of a method can be a symbol. The .name property of such a method is still a
string:
• If the symbol has a description, the method’s name is the description in square
brackets.
• Otherwise, the method’s name is the empty string ('').
222 20 The property .name of functions (bonus)
const obj = {
[keyWithDesc]() {},
[keyWithoutDesc]() {},
};
assert.equal(obj[keyWithDesc].name, '[keyWithDesc]');
assert.equal(obj[keyWithoutDesc].name, '');
// Arrow functions
export default () => {};
Function func is created in line A, where it also receives its non-name, the empty string ''.
Using an assignment in line B doesn’t update the initial name. Missing function names
could conceivably be updated later, but doing so would impact performance negatively.
configurable: true,
});
The property not being writable means that we can’t change its value via assignment:
assert.throws(
() => func.name = 'differentName',
/^TypeError: Cannot assign to read only property 'name'/
);
The property is, however, configurable, which means that we can change it by re-defining
it:
Object.defineProperty(
func, 'name', {
value: 'differentName',
});
assert.equal(func.name, 'differentName');