Module: JavaScript
JavaScript·056·8 MIN READ

056: Objects and Property Mechanics

TOPICS COVERED: Objects and Property Mechanics

Learning outcomes

By the end of this lesson, you can:

  • model a product, user, or order with an object literal;
  • read and write properties with dot and bracket notation;
  • use methods and nested objects appropriately;
  • explain object identity and shared references;
  • safely handle a property that may not exist.

Retrieval warm-up

  1. Which method returns one matching array element or undefined?
  2. What does a predicate return conceptually?
  3. Does toSorted() deeply clone object elements?

Answers: find(), truthy/falsy, and no. It creates a new outer array but keeps element references.

Vocabulary

  • Object: Collection of keyed properties forming composite data. — Source: MDN: Object
  • Property: Association between a key and a value on an object. — Source: MDN: Working with objects
  • Key: String/Symbol identifier addressing a property. — Source: MDN: Property accessors
  • Object literal: { key: value } notation constructing an object inline. — Source: MDN: Object initializer
  • Method: Property whose value is a function. — Source: MDN: Method definitions
  • Nested object: Object stored as a property value of another object. — Source: MDN: Working with objects
  • Reference: Shared pointer semantics: assignment copies address, not contents. — Source: MDN: Data structures
  • Identity: Two references equal only when pointing to the same object (===). — Source: MDN: Strict equality
  • Object (official): "An object is a collection of properties, where each property is a key-value pair." — Source: MDN: Object
  • Property (official): "A property is a key (string or Symbol) associated with a value in an object." — Source: MDN: Working with objects

Beginner mental model

An array is useful for an ordered shelf of similar values. An object is useful for one record with labeled fields. A product array answers "which product is at position 2?" A product object answers "what is this product's price?"

js
const product = {
  id: "p3",
  name: "Water Bottle",
  price: 16,
  inStock: true,
};

Use dot notation for a known identifier-like key: product.price. Use bracket notation when the key is stored in a variable or cannot be written after a dot:

js
const selectedField = "price";
console.log(product[selectedField]); // 16

const importedProduct = { "warehouse-code": "A-12" };
console.log(importedProduct["warehouse-code"]); // A-12

product.selectedField would look for a property literally named selectedField; it would not use the variable's value. Reading a missing property normally returns undefined.

Objects are reference values. const prevents reassigning the variable, not changing the object:

js
const item = { stock: 5 };
item.stock = 4;       // allowed
// item = { stock: 4 }; // TypeError: assignment to constant variable

Two equal-looking literals are distinct objects, so { id: 1 } === { id: 1 } is false. Assignment copies a reference, not the whole object:

js
const original = { name: "Notebook", stock: 12 };
const alias = original;
alias.stock = 10;
console.log(original.stock); // 10
console.log(alias === original); // true

This is not an obscure detail. Product arrays, carts, and application state often share object references.

Worked beginner example: an API-shaped order

APIs commonly return nested records. We will model an order with a customer, shipping information, and line items.

js
const order = {
  id: "ord-1042",
  status: "processing",
  customer: {
    id: "u7",
    name: "Maya",
    email: "maya@example.com",
  },
  shipping: {
    city: "Chennai",
    method: "standard",
  },
  items: [
    { productId: "p1", name: "Notebook", price: 4, quantity: 3 },
    { productId: "p3", name: "Water Bottle", price: 16, quantity: 2 },
  ],
  getItemCount() {
    return this.items.reduce(
      (count, item) => count + item.quantity,
      0,
    );
  },
  getSubtotal() {
    return this.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0,
    );
  },
};

console.log(order.id);
console.log(order.customer.name);
console.log(order["status"]);
console.log(order.getItemCount());
console.log(order.getSubtotal());

order.status = "packed";
console.log(order.status);
console.log(order.shipping.trackingCode);

Output:

text
ord-1042
Maya
processing
5
44
packed
undefined

Trace the shape rather than reading punctuation: order is one object; customer and shipping each point to nested objects; items points to an array; each array element points to a line-item object. The methods use method shorthand and this to read the receiving order. Call them as order.getSubtotal() so this is order.

For ordinary product calculations, standalone functions are often easier to reuse and test than methods. Methods are useful when the behavior naturally belongs to one record. Avoid arrow functions for methods that require their own this, because arrow functions do not bind this from the call.

Dynamic fields and safe access

Suppose a table lets the user choose a field:

js
function displayField(product, field) {
  const allowedFields = ["name", "price", "stock"];

  if (!allowedFields.includes(field)) {
    return "Unsupported field";
  }

  return product[field] ?? "Not provided";
}

console.log(displayField({ name: "Lamp", stock: 0 }, "stock")); // 0
console.log(displayField({ name: "Lamp" }, "price")); // Not provided

Use ??, not ||, when 0, false, or "" are valid property values. Restrict externally supplied keys rather than allowing arbitrary object access.

Optional chaining stops safely when a base is null or undefined:

js
console.log(order.delivery?.estimatedDate ?? "No estimate yet");

It does not validate all data. If delivery is a string instead of the expected object, the data shape is still wrong and should be addressed.

Intermediate example: model and summarize an order

Prefer a factory function when creating multiple consistently shaped plain objects. This is still object-literal modeling, not a constructor/prototype lesson.

js
function createProduct(id, name, price, stock) {
  return {
    id,
    name,
    price,
    stock,
    isAvailable() {
      return this.stock > 0;
    },
  };
}

const notebook = createProduct("p1", "Notebook", 4, 12);
const bottle = createProduct("p3", "Water Bottle", 16, 0);

console.log(notebook.isAvailable()); // true
console.log(bottle.isAvailable());   // false

const catalog = [notebook, bottle];
const cartLine = { product: notebook, quantity: 2 };
cartLine.product.stock -= 2;

console.log(notebook.stock);   // 10
console.log(catalog[0].stock); // 10

The last output demonstrates intentional shared identity: notebook, catalog[0], and cartLine.product refer to the same object. This can be useful, but invisible mutation becomes difficult to track. Later lessons favor returning updated records and arrays.

Interview focus: const, freezing, prototypes, and classes

const and Object.freeze() answer different questions. const user = ... prevents user = anotherUser; it does not prevent user.name = .... Object.freeze(user) prevents changes to the object's own properties in strict code, but it is shallow: a nested object can still be changed unless it is also frozen.

js
const settings = Object.freeze({ theme: "light", nested: { enabled: true } });
// settings.theme = "dark"; // TypeError in strict/module code
settings.nested.enabled = false; // still possible: nested is not frozen

A prototype is another object consulted after an object's own properties when a property is not found. It enables shared behavior without copying a method onto every instance. class is clearer syntax over this prototype-based mechanism; it does not turn JavaScript into a class-only language.

js
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  label() {
    return `${this.name}: $${this.price}`;
  }
}

const first = new Product("Notebook", 4);
const second = new Product("Bottle", 16);
console.log(first.label());                 // Notebook: $4
console.log(first.label === second.label);  // true
console.log(Object.getPrototypeOf(first) === Product.prototype); // true

Interview follow-ups: own property or inherited property? Check with Object.hasOwn(value, key). Why is first.label === second.label true? The method is shared on the prototype. What does new do conceptually? It creates an object, links it to the constructor's prototype, calls the constructor with that object as this, and returns the object unless the constructor explicitly returns another object. A class constructor must be called with new.

Optional advanced extension

Computed property names create a key from an expression:

js
function addMetric(product, metricName, value) {
  return {
    product,
    metrics: {
      [metricName]: value,
    },
  };
}

console.log(addMetric(notebook, "views", 240));
// { product: { ... }, metrics: { views: 240 } }

Computed keys are useful for controlled dynamic data. They are not a reason to accept arbitrary untrusted keys into important objects.

Common mistakes and debugging

  • Dot notation with a variable: use object[key], not object.key.
  • Misspelled/case-changed key: product.Price is different from product.price. Inspect with Object.keys(product).
  • Reading too deeply: order.delivery.city throws if delivery is missing. Validate or use order.delivery?.city when absence is expected.
  • Assuming const freezes data: it fixes the binding only. Properties can still change.
  • Comparing by shape: distinct object literals are not strictly equal. Compare stable IDs when identity by record is intended.
  • Unexpected alias mutation: test a === b and inspect every assignment that copied the object reference.
  • Detached this method: const subtotal = order.getSubtotal; subtotal() loses its receiver. Prefer a standalone function when passing behavior around.
  • Arrow method with this: use method shorthand for a receiver-based method.

Best practices

  • Model one entity with one clear, consistently shaped object.
  • Use stable IDs and compare product.id, not entire object identity, for business matching.
  • Prefer dot notation for known keys and bracket notation for controlled dynamic keys.
  • Treat undefined from a missing property deliberately.
  • Keep calculations pure and standalone unless method ownership adds clarity.
  • Avoid deep or prototype-focused abstractions while a plain object solves the problem.

Checkpoint

Represent the order as boxes and arrows. Classify each value as primitive, array reference, object reference, or function. Then assign const secondName = order.customer.name and const secondCustomer = order.customer; determine which later changes each variable can observe. The string is copied as a primitive value, while secondCustomer shares the nested object. This visual exercise should precede copying syntax so reference behavior is understood rather than memorized.

Exercises

Core

Create a product object with id, name, price, and nested supplier.name. Print the name and supplier using dot notation.

js
const product = {
  id: "p5",
  name: "USB Cable",
  price: 9,
  supplier: {
    name: "Wire Works",
  },
};

console.log(product.name);
console.log(product.supplier.name);

Output:

text
USB Cable
Wire Works

Practice

Write getProductField(product, field) for only name, price, and stock. Return "Invalid field" otherwise, preserving valid 0 values.

js
function getProductField(product, field) {
  const allowed = ["name", "price", "stock"];

  if (!allowed.includes(field)) {
    return "Invalid field";
  }

  return product[field] ?? "Missing value";
}

console.log(getProductField({ name: "Cable", stock: 0 }, "stock"));
console.log(getProductField({ name: "Cable" }, "rating"));

Output:

text
0
Invalid field

Professional Extension

Create an order object and a getOrderSummary(order) function returning an object with customer name, item count, and subtotal. Do not modify the order.

js
const sampleOrder = {
  id: "ord-2",
  customer: { name: "Ravi" },
  items: [
    { name: "Notebook", price: 4, quantity: 2 },
    { name: "Backpack", price: 45, quantity: 1 },
  ],
};

function getOrderSummary(order) {
  return {
    customerName: order.customer.name,
    itemCount: order.items.reduce(
      (count, item) => count + item.quantity,
      0,
    ),
    subtotal: order.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0,
    ),
  };
}

console.log(getOrderSummary(sampleOrder));

Output:

text
{ customerName: "Ravi", itemCount: 3, subtotal: 53 }

Recap

When is bracket notation required? What does a missing property return? Why can const object properties change? Explain why changing alias.stock can change original.stock. Finally, sketch the data shape of an order containing customer and item records.

Official references

Prototype lookup in full

Every ordinary object has an internal [[Prototype]] link. Property lookup checks the object itself first, then its prototype, then that prototype's prototype, until it finds a property or reaches null:

js
const animal = { eats: true };
const dog = Object.create(animal);
dog.name = "Ada";

console.log(dog.name); // own property: Ada
console.log(dog.eats); // inherited: true
console.log(dog.missing); // undefined after reaching null
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null

An own property shadows an inherited property. Assignment usually writes an own property on the receiver; it does not edit the prototype:

js
dog.eats = false;
console.log(dog.eats, animal.eats); // false true
console.log(Object.hasOwn(dog, "eats")); // true

prototype and __proto__ are different ideas. A constructor function/class has a public prototype object used for instances. An instance's __proto__ is a legacy accessor for its internal prototype link. Prefer Object.getPrototypeOf() and Object.setPrototypeOf() only when necessary.

js
function User(name) { this.name = name; }
User.prototype.greet = function () { return `Hi ${this.name}`; };
const user = new User("Maya");

console.log(user.__proto__ === User.prototype); // true, legacy spelling
console.log(Object.getPrototypeOf(user) === User.prototype); // true, preferred
console.log(user.greet()); // Hi Maya

Constructors initialize instance-owned state; prototype methods are shared. A class expresses the same model with clearer syntax. super() initializes the parent portion of a derived instance and must run before using this:

js
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
  label() { return `${this.name}: $${this.price}`; }
}

class SaleProduct extends Product {
  constructor(name, price, percentOff) {
    super(name, price);
    this.percentOff = percentOff;
  }
  label() { return `${super.label()} (${this.percentOff}% off)`; }
}

const sale = new SaleProduct("Notebook", 4, 25);
console.log(sale.label()); // Notebook: $4 (25% off)
console.assert(Object.getPrototypeOf(sale) === SaleProduct.prototype);
console.assert(Object.getPrototypeOf(SaleProduct.prototype) === Product.prototype);
console.assert(sale instanceof Product && sale instanceof SaleProduct);

Check ownership deliberately. in walks the full chain, Object.hasOwn() checks only own properties, and propertyIsEnumerable() additionally rejects non- enumerable own properties:

js
const record = Object.create({ inherited: 1 });
record.own = 2;
Object.defineProperty(record, "hidden", { value: 3, enumerable: false });

console.assert("own" in record);
console.assert("inherited" in record);
console.assert(!Object.hasOwn(record, "inherited"));
console.assert(Object.hasOwn(record, "hidden"));
console.assert(!record.propertyIsEnumerable("hidden"));

Prototype pollution edge case

Prototype pollution occurs when attacker-controlled keys modify a shared prototype, commonly through unsafe deep assignment or an unchecked merge. Do not merge arbitrary request keys into configuration or use __proto__ as a path. Allow-list fields, use Object.hasOwn(), and use Object.create(null) for a dictionary that needs no inherited behavior:

js
function applyPublicOptions(target, input) {
  for (const key of ["theme", "pageSize"]) {
    if (Object.hasOwn(input, key)) target[key] = input[key];
  }
  return target;
}

const options = applyPublicOptions({}, JSON.parse('{"__proto__":{"admin":true},"theme":"dark"}'));
console.assert(options.theme === "dark");
console.assert(({}).admin === undefined);
const counts = Object.create(null);
counts["toString"] = 1;
console.assert(counts.toString === 1);

Do not confuse an inherited default with an own user value. Security checks should usually use Object.hasOwn(input, key), not merely key in input.

Interview questions and tests

  1. Trace child.missing: which objects are checked, and where does lookup stop?
  2. Why does child.x = 2 normally not change parent.x?
  3. Explain User.prototype, Object.getPrototypeOf(user), and user.__proto__.
  4. Why must a derived constructor call super() before this?
  5. Which check detects inherited keys: in or Object.hasOwn()?
  6. How can an unchecked __proto__ key pollute future objects, and what input policy prevents it?