056: 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
- Which method returns one matching array element or
undefined? - What does a predicate return conceptually?
- 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?"
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:
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:
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:
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.
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:
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:
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:
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.
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.
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.
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:
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], notobject.key. - Misspelled/case-changed key:
product.Priceis different fromproduct.price. Inspect withObject.keys(product). - Reading too deeply:
order.delivery.citythrows ifdeliveryis missing. Validate or useorder.delivery?.citywhen absence is expected. - Assuming
constfreezes 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 === band inspect every assignment that copied the object reference. - Detached
thismethod: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
undefinedfrom 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.
const product = {
id: "p5",
name: "USB Cable",
price: 9,
supplier: {
name: "Wire Works",
},
};
console.log(product.name);
console.log(product.supplier.name);
Output:
USB Cable Wire Works
Practice
Write getProductField(product, field) for only name, price, and stock. Return "Invalid field" otherwise, preserving valid 0 values.
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:
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.
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:
{ 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
- MDN: Working with objects
- MDN: Object initializer
- MDN: Property accessors
- MDN: Optional chaining
- ECMA-262: Object Initializer
- MDN: Inheritance and the prototype chain
- MDN: Classes
- MDN:
Object.freeze()
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:
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:
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.
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:
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:
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:
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
- Trace
child.missing: which objects are checked, and where does lookup stop? - Why does
child.x = 2normally not changeparent.x? - Explain
User.prototype,Object.getPrototypeOf(user), anduser.__proto__. - Why must a derived constructor call
super()beforethis? - Which check detects inherited keys:
inorObject.hasOwn()? - How can an unchecked
__proto__key pollute future objects, and what input policy prevents it?
