Please support this book: buy it or donate

(Ad, please don’t block.)

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. 8):

Single objects (this chapter): How do objects, JavaScript’s basic OOP building blocks, work in isolation? Prototype chains (next chapter): Each object has a chain of zero or more prototype objects. Prototypes are JavaScript’s core inheritance mechanism. Classes (next chapter): JavaScript’s classes are factories for objects. The relationship between a class and its instances is based on prototypal inheritance. Subclassing (next chapter): The relationship between a subclass and its superclass is also based on prototypal inheritance.

Figure 8: This book introduces object-oriented programming in JavaScript in four steps.

25.1 What is an object?

In JavaScript:

An object is a set of properties (key-value entries).

A property key can only be a string or a symbol.

25.1.1 Roles of objects: record vs. dictionary

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:

First, we’ll explore objects-as-records. Even though property keys are strings or symbols under the hood, they will appear as fixed identifiers to us, in this part of the chapter.

Later, we’ll explore objects-as-dictionaries. Note that Maps are usually better dictionaries than objects. However, some of the operations that we’ll encounter, can also be useful for objects-as-records.

25.2 Objects as records

Let’s first explore the role record of objects.

25.2.1 Object literals: properties

Object literals are one way of creating objects-as-records. They are a stand-out feature of JavaScript: you can directly create objects – no need for classes! This is an example:

const jane = { 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):

The first property has the key first and the value 'Jane' .

and the value . The second property has the key last and the value 'Doe' .

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, you can use first_name as a property key, but not first-name ). However, reserved words are allowed:

const obj = { 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:

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.

function createPoint (x , y) { (xy) { return {x , y} ; {xy} } . deepEqual ( assert 9 , 2 ) , createPoint( { x : 9 , y : 2 } ) ;

25.2.3 Getting properties

This is how you get (read) a property (line A):

const jane = { jane first : 'Jane' , last : 'Doe' , } ; // Get property .first . equal (jane . first , 'Jane' ) ; // (A) assert(jane

Getting an unknown property produces undefined :

. equal (jane . unknownProperty , undefined ) ; assert(jane

25.2.4 Setting properties

This is how you set (write to) a property:

const obj = { obj prop : 1 , } ; . equal (obj . prop , 1 ) ; assert(obj . prop = 2 ; // (A) obj . equal (obj . prop , 2 ) ; assert(obj

We just changed an existing property via setting. If we set an unknown property, we create a new entry:

const obj = {} ; // empty object obj{} . deepEqual ( assert Object . keys (obj) , []) ; (obj)[]) . unknownProperty = 'abc' ; obj . deepEqual ( assert Object . keys (obj) , [ 'unknownProperty' ]) ; (obj)])

25.2.5 Object literals: methods

The following code shows how to create the method .says() via an object literal:

const jane = { jane first : 'Jane' , // data property // method says(text) { return ` ${ this . first } says “ ${ text } ”` ; // (A) text } , // comma as separator (optional at end) } ; . equal (jane . says ( 'hello' ) , 'Jane says “hello”' ) ; assert(jane

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.6 Object literals: accessors

There are two kinds of accessors in JavaScript:

A getter is a method-like entity that is invoked by getting a property.

A setter is a method-like entity that is invoked by setting a property.

25.2.6.1 Getters

A getter is created by prefixing a method definition with the modifier get :

const jane = { jane first : 'Jane' , last : 'Doe' , get full() { return ` ${ this . first } ${ this . last } ` ; } , } ; . equal (jane . full , 'Jane Doe' ) ; assert(jane . first = 'John' ; jane . equal (jane . full , 'John Doe' ) ; assert(jane

25.2.6.2 Setters

A setter is created by prefixing a method definition with the modifier set :

const jane = { jane first : 'Jane' , last : 'Doe' , set full(fullName) { const parts = fullName . split ( ' ' ) ; partsfullName this . first = parts[ 0 ] ; parts[ this . last = parts[ 1 ] ; parts[ } , } ; . full = 'Richard Roe' ; jane . equal (jane . first , 'Richard' ) ; assert(jane . equal (jane . last , 'Roe' ) ; assert(jane

Exercise: Creating an object via an object literal exercises/single-objects/color_point_object_test.mjs

25.3 Spreading into object literals ( ... ) [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 :

Property .length of strings and of Arrays is hidden from this kind of operation (it is not enumerable; see §25.8.3 “Property attributes and property descriptors [ES5]” for more information).

25.3.1 Use case for spreading: copying objects

You can use spreading to create a copy of an object original :

const copy = { ... original } ; copy

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 } } ; original} } const copy = { ... original } ; copy

The first level of copy is really a copy: If you 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 you need such an operation, you have to implement it yourself.

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 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' } ; DEFAULTS const providedData = { foo : 1 } ; providedData const allData = { ... DEFAULTS , ... providedData } ; allData . deepEqual (allData , { foo : 1 , bar : 'b' }) ; assert(allData})

The result, the object allData , is created by copying 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:

const providedData = { foo : 1 } ; providedData const allData = { foo : 'a' , bar : 'b' , ... providedData } ; allData . deepEqual (allData , { foo : 1 , bar : 'b' }) ; assert(allData})

25.3.3 Use case for spreading: non-destructively changing properties

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' } ; obj . foo = 1 ; // (A) obj . deepEqual (obj , { foo : 1 , bar : 'b' }) ; assert(obj})

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' } ; obj const updatedObj = { ... obj , foo : 1 } ; updatedObj . deepEqual (updatedObj , { foo : 1 , bar : 'b' }) ; assert(updatedObj})

Exercise: Non-destructively updating a property via spreading (fixed key) exercises/single-objects/update_name_test.mjs

25.4 Methods

25.4.1 Methods are properties whose values are functions

Let’s revisit the example that was used to introduce methods:

const jane = { jane first : 'Jane' , says(text) { return ` ${ this . first } says “ ${ text } ”` ; text } , } ;

Somewhat surprisingly, methods are functions:

. equal ( typeof jane . says , 'function' ) ; assertjane

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 = { jane first : 'Jane' , says : function (text) { (text) { return ` ${ this . first } says “ ${ text } ”` ; text } , } ;

25.4.2 .call() : specifying this via a parameter

Remember that each function someFunc is also an object and therefore has methods. One such method is .call() – it lets you call a function while specifying this via a parameter:

. call (thisValue , arg1 , arg2 , arg3) ; someFunc(thisValuearg1arg2arg3)

25.4.2.1 Methods and .call()

If you make a method call, this is an implicit parameter that is filled in via the receiver of the call:

const obj = { obj method(x) { . equal ( this , obj) ; // implicit parameter assertobj) . equal (x , 'a' ) ; assert(x } , } ; . method ( 'a' ) ; // receiver is `obj` obj

The method call in the last line sets up this as follows:

. method . call (obj , 'a' ) ; obj(obj

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 for this .

25.4.2.2 Functions and .call()

If you function-call an ordinary function, its implicit parameter this is also provided – it is implicitly set to undefined :

function func (x) { (x) { . equal ( this , undefined ) ; // implicit parameter assert . equal (x , 'a' ) ; assert(x } 'a' ) ; func(

The method call in the last line sets up this as follows:

. call ( undefined , 'a' ) ; func

this being set to undefined during a function call, indicates that it is a feature that is only needed during a method call.

Next, we’ll examine the pitfalls of using this . Before we can do that, we need one more tool: 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:

const boundFunc = someFunc . bind (thisValue , arg1 , arg2) ; boundFuncsomeFunc(thisValuearg1arg2)

.bind() returns a new function boundFunc() . Calling that function invokes someFunc() with this set to thisValue and these parameters: arg1 , arg2 , followed by the parameters of boundFunc() .

That is, the following two function calls are equivalent:

'a' , 'b' ) boundFunc( . call (thisValue , arg1 , arg2 , 'a' , 'b' ) someFunc(thisValuearg1arg2

25.4.3.1 An alternative to .bind()

Another way of pre-filling this and parameters is via an arrow function:

const boundFunc2 = ( ... args ) => boundFunc2 . call (thisValue , arg1 , arg2 , ... args ) ; someFunc(thisValuearg1arg2

25.4.3.2 An implementation of .bind()

Considering the previous section, .bind() can be implemented as a real function as follows:

function bind (func , thisValue , ... boundArgs ) { (functhisValue) { return ( ... args ) => . call (thisValue , ... boundArgs , ... args ) ; func(thisValue }

25.4.3.3 Example: binding a real function

Using .bind() for real functions is somewhat unintuitive because you have to provide a value for this . Given that it is undefined during function calls, it is usually set to undefined or null .

In the following example, we create add8() , a function that has one parameter, by binding the first parameter of add() to 8 .

function add (x , y) { (xy) { return x + y ; } const add8 = add . bind ( undefined , 8 ) ; add8add . equal (add8( 1 ) , 9 ) ; assert(add8(

25.4.3.4 Example: binding a method

In the following code, we turn method .says() into the stand-alone function func() :

const jane = { jane first : 'Jane' , says(text) { return ` ${ this . first } says “ ${ text } ”` ; // (A) text } , } ; const func = jane . says . bind (jane , 'hello' ) ; funcjane(jane . equal (func() , 'Jane says “hello”' ) ; assert(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() .

const jane = { jane first : 'Jane' , says(text) { return ` ${ this . first } says “ ${ text } ”` ; text } , } ; const func = jane . says ; // extract the method funcjane . throws ( assert => func( 'hello' ) , // (A) ()func( { name : 'TypeError' , message : "Cannot read property 'first' of undefined" , ; })

The function call in line A is equivalent to:

. throws ( assert => jane . says . call ( undefined , 'hello' ) , // `this` is undefined! ()jane { name : 'TypeError' , message : "Cannot read property 'first' of undefined" , ; })

So how do we fix this? We need to use .bind() to extract method .says() :

const func2 = jane . says . bind (jane) ; func2jane(jane) . equal (func2( 'hello' ) , 'Jane says “hello”' ) ; assert(func2(

The .bind() ensures that this is always jane when we call func() .

You can also use arrow functions to extract methods:

const func3 = text => jane . says (text) ; func3textjane(text) . equal (func3( 'hello' ) , 'Jane says “hello”' ) ; assert(func3(

25.4.4.1 Example: extracting a method

The following is a simplified version of code that you may see in actual web development:

class ClickHandler { ClickHandler { , elem) { constructor(idelem) { this . id = id ; id . addEventListener ( 'click' , this . handleClick ) ; // (A) elem } event ) { handleClick() { 'Clicked ' + this . id ) ; alert( } }

In line A, we don’t extract the method .handleClick() properly. Instead, we should do:

. addEventListener ( 'click' , this . handleClick . bind ( this )) ; elem))

Exercise: Extracting a method exercises/single-objects/method_extraction_exrc.mjs

25.4.5 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 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:

const prefixer = { prefixer prefix : '==> ' , prefixStringArray(stringArray) { return stringArray . map ( stringArray function (x) { (x) { return this . prefix + x ; // (A) ; }) } , } ; . throws ( assert => prefixer . prefixStringArray ([ 'a' , 'b' ]) , ()prefixer([]) / ^ 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 = { prefixer prefix : '==> ' , prefixStringArray(stringArray) { return stringArray . map ( stringArray => { (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) that return stringArray . map ( stringArray function (x) { (x) { return that . prefix + x ; that ; }) } ,

Another option is to specify a fixed this for the callback via .bind() (line A):

prefixStringArray(stringArray) { return stringArray . map ( stringArray function (x) { (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 ( stringArray function (x) { (x) { return this . prefix + x ; } , this ) ; // (A) } ,

25.4.6 Avoiding the pitfalls of this

We have seen two big this -related pitfalls:

One simple rule helps avoid the second pitfall:

“Avoid the keyword function ”: Never use ordinary functions, only arrow functions (for real functions) and method definitions.

Following this rule has two benefits:

It prevents the second pitfall because ordinary functions are never used as real functions.

this becomes easier to understand because it will only appear inside methods (never inside ordinary functions). That makes it clear that this is an OOP feature.

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 to this inside them. The static checking tool ESLint can warn you during development when you do this wrong via a built-in rule.

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 (in strict mode) Arrow functions: this is same as in surrounding scope (lexical this )

Method call: this is receiver of call

is receiver of call new : this refers to newly created instance

You can also access this in all common top-level scopes:

<script> element: this === globalThis

element: ECMAScript modules: this === undefined

CommonJS modules: this === module.exports

However, I like to pretend that you can’t access this in top-level scopes because top-level this is confusing and rarely useful.

25.5 Optional chaining for property accesses and method calls (advanced) [ES2020]

The following kinds of optional chaining operations exist:

?. prop // optional static property access obj ?. [«expr»] // optional dynamic property access obj[«expr»] ?. («arg0» , «arg1») // optional function or method call func(«arg0»«arg1»)

The rough idea is:

If the value before the question mark is neither undefined nor null , then perform the operation after the question mark.

nor , then perform the operation after the question mark. Otherwise, return undefined .

25.5.1 Example: optional static property access

Consider the following data:

const persons = [ 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 ( streetNamespersons p => p . address ?. street ?. name ) ; . deepEqual ( assert , [ 'Sesame Street' , undefined , undefined ] streetNames ) ;

25.5.1.1 Handling defaults via nullish coalescing

The nullish coalescing operator allows us to use the default value '(no street)' instead of undefined :

const streetNames = persons . map ( streetNamespersons p => p . address ?. street ?. name ?? '(no name)' ) ; . deepEqual ( assert , [ 'Sesame Street' , '(no name)' , '(no name)' ] streetNames ) ;

25.5.2 The operators in more detail (advanced)

25.5.2.1 Optional static property access

The following two expressions are equivalent:

o ?. prop !== undefined && o !== null ) ? o . prop : undefined (o

Examples:

. equal ( undefined ?. prop , undefined ) ; assert . equal ( null ?. prop , undefined ) ; assert . equal ({ prop : 1 } ?. prop , 1 ) ; assert({

25.5.2.2 Optional dynamic property access

The following two expressions are equivalent:

o ?. [«expr»] [«expr»] !== undefined && o !== null ) ? o[«expr»] : undefined (oo[«expr»]

Examples:

const key = 'prop' ; key . equal ( undefined ?. [key] , undefined ) ; assert[key] . equal ( null ?. [key] , undefined ) ; assert[key] . equal ({ prop : 1 } ?. [key] , 1 ) ; assert({[key]

25.5.2.3 Optional function or method call

The following two expressions are equivalent:

f ?. (arg0 , arg1) (arg0arg1) !== undefined && f !== null ) ? f(arg0 , arg1) : undefined (ff(arg0arg1)

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.

25.5.3 Short-circuiting (advanced)

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) { (obj) { let invoked = false ; invoked ?. a . b . m (invoked = true ) ; obj(invoked return invoked ; invoked } . equal ( assert a : { b : {m() {}}}}) , true ) ; isInvoked({{m() {}}}}) // The left-hand side of ?. is undefined // and the assignment is not executed . equal ( assert undefined ) , false ) ; isInvoked(

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

25.5.4 Frequently asked questions

25.5.4.1 Why are there dots in o?.[x] and f?.() ?

The syntaxes of the following two optional operator are not ideal:

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(xx) ? [ 'a' , 'b' , 'c' ] . map (x => x + x) : [] obj(xx)[]

25.5.4.2 Why does 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.

25.6 Objects as dictionaries (advanced)

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).

25.6.1 Arbitrary fixed strings as property keys

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 = { obj mustBeAnIdentifier : 123 , } ; // Get property . equal (obj . mustBeAnIdentifier , 123 ) ; assert(obj // Set property . mustBeAnIdentifier = 'abc' ; obj . equal (obj . mustBeAnIdentifier , 'abc' ) ; assert(obj

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 = { 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(obj[ // Set property 'Can be any string!' ] = 'abc' ; obj[ . equal (obj[ 'Can be any string!' ] , 'abc' ) ; assert(obj[

You can also use these techniques for methods:

const obj = { obj 'A nice method' () { () { return 'Yes!' ; } , } ; . equal (obj[ 'A nice method' ]() , 'Yes!' ) ; assert(obj[]()

25.6.2 Computed property keys

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 = { obj [ 'Hello world!' ] : true , [ 'f' + 'o' + 'o' ] : 123 , [ Symbol . toStringTag ] : 'Goodbye' , // (A) } ; . equal (obj[ 'Hello world!' ] , true ) ; assert(obj[ . equal (obj . foo , 123 ) ; assert(obj . equal (obj[ Symbol . toStringTag ] , 'Goodbye' ) ; assert(obj[

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(obj[ . equal (obj[ '==> foo' . slice ( - 3 )] , 123 ) ; assert(obj[)]

Methods can have computed property keys, too:

const methodKey = Symbol () ; methodKey() const obj = { obj [methodKey]() { return 'Yes!' ; } , } ; . equal (obj[methodKey]() , 'Yes!' ) ; assert(obj[methodKey]()

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

25.6.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:

const obj = { obj foo : 'abc' , bar : false , } ; . equal ( 'foo' in obj , true ) ; assertobj . equal ( 'unknownKey' in obj , false ) ; assertobj

25.6.3.1 Checking if a property exists via truthiness

You 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'

25.6.4 Deleting properties

You can delete properties via the delete operator:

const obj = { obj foo : 123 , } ; . deepEqual ( Object . keys (obj) , [ 'foo' ]) ; assert(obj)]) delete obj . foo ; obj . deepEqual ( Object . keys (obj) , []) ; assert(obj)[])

25.6.5 Listing property keys

Table 18: Standard library methods for listing own (non-inherited) property keys. All of them return Arrays with strings and/or symbols. enumerable non-e. string symbol Object.keys() ✔ ✔ Object.getOwnPropertyNames() ✔ ✔ ✔ Object.getOwnPropertySymbols() ✔ ✔ ✔ Reflect.ownKeys() ✔ ✔ ✔ ✔

Each of the methods in tbl. 18 returns an Array with the own property keys of the parameter. In the names of the methods, you can see that the following distinction is made:

A property key can be either a string or a symbol.

A property name is a property key whose value is a string.

A property symbol is a property key whose value is a symbol.

The next section describes the term enumerable and demonstrates each of the methods.

25.6.5.1 Enumerability

Enumerability is an attribute of a property. Non-enumerable properties are ignored by some operations – for example, by Object.keys() (see tbl. 18) 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' ) ; enumerableSymbolKey const nonEnumSymbolKey = Symbol ( 'nonEnumSymbolKey' ) ; nonEnumSymbolKey // We create enumerable properties via an object literal const obj = { obj enumerableStringKey : 1 , : 2 , [enumerableSymbolKey] } // For non-enumerable properties, we need a more powerful tool Object . defineProperties (obj , { (obj nonEnumStringKey : { value : 3 , enumerable : false , } , : { [nonEnumSymbolKey] value : 4 , enumerable : false , } , ; }) . deepEqual ( assert Object . keys (obj) , (obj) [ 'enumerableStringKey' ]) ; ]) . deepEqual ( assert Object . getOwnPropertyNames (obj) , (obj) [ 'enumerableStringKey' , 'nonEnumStringKey' ]) ; ]) . deepEqual ( assert Object . getOwnPropertySymbols (obj) , (obj) , nonEnumSymbolKey ]) ; [ enumerableSymbolKeynonEnumSymbolKey ]) . deepEqual ( assert Reflect . ownKeys (obj) , (obj) [ 'enumerableStringKey' , 'nonEnumStringKey' , , nonEnumSymbolKey , enumerableSymbolKeynonEnumSymbolKey ; ])

Object.defineProperties() is explained later in this chapter.

25.6.6 Listing property values via Object.values()

Object.values() lists the values of all enumerable properties of an object:

const obj = { foo : 1 , bar : 2 } ; obj . deepEqual ( assert Object . values (obj) , (obj) [ 1 , 2 ]) ; ])

25.6.7 Listing property entries via Object.entries()

Object.entries() lists key-value pairs of enumerable properties. Each pair is encoded as a two-element Array:

const obj = { foo : 1 , bar : 2 } ; obj . deepEqual ( assert Object . entries (obj) , (obj) [ [ 'foo' , 1 ] , [ 'bar' , 2 ] , ; ])

Exercise: Object.entries() exercises/single-objects/find_key_test.mjs

25.6.8 Properties are listed deterministically

Own (non-inherited) properties of objects are always listed in the following order:

Properties with string keys that contain integer indices (that includes 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:

The order of properties The ECMAScript specification describes in more detail how properties are ordered.

25.6.9 Assembling objects via Object.fromEntries()

Given an iterable over [key, value] pairs, Object.fromEntries() creates an object:

. deepEqual ( assert Object . 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.

25.6.9.1 Example: pick(object, ...keys)

pick returns a copy of object that only has those properties whose keys are mentioned as arguments:

const address = { address street : 'Evergreen Terrace' , number : '742' , city : 'Springfield' , state : 'NT' , zip : '49007' , } ; . deepEqual ( assert , 'street' , 'number' ) , pick(address { street : 'Evergreen Terrace' , number : '742' , } ) ;

We can implement pick() as follows:

function pick (object , ... keys ) { (object) { const filteredEntries = Object . entries (object) filteredEntries(object) . filter (([key , _value]) => keys . includes (key)) ; (([key_value])keys(key)) return Object . fromEntries (filteredEntries) ; (filteredEntries) }

25.6.9.2 Example: invert(object)

invert returns a copy of object where the keys and values of all properties are swapped:

. deepEqual ( assert a : 1 , b : 2 , c : 3 }) , invert({}) { 1 : 'a' , 2 : 'b' , 3 : 'c' } ) ;

We can implement invert() like this:

function invert (object) { (object) { const reversedEntries = Object . entries (object) reversedEntries(object) . map (([key , value]) => [value , key]) ; (([keyvalue])[valuekey]) return Object . fromEntries (reversedEntries) ; (reversedEntries) }

25.6.9.3 A simple implementation of Object.fromEntries()

The following function is a simplified version of Object.fromEntries() :

function fromEntries (iterable) { (iterable) { const result = {} ; result{} for ( const [key , value] of iterable) { [keyvalue]iterable) { let coercedKey ; coercedKey if ( typeof key === 'string' || typeof key === 'symbol' ) { keykey) { = key ; coercedKeykey } else { = String (key) ; coercedKey(key) } = value ; result[coercedKey]value } return result ; result }

25.6.9.4 A polyfill for Object.fromEntries()

The npm package object.fromentries is a polyfill for Object.entries() : it installs its own implementation if that method doesn’t exist on the current platform.

Exercise: Object.entries() and Object.fromEntries() exercises/single-objects/omit_properties_test.mjs

25.6.10 The pitfalls of using an object as a dictionary

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:

const dict = {} ; dict{} . equal ( 'toString' in dict , true ) ; assertdict

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):

const dict = {} ; dict{} '__proto__' ] = 123 ; dict[ // No property was added to dict: . deepEqual ( Object . keys (dict) , []) ; assert(dict)[])

So how do we avoid 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.

The following code demonstrates using objects without prototypes as dictionaries:

const dict = Object . create ( null ) ; // no prototype dict . equal ( 'toString' in dict , false ) ; // (A) assertdict '__proto__' ] = 123 ; dict[ . deepEqual ( Object . keys (dict) , [ '__proto__' ]) ; assert(dict)])

We avoided both pitfalls: First, a property without a prototype does not inherit any properties (line A). Second, in modern JavaScript, __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

25.7 Standard methods (advanced)

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()

25.7.1 .toString()

.toString() determines how objects are converted to strings:

> String({toString() { return 'Hello!' }}) 'Hello!' > String({}) '[object Object]'

25.7.2 .valueOf()

.valueOf() determines how objects are converted to numbers:

> Number({valueOf() { return 123 }}) 123 > Number({}) NaN

25.8 Advanced topics

The following subsections give brief overviews of a few advanced topics.

25.8.1 Object.assign() [ES6]

Object.assign() is a tool method:

Object . assign (target , source_1 , source_2 , ···) (targetsource_1source_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 } ; target const result = Object . assign ( result , target { bar : 2 } , { baz : 3 , bar : 4 }) ; }) . deepEqual ( assert , { foo : 1 , bar : 4 , baz : 3 }) ; result}) // target was modified and returned: . equal (result , target) ; assert(resulttarget)

The use cases for Object.assign() are similar to those for spread properties. In a way, it spreads destructively.

25.8.2 Freezing objects [ES5]

Object.freeze(obj) makes obj completely immutable: You can’t change properties, add properties, or change its prototype – for example:

const frozen = Object . freeze ({ x : 2 , y : 5 }) ; frozen({}) . throws ( assert => { frozen . x = 7 } , (){ frozen { 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.

25.8.3 Property attributes and property descriptors [ES5]

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?

: Is it possible to change the value of the property? enumerable : Is the property considered by Object.keys() , spreading, etc.?

When you 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 you read the attributes of a property obj.foo :

const obj = { foo : 123 } ; obj . deepEqual ( assert Object . getOwnPropertyDescriptor (obj , 'foo' ) , (obj { value : 123 , writable : true , enumerable : true , configurable : true , ; })

And this is how you set the attributes of a property obj.bar :

const obj = { obj foo : 1 , bar : 2 , } ; . deepEqual ( Object . keys (obj) , [ 'foo' , 'bar' ]) ; assert(obj)]) // Hide property `bar` from Object.keys() Object . defineProperty (obj , 'bar' , { (obj enumerable : false , ; }) . deepEqual ( Object . keys (obj) , [ 'foo' ]) ; assert(obj)])

Further reading:

Enumerability is covered in greater detail earlier in this chapter.

For more information on property attributes and property descriptors, see Deep JavaScript.