JavaScript Objects - Exercise 2

Object destructuring lets you unpack properties from an object into separate variables in one go, instead of writing multiple assignment lines. You match the variable name to the property name inside curly braces on the left side of an assignment. You can also rename while destructuring, set default values for missing properties, and even destructure nested objects - it keeps your code clean and saves a lot of repetition.

javascript

const user = { name: 'Grace', age: 32, city: 'Berlin' };

// Basic destructuring
const { name, age } = user;
console.log(name); // 'Grace'
console.log(age);  // 32

// Rename while destructuring
const { name: fullName, city: location } = user;
console.log(fullName);  // 'Grace'
console.log(location);  // 'Berlin'

// Default value if property is missing
const { name: n, country = 'Unknown' } = user;
console.log(country); // 'Unknown'

// Nested destructuring
const order = { id: 1, customer: { name: 'Henry', email: 'h@test.com' } };
const { customer: { name: customerName } } = order;
console.log(customerName); // 'Henry'

The two common ways to merge objects are the spread operator and Object.assign(). Both copy the properties from source objects into a target. When a key exists in both objects, the one that comes last wins - later properties overwrite earlier ones. The spread approach is more readable and widely preferred in modern code. Neither method does a deep merge - nested objects are still shared by reference.

javascript

const defaults = { theme: 'light', fontSize: 14, lang: 'en' };
const userPrefs = { theme: 'dark', notifications: true };

// Spread - later properties win
const merged = { ...defaults, ...userPrefs };
console.log(merged);
// { theme: 'dark', fontSize: 14, lang: 'en', notifications: true }

// Object.assign - same result
const merged2 = Object.assign({}, defaults, userPrefs);
console.log(merged2);
// { theme: 'dark', fontSize: 14, lang: 'en', notifications: true }

// Merge multiple objects at once
const a = { x: 1 };
const b = { y: 2 };
const c = { z: 3 };
const all = { ...a, ...b, ...c };
console.log(all); // { x: 1, y: 2, z: 3 }

Computed property names let you use an expression as a property key when creating an object literal - wrap the expression in square brackets inside the curly braces. This is really handy when you need to build objects dynamically, like when the key comes from a variable, a function call, or a template string. Without computed keys, you'd have to create the object first and then add the property separately.

javascript

const field = 'username';
const prefix = 'get';

// Computed key from a variable
const obj = {
  [field]: 'alice123',
  [`${prefix}Name`]: function() { return this[field]; }
};

console.log(obj.username);   // 'alice123'
console.log(obj.getName());  // 'alice123'

// Building objects dynamically
function makeConfig(env) {
  return {
    [env + '_host']: 'localhost',
    [env + '_port']: env === 'prod' ? 443 : 3000
  };
}

console.log(makeConfig('dev'));
// { dev_host: 'localhost', dev_port: 3000 }

When a variable name matches the property name you want in an object, you can just write the variable name once instead of name: name. This is called shorthand property syntax and it was introduced in ES6. The same concept applies to methods too - you can write greet() {} instead of greet: function() {}. It makes object literals much more concise, especially when building objects from existing variables.

javascript

const name = 'Ivy';
const age = 27;
const city = 'Tokyo';

// Without shorthand (verbose)
const personOld = { name: name, age: age, city: city };

// With shorthand (clean)
const person = { name, age, city };
console.log(person); // { name: 'Ivy', age: 27, city: 'Tokyo' }

// Shorthand methods
const calculator = {
  value: 0,
  add(n)      { this.value += n; return this; },
  subtract(n) { this.value -= n; return this; },
  result()    { return this.value; }
};

console.log(calculator.add(10).subtract(3).result()); // 7

Inside an object method, this refers to the object that the method was called on. It lets a method access and work with the object's own properties without needing to know the object's variable name. One important thing to watch: arrow functions don't have their own this - they inherit it from the surrounding scope. So if you need this to point to the object, use a regular function, not an arrow function, for the method.

javascript

const counter = {
  count: 0,
  increment() {
    this.count++;         // 'this' is the counter object
    return this.count;
  },
  reset() {
    this.count = 0;
  }
};

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
counter.reset();
console.log(counter.count);       // 0

// Arrow function - 'this' is NOT the object
const broken = {
  value: 42,
  getValue: () => this.value  // 'this' here is the outer scope (window/undefined)
};
console.log(broken.getValue()); // undefined

Object.entries() returns an array of [key, value] pairs for all own enumerable properties of an object. Each pair is itself a two-element array. It's great when you need both the key and the value at the same time - for example, when transforming an object, filtering it, or displaying its contents. You can combine it nicely with destructuring inside forEach or for...of to keep things readable.

javascript

const inventory = { apples: 5, bananas: 12, oranges: 0 };

// Get all [key, value] pairs
console.log(Object.entries(inventory));
// [['apples', 5], ['bananas', 12], ['oranges', 0]]

// Loop with destructuring
for (const [item, qty] of Object.entries(inventory)) {
  console.log(`${item}: ${qty} in stock`);
}

// Filter only items in stock
const inStock = Object.entries(inventory)
  .filter(([, qty]) => qty > 0)
  .map(([item]) => item);
console.log(inStock); // ['apples', 'bananas']

// Convert back to object after transformation
const doubled = Object.fromEntries(
  Object.entries(inventory).map(([key, val]) => [key, val * 2])
);
console.log(doubled); // { apples: 10, bananas: 24, oranges: 0 }

Object.freeze() makes an object immutable at the top level - you can't add new properties, delete existing ones, or change their values. Any attempt to modify a frozen object is silently ignored in non-strict mode, or throws a TypeError in strict mode. The key limitation: it only freezes one level deep. Nested objects are still mutable unless you freeze them separately. Use it when you want to treat an object as a constant, like a configuration object.

javascript

const config = Object.freeze({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
});

// These all fail silently (no error in normal mode)
config.apiUrl = 'https://other.com'; // ignored
config.newProp = 'test';             // ignored
delete config.timeout;               // ignored

console.log(config.apiUrl);  // 'https://api.example.com' - unchanged
console.log(config.timeout); // 5000 - still there

// Check if frozen
console.log(Object.isFrozen(config)); // true

// Shallow only - nested objects are NOT frozen
const obj = Object.freeze({ nested: { x: 1 } });
obj.nested.x = 99; // This WORKS - nested objects aren't frozen
console.log(obj.nested.x); // 99

Getters and setters are special methods that let you define computed properties or add logic when reading or writing a value. A getter runs when you read the property, and a setter runs when you assign to it. From the outside, they look just like regular properties - you don't call them with parentheses. They're great for derived values (like a full name from first and last), or for validating input before storing it.

javascript

const person = {
  firstName: 'James',
  lastName: 'Bond',

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set fullName(name) {
    const parts = name.split(' ');
    this.firstName = parts[0];
    this.lastName  = parts[1] || '';
  }
};

// Getter - accessed like a property, not a method
console.log(person.fullName); // 'James Bond'

// Setter - triggered by assignment
person.fullName = 'Sherlock Holmes';
console.log(person.firstName); // 'Sherlock'
console.log(person.lastName);  // 'Holmes'

// Validation in a setter
const temperature = {
  _celsius: 0,
  get fahrenheit() { return this._celsius * 9 / 5 + 32; },
  set fahrenheit(f) {
    if (f < -459.67) throw new RangeError('Below absolute zero!');
    this._celsius = (f - 32) * 5 / 9;
  }
};
temperature.fahrenheit = 212;
console.log(temperature._celsius); // 100

Optional chaining (?.) lets you safely access deeply nested properties without throwing an error if something along the chain is null or undefined. Without it, you'd need a chain of && checks. With it, if any part of the path is nullish, the whole expression short-circuits and returns undefined instead of crashing. It works with dot access, bracket access, and even method calls.

javascript

const user = {
  name: 'Kate',
  address: {
    street: '10 Downing St',
    city: 'London'
  }
};

// Normal access - works fine
console.log(user.address.city); // 'London'

// Optional chaining - safe access
console.log(user.address?.postcode); // undefined (no error)
console.log(user.phone?.mobile);     // undefined (no error)

// Without optional chaining - would throw TypeError:
// user.phone.mobile  ← TypeError: Cannot read properties of undefined

// Works with methods too
console.log(user.getAge?.()); // undefined - method doesn't exist, no crash

// Combine with nullish coalescing for defaults
const city = user.location?.city ?? 'City not set';
console.log(city); // 'City not set'

There are three static methods for converting objects to arrays: Object.keys() gives you the property names, Object.values() gives you the values, and Object.entries() gives you [key, value] pairs. All three return regular arrays that you can sort, filter, map, or reduce however you like. The counterpart - turning an array of pairs back into an object - is Object.fromEntries(), which works great after transformations.

javascript

const grades = { Alice: 88, Bob: 74, Carol: 95, Dave: 61 };

// Keys only
console.log(Object.keys(grades));   // ['Alice', 'Bob', 'Carol', 'Dave']

// Values only
console.log(Object.values(grades)); // [88, 74, 95, 61]

// Key-value pairs
console.log(Object.entries(grades));
// [['Alice', 88], ['Bob', 74], ['Carol', 95], ['Dave', 61]]

// Sort students by grade (highest first)
const ranked = Object.entries(grades)
  .sort(([, a], [, b]) => b - a)
  .map(([name, score]) => `${name}: ${score}`);
console.log(ranked);
// ['Carol: 95', 'Alice: 88', 'Bob: 74', 'Dave: 61']

// Convert back to object after filtering passing grades
const passing = Object.fromEntries(
  Object.entries(grades).filter(([, score]) => score >= 75)
);
console.log(passing); // { Alice: 88, Carol: 95 }