- 25. Single objects
- 25.1. The two roles of objects in JavaScript
- 25.2. Objects as records
- 25.3. Spreading into object literals (…)
- 25.4. Methods
- 25.4.1. Methods are properties whose values are functions
- 25.4.2. .call(): explicit parameter this
- 25.4.3. .bind(): pre-filling this and parameters of functions
- 25.4.4. this pitfall: extracting methods
- 25.4.5. this pitfall: accidentally shadowing this
- 25.4.6. Avoiding the pitfalls of this
- 25.4.7. The value of this in various contexts
- 25.5. Objects as dictionaries
- 25.5.1. Arbitrary fixed strings as property keys
- 25.5.2. Computed property keys
- 25.5.3. The in operator: is there a property with a given key?
- 25.5.4. Deleting properties
- 25.5.5. Dictionary pitfalls
- 25.5.6. Listing property keys
- 25.5.7. Listing property values via Object.values()
- 25.5.8. Listing property entries via Object.entries()
- 25.5.9. Properties are listed deterministically
- 25.5.10. Assembling objects via Object.fromEntries()
- 25.6. Standard methods
- 25.7. Advanced topics
25. Single objects
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. 7):
- Single objects: How do objects, JavaScript’s basic OOP building blocks, work in isolation?
- Prototype chains: Each object has a chain of zero or more prototype objects. Prototypes are JavaScript’s core inheritance mechanism.
- Classes: JavaScript’s classes are factories for objects. The relationship between a class and its instances is based on prototypal inheritance.
- Subclassing: The relationship between a subclass and its superclass is also based on prototypal inheritance.
Figure 7: This book introduces object-oriented programming in JavaScript in four steps.
25.1. The two roles of objects in JavaScript
In JavaScript, an object is a set of key-value entries that are called properties.
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. This way of using objects is covered first in this chapter.
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. It is usually better to use Maps as dictionaries than objects (which is covered later in this chapter).
25.2. Objects as records
25.2.1. Object literals: properties
Objects as records are created via so-called object literals. Object literals are a stand-out feature of JavaScript: they allow you to directly create objects. No need for classes! This is an example:
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):
- The first property has the key
first
and the value'Jane'
. - The second property has the key
last
and the value'Doe'
.
If they are written this way, property keys must follow the rules of JavaScript variable names, with the exception that reserved words are allowed.
The properties are accessed as follows:
25.2.2. Object literals: property value shorthands
Whenever the value of a property is defined via a variable name and that name is the same as the key, you can omit the key.
25.2.3. Terminology: property keys, property names, property symbols
Given that property keys can be strings and symbols, the following distinction is made:
- If a property key is a string, it is also called a property name.
If a property key is a symbol, it is also called a property symbol.
This terminology is used in the JavaScript standard library (“own” means “not inherited” and is explained in the next chapter):Object.keys(obj)
: returns all property keys ofobj
Object.getOwnPropertyNames(obj)
Object.getOwnPropertySymbols(obj)
25.2.4. Getting properties
This is how you get (read) a property:
If obj
does not have a property whose key is propKey
, this expression evaluates to undefined
:
25.2.5. Setting properties
This is how you set (write to) a property:
If obj
already has a property whose key is propKey
, this statement changes that property. Otherwise, it creates a new property:
25.2.6. Object literals: methods
The following code shows how to create the method .describe()
via an object literal:
During the method call jane.says('hello')
, jane
is called the receiver of the method call and assigned to the special variable this
. That enables method .says()
to access the sibling property .first
in line A.
25.2.7. Object literals: accessors
There are two kinds of accessors in JavaScript:
- A getter is a method that is invoked by getting (reading) a property.
- A setter is a method that is invoked by setting (writing) a property.
25.2.7.1. Getters
A getter is created by prefixing a method definition with the keyword get
:
25.2.7.2. Setters
A setter is created by prefixing a method definition with the keyword set
:
25.3. Spreading into object literals (…)
We have already seen spreading (…
) being used in function calls, where it turns the contents of an iterable into arguments.
Inside an object literal, a spread property adds the properties of another object to the current one:
If property keys clash, the property that is mentioned last “wins”:
25.3.1. Use case for spreading: copying objects
You can use spread to create a copy of an object original
:
Caveat – copying is shallow: copy
is a fresh object with a copy of all properties (key-value pairs) of original
. But if property values are objects, then those are not copied; they are shared between original
and copy
. The following code demonstrates what that means.
const original = { a: 1, b: {foo: true} };
const copy = {...original};
// The first level is a true copy:
assert.deepEqual(
copy, { a: 1, b: {foo: true} });
original.a = 2;
assert.deepEqual(
copy, { a: 1, b: {foo: true} }); // no change
// Deeper levels are not copied:
original.b.foo = false;
// The value of property `b` is shared
// between original and copy.
assert.deepEqual(
copy, { a: 1, b: {foo: false} });
25.3.2. Use case for spreading: default values for missing properties
If one of the inputs of your code is an object with data, you can make properties optional if you specify default values for them. One technique for doing so is via an object whose properties contain the default values. In the following example, that object is DEFAULTS
:
The result, the object allData
, is created by creating a copy of DEFAULTS
and overriding its properties with those of providedData
.
But you don’t need an object to specify the default values, you can also specify them inside the object literal, individually:
25.3.3. Use case for spreading: non-destructively changing properties
So far, we have encountered one way of changing a property of an object: We set it and mutate the object. That is, this way of changing a property is destructive
With spreading, you can change a property non-destructively: You make a copy of the object where the property has a different value.
For example, this code non-destructively updates property .foo
:
25.4. Methods
25.4.1. Methods are properties whose values are functions
Let’s revisit the example that was used to introduce methods:
Somewhat surprisingly, methods are functions:
Why is that? Remember that, in the chapter on callable entities, we learned that ordinary functions play several roles. Method is one of those roles. Therefore, under the hood, jane
roughly looks as follows.
25.4.2. .call(): explicit parameter this
Remember that each function someFunc
is also an object and therefore has methods. One such method is .call()
– it lets you call functions while specifying this
explicitly:
25.4.2.1. Methods and .call()
If you make a method call, this
is always an implicit parameter:
As an aside, that means that there are actually two different dot operators:
- One for accessing properties:
obj.prop
- One for making method calls:
obj.prop()
They are different in that (2) is not just (1), followed by the function call operator()
. Instead, (2) additionally specifies a value forthis
(as shown in the previous example).
25.4.2.2. Functions and .call()
However, this
is also an implicit parameter if you function-call an ordinary function:
That is, during a function call, ordinary functions have a this
, but it is set to undefined
, which indicates that it doesn’t really have a purpose here.
Next, we’ll examine the pitfalls of using this
. Before we can do that, we need one more tool: the method .bind()
of functions.
25.4.3. .bind(): pre-filling this and parameters of functions
.bind()
is another method of function objects. This method is invoked as follows.
.bind()
returns a new function boundFunc()
. Calling that function invokes someFunc()
with this
set to thisValue
and these parameters: arg1
, arg2
, arg3
, followed by the parameters of boundFunc()
.
That is, the following two function calls are equivalent:
Another way of pre-filling this
and parameters, is via an arrow function:
Therefore, .bind()
can be implemented as a real function as follows:
25.4.3.1. Example: binding a real function
Using .bind()
for real functions is somewhat unintuitive, because you have to provide a value for this
. That value is usually undefined
, mirroring what happens during function calls.
In the following example, we create add8()
, a function that has one parameter, by binding the first parameter of add()
to 8
.
25.4.3.2. Example: binding a method
In the following code, we turn method .says()
into the stand-alone function func()
:
Setting this
to jane
via .bind()
is crucial here. Otherwise, func()
wouldn’t work properly, because this
is used in line A.
25.4.4. 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 you 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()
.
The function call in line A is equivalent to:
So how do we fix this? We need to use .bind()
to extract method .says()
:
The .bind()
ensures that this
is always jane
when we call func()
.
You can also use arrow functions to extract methods:
25.4.4.1. Example: extracting a method
The following is a simplified version of code that you may see in actual web development:
In line A, we don’t extract the method .handleClick()
properly. Instead, we should do:
25.4.5. this pitfall: accidentally shadowing this
Accidentally shadowing this
is only an issue if you use ordinary functions.
Consider the following problem: When you are inside an ordinary function, you 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:
Why the error? The this
in line B isn’t the this
of .sayHiTo()
, it is the this
of the ordinary function starting in line B.
There are several ways to fix this. The easiest is to use an arrow function – which doesn’t have its own this
, so shadowing is not an issue.
25.4.6. Avoiding the pitfalls of this
We have seen two big this
-related pitfalls:
- Extracting methods
- Accidentally shadowing
this
One simple rule helps avoid the second pitfall:
“Avoid the keywordfunction
”: Never use ordinary functions, only arrow functions (for real functions) and method definitions.
Let’s break down this rule:
- If all real functions are arrow functions, the second pitfall can never occur.
- Using method definitions means that you’ll only see
this
inside methods, which makes this feature less confusing.
However, even though I don’t use (ordinary) function expressions, anymore, I do like function declarations syntactically. You can use them safely if you don’t refer tothis
inside them. The checking tool ESLint has a rule that helps with that.
Alas, there is no simple way around the first pitfall: Whenever you extract a method, you have to be careful and do it properly. For example, by binding this
.
25.4.7. The value of this in various contexts
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:
- Function call:
- Ordinary functions:
this === undefined
- Arrow functions:
this
is same as in surrounding scope (lexicalthis
)
- Ordinary functions:
- Method call:
this
is receiver of call new
:this
refers to newly created instance
You can also accessthis
in all common top-level scopes:<script>
element:this === window
- ES modules:
this === undefined
- CommonJS modules:
this === module.exports
However, I like to pretend that you can’t accessthis
in top-level scopes, because top-levelthis
is confusing and not that useful.
25.5. Objects as dictionaries
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. As a consequence, keys had to be strings, but values could have arbitrary types.
We first look at features of objects that are related to dictionaries, but also occasionally useful for objects-as-records. This section concludes with tips for actually using objects as dictionaries (spoiler: avoid, use Maps if you can).
25.5.1. Arbitrary fixed strings as property keys
When going from objects-as-records to objects-as-dictionaries, one important change is that we must be able to use arbitrary strings as property keys. This subsection explains how to achieve that for fixed string keys. The next subsection explains how to dynamically compute arbitrary keys.
So far, we have only seen legal JavaScript identifiers as property keys (with the exception of symbols):
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):
Second – when getting or setting properties, we can use square brackets with strings inside them:
You can also quote the keys of methods:
25.5.2. Computed property keys
So far, we were limited by what we could do with property keys inside object literals: They were always fixed and they were always strings. We can dynamically compute arbitrary keys if we put expressions in square brackets:
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:
Methods can have computed property keys, too:
We are now switching back to fixed property keys, but you can always use square brackets if you need computed property keys.
25.5.3. The in operator: is there a property with a given key?
The in
operator checks if an object has a property with a given key:
25.5.3.1. Checking if a property exists via truthiness
You can also use a truthiness check to determine if a property exists:
The previous check works, because reading a non-existent property returns undefined
, which is falsy. And because obj.foo
is truthy.
There is, however, one important caveat: Truthiness checks fail if the property exists, but has a falsy value (undefined
, null
, false
, 0
, ""
, etc.):
25.5.4. Deleting properties
You can delete properties via the delete
operator:
25.5.5. Dictionary pitfalls
If you use plain objects (created via object literals) as dictionaries, you have to look out for two pitfalls.
The first pitfall is that the in
operator also finds inherited properties:
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 you can’t use the property key proto
, because it has special powers (it sets the prototype of the object):
So how do we navigate around these pitfalls?
Whenever you can, use Maps. They are the best solution for dictionaries.
If you can’t: use a library for objects-as-dictionaries that does everything safely.
If you can’t: use an object without a prototype. That eliminates the two pitfalls in modern JavaScript.
25.5.6. Listing property keys
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 you can see the distinction between property keys (strings and symbols), property names (only strings) and property symbols (only symbols) that we discussed previously.
Enumerability is an attribute of properties. By default, properties are enumerable, but there are ways to change that (shown in the next example and described in slightly more detail later).
For example:
const enumerableSymbolKey = Symbol('enumerableSymbolKey');
const nonEnumSymbolKey = Symbol('nonEnumSymbolKey');
// We create the enumerable properties via an object literal
const obj = {
enumerableStringKey: 1,
[enumerableSymbolKey]: 2,
}
// For the non-enumerable properties,
// we need a more powerful tool:
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 ]);
25.5.7. Listing property values via Object.values()
Object.values()
lists the values of all enumerable properties of an object:
25.5.8. Listing property entries via Object.entries()
Object.entries()
lists key-value pairs of enumerable properties. Each pair is encoded as a two-element Array:
25.5.9. Properties are listed deterministically
Own (non-inherited) properties of objects are always listed in the following order:
- Properties with integer indices (e.g. Array indices)
- In ascending numeric order
- Remaining properties with string keys
- In the order in which they were added
- Properties with symbol keys
- In the order in which they were added
The following example demonstrates how property keys are sorted according to these rules:
(You can look up the details in the spec.)
25.5.10. Assembling objects via Object.fromEntries()
Given an iterable over [key,value] pairs, Object.fromEntries()
creates an object:
It does the opposite of Object.entries()
.
Next, we’ll use Object.entries()
and Object.fromEntries()
to implement several tool functions from the library Underscore.
25.5.10.1. Example: pick(object, …keys)
pick()
removes all properties from object
whose keys are not among keys
. The removal is non-destructive: pick()
creates a modified copy and does not change the original. For example:
We can implement pick()
as follows:
25.5.10.2. Example: invert(object)
invert()
non-destructively swaps the keys and the values of an object:
We can implement it like this:
25.5.10.3. A simple implementation of Object.fromEntries()
Object.fromEntries()
could be implemented as follows (I’ve omitted a few checks):
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);
}
Object.defineProperty(result, coercedKey, {
value,
writable: true,
enumerable: true,
configurable: true,
});
}
return result;
}
Notes:
Object.defineProperty()
is explained later in this chapter.- The official polyfill is available via the npm package
object.fromentries
.
25.6. Standard methods
Object.prototype
defines several standard methods that can be overridden. Two important ones are:
.toString()
.valueOf()
Roughly,.toString()
configures how objects are converted to strings:
And .valueOf()
configures how objects are converted to numbers:
25.7. Advanced topics
The following subsections give a brief overviews of topics that are beyond the scope of this book.
25.7.1. Object.assign()
Object.assign()
is a tool method:
This expression (destructively) merges source_1
into target
, then source_2
etc. At the end, it returns target
. For example:
The use cases for Object.assign()
are similar to those for spread properties. In a way, it spreads destructively.
For more information on Object.assign()
, consult “Exploring ES6”.
25.7.2. Freezing objects
Object.freeze(obj)
makes obj
immutable: You can’t change or add properties or change the prototype of obj
.
For example:
There is one caveat: Object.freeze(obj)
freezes shallowly. That is, only the properties of obj
are frozen, but not objects stored in properties.
For more information on Object.freeze()
, consult “Speaking JavaScript”.
25.7.3. Property attributes and property descriptors
Just as objects are composed of properties, properties are composed of attributes. That is, you can configure more than just the value of a property – which is just one of several attributes. Other attributes include:
writable
: Is it possible to change the value of the property?enumerable
: Is the property listed byObject.keys()
?
When you are using one of the operations for accessing property attributes, attributes are specified via property descriptors: objects where each property represents one attribute. For example, this is how you read the attributes of a propertyobj.foo
:
And this is how you set the attributes of a property obj.bar
:
For more on property attributes and property descriptors, consult “Speaking JavaScript”.