javascript
exercises
exercises.js⚡javascript
/**
* =====================================================
* 7.2 OBJECT METHODS - EXERCISES
* =====================================================
* Practice exercises for mastering object methods
*/
/**
* EXERCISE 1: Basic Method Definition
* Difficulty: Easy
*
* Create a person object with firstName, lastName properties
* and methods: getFullName(), greet(greeting)
*/
console.log('=== Exercise 1: Basic Method Definition ===');
// TODO: Create the person object
const person = {
firstName: 'John',
lastName: 'Doe',
// Add getFullName method
// Add greet method that takes a greeting parameter
};
// Test:
// console.log(person.getFullName()); // "John Doe"
// console.log(person.greet("Hello")); // "Hello, John Doe!"
/* SOLUTION:
const person = {
firstName: "John",
lastName: "Doe",
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
greet(greeting) {
return `${greeting}, ${this.getFullName()}!`;
}
};
console.log(person.getFullName()); // "John Doe"
console.log(person.greet("Hello")); // "Hello, John Doe!"
*/
/**
* EXERCISE 2: Counter Object
* Difficulty: Easy
*
* Create a counter object with:
* - count property (starts at 0)
* - increment() method
* - decrement() method
* - reset() method
* - getCount() method
* All methods should return the counter for chaining
*/
console.log('\n=== Exercise 2: Counter Object ===');
// TODO: Create the counter object
const counter = {
// Your code here
};
// Test:
// counter.increment().increment().increment().decrement();
// console.log(counter.getCount()); // 2
// counter.reset();
// console.log(counter.getCount()); // 0
/* SOLUTION:
const counter = {
count: 0,
increment() {
this.count++;
return this;
},
decrement() {
this.count--;
return this;
},
reset() {
this.count = 0;
return this;
},
getCount() {
return this.count;
}
};
counter.increment().increment().increment().decrement();
console.log(counter.getCount()); // 2
counter.reset();
console.log(counter.getCount()); // 0
*/
/**
* EXERCISE 3: Fix the `this` Problem
* Difficulty: Medium
*
* The code below has a bug - fix it using bind, arrow function,
* or another technique.
*/
console.log('\n=== Exercise 3: Fix the `this` Problem ===');
const buggyTimer = {
seconds: 0,
start() {
// BUG: setTimeout callback loses `this` context
setTimeout(function () {
this.seconds++;
console.log(`Seconds: ${this.seconds}`);
}, 1000);
},
};
// TODO: Create a fixed version
const fixedTimer = {
seconds: 0,
start() {
// Fix the bug here
},
};
// Test: fixedTimer.start();
/* SOLUTION 1: Using arrow function
const fixedTimer = {
seconds: 0,
start() {
setTimeout(() => {
this.seconds++;
console.log(`Seconds: ${this.seconds}`);
}, 1000);
}
};
SOLUTION 2: Using bind
const fixedTimer = {
seconds: 0,
start() {
setTimeout(function() {
this.seconds++;
console.log(`Seconds: ${this.seconds}`);
}.bind(this), 1000);
}
};
SOLUTION 3: Using saved reference
const fixedTimer = {
seconds: 0,
start() {
const self = this;
setTimeout(function() {
self.seconds++;
console.log(`Seconds: ${self.seconds}`);
}, 1000);
}
};
*/
/**
* EXERCISE 4: Calculator with Chaining
* Difficulty: Medium
*
* Create a calculator that supports method chaining:
* calculator.add(5).subtract(2).multiply(3).divide(2).getResult()
*/
console.log('\n=== Exercise 4: Calculator with Chaining ===');
// TODO: Create the calculator
const calculator = {
// Your code here
};
// Test:
// const result = calculator.reset().add(10).subtract(3).multiply(2).divide(7).getResult();
// console.log(result); // 2
/* SOLUTION:
const calculator = {
value: 0,
reset() {
this.value = 0;
return this;
},
add(n) {
this.value += n;
return this;
},
subtract(n) {
this.value -= n;
return this;
},
multiply(n) {
this.value *= n;
return this;
},
divide(n) {
if (n !== 0) {
this.value /= n;
}
return this;
},
getResult() {
return this.value;
}
};
const result = calculator.reset().add(10).subtract(3).multiply(2).divide(7).getResult();
console.log(result); // 2
*/
/**
* EXERCISE 5: Shopping Cart
* Difficulty: Medium
*
* Create a shopping cart object with methods:
* - addItem(name, price, quantity)
* - removeItem(name)
* - updateQuantity(name, quantity)
* - getTotal()
* - getItemCount()
* - clear()
*/
console.log('\n=== Exercise 5: Shopping Cart ===');
// TODO: Create the shopping cart
const cart = {
// Your code here
};
// Test:
// cart.addItem("Apple", 0.50, 5);
// cart.addItem("Banana", 0.30, 3);
// console.log(cart.getTotal()); // 3.40
// console.log(cart.getItemCount()); // 8
// cart.updateQuantity("Apple", 10);
// console.log(cart.getTotal()); // 5.90
// cart.removeItem("Banana");
// console.log(cart.getItemCount()); // 10
/* SOLUTION:
const cart = {
items: [],
addItem(name, price, quantity) {
const existingItem = this.items.find(item => item.name === name);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ name, price, quantity });
}
return this;
},
removeItem(name) {
this.items = this.items.filter(item => item.name !== name);
return this;
},
updateQuantity(name, quantity) {
const item = this.items.find(item => item.name === name);
if (item) {
item.quantity = quantity;
}
return this;
},
getTotal() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
getItemCount() {
return this.items.reduce((count, item) => count + item.quantity, 0);
},
clear() {
this.items = [];
return this;
}
};
cart.addItem("Apple", 0.50, 5);
cart.addItem("Banana", 0.30, 3);
console.log(cart.getTotal()); // 3.40
console.log(cart.getItemCount()); // 8
cart.updateQuantity("Apple", 10);
console.log(cart.getTotal()); // 5.90
cart.removeItem("Banana");
console.log(cart.getItemCount()); // 10
*/
/**
* EXERCISE 6: String Manipulation Object
* Difficulty: Medium
*
* Create a string manipulator with chainable methods:
* - setValue(str)
* - reverse()
* - capitalize()
* - truncate(maxLength)
* - slugify()
* - getValue()
*/
console.log('\n=== Exercise 6: String Manipulation Object ===');
// TODO: Create the stringManipulator
const stringManipulator = {
// Your code here
};
// Test:
// console.log(stringManipulator.setValue(" Hello World ").trim().reverse().getValue());
// // "dlroW olleH"
// console.log(stringManipulator.setValue("hello world").capitalize().getValue());
// // "Hello World"
// console.log(stringManipulator.setValue("My Blog Post Title").slugify().getValue());
// // "my-blog-post-title"
/* SOLUTION:
const stringManipulator = {
value: "",
setValue(str) {
this.value = str;
return this;
},
trim() {
this.value = this.value.trim();
return this;
},
reverse() {
this.value = this.value.split("").reverse().join("");
return this;
},
capitalize() {
this.value = this.value.split(" ")
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
return this;
},
truncate(maxLength) {
if (this.value.length > maxLength) {
this.value = this.value.substring(0, maxLength) + "...";
}
return this;
},
slugify() {
this.value = this.value
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
return this;
},
getValue() {
return this.value;
}
};
*/
/**
* EXERCISE 7: Computed Method Names
* Difficulty: Medium
*
* Create an object with dynamically named getter/setter methods
* for properties: name, age, email
*/
console.log('\n=== Exercise 7: Computed Method Names ===');
const properties = ['name', 'age', 'email'];
// TODO: Create entity object with dynamic get/set methods
const entity = {
_data: {},
// Use computed property names to create:
// getName(), setName(), getAge(), setAge(), getEmail(), setEmail()
};
// Test:
// entity.setName("Alice");
// entity.setAge(25);
// entity.setEmail("alice@example.com");
// console.log(entity.getName()); // "Alice"
// console.log(entity.getAge()); // 25
// console.log(entity.getEmail()); // "alice@example.com"
/* SOLUTION:
const properties = ["name", "age", "email"];
const entity = {
_data: {},
...properties.reduce((methods, prop) => {
const capitalizedProp = prop.charAt(0).toUpperCase() + prop.slice(1);
methods[`get${capitalizedProp}`] = function() {
return this._data[prop];
};
methods[`set${capitalizedProp}`] = function(value) {
this._data[prop] = value;
return this;
};
return methods;
}, {})
};
entity.setName("Alice");
entity.setAge(25);
entity.setEmail("alice@example.com");
console.log(entity.getName()); // "Alice"
console.log(entity.getAge()); // 25
console.log(entity.getEmail()); // "alice@example.com"
*/
/**
* EXERCISE 8: Bank Account with Private State
* Difficulty: Hard
*
* Create a createAccount factory function that returns an object
* with methods but keeps balance private using closures.
*/
console.log('\n=== Exercise 8: Bank Account with Private State ===');
// TODO: Create the factory function
function createAccount(initialBalance = 0) {
// Private state
// Return object with methods
}
// Test:
// const myAccount = createAccount(100);
// myAccount.deposit(50);
// console.log(myAccount.getBalance()); // 150
// myAccount.withdraw(30);
// console.log(myAccount.getBalance()); // 120
// console.log(myAccount.balance); // undefined (private!)
/* SOLUTION:
function createAccount(initialBalance = 0) {
let balance = initialBalance;
const transactions = [];
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
balance,
date: new Date()
});
}
return {
deposit(amount) {
if (amount <= 0) {
throw new Error("Invalid deposit amount");
}
balance += amount;
recordTransaction("deposit", amount);
return this;
},
withdraw(amount) {
if (amount <= 0) {
throw new Error("Invalid withdrawal amount");
}
if (amount > balance) {
throw new Error("Insufficient funds");
}
balance -= amount;
recordTransaction("withdrawal", amount);
return this;
},
getBalance() {
return balance;
},
getTransactionHistory() {
return [...transactions];
}
};
}
const myAccount = createAccount(100);
myAccount.deposit(50);
console.log(myAccount.getBalance()); // 150
myAccount.withdraw(30);
console.log(myAccount.getBalance()); // 120
console.log(myAccount.balance); // undefined
*/
/**
* EXERCISE 9: Event Emitter
* Difficulty: Hard
*
* Create an event emitter object with methods:
* - on(event, callback)
* - off(event, callback)
* - emit(event, ...args)
* - once(event, callback)
*/
console.log('\n=== Exercise 9: Event Emitter ===');
// TODO: Create the event emitter
const eventEmitter = {
// Your code here
};
// Test:
// const handler = (name) => console.log(`Hello, ${name}!`);
// eventEmitter.on("greet", handler);
// eventEmitter.emit("greet", "World"); // "Hello, World!"
// eventEmitter.off("greet", handler);
// eventEmitter.emit("greet", "World"); // (nothing)
//
// eventEmitter.once("init", () => console.log("Initialized!"));
// eventEmitter.emit("init"); // "Initialized!"
// eventEmitter.emit("init"); // (nothing - only fires once)
/* SOLUTION:
const eventEmitter = {
events: {},
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return this;
},
off(event, callback) {
if (!this.events[event]) return this;
this.events[event] = this.events[event].filter(cb => cb !== callback);
return this;
},
emit(event, ...args) {
if (!this.events[event]) return this;
this.events[event].forEach(callback => {
callback(...args);
});
return this;
},
once(event, callback) {
const onceWrapper = (...args) => {
callback(...args);
this.off(event, onceWrapper);
};
return this.on(event, onceWrapper);
}
};
*/
/**
* EXERCISE 10: Method Borrowing
* Difficulty: Medium
*
* Use call() or apply() to borrow methods between objects.
*/
console.log('\n=== Exercise 10: Method Borrowing ===');
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
};
// TODO: Use Array methods on arrayLike object
// 1. Convert arrayLike to a real array using Array.prototype.slice
// 2. Join arrayLike elements using Array.prototype.join
// 3. Find index of "b" using Array.prototype.indexOf
// Your code here
/* SOLUTION:
// Convert to array
const realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // ["a", "b", "c"]
// Join elements
const joined = Array.prototype.join.call(arrayLike, "-");
console.log(joined); // "a-b-c"
// Find index
const index = Array.prototype.indexOf.call(arrayLike, "b");
console.log(index); // 1
// Modern alternative: Array.from
const modernArray = Array.from(arrayLike);
console.log(modernArray); // ["a", "b", "c"]
*/
/**
* EXERCISE 11: Observable Object
* Difficulty: Hard
*
* Create an observable object that notifies listeners when properties change.
*/
console.log('\n=== Exercise 11: Observable Object ===');
// TODO: Create an observable wrapper
function createObservable(target) {
// Your code here
}
// Test:
// const observable = createObservable({ name: "John", age: 30 });
// observable.onChange((prop, oldVal, newVal) => {
// console.log(`${prop} changed from ${oldVal} to ${newVal}`);
// });
// observable.set("name", "Jane"); // "name changed from John to Jane"
// observable.set("age", 31); // "age changed from 30 to 31"
// console.log(observable.get("name")); // "Jane"
/* SOLUTION:
function createObservable(target) {
const data = { ...target };
const listeners = [];
return {
get(prop) {
return data[prop];
},
set(prop, value) {
const oldValue = data[prop];
if (oldValue !== value) {
data[prop] = value;
listeners.forEach(callback => {
callback(prop, oldValue, value);
});
}
return this;
},
onChange(callback) {
listeners.push(callback);
return this;
},
offChange(callback) {
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
return this;
},
getAll() {
return { ...data };
}
};
}
const observable = createObservable({ name: "John", age: 30 });
observable.onChange((prop, oldVal, newVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
});
observable.set("name", "Jane");
observable.set("age", 31);
console.log(observable.get("name"));
*/
/**
* EXERCISE 12: State Machine
* Difficulty: Hard
*
* Create a finite state machine for a traffic light.
* States: red, yellow, green
* Transitions: red -> green -> yellow -> red
*/
console.log('\n=== Exercise 12: State Machine ===');
// TODO: Create the state machine
const trafficLight = {
// Your code here
};
// Test:
// console.log(trafficLight.getState()); // "red"
// trafficLight.next();
// console.log(trafficLight.getState()); // "green"
// trafficLight.next();
// console.log(trafficLight.getState()); // "yellow"
// trafficLight.next();
// console.log(trafficLight.getState()); // "red"
// console.log(trafficLight.canGo()); // false
// trafficLight.next();
// console.log(trafficLight.canGo()); // true
/* SOLUTION:
const trafficLight = {
states: ["red", "green", "yellow"],
transitions: {
red: "green",
green: "yellow",
yellow: "red"
},
currentState: "red",
getState() {
return this.currentState;
},
next() {
const prevState = this.currentState;
this.currentState = this.transitions[this.currentState];
console.log(`${prevState} -> ${this.currentState}`);
return this;
},
canGo() {
return this.currentState === "green";
},
shouldStop() {
return this.currentState === "red";
},
shouldSlowDown() {
return this.currentState === "yellow";
},
reset() {
this.currentState = "red";
return this;
}
};
*/
/**
* EXERCISE 13: Fluent Validation API
* Difficulty: Hard
*
* Create a validation object with a fluent API.
*/
console.log('\n=== Exercise 13: Fluent Validation API ===');
// TODO: Create the validator
function createValidator() {
// Your code here
}
// Test:
// const validator = createValidator();
// const result = validator
// .field("username")
// .required()
// .minLength(3)
// .maxLength(20)
// .matches(/^[a-zA-Z0-9_]+$/)
// .field("email")
// .required()
// .email()
// .field("age")
// .required()
// .number()
// .min(18)
// .validate({ username: "jo", email: "invalid", age: 15 });
//
// console.log(result.isValid); // false
// console.log(result.errors); // Array of error messages
/* SOLUTION:
function createValidator() {
const rules = {};
let currentField = null;
return {
field(name) {
currentField = name;
if (!rules[name]) {
rules[name] = [];
}
return this;
},
required() {
rules[currentField].push({
test: (value) => value !== undefined && value !== null && value !== "",
message: `${currentField} is required`
});
return this;
},
minLength(min) {
rules[currentField].push({
test: (value) => !value || value.length >= min,
message: `${currentField} must be at least ${min} characters`
});
return this;
},
maxLength(max) {
rules[currentField].push({
test: (value) => !value || value.length <= max,
message: `${currentField} must be at most ${max} characters`
});
return this;
},
matches(regex) {
rules[currentField].push({
test: (value) => !value || regex.test(value),
message: `${currentField} format is invalid`
});
return this;
},
email() {
return this.matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
},
number() {
rules[currentField].push({
test: (value) => typeof value === "number" && !isNaN(value),
message: `${currentField} must be a number`
});
return this;
},
min(minVal) {
rules[currentField].push({
test: (value) => value >= minVal,
message: `${currentField} must be at least ${minVal}`
});
return this;
},
validate(data) {
const errors = [];
for (const [field, fieldRules] of Object.entries(rules)) {
const value = data[field];
for (const rule of fieldRules) {
if (!rule.test(value)) {
errors.push(rule.message);
}
}
}
return {
isValid: errors.length === 0,
errors
};
}
};
}
*/
/**
* EXERCISE 14: Mixin Pattern
* Difficulty: Medium
*
* Create reusable mixin objects and apply them to target objects.
*/
console.log('\n=== Exercise 14: Mixin Pattern ===');
// TODO: Create mixins and apply them
const timestampMixin = {
// Methods related to timestamps
};
const serializableMixin = {
// Methods related to serialization
};
// Apply mixins to create a full-featured object
const myObject = {
name: 'Example',
value: 42,
};
// Test after applying mixins:
// myObject.touch(); // Updates lastModified
// console.log(myObject.getLastModified());
// console.log(myObject.toJSON()); // JSON string
// console.log(myObject.clone()); // Deep clone
/* SOLUTION:
const timestampMixin = {
createdAt: null,
lastModified: null,
initTimestamps() {
this.createdAt = new Date();
this.lastModified = new Date();
return this;
},
touch() {
this.lastModified = new Date();
return this;
},
getCreatedAt() {
return this.createdAt;
},
getLastModified() {
return this.lastModified;
}
};
const serializableMixin = {
toJSON() {
return JSON.stringify(this);
},
clone() {
return JSON.parse(JSON.stringify(this));
},
fromJSON(json) {
const data = typeof json === "string" ? JSON.parse(json) : json;
Object.assign(this, data);
return this;
}
};
function applyMixins(target, ...mixins) {
mixins.forEach(mixin => {
Object.getOwnPropertyNames(mixin).forEach(name => {
if (name !== "constructor") {
target[name] = mixin[name];
}
});
});
return target;
}
const myObject = {
name: "Example",
value: 42
};
applyMixins(myObject, timestampMixin, serializableMixin);
myObject.initTimestamps();
myObject.touch();
console.log(myObject.getLastModified());
console.log(myObject.toJSON());
console.log(myObject.clone());
*/
/**
* EXERCISE 15: Command Pattern
* Difficulty: Hard
*
* Implement undo/redo functionality using the command pattern.
*/
console.log('\n=== Exercise 15: Command Pattern ===');
// TODO: Create a text editor with undo/redo
const textEditor = {
// Your code here
};
// Test:
// textEditor.type("Hello");
// textEditor.type(" World");
// console.log(textEditor.getText()); // "Hello World"
// textEditor.undo();
// console.log(textEditor.getText()); // "Hello"
// textEditor.undo();
// console.log(textEditor.getText()); // ""
// textEditor.redo();
// console.log(textEditor.getText()); // "Hello"
// textEditor.redo();
// console.log(textEditor.getText()); // "Hello World"
/* SOLUTION:
const textEditor = {
text: "",
undoStack: [],
redoStack: [],
executeCommand(command) {
command.execute(this);
this.undoStack.push(command);
this.redoStack = [];
return this;
},
type(str) {
return this.executeCommand({
value: str,
execute(editor) {
editor.text += this.value;
},
undo(editor) {
editor.text = editor.text.slice(0, -this.value.length);
}
});
},
backspace(count = 1) {
const deleted = this.text.slice(-count);
return this.executeCommand({
value: deleted,
count,
execute(editor) {
editor.text = editor.text.slice(0, -this.count);
},
undo(editor) {
editor.text += this.value;
}
});
},
undo() {
if (this.undoStack.length === 0) return this;
const command = this.undoStack.pop();
command.undo(this);
this.redoStack.push(command);
return this;
},
redo() {
if (this.redoStack.length === 0) return this;
const command = this.redoStack.pop();
command.execute(this);
this.undoStack.push(command);
return this;
},
getText() {
return this.text;
},
clear() {
this.text = "";
this.undoStack = [];
this.redoStack = [];
return this;
}
};
textEditor.type("Hello");
textEditor.type(" World");
console.log(textEditor.getText());
textEditor.undo();
console.log(textEditor.getText());
textEditor.undo();
console.log(textEditor.getText());
textEditor.redo();
console.log(textEditor.getText());
textEditor.redo();
console.log(textEditor.getText());
*/
console.log('\n=== All Exercises Complete ===');
console.log('Check the SOLUTION comments for answers!');