Important: Data descriptors (value/writable) and accessor descriptors (get/set) are mutually
exclusive. A property can have value OR get/set, not both. Non-configurable properties
cannot be deleted or have their descriptor changed (except writable can go from true to false).
Pre-ES6 classes, instance creation with shared methods
Factory Function
function create() { return { }; }
Object.prototype (or custom)
Object creation without 'new', private state
Class Syntax ES6
class C { } + new C()
C.prototype
Modern OOP, cleaner syntax over constructors
Method
Description
Returns
Object.create(proto)
Create new object with specified prototype
New object inheriting from proto
Object.create(proto, descriptors)
Create object with prototype and property descriptors
New object with specified properties
Object.create(null)
Create object with no prototype (truly empty)
Object without any inherited properties
Object.setPrototypeOf(obj, proto)
Change prototype of existing object (slow, avoid)
The modified object
Object.getPrototypeOf(obj)
Get prototype of object
Prototype object or null
Example: Object.create() patterns
// Create object with specific prototypeconst personProto = { greet() { return `Hello, I'm ${this.name}`; }};const person = Object.create(personProto);person.name = 'Alice';console.log(person.greet()); // "Hello, I'm Alice"console.log(Object.getPrototypeOf(person) === personProto); // true// Create with property descriptorsconst user = Object.create(personProto, { name: { value: 'Bob', writable: true, enumerable: true }, age: { value: 30, writable: true, enumerable: true }});console.log(user.name); // 'Bob'console.log(user.greet()); // "Hello, I'm Bob"// Create object with null prototype (no inherited properties)const dict = Object.create(null);dict.key = 'value';console.log(dict.toString); // undefined (no inherited methods)console.log(dict.hasOwnProperty); // undefined// Useful for pure data storage, no prototype pollution// Compare with literalconst literal = {};console.log(literal.toString); // [Function: toString] (inherited)console.log(Object.getPrototypeOf(literal) === Object.prototype); // true
Example: Constructor functions
// Constructor function (pre-ES6 classes)function Person(name, age) { // Instance properties (unique per instance) this.name = name; this.age = age;}// Shared methods on prototype (memory efficient)Person.prototype.greet = function() { return `Hello, I'm ${this.name}`;};Person.prototype.getAge = function() { return this.age;};// Create instances with 'new'const alice = new Person('Alice', 30);const bob = new Person('Bob', 25);console.log(alice.greet()); // "Hello, I'm Alice"console.log(bob.greet()); // "Hello, I'm Bob"console.log(alice.greet === bob.greet); // true (shared method)// Verify prototypeconsole.log(Object.getPrototypeOf(alice) === Person.prototype); // trueconsole.log(alice instanceof Person); // true// Forgetting 'new' - common mistakefunction SafeConstructor(name) { // Guard against missing 'new' if (!(this instanceof SafeConstructor)) { return new SafeConstructor(name); } this.name = name;}const instance1 = new SafeConstructor('Alice');const instance2 = SafeConstructor('Bob'); // Works without 'new'console.log(instance2 instanceof SafeConstructor); // true
Example: Factory functions
// Factory function - no 'new' neededfunction createPerson(name, age) { // Private variables (closure) let privateData = 'secret'; // Return object with public interface return { name, age, greet() { return `Hello, I'm ${this.name}`; }, getPrivate() { return privateData; // Access private via closure } };}const person1 = createPerson('Alice', 30); // No 'new'const person2 = createPerson('Bob', 25);console.log(person1.greet()); // "Hello, I'm Alice"console.log(person1.getPrivate()); // 'secret'console.log(person1.privateData); // undefined (truly private)// Factory with prototypal inheritancefunction createUser(name) { const proto = { greet() { return `Hello ${this.name}`; } }; const user = Object.create(proto); user.name = name; return user;}const user = createUser('Charlie');console.log(user.greet()); // "Hello Charlie"// Modern: ES6 class (preferred over constructors)class PersonClass { constructor(name, age) { this.name = name; this.age = age; } greet() { return `Hello, I'm ${this.name}`; }}const personInstance = new PersonClass('Dave', 35);console.log(personInstance.greet()); // "Hello, I'm Dave"
Best Practice: Modern JavaScript prefers ES6 classes over constructor functions for clarity.
Use factory functions when you need private state via closures or want to avoid new. Use
Object.create(null) for dictionaries/maps to avoid prototype pollution. Avoid
Object.setPrototypeOf() as it's very slow.
5. Object Cloning and Merging (Object.assign, spread)
Method
Syntax
Copy Type
Nested Objects
Object.assign() ES6
Object.assign(target, ...sources)
Shallow copy
Copies references (not deep)
Spread Operator ES2018
{ ...obj }
Shallow copy
Copies references (not deep)
JSON.parse/stringify
JSON.parse(JSON.stringify(obj))
Deep copy
✓ Clones nested, but loses functions/symbols/dates
structuredClone() NEW
structuredClone(obj)
Deep copy
✓ Clones nested, preserves types (not functions)
Manual Deep Clone
Recursive function
Deep copy
✓ Full control, handle all types
Feature
Object.assign()
Spread {...}
Differences
Copy enumerable props
✓ Yes
✓ Yes
Both copy own enumerable properties
Trigger setters
✓ Yes
✗ No
assign() invokes setters, spread defines new props
Copy getters/setters
✗ No (executes getter)
✗ No (executes getter)
Both execute getters, copy resulting values
Mutate target
✓ Yes (modifies first arg)
✗ No (creates new object)
assign() mutates, spread creates new
Syntax
Function call
Operator (concise)
Spread is more concise and readable
Example: Shallow copying with Object.assign and spread
Warning: Both Object.assign() and spread create shallow
copies. Nested objects are copied by reference, not value. Modifying nested properties affects the
original. For true deep copies, use structuredClone() (modern) or implement custom deep clone
logic.
set age(v) { if (v < 0) throw Error; this._age = v; }
Lazy Initialization
Getter computes value on first access
get data() { return this._data || (this._data = load()); }
Side Effects
Setter triggers additional actions
set value(v) { this._value = v; this.notify(); }
Read-only Property
Getter without setter
get id() { return this._id; } (no setter)
Private Backing Field
Store value in _property, expose via getter/setter
get name() { return this._name; }
Example: Getters and setters in object literals
const person = { firstName: 'John', lastName: 'Doe', _age: 30, // Backing field (convention: underscore = private-like) // Getter: computed property get fullName() { return `${this.firstName} ${this.lastName}`; }, // Setter: parse and set multiple properties set fullName(name) { const parts = name.split(' '); this.firstName = parts[0]; this.lastName = parts[1]; }, // Getter with validation/transformation get age() { return this._age; }, // Setter with validation set age(value) { if (typeof value !== 'number' || value < 0 || value > 150) { throw new Error('Invalid age'); } this._age = value; }, // Read-only property (getter only) get birthYear() { return new Date().getFullYear() - this._age; }};// Using getters and settersconsole.log(person.fullName); // "John Doe" (getter called)person.fullName = 'Jane Smith'; // Setter calledconsole.log(person.firstName); // 'Jane'console.log(person.lastName); // 'Smith'console.log(person.age); // 30 (getter)person.age = 25; // Setter with validation// person.age = -5; // Error: Invalid ageconsole.log(person.birthYear); // Computed from age// person.birthYear = 1990; // TypeError: no setter (read-only)
Example: Getters/setters in classes
class Rectangle { constructor(width, height) { this._width = width; this._height = height; } // Getter for computed property get area() { return this._width * this._height; } get perimeter() { return 2 * (this._width + this._height); } // Getter/setter with validation get width() { return this._width; } set width(value) { if (value <= 0) { throw new Error('Width must be positive'); } this._width = value; } get height() { return this._height; } set height(value) { if (value <= 0) { throw new Error('Height must be positive'); } this._height = value; }}const rect = new Rectangle(10, 5);console.log(rect.area); // 50 (computed)console.log(rect.perimeter); // 30 (computed)rect.width = 20; // Setter with validationconsole.log(rect.area); // 100 (auto-updated)// rect.width = -5; // Error: Width must be positive// rect.area = 200; // TypeError: no setter (read-only)
Example: Advanced getter/setter patterns
// Lazy initialization with getterclass DataLoader { constructor(url) { this.url = url; this._data = null; } get data() { // Load data on first access, cache for subsequent access if (this._data === null) { console.log('Loading data...'); this._data = this.loadData(); // Expensive operation } return this._data; } loadData() { // Simulate data loading return { loaded: true, url: this.url }; }}const loader = new DataLoader('/api/data');console.log(loader.data); // Logs: "Loading data...", returns dataconsole.log(loader.data); // No log, returns cached data// Observable pattern with settersclass Observable { constructor() { this._observers = []; this._value = null; } get value() { return this._value; } set value(newValue) { const oldValue = this._value; this._value = newValue; this.notify(oldValue, newValue); } subscribe(observer) { this._observers.push(observer); } notify(oldValue, newValue) { this._observers.forEach(fn => fn(newValue, oldValue)); }}const obs = new Observable();obs.subscribe((newVal, oldVal) => { console.log(`Changed from ${oldVal} to ${newVal}`);});obs.value = 10; // Logs: "Changed from null to 10"obs.value = 20; // Logs: "Changed from 10 to 20"// Type conversion in setterconst config = { _port: 8080, get port() { return this._port; }, set port(value) { // Always store as number this._port = Number(value); }};config.port = '3000'; // String inputconsole.log(config.port); // 3000 (number)console.log(typeof config.port); // 'number'
Best Practice: Use getters for computed properties and derived state. Use setters for
validation, type conversion, and triggering side effects. Follow naming convention: use underscore prefix for
backing fields (e.g., _value). For true privacy in modern JS, use private fields
(#field) instead of convention.
7. Object Freezing and Sealing (freeze, seal, preventExtensions)
Method
Add Props
Delete Props
Modify Values
Change Config
Normal Object
✓ Yes
✓ Yes
✓ Yes
✓ Yes
Object.preventExtensions()
✗ No
✓ Yes
✓ Yes
✓ Yes
Object.seal()
✗ No
✗ No
✓ Yes
✗ No
Object.freeze()
✗ No
✗ No
✗ No
✗ No
Method
Effect
Check Method
Use Case
Object.preventExtensions(obj)
Cannot add new properties
Object.isExtensible(obj)
Prevent accidental property additions
Object.seal(obj)
Cannot add/delete, can modify existing
Object.isSealed(obj)
Fixed property set, mutable values
Object.freeze(obj)
Completely immutable (shallow)
Object.isFrozen(obj)
Constants, immutable config
Example: Object.preventExtensions()
const obj = { a: 1, b: 2 };// Prevent adding new propertiesObject.preventExtensions(obj);// Existing properties work normallyobj.a = 10; // ✓ Works (can modify)delete obj.b; // ✓ Works (can delete)console.log(obj); // { a: 10 }// Cannot add new propertiesobj.c = 3; // ✗ Fails silently (strict mode: TypeError)console.log(obj.c); // undefined// Check if extensibleconsole.log(Object.isExtensible(obj)); // false// Cannot make extensible again (one-way operation)// No Object.makeExtensible() method