Lesson 5 of 30

JavaScript Objects

Grouping Related Data Under One Name

An object groups related values together, with every value labelled by a key. Where an array is a numbered list of things of the same kind, an object is a labelled record describing one thing. A student has a name, a roll number, a set of marks and an enrolment status; none of those are interchangeable, so numbering them 0 to 3 would tell you nothing. Naming them is the entire point.

You write an object as a pair of braces containing key: value pairs separated by commas. Keys are strings underneath, but you can write them without quotes as long as they are valid identifiers, and most code does. Values can be anything at all — numbers, strings, booleans, arrays, other objects, and functions. An object holding a function is how JavaScript expresses "this thing can also do something", and that combination of data and behaviour is what makes objects the backbone of the language.

Objects turn up everywhere, not only where you create them. The response from an API is an object. The event handed to a click handler is an object. The configuration you pass to a library is an object. document itself is an object. Getting comfortable reading and reshaping objects is therefore not one topic among thirty — it is what you will actually be doing in most of the lessons that follow.

Nesting is normal and expected. A property can hold another object, which can hold an array of objects, and so on down. That is how real data arrives: an order contains a customer, and a list of items, and each item has a product with fields of its own. There is no depth limit, only the practical one that deeply nested data is tedious to work with — which is why destructuring, later in this lesson, exists.

Example
const student = {
  name: 'Ananya Sharma',
  rollNo: 'CS21-047',
  marks: { maths: 82, physics: 74, chemistry: 79 },
  subjects: ['Maths', 'Physics', 'Chemistry'],
  isEnrolled: true
};

console.log(student.name);                 // 'Ananya Sharma'
console.log(student.marks.physics);        // 74
console.log(student.subjects[0]);          // 'Maths'
console.log(typeof student);               // 'object'
console.log(Object.keys(student).length);  // 5

Dot Notation, Bracket Notation and Missing Keys

There are two ways to reach a property. Dot notation — student.name — is what you use whenever you know the key at the moment you are writing the code. Bracket notation — student['name'] — takes the key as a string expression, which means it can be worked out while the program is running.

That difference is the whole reason bracket notation exists. If a variable holds the key, only brackets will do: const field = 'rollNo'; student[field] gives the roll number, whereas student.field looks for a property literally called field and finds nothing. Brackets are also compulsory for keys that are not valid identifiers, such as report['total-marks'] or report['2024'], because the dot form of those would be a syntax error.

Reading a property that does not exist returns undefined instead of throwing. That is forgiving right up until the missing property is itself an object you then try to read through — student.address.city throws TypeError: Cannot read properties of undefined. Optional chaining is the fix: student.address?.city returns undefined quietly. Use it where a value is genuinely allowed to be absent, not as a blanket habit that hides real mistakes until later.

Adding a property is simply assigning to a key that does not exist yet, and removing one uses the delete operator. To tell the difference between "this key is missing" and "this key exists and holds undefined", use 'key' in obj or Object.hasOwn(obj, 'key') — a plain === undefined check gives the same answer for both cases and so cannot distinguish them.

Example
const student = { name: 'Ananya Sharma', rollNo: 'CS21-047' };

// Dot notation: the key is known while writing the code
console.log(student.name);        // 'Ananya Sharma'

// Bracket notation: the key is decided at runtime
const field = 'rollNo';
console.log(student[field]);      // 'CS21-047'
console.log(student.field);       // undefined - looks for a key named "field"

// Brackets are required for keys that are not valid identifiers
const report = { 'total-marks': 235, '2024': 'passed' };
console.log(report['total-marks']);   // 235

// Add, update, delete
student.email = 'ananya@example.com';
student.name = 'Ananya S.';
delete student.rollNo;
console.log(student);   // { name: 'Ananya S.', email: 'ananya@example.com' }

// "Missing" and "present but undefined" look identical to ===
const settings = { theme: undefined };
console.log(settings.theme === undefined);      // true
console.log(settings.colour === undefined);     // true  <- same answer
console.log('theme' in settings);               // true
console.log('colour' in settings);              // false
console.log(Object.hasOwn(settings, 'theme'));  // true
Notes
  • Computed keys let you build an object whose key is decided at runtime: const key = 'maths'; const m = { [key]: 82 }; produces { maths: 82 }. Leave out the square brackets and you get a property literally named key instead.

Methods, and What this Actually Points At

A function stored on an object is called a method. Modern JavaScript has a shorthand for writing one: drop the : function and write addItem(name) { ... } directly inside the braces. Inside a method, this refers to the object the method was called on, which is what lets a method read and update its own object's properties.

The precise rule matters, because this is where nearly all confusion about this begins. this is not decided by where the function was written. It is decided by how the function was called. Call cart.addItem(...) and this is cart, because cart is what sits to the left of the dot. Pull the same function out into a plain variable and call it on its own, and there is nothing to the left of any dot — so this becomes undefined in a module or in strict mode, and the global object in an old-style script. Neither is your object, and reading a property off undefined throws.

This is exactly why callbacks lose this. Writing setTimeout(cart.describe, 1000) hands over the function on its own, with no memory of which object it came from, and a second later it runs ownerless. Two fixes are standard: wrap the call in an arrow, setTimeout(() => cart.describe(), 1000), or bind it permanently with cart.describe.bind(cart).

The other half of the rule concerns arrow functions. An arrow has no this of its own and simply uses whatever this meant in the code surrounding it. That makes an arrow the wrong choice for a method written directly on an object literal, because the surrounding code is not the object — so this ends up as the global object or undefined instead of what you meant. The working rule is short: regular function or method shorthand for methods, arrows for the callbacks inside them.

Example
const cart = {
  items: [],
  total: 0,

  // Method shorthand
  addItem(name, price) {
    this.items.push(name);   // "this" is cart, because we called cart.addItem(...)
    this.total += price;
    return this.total;
  },

  describe() {
    return `${this.items.length} item(s), total ${this.total}`;
  },

  // An arrow used as a method: "this" is NOT cart
  brokenDescribe: () => {
    return `items: ${this?.items}`;
  }
};

cart.addItem('Notebook', 60);
cart.addItem('Pen', 15);
console.log(cart.describe());        // '2 item(s), total 75'
console.log(cart.brokenDescribe());  // 'items: undefined'

// Detaching the method loses the object
const describe = cart.describe;
// describe();   // TypeError - cannot read properties of undefined

// Two ways to keep it attached
const bound = cart.describe.bind(cart);
console.log(bound());                               // works
setTimeout(() => console.log(cart.describe()), 0);  // also works
Notes
  • Three questions settle almost every this puzzle. Was it called with something to the left of a dot? Then this is that thing. Is it an arrow function? Then this came from the surrounding code. Neither of those? Then this is undefined in a module and the global object in a plain script.

Objects Are References, and What That Costs You

A variable holding an object does not hold the object. It holds a reference — effectively the address of the object in memory. Assigning that variable to another name copies the address, not the contents, so both names now point at one single object. Change it through either name and the change is visible through both.

This surprises people because primitives behave the opposite way. let a = 5; let b = a; b = 6; leaves a at 5, because the value itself was copied. Do the same with an object and you have two labels stuck on one box. It is also the reason { a: 1 } === { a: 1 } is false: on objects, === asks whether the two references point at the same object, and never whether the contents happen to match.

The everyday fix is the spread operator, { ...original }, which builds a brand-new object and copies the original's own properties into it. That is enough whenever the object is flat. It is a shallow copy, though — any property that itself holds an object is copied as a reference, so that nested object is still shared between the two. Editing copy.marks.maths after a spread changes the original's marks as well, and this one catches out experienced developers, not just beginners.

For a genuinely independent copy of nested data, use structuredClone(obj), available in modern browsers and in Node 17 and later. The older trick of JSON.parse(JSON.stringify(obj)) also produces a deep copy but silently destroys anything JSON cannot represent: dates come back as strings, and functions and undefined values vanish entirely. Prefer structuredClone, and keep the JSON trick for data you know is plain.

Example
// Primitives copy the value
let a = 5;
let b = a;
b = 6;
console.log(a, b);            // 5 6

// Objects copy the reference
const original = { name: 'Ananya', marks: { maths: 82 } };
const alias = original;
alias.name = 'Rahul';
console.log(original.name);   // 'Rahul' - one object, two names

// === compares identity, not contents
console.log({ a: 1 } === { a: 1 });   // false
console.log(original === alias);      // true

// Spread makes a SHALLOW copy
const copy = { ...original };
copy.name = 'Priya';
console.log(original.name);           // 'Rahul' - top level is independent
copy.marks.maths = 95;
console.log(original.marks.maths);    // 95     - the nested object is shared

// A real deep copy
const deep = structuredClone(original);
deep.marks.maths = 40;
console.log(original.marks.maths);    // 95 - unaffected this time
Notes
  • Object.assign({}, original) does the same job as { ...original } and has exactly the same shallow-copy limitation. Where spread syntax is available, prefer it — it reads better and there is no empty target object to explain.

Destructuring, Shorthand and Spread

Destructuring pulls properties out of an object into standalone variables in a single line. Instead of four lines that each begin const x = obj.x, you write the keys you want inside braces on the left of the assignment. Those braces are not an object literal — they are a pattern describing what to take out.

Three extras make it genuinely useful rather than merely shorter. You can rename on the way out with { name: studentName }, which matters when two different objects both have a name. You can supply a default with { city = 'Not set' }, which fires when the property is missing or undefined. And you can gather everything you did not name with a rest pattern, { name, ...rest } — the tidy way to drop one field from an object without mutating the original.

Destructuring works on function parameters too, and that is where you will meet it most often. A function that takes an options object can name the pieces it cares about right in its parameter list. The call site then reads clearly, because arguments are labelled rather than positional, and you get a per-option default for free.

The mirror image is property shorthand: when the variable name and the key are identical, write it once. { name, marks } means { name: name, marks: marks }. Combined with spread, this is how most modern code produces a modified copy of an object — { ...student, marks: 90 } takes everything from student and overrides one field while leaving the original untouched. Later keys win, so the order inside the braces is what decides the result.

Example
const student = { name: 'Ananya', rollNo: 'CS21-047', marks: 82 };

// Basic destructuring
const { name, marks } = student;
console.log(name, marks);        // 'Ananya' 82

// Rename, default and rest, all in one pattern
const { name: studentName, city = 'Not set', ...rest } = student;
console.log(studentName);        // 'Ananya'
console.log(city);               // 'Not set' - there is no city property
console.log(rest);               // { rollNo: 'CS21-047', marks: 82 }

// Destructuring in a parameter list, with a default
function printReport({ name, marks, outOf = 100 }) {
  console.log(`${name}: ${marks}/${outOf}`);
}
printReport(student);            // 'Ananya: 82/100'

// Shorthand when the variable and the key share a name
const rollNo = 'CS21-099';
const newStudent = { name, rollNo };
console.log(newStudent);         // { name: 'Ananya', rollNo: 'CS21-099' }

// Copy-and-override instead of mutating - the later key wins
const updated = { ...student, marks: 90 };
console.log(updated.marks);      // 90
console.log(student.marks);      // 82 - original untouched
  • const { a } = obj — pull one property out into a variable
  • const { a: b } = obj — pull it out under a different name
  • const { a = 1 } = obj — default, used when the property is missing or undefined
  • const { a, ...rest } = obj — take one, collect everything else
  • { a, b } — shorthand for { a: a, b: b }
  • { ...obj, a: 2 } — copy then override; the last key listed wins
  • Patterns nest: const { marks: { maths } } = student reaches two levels down
Notes
  • Destructuring a value that is undefined throws immediately. When a function might be called with no argument at all, give the whole parameter a default: function printReport({ name } = {}).

Looping Over an Object

Objects are not iterable, so for...of does not work on them directly. The modern approach is to turn the object into an array first, using one of three built-in helpers. Object.keys(obj) returns an array of the keys, Object.values(obj) returns the values, and Object.entries(obj) returns an array of two-element [key, value] arrays. Once you hold an array, every method from Lesson 10 becomes available.

Object.entries combined with destructuring is the pattern worth memorising. Writing for (const [subject, score] of Object.entries(marks)) reads almost like a sentence and hands you both halves with no indexing at all. It is what you will reach for when rendering a table, summing a set of values, or finding the highest entry.

The older for...in loop also walks an object's keys, and it carries a trap that Object.keys does not: it visits inherited enumerable properties as well as the object's own. On a plain object literal that makes no difference, but on an object created from a class or handed to you by a library it can produce keys you never set. If you do use for...in, guard the body with Object.hasOwn(obj, key) — or use Object.keys and sidestep the question entirely.

Key order is defined by the specification but is slightly odd, and it is better to learn it than to be surprised by it. Keys that look like non-negative integers come first, in ascending numeric order; every other string key follows in the order it was inserted. So an object keyed by numbers such as '10' and '2' will not iterate in the order you added them. When order genuinely matters, use an array of objects, or a Map, which keeps strict insertion order for every key type.

Example
const marks = { maths: 82, physics: 74, chemistry: 79 };

console.log(Object.keys(marks));     // ['maths', 'physics', 'chemistry']
console.log(Object.values(marks));   // [82, 74, 79]
console.log(Object.entries(marks));  // [['maths',82], ['physics',74], ['chemistry',79]]

// entries + destructuring: the pattern to remember
for (const [subject, score] of Object.entries(marks)) {
  console.log(`${subject}: ${score}`);
}

// Once it is an array, totals and averages are free
const scores = Object.values(marks);
const total = scores.reduce((sum, n) => sum + n, 0);
console.log(total);                       // 235
console.log((total / scores.length).toFixed(2));   // '78.33'

// Integer-like keys jump to the front, whatever order you added them
const byRoll = {};
byRoll['10'] = 'Priya';
byRoll['2'] = 'Rahul';
byRoll['name'] = 'header';
console.log(Object.keys(byRoll));    // ['2', '10', 'name']
Notes
  • Object.keys, Object.values and Object.entries all skip inherited properties and return only the object's own. That is nearly always what you want, and it is one more reason to prefer them over for...in.
Ask AI