...
) [ES2018]
this
in
operator: is there a property with a given key?Object.values()
Object.entries()
[ES2017]Object.fromEntries()
[ES2019].toString()
.valueOf()
In this book, JavaScript’s style of object-oriented programming (OOP) is introduced in four steps. This chapter covers step 1; the next chapter covers steps 2–4. The steps are (fig. 8):
In JavaScript:
Objects play two roles in JavaScript:
Records: Objects-as-records have a fixed number of properties, whose keys are known at development time. Their values can have different types.
Dictionaries: Objects-as-dictionaries have a variable number of properties, whose keys are not known at development time. All of their values have the same type.
These roles influence how objects are explained in this chapter:
Let’s first explore the role record of objects.
Object literals are one way of creating objects-as-records. 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 name and that name is the same as the key, we can omit the key.
function createPoint(x, y) {
return {x, y};
}.deepEqual(
assertcreatePoint(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
.equal(jane.first, 'Jane'); // (A) assert
Getting an unknown property produces undefined
:
.equal(jane.unknownProperty, undefined); assert
This is how we set (write to) a property:
const obj = {
prop: 1,
;
}.equal(obj.prop, 1);
assert.prop = 2; // (A)
obj.equal(obj.prop, 2); assert
We just changed an existing property via setting. If we set an unknown property, we create a new entry:
const obj = {}; // empty object
.deepEqual(
assertObject.keys(obj), []);
.unknownProperty = 'abc';
obj.deepEqual(
assertObject.keys(obj), ['unknownProperty']);
The following code shows how to create the method .says()
via an object literal:
const jane = {
first: 'Jane', // data property
says(text) { // method
return `${this.first} says “${text}”`; // (A)
, // comma as separator (optional at end)
};
}.equal(jane.says('hello'), 'Jane says “hello”'); assert
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 §28.4 “Methods and the special variable this
”). That enables method .says()
to access the sibling property .first
in line A.
There are two kinds of accessors in JavaScript:
A getter is created by prefixing a method definition with the modifier get
:
const jane = {
first: 'Jane',
last: 'Doe',
full() {
get return `${this.first} ${this.last}`;
,
};
}
.equal(jane.full, 'Jane Doe');
assert.first = 'John';
jane.equal(jane.full, 'John Doe'); assert
A setter is created by prefixing a method definition with the modifier set
:
const jane = {
first: 'Jane',
last: 'Doe',
full(fullName) {
set const parts = fullName.split(' ');
this.first = parts[0];
this.last = parts[1];
,
};
}
.full = 'Richard Roe';
jane.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe'); assert
Exercise: Creating an object via an object literal
exercises/single-objects/color_point_object_test.mjs
...
) [ES2018]Inside a function call, spreading (...
) turns the iterated values of an iterable object into arguments.
Inside an object literal, a spread property adds the properties of another object to the current one:
> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}{ foo: 1, bar: 2, baz: 3 }
If property keys clash, the property that is mentioned last “wins”:
> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}{ foo: 1, bar: 2, baz: 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 of Arrays is hidden from this kind of operation (it is not enumerable; see §28.8.3 “Property attributes and property descriptors [ES5]” for more information).
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: {foo: 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:
.a = 2;
copy.deepEqual(
assert, { a: 1, b: {foo: true} }); // no change original
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.
.b.foo = false;
copy.deepEqual(
assert, { a: 1, b: {foo: false} }); original
JavaScript doesn’t have built-in support for deep copying
Deep copies of objects (where all levels are copied) are notoriously difficult to do generically. Therefore, JavaScript does not have a built-in operation for them (for now). If we need such an operation, we have to implement it ourselves.
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 = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};
const allData = {...DEFAULTS, ...providedData};
.deepEqual(allData, {foo: 1, bar: 'b'}); assert
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 = {foo: 1};
const allData = {foo: 'a', bar: 'b', ...providedData};
.deepEqual(allData, {foo: 1, bar: 'b'}); assert
So far, we have encountered one way of changing a property .foo
of an object: We set it (line A) and mutate the object. That is, this way of changing a property is destructive.
const obj = {foo: 'a', bar: 'b'};
.foo = 1; // (A)
obj.deepEqual(obj, {foo: 1, bar: 'b'}); assert
With spreading, we can change .foo
non-destructively – we make a copy of obj
where .foo
has a different value:
const obj = {foo: 'a', bar: 'b'};
const updatedObj = {...obj, foo: 1};
.deepEqual(updatedObj, {foo: 1, bar: 'b'}); assert
Exercise: Non-destructively updating a property via spreading (fixed key)
exercises/single-objects/update_name_test.mjs
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:
.equal(typeof jane.says, 'function'); assert
Why is that? We learned in the chapter on callable values, that ordinary functions play several roles. Method is one of those roles. Therefore, under the hood, 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) {
.equal(this, obj); // (A)
assert.equal(x, 'a');
assert.equal(y, 'b');
assert
};
}.someMethod('a', 'b'); // (B) obj
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).
This is an important point: The best way to understand this
is as an implicit parameter of ordinary functions (and therefore methods, too).
.call()
Methods are functions and in §25.7 “Methods of functions: .call()
, .apply()
, .bind()
”, we saw that 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:
.someMethod('a', 'b') obj
This invocation is equivalent to:
.someMethod.call(obj, 'a', 'b'); obj
Which is also equivalent to:
const func = obj.someMethod;
.call(obj, 'a', 'b'); func
.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');
.equal(func(), 'Jane says “hello”'); assert
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 methodsWe 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
.throws(
assert=> func('hello'), // (A)
()
{name: 'TypeError',
message: "Cannot read property 'first' of undefined",
; })
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:
.throws(
assert=> jane.says.call(undefined, 'hello'), // `this` is undefined!
()
{name: 'TypeError',
message: "Cannot read property 'first' of undefined",
; })
How do we fix this? We need to use .bind()
to extract method .says()
:
const func2 = jane.says.bind(jane);
.equal(func2('hello'), 'Jane says “hello”'); assert
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);
.equal(func3('hello'), 'Jane says “hello”'); assert
The following is a simplified version of code that we may see in actual web development:
class ClickHandler {
constructor(id, elem) {
this.id = id;
.addEventListener('click', this.handleClick); // (A)
elem
}handleClick(event) {
alert('Clicked ' + this.id);
} }
In line A, we don’t extract the method .handleClick()
properly. Instead, we should do:
.addEventListener('click', this.handleClick.bind(this)); elem
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/single-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)
;
}),
};
}.throws(
assert=> prefixer.prefixStringArray(['a', 'b']),
() /^TypeError: Cannot read property 'prefix' of undefined$/);
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 (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;
;
}),
};
}.deepEqual(
assert.prefixStringArray(['a', 'b']),
prefixer'==> 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 you follow the advice in §25.3.4 “Recommendation: prefer specialized functions over ordinary functions”, you 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 you can either use arrow functions or function declarations. If you do the latter, 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 callnew
: this
refers to newly created instanceWe 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 rarely useful.
The following kinds of optional chaining operations exist:
?.prop // optional static property access
obj?.[«expr»] // optional dynamic property access
obj?.(«arg0», «arg1») // optional function or method call func
The rough idea is:
undefined
nor null
, then perform the operation after the question mark.undefined
.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.address?.street?.name);
p .deepEqual(
assert, ['Sesame Street', undefined, undefined]
streetNames; )
The nullish coalescing operator allows us to use the default value '(no street)'
instead of undefined
:
const streetNames = persons.map(
=> p.address?.street?.name ?? '(no name)');
p .deepEqual(
assert, ['Sesame Street', '(no name)', '(no name)']
streetNames; )
The following two expressions are equivalent:
?.prop
o!== undefined && o !== null) ? o.prop : undefined (o
Examples:
.equal(undefined?.prop, undefined);
assert.equal(null?.prop, undefined);
assert.equal({prop:1}?.prop, 1); assert
The following two expressions are equivalent:
?.[«expr»]
o!== undefined && o !== null) ? o[«expr»] : undefined (o
Examples:
const key = 'prop';
.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1); assert
The following two expressions are equivalent:
?.(arg0, arg1)
f!== undefined && f !== null) ? f(arg0, arg1) : undefined (f
Examples:
.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123'); assert
Note that this operator produces an error if its left-hand side is not callable:
.throws(
assert=> 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 accesses and function/method invocations, evaluation stops once the first optional operator encounters undefined
or null
at its left-hand side:
function isInvoked(obj) {
let invoked = false;
?.a.b.m(invoked = true);
objreturn invoked;
}
.equal(
assertisInvoked({a: {b: {m() {}}}}), true);
// The left-hand side of ?. is undefined
// and the assignment is not executed
.equal(
assertisInvoked(undefined), false);
This behavior differs from a normal operator/function where JavaScript always evaluates all operands/arguments before evaluating the operator/function. It is called short-circuiting. Other short-circuiting operators:
a && b
a || b
c ? t : e
o?.[x]
and f?.()
?The syntaxes of the following two optional operator are not ideal:
?.[«expr»] // better: obj?[«expr»]
obj?.(«arg0», «arg1») // better: func?(«arg0», «arg1») func
Alas, the less elegant syntax is necessary, because distinguishing the ideal syntax (first expression) from the conditional operator (second expression) is too complicated:
?['a', 'b', 'c'].map(x => x+x)
obj? ['a', 'b', 'c'].map(x => x+x) : [] obj
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 records. 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: 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 objects-as-records. This section concludes with tips for actually using objects as dictionaries (spoiler: use Maps if you can).
So far, we have always used objects as records. Property keys were fixed tokens that had to be valid identifiers and internally became strings:
const obj = {
mustBeAnIdentifier: 123,
;
}
// Get property
.equal(obj.mustBeAnIdentifier, 123);
assert
// Set property
.mustBeAnIdentifier = 'abc';
obj.equal(obj.mustBeAnIdentifier, 'abc'); assert
As a next step, we’ll go beyond this limitation for property keys: In this section, we’ll use arbitrary fixed strings as keys. In the next subsection, we’ll dynamically compute keys.
Two techniques allow 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
.equal(obj['Can be any string!'], 123);
assert
// Set property
'Can be any string!'] = 'abc';
obj[.equal(obj['Can be any string!'], 'abc'); assert
We can also use these techniques for methods:
const obj = {
'A nice method'() {
return 'Yes!';
,
};
}
.equal(obj['A nice method'](), 'Yes!'); assert
So far, property keys were always 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,
['f'+'o'+'o']: 123,
[Symbol.toStringTag]: 'Goodbye', // (A)
[;
}
.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye'); assert
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:
.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 123); assert
Methods can have computed property keys, too:
const methodKey = Symbol();
const obj = {
[methodKey]() {return 'Yes!';
,
};
}
.equal(obj[methodKey](), 'Yes!'); assert
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/single-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 = {
foo: 'abc',
bar: false,
;
}
.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false); assert
We can also use a truthiness check to determine if a property exists:
.equal(
assert.foo ? 'exists' : 'does not exist',
obj'exists');
.equal(
assert.unknownKey ? 'exists' : 'does not exist',
obj'does not exist');
The previous checks work because obj.foo
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.):
.equal(
assert.bar ? 'exists' : 'does not exist',
obj'does not exist'); // should be: 'exists'
We can delete properties via the delete
operator:
const obj = {
foo: 123,
;
}.deepEqual(Object.keys(obj), ['foo']);
assert
delete obj.foo;
.deepEqual(Object.keys(obj), []); assert
enumerable | non-e. | string | symbol | |
---|---|---|---|---|
Object.keys() |
✔ |
✔ |
||
Object.getOwnPropertyNames() |
✔ |
✔ |
✔ |
|
Object.getOwnPropertySymbols() |
✔ |
✔ |
✔ |
|
Reflect.ownKeys() |
✔ |
✔ |
✔ |
✔ |
Each of the methods in tbl. 19 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:
The next section describes the term enumerable and demonstrates each of the methods.
Enumerability is an attribute of a property. Non-enumerable properties are ignored by some operations – for example, by Object.keys()
(see tbl. 19) and by spread properties. By default, most properties are enumerable. The next example shows how to change that. It also demonstrates the various ways of listing property keys.
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
: 2,
[enumerableSymbolKey]
}
// For non-enumerable properties, we need a more powerful tool
Object.defineProperties(obj, {
nonEnumStringKey: {
value: 3,
enumerable: false,
,
}: {
[nonEnumSymbolKey]value: 4,
enumerable: false,
,
};
})
.deepEqual(
assertObject.keys(obj),
'enumerableStringKey' ]);
[ .deepEqual(
assertObject.getOwnPropertyNames(obj),
'enumerableStringKey', 'nonEnumStringKey' ]);
[ .deepEqual(
assertObject.getOwnPropertySymbols(obj),
, nonEnumSymbolKey ]);
[ enumerableSymbolKey.deepEqual(
assertReflect.ownKeys(obj),
['enumerableStringKey', 'nonEnumStringKey',
, nonEnumSymbolKey,
enumerableSymbolKey; ])
Object.defineProperties()
is explained later in this chapter.
Object.values()
Object.values()
lists the values of all enumerable properties of an object:
const obj = {foo: 1, bar: 2};
.deepEqual(
assertObject.values(obj),
1, 2]); [
Object.entries()
[ES2017]Object.entries()
lists key-value pairs of enumerable properties. Each pair is encoded as a two-element Array:
const obj = {foo: 1, bar: 2};
.deepEqual(
assertObject.entries(obj),
['foo', 1],
['bar', 2],
[; ])
Exercise: Object.entries()
exercises/single-objects/find_key_test.mjs
Own (non-inherited) properties of objects are always listed in the following order:
The following example demonstrates how property keys are sorted according to these rules:
> Object.keys({b:0,a:0, 10:0,2:0})[ '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:
.deepEqual(
assertObject.fromEntries([['foo',1], ['bar',2]]),
{foo: 1,
bar: 2,
}; )
Object.fromEntries()
does the opposite of Object.entries()
.
To demonstrate both, we’ll use them to implement two tool functions from the library Underscore in the next subsubsections.
pick(object, ...keys)
pick
returns a copy of object
that only has those properties whose keys are mentioned as arguments:
const address = {
street: 'Evergreen Terrace',
number: '742',
city: 'Springfield',
state: 'NT',
zip: '49007',
;
}.deepEqual(
assertpick(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(object)
invert
returns a copy of object
where the keys and values of all properties are swapped:
.deepEqual(
assertinvert({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') {
= key;
coercedKey else {
} = String(key);
coercedKey
}= value;
result[coercedKey]
}return result;
}
Exercise: Object.entries()
and Object.fromEntries()
exercises/single-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 = {};
.equal('toString' in dict, true); assert
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 = {};
'__proto__'] = 123;
dict[// No property was added to dict:
.deepEqual(Object.keys(dict), []); assert
So how do you avoid the two pitfalls?
The following code demonstrates using objects without prototypes as dictionaries:
const dict = Object.create(null); // no prototype
.equal('toString' in dict, false); // (A)
assert
'__proto__'] = 123;
dict[.deepEqual(Object.keys(dict), ['__proto__']); assert
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/single-objects/simple_dict_test.mjs
Object.prototype
defines several standard methods that can be overridden to configure how an object is treated by the language. Two important ones are:
.toString()
.valueOf()
.toString()
.toString()
determines how objects are converted to strings:
> String({toString() { return 'Hello!' }})'Hello!'
> String({})'[object Object]'
.valueOf()
.valueOf()
determines how objects are converted to numbers:
> Number({valueOf() { return 123 }})123
> Number({})NaN
The following subsections give brief overviews of a few advanced topics.
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 = { foo: 1 };
const result = Object.assign(
,
targetbar: 2},
{baz: 3, bar: 4});
{
.deepEqual(
assert, { foo: 1, bar: 4, baz: 3 });
result// target was modified and returned:
.equal(result, target); assert
The use cases for Object.assign()
are similar to those for spread properties. In a way, it spreads destructively.
Object.freeze(obj)
makes obj
completely immutable: We can’t change properties, add properties, or change its prototype – for example:
const frozen = Object.freeze({ x: 2, y: 5 });
.throws(
assert=> { frozen.x = 7 },
()
{name: 'TypeError',
message: /^Cannot assign to read only property 'x'/,
; })
There is one caveat: Object.freeze(obj)
freezes shallowly. That is, only the properties of obj
are frozen but not objects stored in properties.
More information
For more information on freezing and other ways of locking down objects, see Deep JavaScript.
Just as objects are composed of properties, properties are composed of attributes. The value of a property is only one of several attributes. Others include:
writable
: Is it possible to change the value of the property?enumerable
: Is the property considered by Object.keys()
, spreading, etc.?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.foo
:
const obj = { foo: 123 };
.deepEqual(
assertObject.getOwnPropertyDescriptor(obj, 'foo'),
{value: 123,
writable: true,
enumerable: true,
configurable: true,
; })
And this is how we set the attributes of a property obj.bar
:
const obj = {
foo: 1,
bar: 2,
;
}
.deepEqual(Object.keys(obj), ['foo', 'bar']);
assert
// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
enumerable: false,
;
})
.deepEqual(Object.keys(obj), ['foo']); assert
Further reading:
Quiz
See quiz app.