In this book, JavaScript’s style of object-oriented programming (OOP) is introduced in four steps. This chapter covers step 1 and 2; the next chapter covers step 3 and 4. The steps are (figure 30.1):
Creating an object via an object literal (starts and ends with a curly brace):
const myObject = { // object literal
myProperty: 1,
myMethod() {
return 2;
}, // comma!
get myAccessor() {
return this.myProperty;
}, // comma!
set myAccessor(value) {
this.myProperty = value;
}, // last comma is optional
};
assert.equal(
myObject.myProperty, 1
);
assert.equal(
myObject.myMethod(), 2
);
assert.equal(
myObject.myAccessor, 1
);
myObject.myAccessor = 3;
assert.equal(
myObject.myProperty, 3
);
Being able to create objects directly (without classes) is one of the highlights of JavaScript.
Spreading into objects:
const original = {
a: 1,
b: {
c: 3,
},
};
// Spreading (...) copies one object “into” another one:
const modifiedCopy = {
...original, // spreading
d: 4,
};
assert.deepEqual(
modifiedCopy,
{
a: 1,
b: {
c: 3,
},
d: 4,
}
);
// Caveat: spreading copies shallowly (property values are shared)
modifiedCopy.a = 5; // does not affect `original`
modifiedCopy.b.c = 6; // affects `original`
assert.deepEqual(
original,
{
a: 1, // unchanged
b: {
c: 6, // changed
},
},
);
We can also use spreading to make an unmodified (shallow) copy of an object:
const exactCopy = {...obj};
Prototypes are JavaScript’s fundamental inheritance mechanism. Even classes are based on it. Each object has null
or an object as its prototype. The latter object can also have a prototype, etc. In general, we get chains of prototypes.
Prototypes are managed like this:
// `obj1` has no prototype (its prototype is `null`)
const obj1 = Object.create(null); // (A)
assert.equal(
Object.getPrototypeOf(obj1), null // (B)
);
// `obj2` has the prototype `proto`
const proto = {
protoProp: 'protoProp',
};
const obj2 = {
__proto__: proto, // (C)
objProp: 'objProp',
}
assert.equal(
Object.getPrototypeOf(obj2), proto
);
Notes:
Each object inherits all the properties of its prototype:
// `obj2` inherits .protoProp from `proto`
assert.equal(
obj2.protoProp, 'protoProp'
);
assert.deepEqual(
Reflect.ownKeys(obj2),
['objProp'] // own properties of `obj2`
);
The non-inherited properties of an object are called its own properties.
The most important use case for prototypes is that several objects can share methods by inheriting them from a common prototype.
Objects in JavaScript:
There are two ways of using objects in JavaScript:
Fixed-layout objects: Used this way, objects work like records in databases. They have a fixed number of properties, whose keys are known at development time. Their values generally have different types.
const fixedLayoutObject = {
product: 'carrot',
quantity: 4,
};
Dictionary objects: Used this way, objects work like lookup tables or maps. They have a variable number of properties, whose keys are not known at development time. All of their values have the same type.
const dictionaryObject = {
['one']: 1,
['two']: 2,
};
Note that the two ways can also be mixed: Some objects are both fixed-layout objects and dictionary objects.
The ways of using objects influence how they are explained in this chapter:
Let’s first explore fixed-layout objects.
Object literals are one way of creating fixed-layout objects. They are a stand-out feature of JavaScript: we can directly create objects – no need for classes! This is an example:
const jane = {
first: 'Jane',
last: 'Doe', // optional trailing comma
};
In the example, we created an object via an object literal, which starts and ends with curly braces {}
. Inside it, we defined two properties (key-value entries):
first
and the value 'Jane'
.
last
and the value 'Doe'
.
Since ES5, trailing commas are allowed in object literals.
We will later see other ways of specifying property keys, but with this way of specifying them, they must follow the rules of JavaScript variable names. For example, we can use first_name
as a property key, but not first-name
). However, reserved words are allowed:
const obj = {
if: true,
const: true,
};
In order to check the effects of various operations on objects, we’ll occasionally use Object.keys()
in this part of the chapter. It lists property keys:
> Object.keys({a:1, b:2})
[ 'a', 'b' ]
Whenever the value of a property is defined via a variable that has the same name as the key, we can omit the key.
function createPoint(x, y) {
return {x, y}; // Same as: {x: x, y: y}
}
assert.deepEqual(
createPoint(9, 2),
{ x: 9, y: 2 }
);
This is how we get (read) a property (line A):
const jane = {
first: 'Jane',
last: 'Doe',
};
// Get property .first
assert.equal(jane.first, 'Jane'); // (A)
Getting an unknown property produces undefined
:
assert.equal(jane.unknownProperty, undefined);
This is how we set (write to) a property (line A):
const obj = {
prop: 1,
};
assert.equal(obj.prop, 1);
obj.prop = 2; // (A)
assert.equal(obj.prop, 2);
We just changed an existing property via setting. If we set an unknown property, we create a new entry:
const obj = {}; // empty object
assert.deepEqual(
Object.keys(obj), []);
obj.unknownProperty = 'abc';
assert.deepEqual(
Object.keys(obj), ['unknownProperty']);
The following code shows how to create the method .says()
via an object literal:
const jane = {
first: 'Jane', // value property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
}, // comma as separator (optional at end)
};
assert.equal(jane.says('hello'), 'Jane says “hello”');
During the method call jane.says('hello')
, jane
is called the receiver of the method call and assigned to the special variable this
(more on this
in “Methods and the special variable this
” (§30.5)). That enables method .says()
to access the sibling property .first
in line A.
An accessor is defined via syntax inside an object literal that looks like methods: a getter and/or a setter (i.e., each accessor has one or both of them).
Invoking an accessor looks like accessing a value property:
A getter is created by prefixing a method definition with the modifier get
:
const jane = {
first: 'Jane',
last: 'Doe',
get full() {
return `${this.first} ${this.last}`;
},
};
assert.equal(jane.full, 'Jane Doe');
jane.first = 'John';
assert.equal(jane.full, 'John Doe');
A setter is created by prefixing a method definition with the modifier set
:
const jane = {
first: 'Jane',
last: 'Doe',
set full(fullName) {
const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
},
};
jane.full = 'Richard Roe';
assert.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe');
Exercise: Creating an object via an object literal
exercises/objects/color_point_object_test.mjs
...
) [ES2018]
Inside an object literal, a spread property adds the properties of another object to the current one:
> const obj = {one: 1, two: 2};
> {...obj, three: 3}
{ one: 1, two: 2, three: 3 }
const obj1 = {one: 1, two: 2};
const obj2 = {three: 3};
assert.deepEqual(
{...obj1, ...obj2, four: 4},
{one: 1, two: 2, three: 3, four: 4}
);
If property keys clash, the property that is mentioned last “wins”:
> const obj = {one: 1, two: 2, three: 3};
> {...obj, one: true}
{ one: true, two: 2, three: 3 }
> {one: true, ...obj}
{ one: 1, two: 2, three: 3 }
All values are spreadable, even undefined
and null
:
> {...undefined}
{}
> {...null}
{}
> {...123}
{}
> {...'abc'}
{ '0': 'a', '1': 'b', '2': 'c' }
> {...['a', 'b']}
{ '0': 'a', '1': 'b' }
Property .length
of strings and Arrays is hidden from this kind of operation (it is not enumerable; see “Property attributes and property descriptors” (§30.8) for more information).
Spreading includes properties whose keys are symbols (which are ignored by Object.keys()
, Object.values()
and Object.entries()
):
const symbolKey = Symbol('symbolKey');
const obj = {
stringKey: 1,
[symbolKey]: 2,
};
assert.deepEqual(
{...obj, anotherStringKey: 3},
{
stringKey: 1,
[symbolKey]: 2,
anotherStringKey: 3,
}
);
We can use spreading to create a copy of an object original
:
const copy = {...original};
Caveat – copying is shallow: copy
is a fresh object with duplicates of all properties (key-value entries) of original
. But if property values are objects, then those are not copied themselves; they are shared between original
and copy
. Let’s look at an example:
const original = { a: 1, b: {prop: true} };
const copy = {...original};
The first level of copy
is really a copy: If we change any properties at that level, it does not affect the original:
copy.a = 2;
assert.deepEqual(
original, { a: 1, b: {prop: true} }); // no change
However, deeper levels are not copied. For example, the value of .b
is shared between original and copy. Changing .b
in the copy also changes it in the original.
copy.b.prop = false;
assert.deepEqual(
original, { a: 1, b: {prop: false} });
JavaScript doesn’t have built-in support for deep copying
JavaScript does not have a built-in operation for deeply copying objects. Options:
structuredClone()
is supported by most JavaScript platforms now – even though it is not part of ECMAScript. Alas, this function has a number of limitations – e.g., if we copy an instance of a class we created, the copy is not an instance of that class.
_.cloneDeep()
and _.cloneDeepWith()
that can help. They have fewer limitations than structuredClone()
.
If one of the inputs of our code is an object with data, we can make properties optional by specifying default values that are used if those properties are missing. One technique for doing so is via an object whose properties contain the default values. In the following example, that object is DEFAULTS
:
const DEFAULTS = {alpha: 'a', beta: 'b'};
const providedData = {alpha: 1};
const allData = {...DEFAULTS, ...providedData};
assert.deepEqual(allData, {alpha: 1, beta: 'b'});
The result, the object allData
, is created by copying DEFAULTS
and overriding its properties with those of providedData
.
But we don’t need an object to specify the default values; we can also specify them inside the object literal, individually:
const providedData = {alpha: 1};
const allData = {alpha: 'a', beta: 'b', ...providedData};
assert.deepEqual(allData, {alpha: 1, beta: 'b'});
So far, we have encountered one way of changing a property .alpha
of an object: We set it (line A) and mutate the object. That is, this way of changing a property is destructive.
const obj = {alpha: 'a', beta: 'b'};
obj.alpha = 1; // (A)
assert.deepEqual(obj, {alpha: 1, beta: 'b'});
With spreading, we can change .alpha
non-destructively – we make a copy of obj
where .alpha
has a different value:
const obj = {alpha: 'a', beta: 'b'};
const updatedObj = {...obj, alpha: 1};
assert.deepEqual(updatedObj, {alpha: 1, beta: 'b'});
Exercise: Non-destructively updating a property via spreading (fixed key)
exercises/objects/update_name_test.mjs
Object.assign()
[ES6]Object.assign()
is a tool method:
Object.assign(target, source_1, source_2, ···)
This expression assigns all properties of source_1
to target
, then all properties of source_2
, etc. At the end, it returns target
– for example:
const target = { a: 1 };
const result = Object.assign(
target,
{b: 2},
{c: 3, b: true});
assert.deepEqual(
result, { a: 1, b: true, c: 3 });
// target was modified and returned:
assert.equal(result, target);
The use cases for Object.assign()
are similar to those for spread properties. In a way, it spreads destructively.
this
Let’s revisit the example that was used to introduce methods:
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
Somewhat surprisingly, methods are functions:
assert.equal(typeof jane.says, 'function');
Why is that? We learned in the chapter on callable values that ordinary functions play several roles. Method is one of those roles. Therefore, internally, jane
roughly looks as follows.
const jane = {
first: 'Jane',
says: function (text) {
return `${this.first} says “${text}”`;
},
};
this
Consider the following code:
const obj = {
someMethod(x, y) {
assert.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
}
};
obj.someMethod('a', 'b'); // (B)
In line B, obj
is the receiver of a method call. It is passed to the function stored in obj.someMethod
via an implicit (hidden) parameter whose name is this
(line A).
How to understand this
The best way to understand this
is as an implicit parameter of ordinary functions (and therefore methods, too).
.call()
Methods are functions and functions have methods themselves. One of those methods is .call()
. Let’s look at an example to understand how this method works.
In the previous section, there was this method invocation:
obj.someMethod('a', 'b')
This invocation is equivalent to:
obj.someMethod.call(obj, 'a', 'b');
Which is also equivalent to:
const func = obj.someMethod;
func.call(obj, 'a', 'b');
.call()
makes the normally implicit parameter this
explicit: When invoking a function via .call()
, the first parameter is this
, followed by the regular (explicit) function parameters.
As an aside, this means that there are actually two different dot operators:
obj.prop
obj.prop()
They are different in that (2) is not just (1) followed by the function call operator ()
. Instead, (2) additionally provides a value for this
.
.bind()
.bind()
is another method of function objects. In the following code, we use .bind()
to turn method .says()
into the stand-alone function func()
:
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`; // (A)
},
};
const func = jane.says.bind(jane, 'hello');
assert.equal(func(), 'Jane says “hello”');
Setting this
to jane
via .bind()
is crucial here. Otherwise, func()
wouldn’t work properly because this
is used in line A. In the next section, we’ll explore why that is.
this
pitfall: extracting methods
We now know quite a bit about functions and methods and are ready to take a look at the biggest pitfall involving methods and this
: function-calling a method extracted from an object can fail if we are not careful.
In the following example, we fail when we extract method jane.says()
, store it in the variable func
, and function-call func
.
const jane = {
first: 'Jane',
says(text) {
return `${this.first} says “${text}”`;
},
};
const func = jane.says; // extract the method
assert.throws(
() => func('hello'), // (A)
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
});
In line A, we are making a normal function call. And in normal function calls, this
is undefined
(if strict mode is active, which it almost always is). Line A is therefore equivalent to:
assert.throws(
() => jane.says.call(undefined, 'hello'), // `this` is undefined!
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'first')",
}
);
How do we fix this? We need to use .bind()
to extract method .says()
:
const func2 = jane.says.bind(jane);
assert.equal(func2('hello'), 'Jane says “hello”');
The .bind()
ensures that this
is always jane
when we call func()
.
We can also use arrow functions to extract methods:
const func3 = text => jane.says(text);
assert.equal(func3('hello'), 'Jane says “hello”');
The following is a simplified version of code that we may see in actual web development:
class ClickHandler {
constructor(id, elem) {
this.id = id;
elem.addEventListener('click', this.handleClick); // (A)
}
handleClick(event) {
alert('Clicked ' + this.id);
}
}
In line A, we don’t extract the method .handleClick()
properly. Instead, we should do:
const listener = this.handleClick.bind(this);
elem.addEventListener('click', listener);
// Later, possibly:
elem.removeEventListener('click', listener);
Each invocation of .bind()
creates a new function. That’s why we need to store the result somewhere if we want to remove it later on.
Alas, there is no simple way around the pitfall of extracting methods: Whenever we extract a method, we have to be careful and do it properly – for example, by binding this
or by using an arrow function.
Exercise: Extracting a method
exercises/objects/method_extraction_exrc.mjs
this
pitfall: accidentally shadowing this
Accidentally shadowing this
is only an issue with ordinary functions
Arrow functions don’t shadow this
.
Consider the following problem: when we are inside an ordinary function, we can’t access the this
of the surrounding scope because the ordinary function has its own this
. In other words, a variable in an inner scope hides a variable in an outer scope. That is called shadowing. The following code is an example:
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x; // (A)
});
},
};
assert.throws(
() => prefixer.prefixStringArray(['a', 'b']),
{
name: 'TypeError',
message: "Cannot read properties of undefined (reading 'prefix')",
}
);
In line A, we want to access the this
of .prefixStringArray()
. But we can’t since the surrounding ordinary function has its own this
that shadows (and blocks access to) the this
of the method. The value of the former this
is undefined
due to the callback being function-called. That explains the error message.
The simplest way to fix this problem is via an arrow function, which doesn’t have its own this
and therefore doesn’t shadow anything:
const prefixer = {
prefix: '==> ',
prefixStringArray(stringArray) {
return stringArray.map(
(x) => {
return this.prefix + x;
});
},
};
assert.deepEqual(
prefixer.prefixStringArray(['a', 'b']),
['==> a', '==> b']);
We can also store this
in a different variable (line A), so that it doesn’t get shadowed:
prefixStringArray(stringArray) {
const that = this; // (A)
return stringArray.map(
function (x) {
return that.prefix + x;
});
},
Another option is to specify a fixed this
for the callback via .bind()
(line A):
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
}.bind(this)); // (A)
},
Lastly, .map()
lets us specify a value for this
(line A) that it uses when invoking the callback:
prefixStringArray(stringArray) {
return stringArray.map(
function (x) {
return this.prefix + x;
},
this); // (A)
},
this
If we follow the advice in “Recommendation: prefer specialized functions over ordinary functions” (§27.3.4), we can avoid the pitfall of accidentally shadowing this
. This is a summary:
Use arrow functions as anonymous inline functions. They don’t have this
as an implicit parameter and don’t shadow it.
For named stand-alone function declarations we can either use arrow functions or function declarations. If we do the latter, we have to make sure this
isn’t mentioned in their bodies.
this
in various contexts (advanced)
What is the value of this
in various contexts?
Inside a callable entity, the value of this
depends on how the callable entity is invoked and what kind of callable entity it is:
this === undefined
(in strict mode)
this
is same as in surrounding scope (lexical this
)
this
is receiver of call
new
: this
refers to the newly created instance
We can also access this
in all common top-level scopes:
<script>
element: this === globalThis
this === undefined
this === module.exports
Tip: pretend that this
doesn’t exist in top-level scopes
I like to do that because top-level this
is confusing and there are better alternatives for its (few) use cases.
The following kinds of optional chaining operations exist:
obj?.prop // optional fixed property getting
obj?.[«expr»] // optional dynamic property getting
func?.(«arg0», «arg1») // optional function or method call
The rough idea is:
undefined
nor null
, then perform the operation after the question mark.
undefined
.
Each of the three syntaxes is covered in more detail later. These are a few first examples:
> null?.prop
undefined
> {prop: 1}?.prop
1
> null?.(123)
undefined
> String?.(123)
'123'
Mnemonic for the optional chaining operator (?.
)
Are you occasionally unsure if the optional chaining operator starts with a dot (.?
) or a question mark (?.
)? Then this mnemonic may help you:
?
) the left-hand side is not nullish
.
) access a property.
Consider the following data:
const persons = [
{
surname: 'Zoe',
address: {
street: {
name: 'Sesame Street',
number: '123',
},
},
},
{
surname: 'Mariner',
},
{
surname: 'Carmen',
address: {
},
},
];
We can use optional chaining to safely extract street names:
const streetNames = persons.map(
p => p.address?.street?.name);
assert.deepEqual(
streetNames, ['Sesame Street', undefined, undefined]
);
The nullish coalescing operator allows us to use the default value '(no name)'
instead of undefined
:
const streetNames = persons.map(
p => p.address?.street?.name ?? '(no name)');
assert.deepEqual(
streetNames, ['Sesame Street', '(no name)', '(no name)']
);
The following two expressions are equivalent:
o?.prop
(o !== undefined && o !== null) ? o.prop : undefined
Examples:
assert.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1);
The following two expressions are equivalent:
o?.[«expr»]
(o !== undefined && o !== null) ? o[«expr»] : undefined
Examples:
const key = 'prop';
assert.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1);
The following two expressions are equivalent:
f?.(arg0, arg1)
(f !== undefined && f !== null) ? f(arg0, arg1) : undefined
Examples:
assert.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123');
Note that this operator produces an error if its left-hand side is not callable:
assert.throws(
() => true?.(123),
TypeError);
Why? The idea is that the operator only tolerates deliberate omissions. An uncallable value (other than undefined
and null
) is probably an error and should be reported, rather than worked around.
In a chain of property gettings and method invocations, evaluation stops once the first optional operator encounters undefined
or null
at its left-hand side:
function invokeM(value) {
return value?.a.b.m(); // (A)
}
const obj = {
a: {
b: {
m() { return 'result' }
}
}
};
assert.equal(
invokeM(obj), 'result'
);
assert.equal(
invokeM(undefined), undefined // (B)
);
Consider invokeM(undefined)
in line B: undefined?.a
is undefined
. Therefore we’d expect .b
to fail in line A. But it doesn’t: The ?.
operator encounters the value undefined
and the evaluation of the whole expression immediately returns undefined
.
This behavior differs from a normal operator where JavaScript always evaluates all operands before evaluating the operator. It is called short-circuiting. Other short-circuiting operators are:
(a && b)
: b
is only evaluated if a
is truthy.
(a || b)
: b
is only evaluated if a
is falsy.
(c ? t : e)
: If c
is truthy, t
is evaluated. Otherwise, e
is evaluated.
Optional chaining also has downsides:
An alternative to optional chaining is to extract the information once, in a single location:
With either approach, it is possible to perform checks and to fail early if there are problems.
Further reading:
o?.[x]
and f?.()
?The syntaxes of the following two optional operator are not ideal:
obj?.[«expr»] // better: obj?[«expr»]
func?.(«arg0», «arg1») // better: func?(«arg0», «arg1»)
Alas, the less elegant syntax is necessary because distinguishing the ideal syntax (first expression) from the conditional operator (second expression) is too complicated:
obj?['a', 'b', 'c'].map(x => x+x)
obj ? ['a', 'b', 'c'].map(x => x+x) : []
null?.prop
evaluate to undefined
and not null
?The operator ?.
is mainly about its right-hand side: Does property .prop
exist? If not, stop early. Therefore, keeping information about its left-hand side is rarely useful. However, only having a single “early termination” value does simplify things.
Objects work best as fixed-layout objects. But before ES6, JavaScript did not have a data structure for dictionaries (ES6 brought Maps). Therefore, objects had to be used as dictionaries, which imposed a signficant constraint: Dictionary keys had to be strings (symbols were also introduced with ES6).
We first look at features of objects that are related to dictionaries but also useful for fixed-layout objects. This section concludes with tips for actually using objects as dictionaries. (Spoiler: If possible, it’s better to use Maps.)
So far, we have always used fixed-layout objects. Property keys were fixed tokens that had to be valid identifiers and internally became strings:
const obj = {
mustBeAnIdentifier: 123,
};
// Get property
assert.equal(obj.mustBeAnIdentifier, 123);
// Set property
obj.mustBeAnIdentifier = 'abc';
assert.equal(obj.mustBeAnIdentifier, 'abc');
As a next step, we’ll go beyond this limitation for property keys: In this subsection, we’ll use arbitrary fixed strings as keys. In the next subsection, we’ll dynamically compute keys.
Two syntaxes enable us to use arbitrary strings as property keys.
First, when creating property keys via object literals, we can quote property keys (with single or double quotes):
const obj = {
'Can be any string!': 123,
};
Second, when getting or setting properties, we can use square brackets with strings inside them:
// Get property
assert.equal(obj['Can be any string!'], 123);
// Set property
obj['Can be any string!'] = 'abc';
assert.equal(obj['Can be any string!'], 'abc');
We can also use these syntaxes for methods:
const obj = {
'A nice method'() {
return 'Yes!';
},
};
assert.equal(obj['A nice method'](), 'Yes!');
In the previous subsection, property keys were specified via fixed strings inside object literals. In this section we learn how to dynamically compute property keys. That enables us to use either arbitrary strings or symbols.
The syntax of dynamically computed property keys in object literals is inspired by dynamically accessing properties. That is, we can use square brackets to wrap expressions:
const obj = {
['Hello world!']: true,
['p'+'r'+'o'+'p']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
};
assert.equal(obj['Hello world!'], true);
assert.equal(obj.prop, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye');
The main use case for computed keys is having symbols as property keys (line A).
Note that the square brackets operator for getting and setting properties works with arbitrary expressions:
assert.equal(obj['p'+'r'+'o'+'p'], 123);
assert.equal(obj['==> prop'.slice(4)], 123);
Methods can have computed property keys, too:
const methodKey = Symbol();
const obj = {
[methodKey]() {
return 'Yes!';
},
};
assert.equal(obj[methodKey](), 'Yes!');
For the remainder of this chapter, we’ll mostly use fixed property keys again (because they are syntactically more convenient). But all features are also available for arbitrary strings and symbols.
Exercise: Non-destructively updating a property via spreading (computed key)
exercises/objects/update_property_test.mjs
in
operator: is there a property with a given key?
The in
operator checks if an object has a property with a given key:
const obj = {
alpha: 'abc',
beta: false,
};
assert.equal('alpha' in obj, true);
assert.equal('beta' in obj, true);
assert.equal('unknownKey' in obj, false);
We can also use a truthiness check to determine if a property exists:
assert.equal(
obj.alpha ? 'exists' : 'does not exist',
'exists');
assert.equal(
obj.unknownKey ? 'exists' : 'does not exist',
'does not exist');
The previous checks work because obj.alpha
is truthy and because reading a missing property returns undefined
(which is falsy).
There is, however, one important caveat: truthiness checks fail if the property exists, but has a falsy value (undefined
, null
, false
, 0
, ""
, etc.):
assert.equal(
obj.beta ? 'exists' : 'does not exist',
'does not exist'); // should be: 'exists'
We can delete properties via the delete
operator:
const obj = {
myProp: 123,
};
assert.deepEqual(Object.keys(obj), ['myProp']);
delete obj.myProp;
assert.deepEqual(Object.keys(obj), []);
Enumerability is an attribute of a property. Non-enumerable properties are ignored by some operations – for example, by Object.keys()
and when spreading properties. By default, most properties are enumerable. The next example shows how to change that and how it affects spreading.
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4,
enumerable: false,
},
});
// Non-enumerable properties are ignored by spreading:
assert.deepEqual(
{...obj},
{
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
);
Object.defineProperties()
is explained later in this chapter. The next subsection shows how these operations are affected by enumerability:
Object.keys()
etc.
enumerable | non-e. | string | symbol | |
---|---|---|---|---|
Object.keys() | ✔ | ✔ | ||
Object.getOwnPropertyNames() | ✔ | ✔ | ✔ | |
Object.getOwnPropertySymbols() | ✔ | ✔ | ✔ | |
Reflect.ownKeys() | ✔ | ✔ | ✔ | ✔ |
Table 30.1: Standard library methods for listing own (non-inherited) property keys. All of them return Arrays with strings and/or symbols.
Each of the methods in table 30.1 returns an Array with the own property keys of the parameter. In the names of the methods, we can see that the following distinction is made:
Object.keys()
is older and does not yet follow this convention.)
To demonstrate the four operations, we revisit the example from the previous subsection:
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
Object.keys(obj),
['enumerableStringKey']
);
assert.deepEqual(
Object.getOwnPropertyNames(obj),
['enumerableStringKey', 'nonEnumStringKey']
);
assert.deepEqual(
Object.getOwnPropertySymbols(obj),
[enumerableSymbolKey, nonEnumSymbolKey]
);
assert.deepEqual(
Reflect.ownKeys(obj),
[
'enumerableStringKey', 'nonEnumStringKey',
enumerableSymbolKey, nonEnumSymbolKey,
]
);
Object.values()
Object.values()
lists the values of all own enumerable string-keyed properties of an object:
const firstName = Symbol('firstName');
const obj = {
[firstName]: 'Jane',
lastName: 'Doe',
};
assert.deepEqual(
Object.values(obj),
['Doe']);
Object.entries()
[ES2017]Object.entries(obj)
returns an Array with one key-value pair for each of its properties:
const firstName = Symbol('firstName');
const obj = {
[firstName]: 'Jane',
lastName: 'Doe',
};
Object.defineProperty(
obj, 'city', {value: 'Metropolis', enumerable: false}
);
assert.deepEqual(
Object.entries(obj),
[
['lastName', 'Doe'],
]);
Object.entries()
The following function is a simplified version of Object.entries()
:
function entries(obj) {
return Object.keys(obj)
.map(key => [key, obj[key]]);
}
Exercise: Object.entries()
exercises/objects/find_key_test.mjs
Own (non-inherited) properties of objects are always listed in the following order:
The following example demonstrates that property keys are sorted according to these rules:
> Object.keys({b:'',a:'', 10:'',2:''})
[ '2', '10', 'b', 'a' ]
The order of properties
The ECMAScript specification describes in more detail how properties are ordered.
Object.fromEntries()
[ES2019]Given an iterable over [key, value] pairs, Object.fromEntries()
creates an object:
const symbolKey = Symbol('symbolKey');
assert.deepEqual(
Object.fromEntries(
[
['stringKey', 1],
[symbolKey, 2],
]
),
{
stringKey: 1,
[symbolKey]: 2,
}
);
Object.fromEntries()
does the opposite of Object.entries()
. However, while Object.entries()
ignores symbol-keyed properties, Object.fromEntries()
doesn’t (see previous example).
To demonstrate both, we’ll use them to implement two tool functions from the library Underscore in the next subsubsections.
pick()
The Underscore function pick()
has the following signature:
pick(object, ...keys)
It returns a copy of object
that has only those properties whose keys are mentioned in the trailing arguments:
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
};
assert.deepEqual(
pick(address, 'street', 'number'),
{
street: 'Evergreen Terrace',
number: '742',
}
);
We can implement pick()
as follows:
function pick(object, ...keys) {
const filteredEntries = Object.entries(object)
.filter(([key, _value]) => keys.includes(key));
return Object.fromEntries(filteredEntries);
}
invert()
The Underscore function invert()
has the following signature:
invert(object)
It returns a copy of object
where the keys and values of all properties are swapped:
assert.deepEqual(
invert({a: 1, b: 2, c: 3}),
{1: 'a', 2: 'b', 3: 'c'}
);
We can implement invert()
like this:
function invert(object) {
const reversedEntries = Object.entries(object)
.map(([key, value]) => [value, key]);
return Object.fromEntries(reversedEntries);
}
Object.fromEntries()
The following function is a simplified version of Object.fromEntries()
:
function fromEntries(iterable) {
const result = {};
for (const [key, value] of iterable) {
let coercedKey;
if (typeof key === 'string' || typeof key === 'symbol') {
coercedKey = key;
} else {
coercedKey = String(key);
}
result[coercedKey] = value;
}
return result;
}
Exercise: Using Object.entries()
and Object.fromEntries()
exercises/objects/omit_properties_test.mjs
If we use plain objects (created via object literals) as dictionaries, we have to look out for two pitfalls.
The first pitfall is that the in
operator also finds inherited properties:
const dict = {};
assert.equal('toString' in dict, true);
We want dict
to be treated as empty, but the in
operator detects the properties it inherits from its prototype, Object.prototype
.
The second pitfall is that we can’t use the property key __proto__
because it has special powers (it sets the prototype of the object):
const dict = {};
dict['__proto__'] = 123;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);
So how do we avoid the two pitfalls?
The following code demonstrates using prototype-less objects as dictionaries:
const dict = Object.create(null); // prototype is `null`
assert.equal('toString' in dict, false); // (A)
dict['__proto__'] = 123;
assert.deepEqual(Object.keys(dict), ['__proto__']);
We avoided both pitfalls:
__proto__
is implemented via Object.prototype
. That means that it is switched off if Object.prototype
is not in the prototype chain.
Exercise: Using an object as a dictionary
exercises/objects/simple_dict_test.mjs
Just as objects are composed of properties, properties are composed of attributes. There are two kinds of properties and they are characterized by their attributes:
value
holds any JavaScript value.
get
, the latter in the attribute set
.
Additionally, there are attributes that both kinds of properties have. The following table lists all attributes and their default values.
Kind of property | Name and type of attribute | Default value |
---|---|---|
All properties | configurable: boolean | false |
enumerable: boolean | false |
|
Data property | value: any | undefined |
writable: boolean | false |
|
Accessor property | get: (this: any) => any | undefined |
set: (this: any, v: any) => void | undefined |
We have already encountered the attributes value
, get
, and set
. The other attributes work as follows:
writable
determines if the value of a data property can be changed.
configurable
determines if the attributes of a property can be changed. If it is false
, then:
value
.
writable
from true
to false
. The rationale behind this anomaly is historical: Property .length
of Arrays has always been writable and non-configurable. Allowing its writable
attribute to be changed enables us to freeze Arrays.
enumerable
influences some operations (such as Object.keys()
). If it is false
, then those operations ignore the property. Enumerability is covered in greater detail earlier in this chapter.
When we are using one of the operations for handling property attributes, attributes are specified via property descriptors: objects where each property represents one attribute. For example, this is how we read the attributes of a property obj.myProp
:
const obj = { myProp: 123 };
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'myProp'),
{
value: 123,
writable: true,
enumerable: true,
configurable: true,
});
And this is how we change the attributes of obj.myProp
:
assert.deepEqual(Object.keys(obj), ['myProp']);
// Hide property `myProp` from Object.keys()
// by making it non-enumerable
Object.defineProperty(obj, 'myProp', {
enumerable: false,
});
assert.deepEqual(Object.keys(obj), []);
Lastly, let’s see what methods and getters look like:
const obj = {
myMethod() {},
get myGetter() {},
};
const propDescs = Object.getOwnPropertyDescriptors(obj);
propDescs.myMethod.value = typeof propDescs.myMethod.value;
propDescs.myGetter.get = typeof propDescs.myGetter.get;
assert.deepEqual(
propDescs,
{
myMethod: {
value: 'function',
writable: true,
enumerable: true,
configurable: true
},
myGetter: {
get: 'function',
set: undefined,
enumerable: true,
configurable: true
}
}
);
Further reading
For more information on property attributes and property descriptors, see Deep JavaScript.
JavaScript has three levels of protecting objects:
Object.preventExtensions(obj)
Object.isExtensible(obj)
Object.seal(obj)
Object.isSealed(obj)
Object.freeze(obj)
Object.isFrozen(obj)
Caveat: Objects are only protected shallowly
All three of the aforementioned Object.*
methods only affect the top level of an object, not objects nested inside it.
This is what using Object.freeze()
looks like:
const frozen = Object.freeze({ x: 2, y: 5 });
assert.throws(
() => frozen.x = 7,
{
name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
}
);
Changing frozen properties only causes an exception in strict mode. In sloppy mode, it fails silently.
Further reading
For more information on freezing and other ways of locking down objects, see Deep JavaScript.
Prototypes are JavaScript’s only inheritance mechanism: Each object has a prototype that is either null
or an object. In the latter case, the object inherits all of the prototype’s properties.
In an object literal, we can set the prototype via the special property __proto__
:
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);
Given that a prototype object can have a prototype itself, we get a chain of objects – the so-called prototype chain. Inheritance gives us the impression that we are dealing with single objects, but we are actually dealing with chains of objects.
Figure 30.2 shows what the prototype chain of obj
looks like.
Non-inherited properties are called own properties. obj
has one own property, .objProp
.
Some operations consider all properties (own and inherited) – for example, getting properties:
> const obj = { one: 1 };
> typeof obj.one // own
'number'
> typeof obj.toString // inherited
'function'
Other operations only consider own properties – for example, Object.keys()
:
> Object.keys(obj)
[ 'one' ]
Read on for another operation that also only considers own properties: setting properties.
Given an object obj
with a chain of prototype objects, it makes sense that setting an own property of obj
only changes obj
. However, setting an inherited property via obj
also only changes obj
. It creates a new own property in obj
that overrides the inherited property. Let’s explore how that works with the following object:
const proto = {
protoProp: 'a',
};
const obj = {
__proto__: proto,
objProp: 'b',
};
In the next code snippet, we set the inherited property obj.protoProp
(line A). That “changes” it by creating an own property: When reading obj.protoProp
, the own property is found first and its value overrides the value of the inherited property.
// In the beginning, obj has one own property
assert.deepEqual(Object.keys(obj), ['objProp']);
obj.protoProp = 'x'; // (A)
// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);
// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');
// The own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');
The prototype chain of obj
is depicted in figure 30.3.
Recommendations for __proto__
:
Don’t use __proto__
as a pseudo-property (a setter of all instances of Object
):
Object
).
For more information on this feature see “Object.prototype.__proto__
(accessor)” (§31.8.8).
Using __proto__
in object literals to set prototypes is different: It’s a feature of object literals that has no pitfalls.
The recommended ways of getting and setting prototypes are:
Getting the prototype of an object:
Object.getPrototypeOf(obj: Object) : Object
The best time to set the prototype of an object is when we are creating it. We can do so via __proto__
in an object literal or via:
Object.create(proto: Object) : Object
If we have to, we can use Object.setPrototypeOf()
to change the prototype of an existing object. But that may affect performance negatively.
This is how these features are used:
const proto1 = {};
const proto2a = {};
const proto2b = {};
const obj1 = {
__proto__: proto1,
};
assert.equal(Object.getPrototypeOf(obj1), proto1);
const obj2 = Object.create(proto2a);
assert.equal(Object.getPrototypeOf(obj2), proto2a);
Object.setPrototypeOf(obj2, proto2b);
assert.equal(Object.getPrototypeOf(obj2), proto2b);
So far, “proto
is a prototype of obj
” always meant “proto
is a direct prototype of obj
”. But it can also be used more loosely and mean that proto
is in the prototype chain of obj
. That looser relationship can be checked via .isPrototypeOf()
:
For example:
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(c.isPrototypeOf(a), false);
assert.equal(a.isPrototypeOf(a), false);
For more information on this method see “Object.prototype.isPrototypeOf()
” (§31.8.6).
Object.hasOwn()
: Is a given property own (non-inherited)? [ES2022]The in
operator (line A) checks if an object has a given property. In contrast, Object.hasOwn()
(lines B and C) checks if a property is own.
const proto = {
protoProp: 'protoProp',
};
const obj = {
__proto__: proto,
objProp: 'objProp',
}
assert.equal('protoProp' in obj, true); // (A)
assert.equal(Object.hasOwn(obj, 'protoProp'), false); // (B)
assert.equal(Object.hasOwn(proto, 'protoProp'), true); // (C)
Alternative before ES2022: .hasOwnProperty()
Before ES2022, we can use another feature: “Object.prototype.hasOwnProperty()
” (§31.8.9). This feature has pitfalls, but the referenced section explains how to work around them.
Consider the following code:
const jane = {
firstName: 'Jane',
describe() {
return 'Person named '+this.firstName;
},
};
const tarzan = {
firstName: 'Tarzan',
describe() {
return 'Person named '+this.firstName;
},
};
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');
We have two objects that are very similar. Both have two properties whose names are .firstName
and .describe
. Additionally, method .describe()
is the same. How can we avoid duplicating that method?
We can move it to an object PersonProto
and make that object a prototype of both jane
and tarzan
:
const PersonProto = {
describe() {
return 'Person named ' + this.firstName;
},
};
const jane = {
__proto__: PersonProto,
firstName: 'Jane',
};
const tarzan = {
__proto__: PersonProto,
firstName: 'Tarzan',
};
The name of the prototype reflects that both jane
and tarzan
are persons.
Figure 30.4 illustrates how the three objects are connected: The objects at the bottom now contain the properties that are specific to jane
and tarzan
. The object at the top contains the properties that are shared between them.
When we make the method call jane.describe()
, this
points to the receiver of that method call, jane
(in the bottom-left corner of the diagram). That’s why the method still works. tarzan.describe()
works similarly.
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');
Looking ahead to the next chapter on classes – this is how classes are organized internally:
“The internals of classes” (§31.3) explains this in more detail.
In principle, objects are unordered. The main reason for ordering properties is so that operations that list entries, keys, or values are deterministic. That helps, e.g., with testing.
Object
Object.*
: creating objects, handling prototypesObject.create(proto, propDescObj?)
[ES5]
proto
.
propDescObj
is an object with property descriptors that is used to define properties in the new object.
> const obj = Object.create(null);
> Object.getPrototypeOf(obj)
null
In the following example, we define own properties via the second parameter:
const obj = Object.create(
null,
{
color: {
value: 'green',
writable: true,
enumerable: true,
configurable: true,
},
}
);
assert.deepEqual(
obj,
{
__proto__: null,
color: 'green',
}
);
Object.getPrototypeOf(obj)
[ES5]
Return the prototype of obj
– which is either an object or null
.
assert.equal(
Object.getPrototypeOf({__proto__: null}), null
);
assert.equal(
Object.getPrototypeOf({}), Object.prototype
);
assert.equal(
Object.getPrototypeOf(Object.prototype), null
);
Object.setPrototypeOf(obj, proto)
[ES6]
Sets the prototype of obj
to proto
(which must be null
or an object) and returns the former.
const obj = {};
assert.equal(
Object.getPrototypeOf(obj), Object.prototype
);
Object.setPrototypeOf(obj, null);
assert.equal(
Object.getPrototypeOf(obj), null
);
Object.*
: property attributesObject.defineProperty(obj, propKey, propDesc)
[ES5]
obj
, as specified by the property key propKey
and the property descriptor propDesc
.
obj
.
const obj = {};
Object.defineProperty(
obj, 'color',
{
value: 'green',
writable: true,
enumerable: true,
configurable: true,
}
);
assert.deepEqual(
obj,
{
color: 'green',
}
);
Object.defineProperties(obj, propDescObj)
[ES5]
obj
, as specified by the object propDescObj
with property descriptors.
obj
.
const obj = {};
Object.defineProperties(
obj,
{
color: {
value: 'green',
writable: true,
enumerable: true,
configurable: true,
},
}
);
assert.deepEqual(
obj,
{
color: 'green',
}
);
Object.getOwnPropertyDescriptor(obj, propKey)
[ES5]
obj
whose key is propKey
. If no such property exists, it returns undefined
.
> Object.getOwnPropertyDescriptor({a: 1, b: 2}, 'a')
{ value: 1, writable: true, enumerable: true, configurable: true }
> Object.getOwnPropertyDescriptor({a: 1, b: 2}, 'x')
undefined
Object.getOwnPropertyDescriptors(obj)
[ES2017]
obj
.
> Object.getOwnPropertyDescriptors({a: 1, b: 2})
{
a: { value: 1, writable: true, enumerable: true, configurable: true },
b: { value: 2, writable: true, enumerable: true, configurable: true },
}
Object.*
: property keys, values, entriesObject.keys(obj)
[ES5]
Returns an Array with all own enumerable property keys that are strings.
const enumSymbolKey = Symbol('enumSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = Object.defineProperties(
{},
{
enumStringKey: {
value: 1, enumerable: true,
},
[enumSymbolKey]: {
value: 2, enumerable: true,
},
nonEnumStringKey: {
value: 3, enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4, enumerable: false,
},
}
);
assert.deepEqual(
Object.keys(obj),
['enumStringKey']
);
Object.getOwnPropertyNames(obj)
[ES5]
Returns an Array with all own property keys that are strings (enumerable and non-enumerable ones).
const enumSymbolKey = Symbol('enumSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = Object.defineProperties(
{},
{
enumStringKey: {
value: 1, enumerable: true,
},
[enumSymbolKey]: {
value: 2, enumerable: true,
},
nonEnumStringKey: {
value: 3, enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4, enumerable: false,
},
}
);
assert.deepEqual(
Object.getOwnPropertyNames(obj),
['enumStringKey', 'nonEnumStringKey']
);
Object.getOwnPropertySymbols(obj)
[ES6]
Returns an Array with all own property keys that are symbols (enumerable and non-enumerable ones).
const enumSymbolKey = Symbol('enumSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
const obj = Object.defineProperties(
{},
{
enumStringKey: {
value: 1, enumerable: true,
},
[enumSymbolKey]: {
value: 2, enumerable: true,
},
nonEnumStringKey: {
value: 3, enumerable: false,
},
[nonEnumSymbolKey]: {
value: 4, enumerable: false,
},
}
);
assert.deepEqual(
Object.getOwnPropertySymbols(obj),
[enumSymbolKey, nonEnumSymbolKey]
);
Object.values(obj)
[ES2017]
Returns an Array with the values of all enumerable own string-keyed properties.
> Object.values({a: 1, b: 2})
[ 1, 2 ]
Object.entries(obj)
[ES2017]
obj
.
Object.fromEntries()
const obj = {
a: 1,
b: 2,
[Symbol('myKey')]: 3,
};
assert.deepEqual(
Object.entries(obj),
[
['a', 1],
['b', 2],
// Property with symbol key is ignored
]
);
Object.fromEntries(keyValueIterable)
[ES2019]
keyValueIterable
.
Object.entries()
> Object.fromEntries([['a', 1], ['b', 2]])
{ a: 1, b: 2 }
Object.hasOwn(obj, key)
[ES2022]
true
if obj
has an own property whose key is key
. If not, it returns false
.
> Object.hasOwn({a: 1, b: 2}, 'a')
true
> Object.hasOwn({a: 1, b: 2}, 'x')
false
Object.*
: protecting objectsMore information: “Protecting objects from being changed” (§30.9)
Object.preventExtensions(obj)
[ES5]
obj
non-extensible and returns it.
obj
is non-extensible: We can’t add properties or change its prototype.
obj
is changed (shallow change). Nested objects are not affected.
Object.isExtensible()
Object.isExtensible(obj)
[ES5]
true
if obj
is extensible and false
if it isn’t.
Object.preventExtensions()
Object.seal(obj)
[ES5]
obj
and returns it.
obj
is non-extensible: We can’t add properties or change its prototype.
obj
is sealed: Additionally, all of its properties are unconfigurable.
obj
is changed (shallow change). Nested objects are not affected.
Object.isSealed()
Object.isSealed(obj)
[ES5]
true
if obj
is sealed and false
if it isn’t.
Object.seal()
Object.freeze(obj)
[ES5]
obj
and returns it.
obj
is non-extensible: We can’t add properties or change its prototype.
obj
is sealed: Additionally, all of its properties are unconfigurable.
obj
is frozen: Additionally, all of its properties are non-writable.
obj
is changed (shallow change). Nested objects are not affected.
Object.isFrozen()
const frozen = Object.freeze({ x: 2, y: 5 });
assert.equal(
Object.isFrozen(frozen), true
);
assert.throws(
() => frozen.x = 7,
{
name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
}
);
Object.isFrozen(obj)
[ES5]
true
if obj
is frozen.
Object.freeze()
Object.*
: miscellaneousObject.assign(target, ...sources)
[ES6]
Assigns all enumerable own string-keyed properties of each of the sources
to target
and returns target
.
> const obj = {a: 1, b: 1};
> Object.assign(obj, {b: 2, c: 2}, {d: 3})
{ a: 1, b: 2, c: 2, d: 3 }
> obj
{ a: 1, b: 2, c: 2, d: 3 }
Object.groupBy(items, computeGroupKey)
[ES2024]
Object.groupBy<K extends PropertyKey, T>(
items: Iterable<T>,
computeGroupKey: (item: T, index: number) => K,
): {[key: K]: Array<T>}
computeGroupKey
returns a group key for each of the items
.
Object.groupBy()
is an object where:
assert.deepEqual(
Object.groupBy(
['orange', 'apricot', 'banana', 'apple', 'blueberry'],
(str) => str[0] // compute group key
),
{
__proto__: null,
'o': ['orange'],
'a': ['apricot', 'apple'],
'b': ['banana', 'blueberry'],
}
);
Object.is(value1, value2)
[ES6]
Is mostly equivalent to value1 === value2
– with two exceptions:
> NaN === NaN
false
> Object.is(NaN, NaN)
true
> -0 === 0
true
> Object.is(-0, 0)
false
NaN
values to be equal can be useful – e.g., when searching for a value in an Array.
-0
is rare and it’s usually best to pretend it is the same as 0
.
Object.prototype.*
Object.prototype
has the following properties:
Object.prototype.__proto__
(getter and setter)
Object.prototype.hasOwnProperty()
Object.prototype.isPrototypeOf()
Object.prototype.propertyIsEnumerable()
Object.prototype.toLocaleString()
Object.prototype.toString()
Object.prototype.valueOf()
These methods are explained in detail in “Quick reference: Object.prototype.*
” (§31.8.1).
Reflect
Reflect
provides functionality for JavaScript proxies that is also occasionally useful elsewhere:
Reflect.apply(target, thisArgument, argumentsList)
target
with the arguments provided by argumentsList
and this
set to thisArgument
.
target.apply(thisArgument, argumentsList)
Reflect.construct(target, argumentsList, newTarget=target)
new
operator as a function.
target
is the constructor to invoke.
newTarget
points to the constructor that started the current chain of constructor calls.
Reflect.defineProperty(target, propertyKey, propDesc)
Object.defineProperty()
.
Reflect.deleteProperty(target, propertyKey)
The delete
operator as a function. It works slightly differently, though:
true
if it successfully deleted the property or if the property never existed.
false
if the property could not be deleted and still exists.
In sloppy mode, the delete
operator returns the same results as this method. But in strict mode, it throws a TypeError
instead of returning false
.
The only way to protect properties from deletion is by making them non-configurable.
Reflect.get(target, propertyKey, receiver=target)
A function that gets properties. The optional parameter receiver
is needed if get
reaches a getter (somewhere in the prototype chain). Then it provides the value for this
.
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Same as Object.getOwnPropertyDescriptor()
.
Reflect.getPrototypeOf(target)
Same as Object.getPrototypeOf()
.
Reflect.has(target, propertyKey)
The in
operator as a function.
Reflect.isExtensible(target)
Same as Object.isExtensible()
.
Reflect.ownKeys(target)
Returns all own property keys (strings and symbols) in an Array.
Reflect.preventExtensions(target)
Object.preventExtensions()
.
Reflect.set(target, propertyKey, value, receiver=target)
Reflect.setPrototypeOf(target, proto)
Object.setPrototypeOf()
.
Reflect.*
vs. Object.*
General recommendations:
Object.*
whenever you can.
Reflect.*
when working with ECMAScript proxies. Its methods are well adapted to ECMAScript’s meta-object protocol (MOP) which also return boolean error flags instead of exceptions.
What are use cases for Reflect
beyond proxies?
Reflect.ownKeys()
lists all own property keys – functionality that isn’t provided anywhere else.
Same functionality as Object
but 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).
Object.defineProperty(obj, propKey, propDesc)
Object.preventExtensions(obj)
Object.setPrototypeOf(obj, proto)
Operators as functions: The following Reflect
methods implement functionality that is otherwise only available via operators:
Reflect.construct(target, argumentsList, newTarget=target)
Reflect.deleteProperty(target, propertyKey)
Reflect.get(target, propertyKey, receiver=target)
Reflect.has(target, propertyKey)
Reflect.set(target, propertyKey, value, receiver=target)
Shorter version of apply()
: If we want to be completely safe about invoking the method apply()
on a function, we can’t do so via dynamic dispatch, because the function may have an own property with the key 'apply'
:
func.apply(thisArg, argArray) // not safe
Function.prototype.apply.call(func, thisArg, argArray) // safe
Using Reflect.apply()
is shorter:
Reflect.apply(func, thisArg, argArray)
No exceptions when deleting properties: the delete
operator throws in strict mode if we try to delete a non-configurable own property. Reflect.deleteProperty()
returns false
in that case.