On this page

Objects and arrays

20 min read TextCh. 2 — Data and structures

Creating objects

Objects are collections of key-value pairs. They are the most commonly used data structure in JavaScript for representing entities:

const product = {
  name: 'Laptop',
  price: 999,
  available: true,
};

When a variable name matches the property name, you can use the shorthand syntax:

const name = 'Laptop';
const price = 999;
const product = { name, price };
// Same as { name: name, price: price }

Computed property names

You can use expressions as property keys by wrapping them in brackets:

const field = 'color';
const item = { [field]: 'red', [`${field}Code`]: '#ff0000' };
// { color: 'red', colorCode: '#ff0000' }

This is especially useful when building objects dynamically from variables or function parameters.

Accessing properties

JavaScript offers two ways to read and write object properties:

const user = { name: 'Ana', age: 28 };

// Dot notation - clean and common
user.name;       // 'Ana'

// Bracket notation - dynamic or special keys
user['age'];     // 28

const key = 'name';
user[key];       // 'Ana'

Use dot notation by default for readability. Use bracket notation when the key is dynamic, stored in a variable, or contains characters like spaces or hyphens.

Object utility methods

Object.keys(), Object.values(), Object.entries()

These static methods let you extract and iterate over an object's contents:

const config = { theme: 'dark', lang: 'en', debug: false };

Object.keys(config);    // ['theme', 'lang', 'debug']
Object.values(config);  // ['dark', 'en', false]
Object.entries(config);
// [['theme', 'dark'], ['lang', 'en'], ['debug', false]]

Object.entries() is particularly powerful for iterating with for...of:

for (const [key, value] of Object.entries(config)) {
  console.log(`${key} = ${value}`);
}

Object.assign() and spread for merging

Both approaches merge properties from one or more source objects into a target:

// Object.assign - mutates the first argument
const defaults = { theme: 'light', lang: 'en' };
const prefs = { theme: 'dark' };
const merged = Object.assign({}, defaults, prefs);
// { theme: 'dark', lang: 'en' }

// Spread - cleaner, same result
const merged2 = { ...defaults, ...prefs };
// { theme: 'dark', lang: 'en' }

[!WARNING] Both Object.assign() and spread create shallow copies. Nested objects are still shared by reference. Use structuredClone() when you need a deep copy.

Optional chaining

The optional chaining operator (?.) lets you safely access deeply nested properties without checking each level manually:

const user = { name: 'Ana', address: { city: 'Oruro' } };

// Without optional chaining (verbose)
const zip = user.address && user.address.zip;

// With optional chaining (clean)
const zip2 = user.address?.zip;           // undefined
const street = user.location?.street;     // undefined (no error)

Combine it with the nullish coalescing operator (??) to provide fallback values:

const city = user.address?.city ?? 'Unknown';
// 'Oruro'

[!TIP] Optional chaining also works with methods (obj.method?.()) and arrays (arr?.[0]).

Getters and setters

Getters and setters let you define computed properties that behave like regular properties but run logic when accessed or assigned:

const account = {
  _balance: 1000,
  get balance() {
    return `$${this._balance.toFixed(2)}`;
  },
  set balance(amount) {
    if (amount < 0) throw new Error('Invalid amount');
    this._balance = amount;
  },
};

console.log(account.balance); // '$1000.00'
account.balance = 2500;
console.log(account.balance); // '$2500.00'

They are useful for validation, formatting, and encapsulating internal state.

Destructuring

Destructuring allows you to extract values from objects and arrays into individual variables in a concise and elegant way.

Object destructuring

const user = { name: 'Ana', age: 28, role: 'admin' };
const { name, age } = user;

Renaming variables during destructuring avoids naming conflicts:

const { name: userName, age: userAge } = user;
// userName = 'Ana', userAge = 28

Default values are used when the property is undefined:

const { name, country = 'Bolivia' } = user;
// country = 'Bolivia' (not in user)

Nested destructuring

Extract values from deeply nested objects in one step:

const company = {
  name: 'Acme',
  address: { city: 'Oruro', country: 'Bolivia' },
};
const { address: { city, country } } = company;
// city = 'Oruro', country = 'Bolivia'

Array destructuring

const colors = ['red', 'green', 'blue', 'yellow'];
const [primary, secondary] = colors;
// primary = 'red', secondary = 'green'

// Skip elements
const [, , third] = colors;
// third = 'blue'

Spread operator

The spread operator (...) expands an iterable (array, object) into individual elements. It is essential for immutable patterns:

// Copying arrays
const original = [1, 2, 3];
const copy = [...original];

// Merging arrays
const all = [...original, 4, 5];

// Copying objects
const user = { name: 'Ana', age: 28 };
const clone = { ...user };

// Merging objects (later properties override earlier ones)
const updated = { ...user, age: 29, role: 'admin' };

Rest in destructuring and functions

The rest syntax (...) collects remaining elements into an array or object. It is the counterpart of spread:

Rest in arrays

const [first, ...remaining] = [10, 20, 30, 40];
// first = 10, remaining = [20, 30, 40]

Rest in objects

const { name, ...details } = { name: 'Ana', age: 28, role: 'admin' };
// name = 'Ana', details = { age: 28, role: 'admin' }

Rest parameters in functions

Rest parameters let a function accept any number of arguments as a real array:

function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3);       // 6
sum(10, 20, 30, 40); // 100

[!INFO] Rest parameters must always be the last parameter in the function signature: function log(level, ...messages).

Multidimensional arrays

Arrays can contain other arrays, creating matrices or grids:

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

// Access: matrix[row][column]
console.log(matrix[0][2]); // 3
console.log(matrix[2][1]); // 8

Iterate over all cells with nested loops:

for (const row of matrix) {
  for (const cell of row) {
    console.log(cell);
  }
}

Practice

  1. Use destructuring with default values: Given the object { name: 'Ana', age: 28 }, extract name, age, and country with a default value of 'Bolivia' using destructuring. Print all three values to the console.
  2. Combine spread and optional chaining: Create a user object with a nested address.street property. Use spread to create a copy with a new age, and optional chaining with ?? to access user.contact?.phone with a default value of 'N/A'.
  3. Iterate with Object.entries: Create a scores object with 3 subjects and their grades. Iterate it with for...of and Object.entries(), printing "Subject: grade" for each one.

In the next lesson we will dive into array methods like map, filter, and reduce for powerful data transformations.

Immutability
Prefer creating new objects/arrays instead of mutating existing ones. This makes your code more predictable and easier to debug. Use spread or Object.assign() for shallow copies.
Shallow copies only
Both spread (...) and Object.assign() create shallow copies. Nested objects are still shared by reference. Use structuredClone() for deep copies.
Bracket notation for dynamic keys
Use dot notation (obj.key) for static property names and bracket notation (obj[key]) when the key is stored in a variable, contains special characters, or is computed at runtime.
javascript
// Creating objects
const user = {
  name: 'Ana',
  age: 28,
  skills: ['JavaScript', 'Angular'],
};

// Computed property names
const field = 'email';
const dynamic = { [field]: '[email protected]' };

// Accessing properties
console.log(user.name);        // 'Ana'
console.log(user['age']);       // 28

// Object.keys / values / entries
const keys = Object.keys(user);
// ['name', 'age', 'skills']
const values = Object.values(user);
// ['Ana', 28, ['JavaScript', 'Angular']]

// Destructuring with renaming and defaults
const { name: userName, role = 'student' } = user;
// userName = 'Ana', role = 'student'

// Spread - immutable copy and merge
const updated = { ...user, age: 29 };
const withEmail = { ...user, ...dynamic };

// Optional chaining + nullish coalescing
const city = user.address?.city ?? 'Not available';

// Rest parameters
function sum(...numbers) {
  return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
javascript
// Getters and setters
const account = {
  _balance: 1000,
  get balance() {
    return `$${this._balance.toFixed(2)}`;
  },
  set balance(amount) {
    if (amount < 0) throw new Error('Invalid amount');
    this._balance = amount;
  },
};
console.log(account.balance); // '$1000.00'
account.balance = 500;

// Nested destructuring
const company = {
  name: 'Acme',
  address: { city: 'Oruro', country: 'Bolivia' },
};
const { address: { city, country } } = company;
// city = 'Oruro', country = 'Bolivia'

// Multidimensional arrays
const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];
console.log(matrix[1][2]); // 6

// Array destructuring with rest
const [first, second, ...others] = [10, 20, 30, 40, 50];
// first = 10, second = 20, others = [30, 40, 50]

// Object.entries for iteration
const scores = { math: 95, science: 88, history: 72 };
for (const [subject, score] of Object.entries(scores)) {
  console.log(`${subject}: ${score}`);
}