JavaScript for impatient programmers (beta)
Please support this book: buy it or donate
(Ad, please don’t block.)

22. 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. 6):

  1. Single objects: How do objects, JavaScript’s basic OOP building blocks, work in isolation?
  2. Prototype chains: Each object has a chain of zero or more prototype objects. Prototypes are JavaScript’s core inheritance mechanism.
  3. Classes: JavaScript’s classes are factories for objects. The relationship between a class and its instances is based on prototypal inheritance.
  4. Subclassing: The relationship between a subclass and its superclass is also based on prototypal inheritance.
Figure 6: This book introduces object-oriented programming in JavaScript in four steps.
Figure 6: This book introduces object-oriented programming in JavaScript in four steps.

22.1. The two roles of objects in JavaScript

Objects play two roles in JavaScript:

22.2. Objects as records

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

const jane = {
  first: 'Jane',
  last: 'Doe', // optional trailing comma
};

assert.equal(jane.first, 'Jane');
jane.first = 'John';
assert.equal(jane.first, 'John');

In this code, we create an object via an object literal, which starts with a curly brace and ends with a curly brace: { ··· }. Inside it, we define two properties:

If they are written this way, property keys must follow the rules of JavaScript variable names, with the exception that reserved words are allowed.

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

const x = 4;
const y = 1;

assert.deepEqual(
  { x, y },
  { x: x, y: y }
);

22.2.3. Terminology: property keys, property names, property symbols

Given that property keys can be strings and symbols, the following distinction is made:

This terminology is used in the JavaScript standard library (“own” means “not inherited” and is explained in the next chapter):

22.2.4. Getting properties

This is how you get (read) a property:

obj.propKey

If obj does not have a property whose key is propKey, this expression evaluates to undefined:

> const obj = {};
> obj.foo
undefined

22.2.5. Setting properties

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

obj.propKey = value;

If obj already has a property whose key is propKey, this statement changes that property. Otherwise, it creates a new property:

const obj = {};

assert.deepEqual(Object.keys(obj), []);
obj.foo = 123;
assert.deepEqual(Object.keys(obj), ['foo']);

22.2.6. Object literals: methods

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

const jane = {
  first: 'Jane', // data property
  describe() { // method
    return `Person named ${this.first}`; // (A)
  }, // comma as separator (optional at the end)
};

assert.equal(jane.describe(), 'Person named Jane');

Inside methods, you can use the special variable this to access sibling properties (line A).

22.2.7. Object literals: accessors

There are two kinds of accessors in JavaScript:

22.2.7.1. Getters

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

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

This is how you invoke the getter:

> jane.full
'Jane Doe'
> jane.first = 'John';
> jane.full
'John Doe'
22.2.7.2. Setters

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

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

assert.equal(jane.first, 'Jane');
assert.equal(jane.last, 'Doe');

jane.full = 'Richard Roe';

assert.equal(jane.first, 'Richard');
assert.equal(jane.last, 'Roe');

  Exercise: Creating an object via an object literal

exercises/single-objects/color_point_object_test.js

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

> 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 }

22.3.1. Use case for spreading: copying objects

You can use spread to create a copy of an object:

const copy = {...obj};

Caveat – the copy is shallow:

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} });

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

const DEFAULTS = {foo: 'a', bar: 'b'};
const providedData = {foo: 1};

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

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:

const providedData = {foo: 1};

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

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

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

  Exercise: Non-destructively updating properties via spreading (fixed key)

exercises/single-objects/update_name_test.js

22.4. Methods

22.4.1. Methods are properties whose values are functions

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

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

Somewhat unexpectedly, methods are functions:

assert.equal(jane.says instanceof Function, true);

Why is that? Remember that in the chapter on callable entities, it was mentioned that ordinary functions play several roles, including the role method. It turns out that, under the hood, jane roughly looks as follows.

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

22.4.2. .call(): explicit parameter this

Ordinary functions always have this as an implicit parameter that is automatically filled in for function calls, method calls and constructor calls (new). All function objects have a method .call() with this as an explicit parameter.

This is how to use it:

someFunc.call(thisValue, arg1, arg2, arg3);
22.4.2.1. How does JavaScript set up this in function and method calls?

Now we can explore how JavaScript normally sets up this.

Regarding function calls, the following two expressions are equivalent:

func('a')
func.call(undefined, 'a')

Regarding method calls, the following two expressions are equivalent:

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

That means that the following two dot operators are different:

obj.prop
obj.prop()

Why? obj.prop() does not just get obj.prop and function-call the result – it additionally specifies a value for this (as shown in the previous example).

22.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, arg3);

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

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

We also could have created boundFunc() as follows.

const boundFunc = (...args) =>
  someFunc.call(thisValue, arg1, arg2, arg3, ...args);

Therefore, .bind() could be implemented as a real function as follows:

function bind(func, thisValue, ...boundArgs) {
  return (...args) =>
    func.call(thisValue, ...boundArgs, ...args);
}
22.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.

function add(x, y) {
  return x + y;
}

const add8 = add.bind(undefined, 8);
assert.equal(add8(1), 9);
22.4.3.2. Example: binding a method

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

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

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

Setting this to jane via .bind() is crucial here. Otherwise, func() wouldn’t work properly, because this is used in line A.

22.4.4. 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 = {
  first: 'Jane',
  says(words) {
    return `${this.first} says “${words}”`;
  },
};
const func = jane.says; // extract the method
assert.throws(
  () => func('hello'), // (A)
  {
    name: 'TypeError',
    message: "Cannot read property 'first' of undefined"
  });

The function call in line A is equivalent to:

assert.throws(
  () => jane.says.call(undefined, 'hello'), // `this` is undefined!
  { 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);
assert.equal(func2('hello'), 'Jane says “hello”');

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

You can also use arrow functions to extract methods:

const func3 = words => jane.says(words);
assert.equal(func3('hello'), 'Jane says “hello”');
22.4.4.1. Another example of a method extraction (React)

The following is code based on the front-end library React:

class ClickMeButton extends React.Component {
  render() {
    return <button onClick={this.handleClick}>
      Click me!
    </button>;
  }
  handleClick() {
    /* Accesses `this` */
  }
}

(Aside: this is a class. Classes are explained in the next chapter.)

You can see that, in the embedded HTML, we specify a handler for clicks, via the onClick attribute (spelled differently than in real HTML). That handler is a function, an improperly extracted method.

Once again, we can fix the code via .bind():

<button onClick={this.handleClick.bind(this)}>

(Note: many React programmers prefer a different approach to extracting methods, but that is beyond the scope of this book.)

  Exercise: Extracting a method

exercises/single-objects/method_extraction_exrc.js

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

const obj = {
  name: 'Jane',
  sayHiTo(friends) {
    return friends.map(
      function (friend) { // (A)
        return `${this.name} says hi to ${friend}`; // (B)
      });
  }
};
assert.throws(
  () => obj.sayHiTo(['Tarzan', 'Cheeta']),
  { name: 'TypeError' ,
    message: "Cannot read property 'name' of undefined" });

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.

const obj = {
  name: 'Jane',
  sayHiTo(friends) {
    return friends.map(
      (friend) => {
        return `${this.name} says hi to ${friend}`;
      });
  }
};
assert.deepEqual(
  obj.sayHiTo(['Tarzan', 'Cheeta']),
  ['Jane says hi to Tarzan', 'Jane says hi to Cheeta']);

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

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

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

22.4.7. Tips for using this

We have seen two big this-related pitfalls:

  1. Accidentally shadowing this.
  2. Extracting methods.

One simple rule helps avoid the first pitfall:

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

Let’s break down this rule:

However, while I never use function expressions anymore, I do like function declarations syntactically and hoisting is also convenient. You can use them safely if you don’t refer to this inside them. The checking tool ESLint has a rule that helps with that.

Alas, there is no simple way around the second pitfall: Whenever you extract a method, you have to be careful and do it properly. For example, by binding this.

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

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

const obj = {
  mustBeAnIdentifier: 123,
};

// Get property
assert.equal(obj.mustBeAnIdentifier, 123);

// Set property
obj.mustBeAnIdentifier = 'abc';
assert.equal(obj.mustBeAnIdentifier, 'abc');

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
assert.equal(obj['Can be any string!'], 123);

// Set property
obj['Can be any string!'] = 'abc';
assert.equal(obj['Can be any string!'], 'abc');

You can also quote the keys of methods:

const obj = {
  'A nice method'() {
    return 'Yes!';
  },
};

assert.equal(obj['A nice method'](), 'Yes!');

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

const obj = {
  ['Hello world!']: true,
  ['f'+'o'+'o']: 123,
  [Symbol.toStringTag]: 'Goodbye', // (A)
};

assert.equal(obj['Hello world!'], true);
assert.equal(obj.foo, 123);
assert.equal(obj[Symbol.toStringTag], 'Goodbye');

The main use case for computed keys is having symbols as property keys (line A).

Note that the square brackets operator for getting and setting properties works with arbitrary expressions:

assert.equal(obj['f'+'o'+'o'], 123);
assert.equal(obj['==> foo'.slice(-3)], 123);

Methods can have computed property keys, too:

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

assert.equal(obj[methodKey](), 'Yes!');

We are now switching back to fixed property keys, but you can always use square brackets if you need computed property keys.

  Exercise: Non-destructively updating properties via spreading (computed key)

exercises/single-objects/update_property_test.js

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

const obj = {
  foo: 'abc',
  bar: undefined,
};

assert.equal('foo' in obj, true);
assert.equal('unknownKey' in obj, false);

You can also use a truthiness check to determine if a property exists:

assert.equal(
  obj.unknownKey ? 'exists' : 'does not exist',
  'does not exist');
assert.equal(
  obj.foo ? 'exists' : 'does not exist',
  '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.):

assert.equal(
  obj.bar ? 'exists' : 'does not exist',
  'does not exist'); // alas: not 'exists'

22.5.4. Deleting properties

You can delete properties via the delete operator:

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

delete obj.foo;

assert.deepEqual(Object.keys(obj), []);

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

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

We want dict to be treated as empty, but the in operator detects the properties it inherits from its prototype, Object.prototype.

The second pitfall is that 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;
// No property was added to dict:
assert.deepEqual(Object.keys(dict), []);

So how do we navigate around these pitfalls?

  Exercise: Using an object as a dictionary

exercises/single-objects/simple_dict_test.js

22.5.6. Listing property keys

Table 19: 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. 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 ]);

22.5.7. Listing property values

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

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

22.5.8. Listing property 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};
assert.deepEqual(
  Object.entries(obj),
  [
    ['foo', 1],
    ['bar', 2],
  ]);

22.5.9. Properties are listed deterministically

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

  1. Properties with integer indices (e.g. Array indices)
  1. Remaining properties with string keys
  1. Properties with symbol keys

The following example demonstrates how property keys are sorted according to these rules:

> Object.keys({b:0,a:0, 2:0,1:0})
[ '1', '2', 'b', 'a' ]

(You can look up the details in the spec.)

22.6. Standard methods

Object.prototype defines several standard methods that can be overridden. Two important ones are:

Roughly, .toString() configures how objects are converted to strings:

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

And .valueOf() configures how objects are converted to numbers:

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

22.7. Advanced topics

The following subsections give a brief overviews of topics that are beyond the scope of this book.

22.7.1. Object.assign()

Object.assign() is a tool method:

Object.assign(target, source_1, source_2, ···)

This expression (destructively) merges source_1 into target, then source_2 etc. At the end, it returns target. For example:

const target = { foo: 1 };
const result = Object.assign(
  target,
  {bar: 2},
  {baz: 3, bar: 4});
assert.deepEqual(
  result, { foo: 1, bar: 4, baz: 3 });
// target was changed!
assert.deepEqual(target, result);

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

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

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

For more information on Object.freeze(), consult “Speaking JavaScript”.

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

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 property obj.foo:

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

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

const obj = {
  foo: 1,
  bar: 2,
};

assert.deepEqual(Object.keys(obj), ['foo', 'bar']);

// Hide property `bar` from Object.keys()
Object.defineProperty(obj, 'bar', {
  enumerable: false,
});

assert.deepEqual(Object.keys(obj), ['foo']);

For more on property attributes and property descriptors, consult “Speaking JavaScript”.

  Quiz

See quiz app.