javascript

exercises

exercises.js
/**
 * ========================================
 * 7.5 PROTOTYPES - EXERCISES
 * ========================================
 * Difficulty: ⭐ = Easy, ⭐⭐ = Medium, ⭐⭐⭐ = Hard
 */

/**
 * EXERCISE 1: Basic Prototype Chain ⭐
 *
 * Create an object `animal` with properties `type: "animal"` and
 * a method `eat()` that logs "Eating...".
 * Create an object `dog` that inherits from `animal` and add
 * an `own` property `breed: "Labrador"`.
 * Return an object with:
 * - dog: the dog object
 * - canEat: boolean, whether dog has access to eat method
 * - ownBreed: boolean, whether breed is own property
 */
function exercise1() {
  // Your code here
}

// console.log(exercise1());
// Expected: { dog: {...}, canEat: true, ownBreed: true }

/**
 * EXERCISE 2: Prototype Chain Length ⭐
 *
 * Given an object, count how many objects are in its prototype chain
 * (not including null but including Object.prototype).
 * For example, a plain object {} has 1 (just Object.prototype).
 */
function exercise2(obj) {
  // Your code here
}

// console.log(exercise2({})); // 1
// console.log(exercise2(Object.create({}))); // 2
// console.log(exercise2(Object.create(null))); // 0

/**
 * EXERCISE 3: Object.create with Properties ⭐⭐
 *
 * Create a function that creates a "Point" object using Object.create().
 * The prototype should have:
 * - `distanceFromOrigin()` method: returns sqrt(x² + y²)
 * - `toString()` method: returns "(x, y)"
 * Use property descriptors to make x and y enumerable but not writable.
 */
function exercise3(x, y) {
  // Your code here
}

// const point = exercise3(3, 4);
// console.log(point.distanceFromOrigin()); // 5
// console.log(point.toString()); // "(3, 4)"
// point.x = 10; // Should fail silently or throw in strict mode
// console.log(point.x); // Still 3

/**
 * EXERCISE 4: Constructor Function ⭐⭐
 *
 * Create a constructor function `Counter` that:
 * - Takes an initial count value (default 0)
 * - Has methods on prototype: increment(), decrement(), getCount()
 * - increment/decrement should return `this` for chaining
 */
function Counter(initial = 0) {
  // Your code here
}

// Add prototype methods here

// const counter = new Counter(5);
// console.log(counter.increment().increment().getCount()); // 7
// console.log(counter.decrement().getCount()); // 6

/**
 * EXERCISE 5: Inheritance with Constructors ⭐⭐
 *
 * Create a `Shape` constructor with:
 * - Property: name
 * - Prototype method: describe() returns `"This is a ${name}"`
 *
 * Create a `Circle` constructor that:
 * - Inherits from Shape
 * - Has additional property: radius
 * - Has method: area() returns π * radius²
 * - Overrides describe() to return `"This is a circle with radius ${radius}"`
 */
function Shape(name) {
  // Your code here
}

function Circle(radius) {
  // Your code here
}

// Set up inheritance here

// const circle = new Circle(5);
// console.log(circle.describe()); // "This is a circle with radius 5"
// console.log(circle.area()); // ~78.54
// console.log(circle instanceof Circle); // true
// console.log(circle instanceof Shape); // true

/**
 * EXERCISE 6: Mixin Pattern ⭐⭐
 *
 * Create a mixin object `Logger` with methods:
 * - log(message): logs "[ClassName]: message"
 * - warn(message): logs "WARNING [ClassName]: message"
 * - error(message): logs "ERROR [ClassName]: message"
 *
 * ClassName should come from `this.constructor.name`.
 * Apply this mixin to a User constructor.
 */
const Logger = {
  // Your code here
};

function User(name) {
  this.name = name;
}

// Apply mixin here

// const user = new User("Alice");
// user.log("logged in"); // "[User]: logged in"
// user.warn("password weak"); // "WARNING [User]: password weak"

/**
 * EXERCISE 7: Get All Properties in Chain ⭐⭐
 *
 * Write a function that returns ALL property names in an object's
 * prototype chain (own + inherited), excluding Object.prototype properties.
 */
function exercise7(obj) {
  // Your code here
}

// const proto = { inherited: 1 };
// const child = Object.create(proto);
// child.own = 2;
// console.log(exercise7(child).sort()); // ["inherited", "own"]

/**
 * EXERCISE 8: Property Origin ⭐⭐
 *
 * Write a function that, given an object and property name, returns
 * which object in the chain actually has that property as its OWN property.
 * Return null if property doesn't exist anywhere.
 */
function exercise8(obj, propName) {
  // Your code here
}

// const grandparent = { family: "Smith" };
// const parent = Object.create(grandparent);
// parent.job = "Engineer";
// const child = Object.create(parent);
// child.name = "John";
// console.log(exercise8(child, "name") === child); // true
// console.log(exercise8(child, "job") === parent); // true
// console.log(exercise8(child, "family") === grandparent); // true
// console.log(exercise8(child, "missing")); // null

/**
 * EXERCISE 9: Create Dictionary Object ⭐⭐
 *
 * Create a function that returns a safe dictionary object (null prototype)
 * with the following methods attached directly as own properties:
 * - get(key): returns value or undefined
 * - set(key, value): sets value, returns dictionary for chaining
 * - has(key): returns boolean
 * - delete(key): removes key, returns boolean
 * - keys(): returns array of all keys (excluding method names)
 */
function exercise9() {
  // Your code here
}

// const dict = exercise9();
// dict.set("a", 1).set("b", 2);
// console.log(dict.get("a")); // 1
// console.log(dict.has("a")); // true
// console.log(dict.keys()); // ["a", "b"]
// console.log(dict.toString); // undefined (no prototype!)

/**
 * EXERCISE 10: Prototype Method Override ⭐⭐
 *
 * Create a base constructor `Animal` with:
 * - Property: name
 * - Method on prototype: speak() returns `${name} makes a sound`
 *
 * Create three subconstructors that override speak():
 * - Dog: returns `${name} barks`
 * - Cat: returns `${name} meows`
 * - Cow: returns `${name} moos`
 *
 * Create a function `makeSpeak` that takes an array of animals
 * and returns an array of their speak() outputs.
 */
function Animal(name) {
  // Your code here
}

function Dog(name) {
  // Your code here
}

function Cat(name) {
  // Your code here
}

function Cow(name) {
  // Your code here
}

function makeSpeak(animals) {
  // Your code here
}

// const animals = [new Dog("Rex"), new Cat("Whiskers"), new Cow("Bessie")];
// console.log(makeSpeak(animals));
// ["Rex barks", "Whiskers meows", "Bessie moos"]

/**
 * EXERCISE 11: Prototype Chain Cloner ⭐⭐⭐
 *
 * Write a function that deep clones an object including its prototype chain.
 * The result should have the same prototype chain structure with
 * cloned objects at each level.
 */
function exercise11(obj) {
  // Your code here
}

// const proto1 = { a: 1 };
// const proto2 = Object.create(proto1);
// proto2.b = 2;
// const original = Object.create(proto2);
// original.c = 3;
// const cloned = exercise11(original);
// console.log(cloned.c); // 3
// console.log(cloned.b); // 2
// console.log(cloned.a); // 1
// console.log(cloned !== original); // true
// console.log(Object.getPrototypeOf(cloned) !== proto2); // true

/**
 * EXERCISE 12: Prototype-based Class System ⭐⭐⭐
 *
 * Implement a simple `defineClass` function that:
 * - Takes parent class (or null), instance methods object, static methods object
 * - Returns a constructor function with proper prototype chain
 * - Instance methods should include a special `_super` method to call parent
 */
function defineClass(Parent, instanceMethods, staticMethods) {
  // Your code here
}

// const Vehicle = defineClass(null, {
//     init(make) { this.make = make; },
//     describe() { return `A ${this.make} vehicle`; }
// }, {
//     category: "transport"
// });

// const Car = defineClass(Vehicle, {
//     init(make, model) {
//         this._super("init", make);
//         this.model = model;
//     },
//     describe() {
//         return `${this._super("describe")} - Model: ${this.model}`;
//     }
// }, {});

// const myCar = new Car("Toyota", "Camry");
// console.log(myCar.describe()); // "A Toyota vehicle - Model: Camry"
// console.log(Vehicle.category); // "transport"

/**
 * EXERCISE 13: Property Delegation Pattern ⭐⭐⭐
 *
 * Create a `delegate` function that makes one object delegate
 * specified properties to another object. When the property is
 * accessed on the delegator, it should look it up on the delegate.
 * When set on delegator, it should set on delegate.
 */
function delegate(delegator, delegatee, properties) {
  // Your code here
}

// const settings = { theme: "dark", fontSize: 14 };
// const user = { name: "Alice" };
// delegate(user, settings, ["theme", "fontSize"]);
// console.log(user.theme); // "dark"
// user.fontSize = 16;
// console.log(settings.fontSize); // 16

/**
 * EXERCISE 14: Observable Prototype ⭐⭐⭐
 *
 * Create a function that wraps an object such that any property
 * access or modification triggers a callback. The wrapper should
 * maintain the prototype chain of the original object.
 */
function makeObservable(obj, callback) {
  // Your code here
}

// const proto = { greet() { return "Hello"; } };
// const original = Object.create(proto);
// original.name = "Alice";
// const log = [];
// const observed = makeObservable(original, (type, prop, value) => {
//     log.push({ type, prop, value });
// });
// observed.name;
// observed.age = 30;
// observed.greet();
// console.log(log);
// [{ type: "get", prop: "name", value: "Alice" },
//  { type: "set", prop: "age", value: 30 },
//  { type: "get", prop: "greet", value: [Function] }]

/**
 * EXERCISE 15: Safe Prototype Extension ⭐⭐⭐
 *
 * Create a function `extendPrototypeSafely` that adds methods to
 * a constructor's prototype but:
 * - Only adds if method doesn't already exist
 * - Makes added methods non-enumerable
 * - Keeps track of added methods for potential rollback
 * Returns an object with:
 * - added: array of method names that were added
 * - skipped: array of method names that already existed
 * - rollback(): function to remove all added methods
 */
function extendPrototypeSafely(Constructor, methods) {
  // Your code here
}

// function MyClass() {}
// MyClass.prototype.existing = function() {};
// const result = extendPrototypeSafely(MyClass, {
//     existing: function() { return "new"; },
//     newMethod: function() { return "added"; }
// });
// console.log(result.added); // ["newMethod"]
// console.log(result.skipped); // ["existing"]
// const instance = new MyClass();
// console.log(instance.newMethod()); // "added"
// result.rollback();
// console.log(instance.newMethod); // undefined

// ============================================
// SOLUTIONS (Hidden below - try first!)
// ============================================

/*
// SOLUTION 1:
function exercise1() {
    const animal = {
        type: "animal",
        eat() {
            console.log("Eating...");
        }
    };
    
    const dog = Object.create(animal);
    dog.breed = "Labrador";
    
    return {
        dog,
        canEat: typeof dog.eat === "function",
        ownBreed: Object.hasOwn(dog, "breed")
    };
}

// SOLUTION 2:
function exercise2(obj) {
    let count = 0;
    let proto = Object.getPrototypeOf(obj);
    while (proto !== null) {
        count++;
        proto = Object.getPrototypeOf(proto);
    }
    return count;
}

// SOLUTION 3:
function exercise3(x, y) {
    const pointProto = {
        distanceFromOrigin() {
            return Math.sqrt(this.x ** 2 + this.y ** 2);
        },
        toString() {
            return `(${this.x}, ${this.y})`;
        }
    };
    
    return Object.create(pointProto, {
        x: { value: x, writable: false, enumerable: true, configurable: false },
        y: { value: y, writable: false, enumerable: true, configurable: false }
    });
}

// SOLUTION 4:
function Counter(initial = 0) {
    this.count = initial;
}

Counter.prototype.increment = function() {
    this.count++;
    return this;
};

Counter.prototype.decrement = function() {
    this.count--;
    return this;
};

Counter.prototype.getCount = function() {
    return this.count;
};

// SOLUTION 5:
function Shape(name) {
    this.name = name;
}

Shape.prototype.describe = function() {
    return `This is a ${this.name}`;
};

function Circle(radius) {
    Shape.call(this, "circle");
    this.radius = radius;
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.area = function() {
    return Math.PI * this.radius ** 2;
};

Circle.prototype.describe = function() {
    return `This is a circle with radius ${this.radius}`;
};

// SOLUTION 6:
const Logger = {
    log(message) {
        console.log(`[${this.constructor.name}]: ${message}`);
    },
    warn(message) {
        console.log(`WARNING [${this.constructor.name}]: ${message}`);
    },
    error(message) {
        console.log(`ERROR [${this.constructor.name}]: ${message}`);
    }
};

function User(name) {
    this.name = name;
}

Object.assign(User.prototype, Logger);

// SOLUTION 7:
function exercise7(obj) {
    const properties = new Set();
    let current = obj;
    
    while (current !== null && current !== Object.prototype) {
        for (const prop of Object.getOwnPropertyNames(current)) {
            properties.add(prop);
        }
        current = Object.getPrototypeOf(current);
    }
    
    return [...properties];
}

// SOLUTION 8:
function exercise8(obj, propName) {
    let current = obj;
    while (current !== null) {
        if (Object.hasOwn(current, propName)) {
            return current;
        }
        current = Object.getPrototypeOf(current);
    }
    return null;
}

// SOLUTION 9:
function exercise9() {
    const dict = Object.create(null);
    const methodNames = ["get", "set", "has", "delete", "keys"];
    
    dict.get = function(key) {
        return this[key];
    };
    
    dict.set = function(key, value) {
        this[key] = value;
        return this;
    };
    
    dict.has = function(key) {
        return key in this;
    };
    
    dict.delete = function(key) {
        if (key in this) {
            delete this[key];
            return true;
        }
        return false;
    };
    
    dict.keys = function() {
        return Object.keys(this).filter(k => !methodNames.includes(k));
    };
    
    return dict;
}

// SOLUTION 10:
function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    return `${this.name} makes a sound`;
};

function Dog(name) {
    Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
    return `${this.name} barks`;
};

function Cat(name) {
    Animal.call(this, name);
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function() {
    return `${this.name} meows`;
};

function Cow(name) {
    Animal.call(this, name);
}
Cow.prototype = Object.create(Animal.prototype);
Cow.prototype.constructor = Cow;
Cow.prototype.speak = function() {
    return `${this.name} moos`;
};

function makeSpeak(animals) {
    return animals.map(animal => animal.speak());
}

// SOLUTION 11:
function exercise11(obj) {
    if (obj === null) return null;
    
    const proto = Object.getPrototypeOf(obj);
    const clonedProto = proto === Object.prototype ? proto : exercise11(proto);
    
    const clone = Object.create(clonedProto);
    
    for (const key of Object.getOwnPropertyNames(obj)) {
        const descriptor = Object.getOwnPropertyDescriptor(obj, key);
        if (descriptor.value && typeof descriptor.value === "object") {
            descriptor.value = structuredClone(descriptor.value);
        }
        Object.defineProperty(clone, key, descriptor);
    }
    
    return clone;
}

// SOLUTION 12:
function defineClass(Parent, instanceMethods, staticMethods) {
    function Class(...args) {
        if (instanceMethods.init) {
            instanceMethods.init.apply(this, args);
        }
    }
    
    if (Parent) {
        Class.prototype = Object.create(Parent.prototype);
        Class.prototype.constructor = Class;
    }
    
    Class.prototype._super = function(methodName, ...args) {
        const parentProto = Parent ? Parent.prototype : Object.prototype;
        if (typeof parentProto[methodName] === "function") {
            return parentProto[methodName].apply(this, args);
        }
    };
    
    for (const key in instanceMethods) {
        if (key !== "init") {
            Class.prototype[key] = instanceMethods[key];
        }
    }
    
    for (const key in staticMethods) {
        Class[key] = staticMethods[key];
    }
    
    return Class;
}

// SOLUTION 13:
function delegate(delegator, delegatee, properties) {
    for (const prop of properties) {
        Object.defineProperty(delegator, prop, {
            get() {
                return delegatee[prop];
            },
            set(value) {
                delegatee[prop] = value;
            },
            enumerable: true,
            configurable: true
        });
    }
}

// SOLUTION 14:
function makeObservable(obj, callback) {
    const proto = Object.getPrototypeOf(obj);
    
    return new Proxy(obj, {
        get(target, prop, receiver) {
            let value;
            if (Object.hasOwn(target, prop)) {
                value = target[prop];
            } else {
                value = Reflect.get(target, prop, receiver);
            }
            callback("get", prop, value);
            return value;
        },
        set(target, prop, value, receiver) {
            callback("set", prop, value);
            return Reflect.set(target, prop, value, receiver);
        },
        getPrototypeOf() {
            return proto;
        }
    });
}

// SOLUTION 15:
function extendPrototypeSafely(Constructor, methods) {
    const added = [];
    const skipped = [];
    
    for (const name in methods) {
        if (name in Constructor.prototype) {
            skipped.push(name);
        } else {
            Object.defineProperty(Constructor.prototype, name, {
                value: methods[name],
                writable: true,
                enumerable: false,
                configurable: true
            });
            added.push(name);
        }
    }
    
    return {
        added,
        skipped,
        rollback() {
            for (const name of added) {
                delete Constructor.prototype[name];
            }
        }
    };
}
*/

console.log('========================================');
console.log('Prototype Exercises Loaded!');
console.log('Uncomment the test code to check your solutions.');
console.log('========================================');
Exercises - JavaScript Tutorial | DeepML