Objects

Beginner40 min

Model data with object literals, shorthand properties, computed keys, in, Object.hasOwn, delete, keys, values and entries.

What you will learn
  • Write object literals with shorthand properties, computed keys and methods
  • Read and write properties with dot and bracket notation
  • Check for a property with in and Object.hasOwn
  • List an object's contents with Object.keys, values and entries and rebuild one with fromEntries
On this page

A record groups named fields into one value. The idea is described in the fundamentals lesson on records and objects. In JavaScript the tool is the object: a collection of properties, each a key and a value. This lesson covers the syntax and the built-in functions for inspecting objects. Classes, which build objects from a template, come later. The this lesson came first and used object literals in passing. Here is the full picture.

Object literals

Write an object with braces and key: value pairs. Keys are strings (or symbols). Quotes around a key are needed only when it is not a valid identifier, like "favourite colour".

objects_basic.mjs
const user = {
name: "Ada",
age: 36,
"favourite colour": "green",
skills: ["maths", "code"],
};
console.log(user.name, user["age"]);
console.log(user["favourite colour"]);
console.log(user.email);
const field = "name";
console.log(user[field]);
Output
Ada 36
green
undefined
Ada

Assigning to a key adds or replaces a property, and delete removes one:

objects_basic.mjs (continued)
user.email = "ada@example.com";
user.age = 37;
console.log(user);
delete user.email;
console.log(user.email, "email" in user);
Output
{
name: 'Ada',
age: 37,
'favourite colour': 'green',
skills: [ 'maths', 'code' ],
email: 'ada@example.com'
}
undefined false

Shorthand and computed keys shorten a literal:

objects_basic.mjs (continued)
const name = "Grace";
const age = 85;
const shorthand = { name, age };
console.log(shorthand);
const key = "level";
const dynamic = { [key]: 3, [`${key}Max`]: 10 };
console.log(dynamic);
Output
{ name: 'Grace', age: 85 }
{ level: 3, levelMax: 10 }
  • Dot notation (user.name) needs a plain identifier. Bracket notation (user["favourite colour"]) accepts any string, including a key held in a variable (user[field]).
  • Reading a property that does not exist gives undefined. It is not an error.
  • Assigning to a new key adds a property. Assigning to an existing key replaces its value. A const object can still gain, lose and change properties.
  • delete user.email removes a property. After that, "email" in user is false.
  • Shorthand: { name, age } means { name: name, age: age }.
  • Computed keys: [expression]: value evaluates the expression and uses the result as the key. The example builds level and levelMax from a variable.

Checking for a property

undefined does not tell you whether a property exists, because a property can hold undefined. Use in or Object.hasOwn.

objects_inspect.mjs
const point = { x: 1, y: 2, label: undefined };
console.log("x" in point, "z" in point);
console.log("label" in point, point.label !== undefined);
console.log(Object.hasOwn(point, "x"), Object.hasOwn(point, "toString"));
console.log("toString" in point);
Output
true false
true false
true false
true

The listing functions, a loop over the entries, and fromEntries to build an object back:

objects_inspect.mjs (continued)
console.log(Object.keys(point));
console.log(Object.values(point));
console.log(Object.entries(point));
for (const [key, value] of Object.entries({ a: 1, b: 2 })) {
console.log(key, "=", value);
}
const prices = { apple: 1, pear: 2 };
const doubled = Object.fromEntries(
Object.entries(prices).map(([name, price]) => [name, price * 2]),
);
console.log(doubled);
Output
[ 'x', 'y', 'label' ]
[ 1, 2, undefined ]
[ [ 'x', 1 ], [ 'y', 2 ], [ 'label', undefined ] ]
a = 1
b = 2
{ apple: 2, pear: 4 }

Whole-number keys are listed first:

objects_inspect.mjs (continued)
const messy = { b: 1, 2: "two", a: 2, 1: "one" };
console.log(Object.keys(messy));
Output
[ '1', '2', 'b', 'a' ]
  • "x" in point is true if the object has the property, including inherited ones. "toString" in point is true because every object inherits toString.
  • Object.hasOwn(point, "x") checks only the object’s own properties, so Object.hasOwn(point, "toString") is false. This is usually what you want. It replaced the older obj.hasOwnProperty(key), which fails on objects that do not inherit from Object.prototype and can be shadowed by a property of the same name.
  • label: undefined exists as a property, so "label" in point is true even though its value is undefined.

Listing the contents

Object.keys, Object.values and Object.entries return arrays of an object’s own enumerable properties. That makes all the array methods available for working on objects. Object.fromEntries goes the other way and builds an object from an array of [key, value] pairs.

The example above shows all four. The loop over Object.entries uses destructuring in the loop head: const [key, value] unpacks each pair. The doubled object is built by mapping over the entries and passing the result to fromEntries.

Key order follows a fixed rule. Keys that look like whole numbers ("1", "2") come first, in ascending order. All other keys follow in the order they were added. That is why messy lists '1', '2', 'b', 'a' although b was written first. If order of insertion matters for arbitrary keys, use a Map, covered later in this module.

Methods and nested data

A function stored in an object is a method. The short form increment() { ... } is the same as increment: function () { ... }. The this rules from the this lesson apply: inside a method called as counter.increment(), this is counter. Returning this allows chaining.

Objects nest. Reading through a missing level fails if you do it with dots, because undefined.host throws a TypeError. The optional chaining operator ?. stops and returns undefined instead, and ?? supplies a fallback, as the null and undefined lesson showed.

objects_methods.mjs
const counter = {
count: 0,
increment() {
this.count += 1;
return this;
},
describe: function () {
return `count is ${this.count}`;
},
};
counter.increment().increment();
console.log(counter.describe());
Output
count is 2

Objects nest, and ?. copes with a missing level:

objects_methods.mjs (continued)
const config = { server: { port: 8080 } };
console.log(config.server.port);
console.log(config.database?.host);
console.log(config.database?.host ?? "localhost");
Output
8080
undefined
localhost

And assigning an object copies the reference, not the object:

objects_methods.mjs (continued)
const original = { a: 1 };
const alias = original;
alias.a = 2;
console.log(original.a, original === alias, { a: 2 } === { a: 2 });
Output
2 true false

The last line is the most important thing to remember about objects. alias and original are two names for one object, so changing one changes the other. Two separate literals that look alike are two objects, and === compares identity, so { a: 2 } === { a: 2 } is false. The references lesson in this module takes this further.

Getters, setters and merging

A property can be computed when you read it. Put get before a method in the literal and it becomes an accessor property: you read it like a plain property, with no brackets after it. Put set before a method and assigning to the name runs it.

accessors.mjs
const rectangle = {
width: 4,
height: 3,
get area() {
return this.width * this.height;
},
set size(value) {
this.width = value;
this.height = value;
},
};

Reading and assigning:

accessors.mjs (continued)
console.log(rectangle.area);
rectangle.size = 5;
console.log(rectangle.width, rectangle.height, rectangle.area);
try {
rectangle.area = 99;
} catch (error) {
console.log(error.name);
}
Output
12
5 5 25
TypeError

rectangle.area looks like a stored value but is calculated each time from width and height, so it is never out of date. Assigning to size runs the setter, which changes both sides. A property with only a getter cannot be assigned to, and in a module that assignment throws a TypeError. Accessors keep the object’s data consistent without asking callers to call methods.

Object.assign(target, ...sources) copies the own properties of each source onto target, left to right, and returns target. Give it an empty object first to get a fresh copy:

object_assign.mjs
const defaults = { theme: "light", size: 12 };
const settings = Object.assign({}, defaults, { size: 16 });
console.log(settings, defaults);
const target = { a: 1 };
const result = Object.assign(target, { b: 2 });
console.log(result === target, target);
Output
{ theme: 'light', size: 16 } { theme: 'light', size: 12 }
true { a: 1, b: 2 }

The first call leaves defaults alone because the target is the new {}. The second changes target itself and returns that same object. Later sources win over earlier ones when keys clash. Lesson 27 shows the spread syntax { ...defaults }, which does the same job more concisely. Both make shallow copies.

Common pitfalls

  • Using dot notation with a variable. user.field looks for a property named field. Use user[field].
  • Testing existence with if (obj.key). It is false for 0, "" and false as well as for missing keys. Use Object.hasOwn or in.
  • Expecting a key order you did not write. Whole-number keys are sorted and come first.
  • Using an object as a dictionary with untrusted keys. Inherited names like toString appear to exist. A Map avoids this.
  • Assuming const freezes an object. It only fixes the variable.
  • Reading through a missing level. Use ?. where a level might be absent.
  • Assigning to a getter-only property. In a module it throws a TypeError. Add a setter, or do not assign.

Exercises

ExerciseModel a library book and member

Create a book object with isbn, title, authors (an array) and available, and a member with a name and a borrowed array. Write lend(book, member) that does nothing and returns false if the book is not available, and otherwise marks it unavailable, adds its ISBN to the member’s list and returns true. Lend the same book twice and print the member and how many properties the book has. The output should be true, false, then the member with one ISBN in borrowed, then 4.

Show solution
library_model.mjs
const book = {
isbn: "978-0-13-468599-1",
title: "The Pragmatic Programmer",
authors: ["Hunt", "Thomas"],
available: true,
};
const member = {
name: "Ada",
borrowed: [],
};

Then the function, and a program that uses it:

library_model.mjs (continued)
function lend(book, member) {
if (!book.available) return false;
book.available = false;
member.borrowed.push(book.isbn);
return true;
}
console.log(lend(book, member));
console.log(lend(book, member));
console.log(member);
console.log(Object.keys(book).length);
Output
true
false
{ name: 'Ada', borrowed: [ '978-0-13-468599-1' ] }
4

The second lend returns false because the first one set available to false. The function changes both objects it receives, which is possible because objects are passed by reference. Object.keys(book).length counts the four properties.

ExerciseList the fruit in stock

Given const stock = { apple: 3, pear: 0, plum: 5 }, print only the names of fruits that are in stock (count above zero), as an array. The output should be [ 'apple', 'plum' ]. Object.keys(stock) gives the names, and stock[name] gives the count for a name.

Show solution
stock_filter.mjs
const stock = { apple: 3, pear: 0, plum: 5 };
const inStock = Object.keys(stock).filter((name) => stock[name] > 0);
console.log(inStock);
Output
[ 'apple', 'plum' ]

Object.keys returns the list of names. filter keeps a name when stock[name] is above zero, using bracket notation because the name is in a variable. pear has a count of 0, so it is dropped.

ExerciseA temperature with two views

Write an object temperature with a celsius property and a fahrenheit accessor. Reading fahrenheit converts from celsius, and assigning to fahrenheit updates celsius. Start at celsius: 100, then print temperature.fahrenheit, assign 32 to fahrenheit and print celsius, then assign 37 to celsius and print fahrenheit. The output should be 212, 0 and 98.6.

Show solution
temperature_accessor.mjs
const temperature = {
celsius: 100,
get fahrenheit() {
return (this.celsius * 9) / 5 + 32;
},
set fahrenheit(value) {
this.celsius = ((value - 32) * 5) / 9;
},
};
console.log(temperature.fahrenheit);
temperature.fahrenheit = 32;
console.log(temperature.celsius);
temperature.celsius = 37;
console.log(temperature.fahrenheit);
Output
212
0
98.6

Only celsius is stored. The getter calculates Fahrenheit from it on every read, and the setter converts back, so the two views cannot disagree. Inside both functions this is temperature, because they are called through it.

Summary

  • Objects hold named properties. Use dot notation for fixed names and brackets for names held in variables or with unusual characters.
  • Shorthand properties, computed keys and method shorthand shorten literals.
  • in includes inherited properties. Object.hasOwn checks only the object’s own.
  • Object.keys, values and entries turn an object into arrays, and fromEntries turns pairs back into an object.
  • get and set in a literal make accessor properties that are computed on read and write. Object.assign copies properties onto a target.
  • Objects are shared, not copied, when you assign them or pass them to functions.

Next: unpacking objects and arrays with destructuring and spread.