javascript

exercises

exercises.js
/**
 * =====================================================
 * 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!');
Exercises - JavaScript Tutorial | DeepML