5.1 Object Basics — Deep Dive
JavaScript objects go far beyond simple key-value stores — they support dynamic properties, descriptors, and getters/setters for powerful data modeling.
Shorthand Property Names
When a variable name matches a property name, use the shorthand syntax.
const name = "Alice";
const age = 30;
const role = "admin";
// ES5 style
const userOld = { name: name, age: age, role: role };
// ES6+ shorthand
const user = { name, age, role };
console.log(user); // { name: "Alice", age: 30, role: "admin" }
// Convenient when returning multiple values from a function
function getCoordinates(id) {
const x = id * 10;
const y = id * 20;
const z = id * 30;
return { x, y, z }; // { x: x, y: y, z: z }
}
const { x, y, z } = getCoordinates(5);
console.log(x, y, z); // 50 100 150
Computed Property Names
Use an expression in brackets [] to dynamically generate property keys.
const prefix = "user";
const timestamp = Date.now();
const dynamicObj = {
[`${prefix}_id`]: 1,
[`${prefix}_name`]: "Alice",
[`created_${timestamp}`]: new Date().toISOString(),
[Symbol.iterator]: function* () { yield* Object.values(this); },
};
console.log(dynamicObj.user_id); // 1
console.log(dynamicObj.user_name); // "Alice"
// Useful in form data handling
function updateField(state, fieldName, value) {
return {
...state,
[fieldName]: value, // dynamic key
[`${fieldName}Error`]: null, // clear related error
};
}
let formState = { email: "", emailError: "Required", password: "" };
formState = updateField(formState, "email", "alice@example.com");
console.log(formState);
// { email: "alice@example.com", emailError: null, password: "" }
// Using object keys like an enum
const ActionType = {
INCREMENT: "INCREMENT",
DECREMENT: "DECREMENT",
RESET: "RESET",
};
const handlers = {
[ActionType.INCREMENT]: (state) => ({ ...state, count: state.count + 1 }),
[ActionType.DECREMENT]: (state) => ({ ...state, count: state.count - 1 }),
[ActionType.RESET]: () => ({ count: 0 }),
};
let state = { count: 5 };
state = handlers[ActionType.INCREMENT](state);
console.log(state.count); // 6
Spread / Rest Operator — Object Usage
const defaults = { theme: "light", lang: "en", fontSize: 14, timeout: 5000 };
const userPrefs = { theme: "dark", fontSize: 16 };
// Spread: merge defaults + user preferences
const config = { ...defaults, ...userPrefs };
console.log(config);
// { theme: "dark", lang: "en", fontSize: 16, timeout: 5000 }
// Rest pattern to exclude specific properties
const { theme, lang, ...rest } = config;
console.log(rest); // { fontSize: 16, timeout: 5000 }
// Immutable update of nested objects
const product = {
id: 1,
name: "Laptop",
specs: { ram: "16GB", storage: "512GB", cpu: "M3" },
price: 1500,
};
const upgraded = {
...product,
specs: { ...product.specs, ram: "32GB" },
price: 1800,
};
console.log(product.specs.ram); // "16GB" — original preserved
console.log(upgraded.specs.ram); // "32GB"
// Object clone (shallow copy)
const original = { a: 1, b: { c: 2 } };
const clone = { ...original };
clone.a = 99;
console.log(original.a); // 1 (independent)
clone.b.c = 99;
console.log(original.b.c); // 99 (shallow copy: nested object is a shared reference)
Object Static Methods
Object.keys / values / entries / fromEntries
const scores = { alice: 85, bob: 92, carol: 78, dave: 95 };
// Convert to arrays of keys, values, or entries
console.log(Object.keys(scores)); // ["alice", "bob", "carol", "dave"]
console.log(Object.values(scores)); // [85, 92, 78, 95]
console.log(Object.entries(scores)); // [["alice", 85], ["bob", 92], ...]
// entries → transform → fromEntries (object transformation pipe)
const bonus = Object.fromEntries(
Object.entries(scores).map(([name, score]) => [name, score * 1.1])
);
console.log(bonus); // { alice: 93.5, bob: 101.2, carol: 85.8, dave: 104.5 }
// Extract top 2
const top2 = Object.fromEntries(
Object.entries(scores)
.sort(([, a], [, b]) => b - a)
.slice(0, 2)
);
console.log(top2); // { dave: 95, bob: 92 }
Object.assign
// Merge multiple source objects into target (mutates the first argument!)
const target = { a: 1 };
const source1 = { b: 2, c: 3 };
const source2 = { c: 4, d: 5 }; // c from source2 overwrites
Object.assign(target, source1, source2);
console.log(target); // { a: 1, b: 2, c: 4, d: 5 }
// Prevent mutating original: use empty object as target
const merged = Object.assign({}, source1, source2);
// Spread is more intuitive
const mergedSpread = { ...source1, ...source2 };
Object.create
// Create an object with a specified prototype
const animalProto = {
breathe() { return `${this.name} is breathing`; },
eat(food) { return `${this.name} eats ${food}`; },
};
const dog = Object.create(animalProto);
dog.name = "Rex";
dog.bark = function() { return "Woof!"; };
console.log(dog.breathe()); // "Rex is breathing"
console.log(dog.bark()); // "Woof!"
console.log(Object.getPrototypeOf(dog) === animalProto); // true
// Null-prototype object (pure dictionary)
const pureDict = Object.create(null);
pureDict.key = "value";
// pureDict.hasOwnProperty — does not exist! (safer)
console.log(Object.prototype.toString.call(pureDict)); // "[object Object]"
Property Descriptors
Every property has an internal descriptor.
// Inspect a descriptor
const obj = { x: 42 };
console.log(Object.getOwnPropertyDescriptor(obj, "x"));
// { value: 42, writable: true, enumerable: true, configurable: true }
// Descriptor attributes:
// - value: the value
// - writable: whether the value can be changed
// - enumerable: whether it shows up in for...in, Object.keys(), etc.
// - configurable: whether the descriptor can be changed or the property deleted
// Control descriptors with Object.defineProperty
const constants = {};
Object.defineProperty(constants, "PI", {
value: 3.14159265358979,
writable: false, // value cannot be changed
enumerable: true, // enumerable
configurable: false, // descriptor cannot be changed
});
// constants.PI = 3; // TypeError (strict mode)
console.log(constants.PI); // 3.14159265358979
// Define multiple properties at once
const point = {};
Object.defineProperties(point, {
x: { value: 0, writable: true, enumerable: true, configurable: true },
y: { value: 0, writable: true, enumerable: true, configurable: true },
_private: { value: "hidden", enumerable: false }, // excluded from keys()
});
console.log(Object.keys(point)); // ["x", "y"] (_private excluded)
Getters and Setters
Execute a function when a property is accessed.
class Circle {
#radius; // private field
constructor(radius) {
this.radius = radius; // calls setter
}
// getter: runs when value is read
get radius() { return this.#radius; }
// setter: runs when value is written (validation possible)
set radius(value) {
if (typeof value !== "number" || value < 0) {
throw new RangeError(`Radius must be a non-negative number. Got: ${value}`);
}
this.#radius = value;
}
// Computed properties (getter only)
get diameter() { return this.#radius * 2; }
get area() { return Math.PI * this.#radius ** 2; }
get circumference() { return 2 * Math.PI * this.#radius; }
}
const circle = new Circle(5);
console.log(circle.radius); // 5
console.log(circle.diameter); // 10
console.log(circle.area.toFixed(2)); // 78.54
circle.radius = 10;
console.log(circle.diameter); // 20
// circle.radius = -1; // RangeError!
// Also usable in object literals
const temperature = {
_celsius: 20,
get celsius() { return this._celsius; },
set celsius(value) { this._celsius = value; },
get fahrenheit() { return this._celsius * 9/5 + 32; },
set fahrenheit(value) { this._celsius = (value - 32) * 5/9; },
get kelvin() { return this._celsius + 273.15; },
};
temperature.celsius = 100;
console.log(temperature.fahrenheit); // 212
temperature.fahrenheit = 32;
console.log(temperature.celsius); // 0
Practical Example: Reactive Form State Management
function createFormState(initialValues) {
const _values = { ...initialValues };
const _errors = {};
const _touched = {};
const _validators = {};
const _changeListeners = new Set();
function notify() {
_changeListeners.forEach(fn => fn(getState()));
}
function getState() {
return {
values: { ..._values },
errors: { ..._errors },
touched: { ..._touched },
isValid: Object.keys(_errors).length === 0,
isDirty: Object.keys(_touched).some(k => _touched[k]),
};
}
return {
registerValidator(field, validatorFn) {
_validators[field] = validatorFn;
return this;
},
setValue(field, value) {
_values[field] = value;
_touched[field] = true;
if (_validators[field]) {
const error = _validators[field](value);
if (error) {
_errors[field] = error;
} else {
delete _errors[field];
}
}
notify();
return this;
},
onChange(listener) {
_changeListeners.add(listener);
return () => _changeListeners.delete(listener);
},
getState,
reset() {
Object.assign(_values, initialValues);
Object.keys(_errors).forEach(k => delete _errors[k]);
Object.keys(_touched).forEach(k => delete _touched[k]);
notify();
},
};
}
const form = createFormState({ email: "", password: "" });
form
.registerValidator("email", v =>
!v.includes("@") ? "Enter a valid email address" : null
)
.registerValidator("password", v =>
v.length < 8 ? "Password must be at least 8 characters" : null
);
const unsubscribe = form.onChange(({ values, errors, isValid }) => {
console.log("Changed:", values.email, "|", Object.values(errors).join(", ") || "No errors");
console.log("Valid:", isValid);
});
form.setValue("email", "notanemail");
// Changed: notanemail | Enter a valid email address
// Valid: false
form.setValue("email", "alice@example.com");
// Changed: alice@example.com | No errors
// Valid: false (password not entered yet)
form.setValue("password", "secure123");
// Changed: alice@example.com | No errors
// Valid: true
unsubscribe(); // unsubscribe
Pro Tips
Tip 1: Full copy with Object.getOwnPropertyDescriptors
// Regular spread copies getters as values
const source = {
get computed() { return Math.random(); }
};
const spread = { ...source };
console.log(typeof Object.getOwnPropertyDescriptor(spread, "computed").get);
// "undefined" — getter is copied as a value
// Full copy preserving getters
const fullCopy = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
console.log(typeof Object.getOwnPropertyDescriptor(fullCopy, "computed").get);
// "function" — getter preserved
Tip 2: Proxy for dynamic defaults object
function withDefaults(target, defaults) {
return new Proxy(target, {
get(obj, key) {
return key in obj ? obj[key] : defaults[key];
}
});
}
const config = withDefaults(
{ theme: "dark" },
{ theme: "light", lang: "en", fontSize: 14 }
);
console.log(config.theme); // "dark" (directly set)
console.log(config.lang); // "en" (default value)
console.log(config.fontSize); // 14 (default value)
Tip 3: WeakMap for truly private properties (pre-class field # pattern)
const _private = new WeakMap();
class SecureStorage {
constructor(key) {
_private.set(this, { key, data: {} });
}
set(name, value) {
const priv = _private.get(this);
priv.data[name] = value;
}
get(name) {
return _private.get(this).data[name];
}
}
const storage = new SecureStorage("secret");
storage.set("token", "abc123");
console.log(storage.get("token")); // "abc123"
// storage._private — inaccessible (WeakMap is external)