JavaScript Objects and Properties
1. Object Literal Syntax and Computed Properties
| Feature | Syntax | ES Version | Description |
|---|---|---|---|
| Basic Object Literal | { key: value } |
ES3 | Traditional property definition with static keys |
| Property Shorthand ES6 | { name } |
ES6 | Equivalent to { name: name } when variable name matches key |
| Method Shorthand ES6 | { method() {} } |
ES6 | Concise method definition, equivalent to { method: function() {} } |
| Computed Property ES6 | { [expression]: value } |
ES6 | Property name computed at runtime from expression |
| Computed Method ES6 | { [expr]() {} } |
ES6 | Method name computed dynamically |
| Spread Properties ES2018 | { ...obj } |
ES2018 | Copy enumerable properties from source object |
| Getters/Setters | { get prop() {}, set prop(v) {} } |
ES5 | Define accessor properties with custom logic |
Example: Modern object literal syntax
// ES6+ object literal features
const name = 'Alice';
const age = 30;
const propKey = 'dynamicKey';
const user = {
// Property shorthand (ES6)
name, // Same as name: name
age, // Same as age: age
// Method shorthand (ES6)
greet() {
return `Hello, I'm ${this.name}`;
},
// Computed property name (ES6)
[propKey]: 'dynamic value',
[`computed_${name}`]: true,
// Computed method name (ES6)
[`get${name}Info`]() {
return `${this.name} is ${this.age}`;
},
// Traditional syntax still works
email: 'alice@example.com',
// Arrow function as property (not a method)
arrowFunc: () => 'arrow',
// Getter and setter
get fullInfo() {
return `${this.name}, ${this.age} years old`;
},
set fullInfo(info) {
[this.name, this.age] = info.split(', ');
}
};
console.log(user.name); // 'Alice'
console.log(user.greet()); // 'Hello, I'm Alice'
console.log(user.dynamicKey); // 'dynamic value'
console.log(user.computed_Alice); // true
console.log(user.getAliceInfo()); // 'Alice is 30'
console.log(user.fullInfo); // 'Alice, 30 years old'
Example: Computed properties in practice
// Dynamic property names
const fields = ['id', 'name', 'email'];
const values = [1, 'Bob', 'bob@example.com'];
// Build object with computed properties
const user = fields.reduce((obj, field, index) => {
obj[field] = values[index];
return obj;
}, {});
// { id: 1, name: 'Bob', email: 'bob@example.com' }
// Using computed properties in literal
const status = 'active';
const config = {
[`is${status.charAt(0).toUpperCase() + status.slice(1)}`]: true
};
// { isActive: true }
// Symbol as computed property
const SECRET = Symbol('secret');
const obj = {
public: 'visible',
[SECRET]: 'hidden' // Property name is a symbol
};
console.log(obj.public); // 'visible'
console.log(obj[SECRET]); // 'hidden'
console.log(Object.keys(obj)); // ['public'] - symbols not enumerable
// Spread properties (ES2018)
const defaults = { theme: 'dark', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'fr', fontSize: 14 }
// userPrefs.lang overwrites defaults.lang
2. Property Descriptors and Object.defineProperty()
| Descriptor Attribute | Type | Default (defineProperty) | Default (Literal) | Description |
|---|---|---|---|---|
| value | any | undefined |
property value | The property's value (data descriptor) |
| writable | boolean | false |
true |
Can property value be changed? |
| enumerable | boolean | false |
true |
Shows in for...in, Object.keys()? |
| configurable | boolean | false |
true |
Can descriptor be changed? Can property be deleted? |
| get | function | undefined |
N/A | Getter function (accessor descriptor) |
| set | function | undefined |
N/A | Setter function (accessor descriptor) |
| Method | Syntax | Description |
|---|---|---|
| Object.defineProperty() | Object.defineProperty(obj, prop, descriptor) |
Define or modify single property with full control over attributes |
| Object.defineProperties() | Object.defineProperties(obj, descriptors) |
Define or modify multiple properties at once |
| Object.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor(obj, prop) |
Get descriptor object for single property |
| Object.getOwnPropertyDescriptors() ES2017 | Object.getOwnPropertyDescriptors(obj) |
Get descriptors for all own properties |
Example: Property descriptors in action
const obj = {};
// Define property with custom descriptor
Object.defineProperty(obj, 'name', {
value: 'Alice',
writable: false, // Cannot change value
enumerable: true, // Shows in for...in, Object.keys()
configurable: false // Cannot delete or reconfigure
});
obj.name = 'Bob'; // Fails silently (strict mode: TypeError)
console.log(obj.name); // 'Alice' (unchanged)
delete obj.name; // Fails silently (configurable: false)
console.log(obj.name); // 'Alice' (still exists)
// Check property descriptor
const desc = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(desc);
// { value: 'Alice', writable: false, enumerable: true, configurable: false }
// Compare with literal property (all attributes true)
const obj2 = { name: 'Charlie' };
const desc2 = Object.getOwnPropertyDescriptor(obj2, 'name');
console.log(desc2);
// { value: 'Charlie', writable: true, enumerable: true, configurable: true }
Example: Accessor properties with get/set
const person = {
firstName: 'John',
lastName: 'Doe'
};
// Define computed property with getter/setter
Object.defineProperty(person, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
[this.firstName, this.lastName] = value.split(' ');
},
enumerable: true,
configurable: true
});
console.log(person.fullName); // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'
// Define multiple properties at once
const account = {};
Object.defineProperties(account, {
_balance: {
value: 0,
writable: true,
enumerable: false // Private-like (convention)
},
balance: {
get() { return this._balance; },
set(val) {
if (val < 0) throw new Error('Balance cannot be negative');
this._balance = val;
},
enumerable: true
},
currency: {
value: 'USD',
writable: false,
enumerable: true
}
});
console.log(account.balance); // 0
account.balance = 100;
console.log(account.balance); // 100
// account.balance = -50; // Error: Balance cannot be negative
Example: Non-enumerable and non-configurable properties
const obj = { a: 1, b: 2 };
// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false // Won't show in iteration
});
console.log(obj.hidden); // 'secret' (accessible directly)
console.log(Object.keys(obj)); // ['a', 'b'] (hidden not listed)
for (let key in obj) {
console.log(key); // Only logs 'a' and 'b'
}
// Non-configurable property
Object.defineProperty(obj, 'permanent', {
value: 'cannot change',
configurable: false
});
// Cannot reconfigure
try {
Object.defineProperty(obj, 'permanent', {
value: 'new value' // TypeError: cannot redefine
});
} catch (e) {
console.log('Cannot reconfigure');
}
// Cannot delete
delete obj.permanent; // Fails silently
console.log(obj.permanent); // Still exists
// Get all descriptors (ES2017)
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
console.log(allDescriptors.hidden);
// { value: 'secret', writable: false, enumerable: false, configurable: false }
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).
3. Object Methods (Object.keys, Object.values, Object.entries)
| Method | Returns | ES Version | Description |
|---|---|---|---|
| Object.keys(obj) | Array of strings | ES5 | Own enumerable property names (keys) |
| Object.values(obj) ES2017 | Array of values | ES2017 | Own enumerable property values |
| Object.entries(obj) ES2017 | Array of [key, value] pairs | ES2017 | Own enumerable properties as key-value pairs |
| Object.fromEntries(entries) ES2019 | Object | ES2019 | Create object from array of [key, value] pairs (inverse of entries) |
| Object.getOwnPropertyNames(obj) | Array of strings | ES5 | All own property names (including non-enumerable) |
| Object.getOwnPropertySymbols(obj) ES6 | Array of symbols | ES6 | All own symbol properties |
| Object.hasOwn(obj, prop) ES2022 | boolean | ES2022 | Safe alternative to hasOwnProperty (recommended) |
| obj.hasOwnProperty(prop) | boolean | ES3 | Check if property is own (not inherited); use Object.hasOwn instead |
| Comparison | for...in | Object.keys() | Object.getOwnPropertyNames() |
|---|---|---|---|
| Own Properties | ✓ Yes | ✓ Yes | ✓ Yes |
| Inherited Properties | ✓ Yes (from prototype chain) | ✗ No | ✗ No |
| Non-enumerable | ✗ No | ✗ No | ✓ Yes |
| Symbol Properties | ✗ No | ✗ No | ✗ No (use getOwnPropertySymbols) |
Example: Object.keys, values, entries
const user = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
active: true
};
// Get all keys
const keys = Object.keys(user);
console.log(keys); // ['id', 'name', 'email', 'active']
// Get all values (ES2017)
const values = Object.values(user);
console.log(values); // [1, 'Alice', 'alice@example.com', true]
// Get key-value pairs (ES2017)
const entries = Object.entries(user);
console.log(entries);
// [['id', 1], ['name', 'Alice'], ['email', 'alice@example.com'], ['active', true]]
// Iterate over entries
Object.entries(user).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
// Transform object using entries
const doubled = Object.fromEntries(
Object.entries({ a: 1, b: 2, c: 3 })
.map(([key, value]) => [key, value * 2])
);
console.log(doubled); // { a: 2, b: 4, c: 6 }
// Filter object properties
const activeUsers = Object.fromEntries(
Object.entries({ u1: { active: true }, u2: { active: false } })
.filter(([key, val]) => val.active)
);
// { u1: { active: true } }
Example: Enumerable vs non-enumerable properties
const obj = { a: 1, b: 2 };
// Add non-enumerable property
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false
});
// Add symbol property
const sym = Symbol('symbol');
obj[sym] = 'symbol value';
console.log(Object.keys(obj)); // ['a', 'b'] (only enumerable string keys)
console.log(Object.getOwnPropertyNames(obj)); // ['a', 'b', 'hidden'] (includes non-enumerable)
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(symbol)]
// for...in includes inherited enumerable properties
function Parent() {}
Parent.prototype.inherited = 'from parent';
const child = Object.create(new Parent());
child.own = 'own property';
for (let key in child) {
console.log(key); // Logs: 'own', 'inherited'
}
// Filter to own properties only
for (let key in child) {
if (Object.hasOwn(child, key)) { // ES2022
console.log(key); // Logs: 'own' only
}
}
// Alternative: hasOwnProperty (older)
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // Logs: 'own' only
}
}
Example: Practical use cases
// Count object properties
const obj = { a: 1, b: 2, c: 3 };
const count = Object.keys(obj).length; // 3
// Check if object is empty
const isEmpty = Object.keys(obj).length === 0;
// Merge objects by converting to entries
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = Object.fromEntries([
...Object.entries(obj1),
...Object.entries(obj2)
]);
// { a: 1, b: 3, c: 4 } (obj2 overwrites obj1)
// Convert object to Map
const map = new Map(Object.entries(obj));
// Convert Map to object
const objFromMap = Object.fromEntries(map);
// Swap keys and values
const original = { a: 1, b: 2, c: 3 };
const swapped = Object.fromEntries(
Object.entries(original).map(([k, v]) => [v, k])
);
// { 1: 'a', 2: 'b', 3: 'c' }
// Pick specific properties
function pick(obj, keys) {
return Object.fromEntries(
keys.filter(key => key in obj)
.map(key => [key, obj[key]])
);
}
const user = { id: 1, name: 'Alice', email: 'a@example.com', age: 30 };
const userPreview = pick(user, ['id', 'name']);
// { id: 1, name: 'Alice' }
4. Object Creation Patterns (Object.create, constructors)
| Pattern | Syntax | Prototype | Use Case |
|---|---|---|---|
| Object Literal | const obj = { } |
Object.prototype | Simple data objects, quick creation |
| Object() Constructor | const obj = new Object() |
Object.prototype | Rarely used (literal preferred) |
| Object.create() ES5 | Object.create(proto, descriptors?) |
Specified proto | Prototypal inheritance, null prototype objects |
| Constructor Function | function Ctor() { } + new Ctor() |
Ctor.prototype | 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 prototype
const 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 descriptors
const 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 literal
const 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 prototype
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(alice instanceof Person); // true
// Forgetting 'new' - common mistake
function 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' needed
function 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 inheritance
function 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 operator)
| 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
const original = {
name: 'Alice',
age: 30,
address: { city: 'NYC', zip: '10001' }
};
// Object.assign - shallow copy
const copy1 = Object.assign({}, original);
copy1.name = 'Bob';
copy1.address.city = 'LA'; // Modifies nested object
console.log(original.name); // 'Alice' (primitive copied)
console.log(original.address.city); // 'LA' (nested reference shared!)
// Spread operator - shallow copy (ES2018)
const copy2 = { ...original };
copy2.age = 25;
copy2.address.zip = '90001'; // Also modifies original!
console.log(original.age); // 30 (primitive copied)
console.log(original.address.zip); // '90001' (nested reference shared!)
// Merge multiple objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const obj3 = { c: 5, d: 6 };
// Using Object.assign
const merged1 = Object.assign({}, obj1, obj2, obj3);
// { a: 1, b: 3, c: 5, d: 6 } (later values overwrite)
// Using spread (more common)
const merged2 = { ...obj1, ...obj2, ...obj3 };
// { a: 1, b: 3, c: 5, d: 6 } (same result)
// Add/override properties while spreading
const enhanced = {
...original,
age: 35, // Override
email: 'alice@example.com' // Add new
};
// { name: 'Alice', age: 35, address: {...}, email: '...' }
Example: Deep cloning techniques
const original = {
name: 'Alice',
age: 30,
hobbies: ['reading', 'coding'],
address: { city: 'NYC', coords: { lat: 40, lng: -74 } },
createdAt: new Date('2024-01-01')
};
// Method 1: JSON (quick but lossy)
const jsonClone = JSON.parse(JSON.stringify(original));
jsonClone.hobbies.push('gaming');
jsonClone.address.city = 'LA';
console.log(original.hobbies); // ['reading', 'coding'] (not affected)
console.log(original.address.city); // 'NYC' (not affected)
console.log(jsonClone.createdAt); // String, not Date! (limitation)
// Method 2: structuredClone() (modern, recommended)
const structClone = structuredClone(original);
structClone.address.coords.lat = 35;
console.log(original.address.coords.lat); // 40 (not affected)
console.log(structClone.createdAt instanceof Date); // true (preserved!)
// Limitations: cannot clone functions
const withFunction = {
data: 'value',
method() { return this.data; }
};
// structuredClone(withFunction); // Error: functions not cloneable
// Method 3: Manual deep clone (full control)
function deepClone(obj, seen = new WeakMap()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') return obj;
// Handle circular references
if (seen.has(obj)) return seen.get(obj);
// Handle Date
if (obj instanceof Date) return new Date(obj);
// Handle Array
if (Array.isArray(obj)) {
const arrCopy = [];
seen.set(obj, arrCopy);
obj.forEach((item, i) => {
arrCopy[i] = deepClone(item, seen);
});
return arrCopy;
}
// Handle Object
const objCopy = {};
seen.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key], seen);
});
return objCopy;
}
const manualClone = deepClone(original);
manualClone.address.coords.lng = -118;
console.log(original.address.coords.lng); // -74 (not affected)
Example: Object.assign vs spread differences
// Difference 1: Mutating vs creating
const target = { a: 1 };
const source = { b: 2 };
Object.assign(target, source); // Mutates target
console.log(target); // { a: 1, b: 2 } (modified)
const newObj = { ...target, ...source }; // Creates new object
// target unchanged
// Difference 2: Setters
const obj = {
_value: 0,
set value(v) {
console.log('Setter called:', v);
this._value = v;
}
};
// Object.assign triggers setter
Object.assign(obj, { value: 10 }); // Logs: "Setter called: 10"
// Spread defines new property (doesn't trigger setter)
const copy = { ...obj, value: 20 }; // No setter call
console.log(copy.value); // 20 (simple property, not setter)
// Use case: Default options
function configure(options = {}) {
const defaults = {
timeout: 5000,
retries: 3,
cache: true
};
// Merge with user options (spread preferred)
return { ...defaults, ...options };
}
const config1 = configure({ timeout: 10000 });
// { timeout: 10000, retries: 3, cache: true }
const config2 = configure({ retries: 5, debug: true });
// { timeout: 5000, retries: 5, cache: true, debug: true }
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.
6. Property Getters and Setters
| Syntax Form | Example | When to Use |
|---|---|---|
| Object Literal Getter | { get prop() { return value; } } |
Computed properties, validation on read |
| Object Literal Setter | { set prop(val) { this._prop = val; } } |
Validation/transformation on write |
| defineProperty Getter | Object.defineProperty(obj, 'prop', { get() {} }) |
Add getter to existing object |
| defineProperty Setter | Object.defineProperty(obj, 'prop', { set(v) {} }) |
Add setter to existing object |
| Class Getter | class C { get prop() { } } |
ES6 class computed properties |
| Class Setter | class C { set prop(v) { } } |
ES6 class property validation |
| Use Case | Pattern | Example |
|---|---|---|
| Computed Property | Getter derives value from other properties | get fullName() { return this.first + ' ' + this.last; } |
| Validation | Setter validates before assignment | 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 setters
console.log(person.fullName); // "John Doe" (getter called)
person.fullName = 'Jane Smith'; // Setter called
console.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 age
console.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 validation
console.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 getter
class 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 data
console.log(loader.data); // No log, returns cached data
// Observable pattern with setters
class 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 setter
const config = {
_port: 8080,
get port() {
return this._port;
},
set port(value) {
// Always store as number
this._port = Number(value);
}
};
config.port = '3000'; // String input
console.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 properties
Object.preventExtensions(obj);
// Existing properties work normally
obj.a = 10; // ✓ Works (can modify)
delete obj.b; // ✓ Works (can delete)
console.log(obj); // { a: 10 }
// Cannot add new properties
obj.c = 3; // ✗ Fails silently (strict mode: TypeError)
console.log(obj.c); // undefined
// Check if extensible
console.log(Object.isExtensible(obj)); // false
// Cannot make extensible again (one-way operation)
// No Object.makeExtensible() method
Example: Object.seal()
const user = { name: 'Alice', age: 30 };
// Seal object - fixed property set
Object.seal(user);
// Can modify existing properties
user.name = 'Bob'; // ✓ Works
user.age = 35; // ✓ Works
console.log(user); // { name: 'Bob', age: 35 }
// Cannot add new properties
user.email = 'test@example.com'; // ✗ Fails silently
console.log(user.email); // undefined
// Cannot delete properties
delete user.age; // ✗ Fails silently
console.log(user.age); // 35 (still exists)
// Cannot reconfigure property descriptors
// Object.defineProperty(user, 'name', { enumerable: false }); // ✗ TypeError
// Check if sealed
console.log(Object.isSealed(user)); // true
// Sealed implies non-extensible
console.log(Object.isExtensible(user)); // false
Example: Object.freeze()
const config = {
API_URL: 'https://api.example.com',
TIMEOUT: 5000,
MAX_RETRIES: 3
};
// Freeze object - completely immutable
Object.freeze(config);
// Cannot modify properties
config.TIMEOUT = 10000; // ✗ Fails silently (strict mode: TypeError)
console.log(config.TIMEOUT); // 5000 (unchanged)
// Cannot add properties
config.DEBUG = true; // ✗ Fails silently
console.log(config.DEBUG); // undefined
// Cannot delete properties
delete config.MAX_RETRIES; // ✗ Fails silently
console.log(config.MAX_RETRIES); // 3 (still exists)
// Check if frozen
console.log(Object.isFrozen(config)); // true
// Frozen implies sealed and non-extensible
console.log(Object.isSealed(config)); // true
console.log(Object.isExtensible(config)); // false
// Common use: constants
const CONSTANTS = Object.freeze({
PI: 3.14159,
E: 2.71828,
MAX_INT: 2147483647
});
// CONSTANTS.PI = 3.14; // ✗ Cannot modify
Example: Shallow vs deep freeze
// Object.freeze is SHALLOW - nested objects not frozen
const user = {
name: 'Alice',
settings: {
theme: 'dark',
notifications: true
}
};
Object.freeze(user);
// Top-level frozen
user.name = 'Bob'; // ✗ Fails
console.log(user.name); // 'Alice' (unchanged)
// Nested object NOT frozen
user.settings.theme = 'light'; // ✓ Works!
user.settings.notifications = false; // ✓ Works!
console.log(user.settings.theme); // 'light' (modified)
// Deep freeze implementation
function deepFreeze(obj) {
// Freeze object itself
Object.freeze(obj);
// Recursively freeze all object properties
Object.getOwnPropertyNames(obj).forEach(prop => {
const value = obj[prop];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return obj;
}
const deepUser = {
name: 'Charlie',
settings: { theme: 'dark', notifications: true }
};
deepFreeze(deepUser);
// Now nested is also frozen
deepUser.settings.theme = 'light'; // ✗ Fails
console.log(deepUser.settings.theme); // 'dark' (unchanged)
Example: Practical use cases
// Use case 1: Enum-like constants
const Status = Object.freeze({
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected'
});
// Status.PENDING = 'other'; // ✗ Cannot modify
// Use case 2: Configuration objects
const appConfig = Object.freeze({
version: '1.0.0',
apiEndpoint: '/api/v1',
features: {
auth: true,
analytics: true
}
});
// Use case 3: Prevent accidental mutations in function
function processUser(user) {
// Prevent function from modifying passed object
const immutableUser = Object.freeze({ ...user });
// immutableUser.name = 'Modified'; // ✗ Fails
// Return new object instead
return { ...immutableUser, processed: true };
}
// Use case 4: Sealed object with mutable values
const counters = Object.seal({
clicks: 0,
views: 0,
purchases: 0
});
// Can update counters
counters.clicks++;
counters.views += 10;
// But cannot add new counters
// counters.downloads = 0; // ✗ Fails
// Comparison matrix
const obj1 = { a: 1 };
Object.preventExtensions(obj1);
console.log({
canAdd: !Object.isExtensible(obj1), // true (cannot add)
canDelete: !Object.isSealed(obj1), // true (can delete)
canModify: !Object.isFrozen(obj1) // true (can modify)
});
const obj2 = { a: 1 };
Object.seal(obj2);
console.log({
canAdd: !Object.isExtensible(obj2), // true (cannot add)
canDelete: !Object.isSealed(obj2), // false (cannot delete)
canModify: !Object.isFrozen(obj2) // true (can modify)
});
const obj3 = { a: 1 };
Object.freeze(obj3);
console.log({
canAdd: !Object.isExtensible(obj3), // true (cannot add)
canDelete: !Object.isSealed(obj3), // false (cannot delete)
canModify: !Object.isFrozen(obj3) // false (cannot modify)
});
Important Limitations:
- Shallow only: All three methods (preventExtensions, seal, freeze) only affect the immediate object, not nested objects
- Irreversible: Cannot undo these operations - once frozen/sealed, always frozen/sealed
- Silent failures: In non-strict mode, violations fail silently (no error thrown)
- Performance: Frozen objects may have optimization benefits but are rarely the bottleneck
Section 7 Summary
- Modern literals: Property/method shorthand, computed properties, spread syntax, getters/setters for clean object creation
- Property descriptors: Control writable/enumerable/configurable attributes; defineProperty for fine-grained control
- Object methods: keys/values/entries for iteration; fromEntries for transformation; hasOwn for safe property checks
- Creation patterns: Object.create for prototypal inheritance; constructors (legacy) vs classes (modern); factory functions for flexibility
- Cloning: Spread/assign for shallow copy; structuredClone for deep copy; JSON for simple deep copy (lossy)
- Getters/setters: Computed properties, validation, lazy initialization; use _prefix convention for backing fields
- Immutability: preventExtensions (no add), seal (no add/delete), freeze (no changes); all shallow, implement deepFreeze for nested