javascript
exercises
exercises.js⚡javascript
/**
* =====================================================
* 7.3 PROPERTY DESCRIPTORS - EXERCISES
* =====================================================
* Practice exercises for mastering property descriptors
*/
/**
* EXERCISE 1: Get Property Descriptor
* Difficulty: Easy
*
* Get the property descriptor of the 'name' property
* and log whether it's writable, enumerable, and configurable.
*/
console.log('=== Exercise 1: Get Property Descriptor ===');
const person = {
name: 'Alice',
age: 30,
};
// TODO: Get the descriptor for 'name' and log its attributes
/* SOLUTION:
const descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log("Value:", descriptor.value);
console.log("Writable:", descriptor.writable);
console.log("Enumerable:", descriptor.enumerable);
console.log("Configurable:", descriptor.configurable);
*/
/**
* EXERCISE 2: Create Read-Only Property
* Difficulty: Easy
*
* Add a read-only 'id' property to the user object that cannot be changed.
*/
console.log('\n=== Exercise 2: Create Read-Only Property ===');
const user = {
name: 'Bob',
};
// TODO: Add read-only 'id' property with value "user_001"
// Test:
// console.log(user.id); // "user_001"
// user.id = "changed";
// console.log(user.id); // Should still be "user_001"
/* SOLUTION:
Object.defineProperty(user, "id", {
value: "user_001",
writable: false,
enumerable: true,
configurable: false
});
console.log(user.id); // "user_001"
user.id = "changed";
console.log(user.id); // "user_001"
*/
/**
* EXERCISE 3: Hidden Property
* Difficulty: Easy
*
* Add a non-enumerable '_secret' property that won't show up
* in Object.keys() or for...in loops.
*/
console.log('\n=== Exercise 3: Hidden Property ===');
const account = {
username: 'alice123',
email: 'alice@example.com',
};
// TODO: Add non-enumerable '_secret' property with value "password123"
// Test:
// console.log(Object.keys(account)); // Should NOT include '_secret'
// console.log(account._secret); // Should be accessible directly
/* SOLUTION:
Object.defineProperty(account, "_secret", {
value: "password123",
enumerable: false,
writable: true,
configurable: true
});
console.log(Object.keys(account)); // ["username", "email"]
console.log(account._secret); // "password123"
console.log(Object.getOwnPropertyNames(account)); // Includes "_secret"
*/
/**
* EXERCISE 4: Non-Deletable Property
* Difficulty: Easy
*
* Create a property that cannot be deleted from the object.
*/
console.log('\n=== Exercise 4: Non-Deletable Property ===');
const config = {};
// TODO: Add a non-configurable 'apiKey' property
// Test:
// delete config.apiKey;
// console.log(config.apiKey); // Should still exist
/* SOLUTION:
Object.defineProperty(config, "apiKey", {
value: "abc123xyz",
writable: false,
enumerable: true,
configurable: false
});
delete config.apiKey;
console.log(config.apiKey); // "abc123xyz"
*/
/**
* EXERCISE 5: Getter and Setter
* Difficulty: Medium
*
* Create a 'fullName' property with a getter that combines
* firstName and lastName, and a setter that splits them.
*/
console.log('\n=== Exercise 5: Getter and Setter ===');
const employee = {
_firstName: 'John',
_lastName: 'Doe',
};
// TODO: Add 'fullName' getter and setter
// Test:
// console.log(employee.fullName); // "John Doe"
// employee.fullName = "Jane Smith";
// console.log(employee._firstName); // "Jane"
// console.log(employee._lastName); // "Smith"
/* SOLUTION:
Object.defineProperty(employee, "fullName", {
get() {
return `${this._firstName} ${this._lastName}`;
},
set(value) {
const parts = value.split(" ");
this._firstName = parts[0] || "";
this._lastName = parts.slice(1).join(" ") || "";
},
enumerable: true,
configurable: true
});
console.log(employee.fullName); // "John Doe"
employee.fullName = "Jane Smith";
console.log(employee._firstName); // "Jane"
console.log(employee._lastName); // "Smith"
*/
/**
* EXERCISE 6: Computed Property
* Difficulty: Medium
*
* Create a rectangle object with width and height properties,
* and computed 'area' and 'perimeter' getters.
*/
console.log('\n=== Exercise 6: Computed Property ===');
const rectangle = {
width: 10,
height: 5,
};
// TODO: Add 'area' and 'perimeter' as computed getters
// Test:
// console.log(rectangle.area); // 50
// console.log(rectangle.perimeter); // 30
// rectangle.width = 20;
// console.log(rectangle.area); // 100
/* SOLUTION:
Object.defineProperties(rectangle, {
area: {
get() {
return this.width * this.height;
},
enumerable: true
},
perimeter: {
get() {
return 2 * (this.width + this.height);
},
enumerable: true
}
});
console.log(rectangle.area); // 50
console.log(rectangle.perimeter); // 30
rectangle.width = 20;
console.log(rectangle.area); // 100
*/
/**
* EXERCISE 7: Validation with Setter
* Difficulty: Medium
*
* Create a product object with a 'price' property that validates
* the value is a positive number when set.
*/
console.log('\n=== Exercise 7: Validation with Setter ===');
const product = {
name: 'Widget',
_price: 0,
};
// TODO: Add 'price' getter/setter with validation
// Test:
// product.price = 99.99;
// console.log(product.price); // 99.99
// product.price = -10; // Should throw error or reject
/* SOLUTION:
Object.defineProperty(product, "price", {
get() {
return this._price;
},
set(value) {
if (typeof value !== "number") {
throw new TypeError("Price must be a number");
}
if (value < 0) {
throw new RangeError("Price cannot be negative");
}
this._price = value;
},
enumerable: true,
configurable: true
});
product.price = 99.99;
console.log(product.price); // 99.99
try {
product.price = -10;
} catch (e) {
console.log("Caught:", e.message); // "Price cannot be negative"
}
*/
/**
* EXERCISE 8: Freeze vs Seal
* Difficulty: Medium
*
* Create two objects: one frozen and one sealed.
* Demonstrate the differences in what can be done to each.
*/
console.log('\n=== Exercise 8: Freeze vs Seal ===');
// TODO: Create frozen and sealed objects and test their behaviors
/* SOLUTION:
const frozenObj = Object.freeze({
name: "Frozen",
value: 10
});
const sealedObj = Object.seal({
name: "Sealed",
value: 10
});
// Frozen - nothing works
frozenObj.name = "Changed"; // Fails
frozenObj.newProp = "test"; // Fails
delete frozenObj.value; // Fails
console.log("Frozen:", frozenObj); // { name: "Frozen", value: 10 }
// Sealed - can change values, but not structure
sealedObj.name = "Changed"; // Works!
sealedObj.newProp = "test"; // Fails
delete sealedObj.value; // Fails
console.log("Sealed:", sealedObj); // { name: "Changed", value: 10 }
console.log("Is Frozen:", Object.isFrozen(frozenObj)); // true
console.log("Is Sealed:", Object.isSealed(sealedObj)); // true
*/
/**
* EXERCISE 9: Deep Freeze
* Difficulty: Medium
*
* Implement a deepFreeze function that freezes an object
* and all its nested objects.
*/
console.log('\n=== Exercise 9: Deep Freeze ===');
// TODO: Implement deepFreeze function
function deepFreeze(obj) {
// Your code here
}
// Test:
// const nested = deepFreeze({
// level1: "frozen",
// child: {
// level2: "also frozen",
// grandchild: {
// level3: "frozen too"
// }
// }
// });
//
// nested.child.level2 = "changed";
// console.log(nested.child.level2); // Should still be "also frozen"
/* SOLUTION:
function deepFreeze(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
// Freeze all nested objects first
Object.getOwnPropertyNames(obj).forEach(name => {
const value = obj[name];
if (value && typeof value === "object") {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
const nested = deepFreeze({
level1: "frozen",
child: {
level2: "also frozen",
grandchild: {
level3: "frozen too"
}
}
});
nested.child.level2 = "changed";
console.log(nested.child.level2); // "also frozen"
nested.child.grandchild.level3 = "changed";
console.log(nested.child.grandchild.level3); // "frozen too"
*/
/**
* EXERCISE 10: Constants Object Factory
* Difficulty: Medium
*
* Create a function that takes an object and returns
* a new object with all properties as non-writable constants.
*/
console.log('\n=== Exercise 10: Constants Object Factory ===');
// TODO: Implement createConstants function
function createConstants(obj) {
// Your code here
}
// Test:
// const COLORS = createConstants({
// RED: "#FF0000",
// GREEN: "#00FF00",
// BLUE: "#0000FF"
// });
//
// console.log(COLORS.RED); // "#FF0000"
// COLORS.RED = "#000000";
// console.log(COLORS.RED); // Should still be "#FF0000"
/* SOLUTION:
function createConstants(obj) {
const constants = {};
for (const [key, value] of Object.entries(obj)) {
Object.defineProperty(constants, key, {
value: value,
writable: false,
enumerable: true,
configurable: false
});
}
return Object.freeze(constants);
}
const COLORS = createConstants({
RED: "#FF0000",
GREEN: "#00FF00",
BLUE: "#0000FF"
});
console.log(COLORS.RED); // "#FF0000"
COLORS.RED = "#000000";
console.log(COLORS.RED); // "#FF0000"
console.log(Object.keys(COLORS)); // ["RED", "GREEN", "BLUE"]
*/
/**
* EXERCISE 11: Observable Property
* Difficulty: Hard
*
* Create a function that adds an observable property to an object.
* When the property changes, it should notify all subscribers.
*/
console.log('\n=== Exercise 11: Observable Property ===');
// TODO: Implement addObservableProperty function
function addObservableProperty(obj, propName, initialValue) {
// Your code here
}
// Test:
// const state = {};
// addObservableProperty(state, "count", 0);
//
// state.subscribeToCount((newVal, oldVal) => {
// console.log(`count changed from ${oldVal} to ${newVal}`);
// });
//
// state.count = 1; // "count changed from 0 to 1"
// state.count = 5; // "count changed from 1 to 5"
/* SOLUTION:
function addObservableProperty(obj, propName, initialValue) {
let value = initialValue;
const listeners = [];
Object.defineProperty(obj, propName, {
get() {
return value;
},
set(newValue) {
const oldValue = value;
if (oldValue !== newValue) {
value = newValue;
listeners.forEach(callback => callback(newValue, oldValue));
}
},
enumerable: true,
configurable: true
});
// Add subscribe method (camelCase with property name)
const subscribeName = `subscribeTo${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
obj[subscribeName] = function(callback) {
listeners.push(callback);
return () => {
const index = listeners.indexOf(callback);
if (index > -1) listeners.splice(index, 1);
};
};
}
const state = {};
addObservableProperty(state, "count", 0);
state.subscribeToCount((newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
});
state.count = 1; // "count changed from 0 to 1"
state.count = 5; // "count changed from 1 to 5"
*/
/**
* EXERCISE 12: Lazy Loading Property
* Difficulty: Hard
*
* Create a function that adds a lazy-loaded property.
* The value should only be computed once when first accessed.
*/
console.log('\n=== Exercise 12: Lazy Loading Property ===');
// TODO: Implement addLazyProperty function
function addLazyProperty(obj, propName, computeFn) {
// Your code here
}
// Test:
// const data = {};
// let computeCount = 0;
//
// addLazyProperty(data, "expensive", () => {
// computeCount++;
// console.log("Computing...");
// return Array.from({ length: 100 }, (_, i) => i * i);
// });
//
// console.log("Before access, computed:", computeCount); // 0
// console.log(data.expensive[0]); // "Computing..." then 0
// console.log("After first access:", computeCount); // 1
// console.log(data.expensive[1]); // 1 (no "Computing...")
// console.log("After second access:", computeCount); // Still 1
/* SOLUTION:
function addLazyProperty(obj, propName, computeFn) {
Object.defineProperty(obj, propName, {
get() {
const value = computeFn();
// Replace getter with the computed value
Object.defineProperty(obj, propName, {
value: value,
writable: false,
enumerable: true,
configurable: false
});
return value;
},
enumerable: true,
configurable: true
});
}
const data = {};
let computeCount = 0;
addLazyProperty(data, "expensive", () => {
computeCount++;
console.log("Computing...");
return Array.from({ length: 100 }, (_, i) => i * i);
});
console.log("Before access, computed:", computeCount);
console.log(data.expensive[0]);
console.log("After first access:", computeCount);
console.log(data.expensive[1]);
console.log("After second access:", computeCount);
*/
/**
* EXERCISE 13: Protected Properties
* Difficulty: Hard
*
* Create an object where properties starting with '_' are
* non-enumerable (hidden from Object.keys).
*/
console.log('\n=== Exercise 13: Protected Properties ===');
// TODO: Create a createProtectedObject function
function createProtectedObject(obj) {
// Your code here
}
// Test:
// const protected = createProtectedObject({
// name: "Public",
// _id: "private_123",
// email: "public@example.com",
// _password: "secret"
// });
//
// console.log(Object.keys(protected)); // ["name", "email"]
// console.log(protected._id); // "private_123" (still accessible)
// console.log(JSON.stringify(protected)); // {"name":"Public","email":"public@example.com"}
/* SOLUTION:
function createProtectedObject(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
const isProtected = key.startsWith("_");
Object.defineProperty(result, key, {
value: value,
writable: true,
enumerable: !isProtected, // Hidden if starts with _
configurable: true
});
}
return result;
}
const protectedObj = createProtectedObject({
name: "Public",
_id: "private_123",
email: "public@example.com",
_password: "secret"
});
console.log(Object.keys(protectedObj)); // ["name", "email"]
console.log(protectedObj._id); // "private_123"
console.log(JSON.stringify(protectedObj)); // {"name":"Public","email":"public@example.com"}
*/
/**
* EXERCISE 14: Clamp Property
* Difficulty: Medium
*
* Create a numeric property that automatically clamps
* its value between min and max.
*/
console.log('\n=== Exercise 14: Clamp Property ===');
// TODO: Implement addClampedProperty function
function addClampedProperty(obj, propName, min, max, initialValue) {
// Your code here
}
// Test:
// const volume = {};
// addClampedProperty(volume, "level", 0, 100, 50);
//
// console.log(volume.level); // 50
// volume.level = 150;
// console.log(volume.level); // 100 (clamped to max)
// volume.level = -20;
// console.log(volume.level); // 0 (clamped to min)
/* SOLUTION:
function addClampedProperty(obj, propName, min, max, initialValue) {
let value = Math.max(min, Math.min(max, initialValue));
Object.defineProperty(obj, propName, {
get() {
return value;
},
set(newValue) {
if (typeof newValue !== "number") {
throw new TypeError("Value must be a number");
}
value = Math.max(min, Math.min(max, newValue));
},
enumerable: true,
configurable: true
});
}
const volume = {};
addClampedProperty(volume, "level", 0, 100, 50);
console.log(volume.level); // 50
volume.level = 150;
console.log(volume.level); // 100
volume.level = -20;
console.log(volume.level); // 0
*/
/**
* EXERCISE 15: Full Object Descriptor Copy
* Difficulty: Hard
*
* Create a function that properly clones an object
* including all its property descriptors.
*/
console.log('\n=== Exercise 15: Full Object Descriptor Copy ===');
// TODO: Implement cloneWithDescriptors function
function cloneWithDescriptors(obj) {
// Your code here
}
// Test:
// const original = {
// visible: "I'm enumerable"
// };
// Object.defineProperty(original, "hidden", {
// value: "I'm not enumerable",
// enumerable: false
// });
// Object.defineProperty(original, "readOnly", {
// value: "I'm not writable",
// writable: false
// });
//
// const clone = cloneWithDescriptors(original);
// console.log(Object.getOwnPropertyDescriptor(clone, "hidden"));
// console.log(Object.getOwnPropertyDescriptor(clone, "readOnly"));
/* SOLUTION:
function cloneWithDescriptors(obj) {
return Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
}
const original = {
visible: "I'm enumerable"
};
Object.defineProperty(original, "hidden", {
value: "I'm not enumerable",
enumerable: false
});
Object.defineProperty(original, "readOnly", {
value: "I'm not writable",
writable: false
});
const clone = cloneWithDescriptors(original);
console.log("Clone hidden descriptor:", Object.getOwnPropertyDescriptor(clone, "hidden"));
// { value: "I'm not enumerable", writable: false, enumerable: false, configurable: false }
console.log("Clone readOnly descriptor:", Object.getOwnPropertyDescriptor(clone, "readOnly"));
// { value: "I'm not writable", writable: false, enumerable: true, configurable: false }
console.log("Keys of clone:", Object.keys(clone)); // ["visible"]
console.log("Clone hidden:", clone.hidden); // "I'm not enumerable"
*/
console.log('\n=== All Exercises Complete ===');
console.log('Check the SOLUTION comments for answers!');