JavaScript · lesson 10 of 24
Beginner
11- 1 Introduction to JavaScript
- 2 Setting Up JavaScript Development Environment
- 3 Variables in JavaScript: var, let, and const
- 4 JavaScript Data Types
- 5 JavaScript Operators
- 6 Control Flow in JavaScript
- 7 Functions in JavaScript
- 8 Working with Strings in JavaScript
- 9 Arrays in JavaScript
- 10 Objects in JavaScript
- 18 DOM Manipulation in JavaScript
Intermediate
10- 11 Error Handling in JavaScript
- 12 Object-Oriented Programming in JavaScript
- 13 Prototypes and Prototype Chain in JavaScript
- 14 Closures in JavaScript
- 15 JavaScript Event Loop Explained
- 16 Asynchronous JavaScript: Callbacks, Promises, and Async/Await
- 17 JavaScript Modules: ESM and CommonJS
- 19 Browser Web APIs in JavaScript
- 20 Testing JavaScript with Jest and Vitest
- 24 JavaScript Interview Preparation: Top 35 Questions
Advanced
3Objects in JavaScript
Learn JavaScript objects — literals, destructuring, spread/rest, Object utility methods, symbols, and patterns like config merging.
By SkillByExample · Updated
An object is a collection of key–value pairs, and almost everything in JavaScript that isn’t a primitive is one. This lesson covers creating, reading, copying and merging them. Every example is a complete file run with node.
Object Literals
Create an object with { }; read with dot notation for fixed names and brackets for dynamic ones.
const user = { id: 1, name: "Alice", age: 30, active: true, "favourite-colour": "teal" };
console.log(user.name, user["age"]);
console.log(user["favourite-colour"]);
const key = "id";
console.log(user[key], user.key);
user.email = "alice@example.com";
user.age = 31;
delete user.active;
console.log(user);
console.log(user.phone);
$ node literal.js
Alice 30
teal
1 undefined
{
id: 1,
name: 'Alice',
age: 31,
'favourite-colour': 'teal',
email: 'alice@example.com'
}
undefined
user[key] looked up the property named by the variable ("id"); user.key looked for a property literally called key and found nothing. A missing property is undefined, not an error. Objects are mutable even when held in a const — const stops you reassigning user, not changing what’s inside it.
Shorthand: Properties, Methods, Computed Keys
Three pieces of literal syntax you’ll see everywhere.
const name = "Bob";
const age = 25;
const field = "email";
const index = 2;
const person = {
name,
age,
[field]: "bob@example.com",
[`item_${index}`]: "banana",
greet() { return `Hi, I'm ${this.name}`; },
};
console.log(person);
console.log(person.greet());
const calculator = {
value: 0,
add(n) { this.value += n; return this; },
subtract(n) { this.value -= n; return this; },
};
console.log(calculator.add(10).add(5).subtract(3).value);
$ node shorthand.js
{
name: 'Bob',
age: 25,
email: 'bob@example.com',
item_2: 'banana',
greet: [Function: greet]
}
Hi, I'm Bob
12
{ name, age } means { name: name, age: age }. Square brackets in a key position evaluate an expression, so the keys became email and item_2. Node prints methods as [Function: greet]. Returning this from each method is what makes the calculator chainable.
Destructuring
Destructuring unpacks properties into variables — with renaming, defaults and nesting.
const product = { id: 42, name: "Laptop", price: 999, discount: undefined, meta: { sku: "LP-1" } };
const { id, name, price } = product;
console.log(id, name, price);
const { name: productName, price: cost } = product;
console.log(productName, cost);
const { discount = 0, category = "general", price: p = 1 } = product;
console.log(discount, category, p);
const { meta: { sku }, meta } = product;
console.log(sku, meta);
function formatAddress({ street, city, country = "US", zip }) {
return `${street}, ${city}, ${country} ${zip}`;
}
console.log(formatAddress({ street: "123 Main St", city: "Austin", zip: "73301" }));
$ node destructure.js
42 Laptop 999
Laptop 999
0 general 999
LP-1 { sku: 'LP-1' }
123 Main St, Austin, US 73301
{ name: productName } reads “take name, call it productName” — the colon renames, it isn’t a type. Defaults apply only when the value is undefined: discount exists but is undefined, so it got 0; price exists, so the default 1 was ignored. Destructuring a function’s parameter documents which options it reads.
Spread and Rest
Spread (...obj inside a literal) copies properties into a new object, later keys winning; rest (...name in a pattern) collects whatever wasn’t named.
const defaults = { theme: "light", fontSize: 14, language: "en" };
const userPrefs = { fontSize: 16, language: "fr" };
const config = { ...defaults, ...userPrefs };
const updated = { ...config, theme: "dark" };
console.log(config);
console.log(updated, config.theme);
const { id, password, ...publicFields } = { id: 1, password: "hunter2", name: "Alice", city: "London" };
console.log(publicFields);
const wrongOrder = { theme: "dark", ...defaults };
console.log(wrongOrder.theme);
$ node spread.js
{ theme: 'light', fontSize: 16, language: 'fr' }
{ theme: 'dark', fontSize: 16, language: 'fr' } light
{ name: 'Alice', city: 'London' }
light
Order is everything: { ...defaults, ...userPrefs } lets the user win, and { theme: "dark", ...defaults } silently loses the override because defaults came later. Rest is the clean way to drop fields — here, stripping password before sending a user object anywhere — without mutating the original.
keys, values, entries, fromEntries
These convert an object to arrays so you can use map, filter and reduce, and back.
const scores = { alice: 90, bob: 78, carol: 95 };
console.log(Object.keys(scores));
console.log(Object.values(scores));
console.log(Object.entries(scores));
const total = Object.values(scores).reduce((sum, n) => sum + n, 0);
console.log("total", total, "average", (total / Object.keys(scores).length).toFixed(1));
const curved = Object.fromEntries(
Object.entries(scores).map(([name, score]) => [name, Math.min(score + 5, 100)]),
);
console.log(curved);
const passed = Object.fromEntries(Object.entries(scores).filter(([, s]) => s >= 80));
console.log(passed);
$ node entries.js
[ 'alice', 'bob', 'carol' ]
[ 90, 78, 95 ]
[ [ 'alice', 90 ], [ 'bob', 78 ], [ 'carol', 95 ] ]
total 263 average 87.7
{ alice: 95, bob: 83, carol: 100 }
{ alice: 90, carol: 95 }
entries → array transform → fromEntries is the general recipe for “map/filter over an object”. In ([, s]) => … the leading comma skips the key.
Key Order
Property order is defined, but it isn’t simply insertion order.
const obj = { b: 1, 10: "ten", a: 2, 2: "two", "-1": "minus one", 1.5: "x" };
console.log(Object.keys(obj));
const byId = Object.fromEntries([[30, "Carol"], [4, "Alice"], [12, "Bob"]]);
console.log(Object.values(byId));
$ node order.js
[ '2', '10', 'b', 'a', '-1', '1.5' ]
[ 'Alice', 'Bob', 'Carol' ]
Keys that look like non-negative integers ("2", "10") come first in ascending numeric order; every other string key follows in insertion order — "-1" and "1.5" count as ordinary strings. So an object keyed by numeric id “re-sorts” itself. If order matters, use an array or a Map, which keeps pure insertion order.
Object.assign, freeze and seal
Object.assign copies into (and mutates) its first argument. freeze and seal lock an object down — shallowly.
"use strict";
const target = { a: 1, b: 2 };
const result = Object.assign(target, { b: 3, c: 4 });
console.log(target, result === target);
const CONFIG = Object.freeze({ timeout: 5000, db: { host: "localhost" } });
try { CONFIG.timeout = 9999; } catch (err) { console.log(err.constructor.name + ":", err.message); }
CONFIG.db.host = "prod-db";
console.log(CONFIG);
const sealed = Object.seal({ x: 1 });
sealed.x = 2;
try { sealed.y = 3; } catch (err) { console.log(err.message); }
console.log(sealed, Object.isFrozen(CONFIG), Object.isSealed(sealed));
$ node freeze.js
{ a: 1, b: 3, c: 4 } true
TypeError: Cannot assign to read only property 'timeout' of object '#<Object>'
{ timeout: 5000, db: { host: 'prod-db' } }
Cannot add property y, object is not extensible
{ x: 2 } true true
In strict mode ("use strict", and always inside classes and ES modules), writing to a frozen object throws; in a plain script without it, the write is silently ignored — easy to miss. Either way freeze is shallow: CONFIG.db is a separate, unfrozen object, so its host changed. seal allows changing existing values but not adding or removing keys.
Symbol Keys
A Symbol key can never collide with a string key and is skipped by Object.keys, for...in and JSON.stringify.
const ID = Symbol("id");
const record = { name: "Alice", [ID]: 1001 };
console.log(record[ID]);
console.log(Object.keys(record), JSON.stringify(record));
for (const k in record) console.log("for...in sees", k);
console.log(Object.getOwnPropertySymbols(record));
console.log(record);
$ node symbols.js
1001
[ 'name' ] {"name":"Alice"}
for...in sees name
[ Symbol(id) ]
{ name: 'Alice', [Symbol(id)]: 1001 }
Symbols are hidden from normal enumeration but not secret: Object.getOwnPropertySymbols lists them and console.log shows them. They’re for avoiding name clashes — a library can tag your objects without risking overwriting your id — not for privacy; use closures or #private fields for that.
Shallow vs. Deep Copies
Spread and Object.assign copy only the top level; nested objects are shared.
const original = {
name: "Alice",
address: { city: "London" },
joined: new Date("2024-01-15"),
tags: new Set(["admin"]),
greet() { return "hi"; },
};
const shallow = { ...original };
shallow.address.city = "Paris";
console.log("original city after shallow copy edit:", original.address.city);
const viaJson = JSON.parse(JSON.stringify(original));
console.log("JSON:", viaJson);
const { greet, ...data } = original;
const cloned = structuredClone(data);
cloned.address.city = "Berlin";
console.log("structuredClone:", cloned.joined instanceof Date, cloned.tags, original.address.city);
try { structuredClone(original); } catch (err) { console.log(err.name + ":", err.message); }
$ node clone.js
original city after shallow copy edit: Paris
JSON: {
name: 'Alice',
address: { city: 'Paris' },
joined: '2024-01-15T00:00:00.000Z',
tags: {}
}
structuredClone: true Set(1) { 'admin' } Paris
DataCloneError: greet() { return "hi"; } could not be cloned.
The shallow copy shared address, so editing it changed the original too. The JSON round-trip deep-copies but mangles types — the Date became a string, the Set became {}, and the method vanished. structuredClone keeps Date, Set, Map and nested objects intact (the original stayed in Paris), but refuses objects containing functions — hence greet was split off first.
Checking Whether a Key Exists
A truthiness check can’t tell “missing” from “present but falsy”.
const config = { debug: false, port: 0, name: "" };
for (const key of ["debug", "port", "name", "missing", "toString"]) {
console.log(
key.padEnd(8),
"truthy:", String(Boolean(config[key])).padEnd(5),
"in:", String(key in config).padEnd(5),
"hasOwn:", Object.hasOwn(config, key),
);
}
$ node exists.js
debug truthy: false in: true hasOwn: true
port truthy: false in: true hasOwn: true
name truthy: false in: true hasOwn: true
missing truthy: false in: false hasOwn: false
toString truthy: true in: true hasOwn: false
All three real keys look “missing” to the truthy test because false, 0 and "" are falsy. in finds them — but also finds toString, which is inherited from Object.prototype. Object.hasOwn answers the question you usually mean: does this object have the key?
this in Arrow Functions vs. Methods
Arrow functions don’t get their own this, so they make poor methods.
const counter = {
value: 42,
getArrow: () => typeof this === "object" ? JSON.stringify(this) : String(this),
getMethod() { return this.value; },
getLater() { return [1, 2].map(() => this.value); },
};
console.log(counter.getArrow());
console.log(counter.getMethod());
console.log(counter.getLater());
$ node this.js
{}
42
[ 42, 42 ]
getArrow’s this is whatever this was outside the object literal — in a CommonJS file that’s the empty module.exports object, {}, and in a browser script it would be window; never counter. That same property is what makes arrows useful inside methods: in getLater, the arrow passed to map inherits the method’s this, so it reads counter.value.
Key Takeaways
- Dot notation for fixed keys, brackets for dynamic ones; missing properties are
undefined. - Destructuring renames with
:and defaults only onundefined. - In spreads, later keys win — put defaults first.
- Integer-like keys are ordered numerically before other keys.
- Spread copies are shallow; use
structuredClonefor deep copies of data. - Use
Object.hasOwnto test for a key, not truthiness.
Practice
1. Write pick(obj, ...keys) that returns a new object with only the given keys.
function pick(obj, ...keys) {
return Object.fromEntries(keys.filter((k) => Object.hasOwn(obj, k)).map((k) => [k, obj[k]]));
}
const user = { id: 1, name: "Alice", password: "hunter2", email: "a@example.com" };
console.log(pick(user, "id", "email"));
console.log(pick(user, "name", "nope"));
$ node p1.js
{ id: 1, email: 'a@example.com' }
{ name: 'Alice' }
Filtering with hasOwn first means asking for a missing key doesn’t create a nope: undefined entry.
2. Build a lookup of users by id and fetch one without scanning the array.
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Carol" },
];
const byId = Object.fromEntries(users.map((u) => [u.id, u]));
console.log(byId[2].name);
console.log(Object.keys(byId));
$ node p2.js
Bob
[ '1', '2', '3' ]
The keys became strings ("1", "2", "3") — object keys always are — but byId[2] still works because the number is converted to "2" on lookup.
3. Merge user options into defaults so that nested headers are merged too, not replaced.
const defaults = { timeout: 5000, headers: { "Content-Type": "application/json" } };
function createClient(options = {}) {
return { ...defaults, ...options, headers: { ...defaults.headers, ...options.headers } };
}
console.log(createClient({ timeout: 10000, headers: { Authorization: "Bearer abc" } }));
console.log({ ...defaults, ...{ headers: { Authorization: "Bearer abc" } } });
$ node p3.js
{
timeout: 10000,
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer abc' }
}
{ timeout: 5000, headers: { Authorization: 'Bearer abc' } }
The second line is the bug this fixes: a plain spread replaced the whole headers object and lost Content-Type.
4. Count how many times each word appears in a sentence, as an object.
const sentence = "the cat and the hat and the bat";
const counts = {};
for (const word of sentence.split(" ")) {
counts[word] = (counts[word] ?? 0) + 1;
}
console.log(counts);
const [top] = Object.entries(counts).sort((a, b) => b[1] - a[1]);
console.log("most common:", top);
$ node p4.js
{ the: 3, cat: 1, and: 2, hat: 1, bat: 1 }
most common: [ 'the', 3 ]
counts[word] ?? 0 supplies 0 the first time a word is seen; sorting the entries by count gives the winner.