javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 8.1 CLASS BASICS - EXERCISES
* ========================================
* Difficulty: ⭐ = Easy, ⭐⭐ = Medium, ⭐⭐⭐ = Hard
*/
/**
* EXERCISE 1: Basic Class ⭐
*
* Create a class `Book` with:
* - Constructor that takes title, author, and pages
* - A method `getSummary()` that returns "Title by Author, X pages"
* - A method `isLong()` that returns true if pages > 300
*/
class Book {
// Your code here
}
// const book = new Book("The Hobbit", "J.R.R. Tolkien", 310);
// console.log(book.getSummary()); // "The Hobbit by J.R.R. Tolkien, 310 pages"
// console.log(book.isLong()); // true
/**
* EXERCISE 2: Getters and Setters ⭐
*
* Create a class `Rectangle` with:
* - Constructor that takes width and height
* - Getter `area` that returns width * height
* - Getter `perimeter` that returns 2 * (width + height)
* - Setter `area` that sets dimensions to a square with that area
*/
class Rectangle {
// Your code here
}
// const rect = new Rectangle(4, 5);
// console.log(rect.area); // 20
// console.log(rect.perimeter); // 18
// rect.area = 16;
// console.log(rect.width, rect.height); // 4, 4
/**
* EXERCISE 3: Method Chaining ⭐⭐
*
* Create a class `Calculator` with:
* - Constructor that takes initial value (default 0)
* - Methods: add(n), subtract(n), multiply(n), divide(n)
* - All methods return `this` for chaining
* - Method `getResult()` returns current value
* - Method `reset()` sets value back to 0
*/
class Calculator {
// Your code here
}
// const calc = new Calculator(10);
// console.log(calc.add(5).multiply(2).subtract(10).getResult()); // 20
// calc.reset();
// console.log(calc.getResult()); // 0
/**
* EXERCISE 4: Validation in Constructor ⭐⭐
*
* Create a class `Email` with:
* - Constructor that takes an email address
* - Throws Error if email format is invalid
* - Getter `address` returns the email (lowercase)
* - Getter `domain` returns the domain part
* - Getter `username` returns the username part
*/
class Email {
// Your code here
}
// const email = new Email("ALICE@Example.COM");
// console.log(email.address); // "alice@example.com"
// console.log(email.domain); // "example.com"
// console.log(email.username); // "alice"
// new Email("invalid"); // Throws Error
/**
* EXERCISE 5: Class with Generator ⭐⭐
*
* Create a class `Countdown` with:
* - Constructor that takes start number
* - Implement Symbol.iterator to count down to 0
* - Method `reset(n)` resets to a new number
*/
class Countdown {
// Your code here
}
// const countdown = new Countdown(5);
// console.log([...countdown]); // [5, 4, 3, 2, 1, 0]
// countdown.reset(3);
// console.log([...countdown]); // [3, 2, 1, 0]
/**
* EXERCISE 6: Factory Methods ⭐⭐
*
* Create a class `Color` with:
* - Constructor that takes r, g, b values (0-255)
* - Static method `fromHex(hex)` creates Color from hex string
* - Static method `fromHSL(h, s, l)` creates Color from HSL
* - Method `toHex()` returns hex string
* - Method `toRGB()` returns "rgb(r, g, b)"
*/
class Color {
// Your code here
}
// const red = new Color(255, 0, 0);
// console.log(red.toHex()); // "#ff0000"
// console.log(red.toRGB()); // "rgb(255, 0, 0)"
// const blue = Color.fromHex("#0000ff");
// console.log(blue.toRGB()); // "rgb(0, 0, 255)"
/**
* EXERCISE 7: Queue Class ⭐⭐
*
* Create a class `Queue` with:
* - Methods: enqueue(item), dequeue(), peek(), isEmpty(), size()
* - Implement Symbol.iterator
* - Method `clear()` empties the queue
*/
class Queue {
// Your code here
}
// const queue = new Queue();
// queue.enqueue(1).enqueue(2).enqueue(3);
// console.log(queue.peek()); // 1
// console.log(queue.dequeue()); // 1
// console.log(queue.size()); // 2
// console.log([...queue]); // [2, 3]
/**
* EXERCISE 8: Timer Class ⭐⭐
*
* Create a class `Timer` with:
* - Methods: start(), stop(), reset()
* - Getter `elapsed` returns milliseconds elapsed
* - Getter `running` returns boolean
* - Method `lap()` returns current elapsed and continues
*/
class Timer {
// Your code here
}
// const timer = new Timer();
// timer.start();
// // ... some time passes ...
// console.log(timer.elapsed); // Time in ms
// console.log(timer.running); // true
// timer.stop();
// console.log(timer.running); // false
/**
* EXERCISE 9: TodoList Class ⭐⭐⭐
*
* Create a class `TodoList` with:
* - Method `add(text, priority = "normal")` - returns todo id
* - Method `complete(id)` - marks todo as complete
* - Method `remove(id)` - removes todo
* - Getter `pending` - returns array of incomplete todos
* - Getter `completed` - returns array of completed todos
* - Getter `all` - returns all todos
* - Implement Symbol.iterator (iterates pending todos)
*/
class TodoList {
// Your code here
}
// const todos = new TodoList();
// const id1 = todos.add("Learn JavaScript", "high");
// const id2 = todos.add("Build project");
// todos.complete(id1);
// console.log(todos.pending.length); // 1
// console.log(todos.completed.length); // 1
/**
* EXERCISE 10: EventEmitter Class ⭐⭐⭐
*
* Create a class `EventEmitter` with:
* - Method `on(event, callback)` - register listener
* - Method `off(event, callback)` - remove listener
* - Method `once(event, callback)` - one-time listener
* - Method `emit(event, ...args)` - trigger event
* - Method `listenerCount(event)` - return count
*/
class EventEmitter {
// Your code here
}
// const emitter = new EventEmitter();
// const handler = (data) => console.log("Received:", data);
// emitter.on("message", handler);
// emitter.emit("message", "Hello"); // "Received: Hello"
// emitter.off("message", handler);
// emitter.emit("message", "World"); // (nothing)
/**
* EXERCISE 11: Validator Class ⭐⭐⭐
*
* Create a class `Validator` with:
* - Method `addRule(name, validatorFn)` - adds validation rule
* - Method `validate(value, rules)` - validates against rules
* - Returns { valid: boolean, errors: string[] }
* - Built-in rules: required, minLength(n), maxLength(n), email, number
*/
class Validator {
// Your code here
}
// const validator = new Validator();
// validator.addRule("uppercase", (val) =>
// val === val.toUpperCase() ? null : "Must be uppercase"
// );
// const result = validator.validate("hello", ["required", "uppercase"]);
// console.log(result); // { valid: false, errors: ["Must be uppercase"] }
/**
* EXERCISE 12: Cache Class ⭐⭐⭐
*
* Create a class `Cache` with:
* - Constructor takes max size (default 100)
* - Method `set(key, value, ttl)` - ttl in ms (optional)
* - Method `get(key)` - returns value or undefined if expired
* - Method `has(key)` - returns boolean
* - Method `delete(key)` - removes entry
* - Method `clear()` - clears all
* - Getter `size` - current number of entries
* - Automatically evicts oldest when max size reached
*/
class Cache {
// Your code here
}
// const cache = new Cache(3);
// cache.set("a", 1);
// cache.set("b", 2);
// cache.set("c", 3);
// cache.set("d", 4); // Evicts "a"
// console.log(cache.get("a")); // undefined
// console.log(cache.get("b")); // 2
/**
* EXERCISE 13: State Machine Class ⭐⭐⭐
*
* Create a class `StateMachine` with:
* - Constructor takes initial state and transitions config
* - Getter `state` returns current state
* - Method `can(event)` - returns if transition is valid
* - Method `transition(event)` - performs transition
* - Method `onEnter(state, callback)` - callback when entering state
* - Method `onLeave(state, callback)` - callback when leaving state
*/
class StateMachine {
// Your code here
}
// const light = new StateMachine("red", {
// red: { timer: "green" },
// green: { timer: "yellow" },
// yellow: { timer: "red" }
// });
// light.onEnter("green", () => console.log("Go!"));
// console.log(light.state); // "red"
// light.transition("timer");
// console.log(light.state); // "green", logs "Go!"
/**
* EXERCISE 14: Observable Class ⭐⭐⭐
*
* Create a class `Observable` with:
* - Constructor takes initial value
* - Getter/setter `value`
* - Method `subscribe(callback)` - returns unsubscribe function
* - Subscribers are called with (newValue, oldValue) when value changes
* - Method `map(fn)` - returns new Observable that transforms value
*/
class Observable {
// Your code here
}
// const num = new Observable(5);
// const unsubscribe = num.subscribe((newVal, oldVal) => {
// console.log(`Changed from ${oldVal} to ${newVal}`);
// });
// num.value = 10; // "Changed from 5 to 10"
// unsubscribe();
// num.value = 15; // (nothing)
/**
* EXERCISE 15: Database Model Class ⭐⭐⭐
*
* Create a class `Model` with:
* - Constructor takes data object
* - Static `fields` property defines field config
* - Getters/setters for each field
* - Method `validate()` returns { valid, errors }
* - Method `toJSON()` returns plain object
* - Static `create(data)` factory method
* - Static `find(predicate)` searches created instances
*/
class Model {
// Your code here
}
// class User extends Model {
// static fields = {
// name: { required: true, minLength: 2 },
// email: { required: true, type: "email" },
// age: { type: "number", min: 0 }
// };
// }
// const user = User.create({ name: "Alice", email: "alice@test.com", age: 25 });
// console.log(user.validate()); // { valid: true, errors: {} }
// ============================================
// SOLUTIONS (Hidden below - try first!)
// ============================================
/*
// SOLUTION 1:
class Book {
constructor(title, author, pages) {
this.title = title;
this.author = author;
this.pages = pages;
}
getSummary() {
return `${this.title} by ${this.author}, ${this.pages} pages`;
}
isLong() {
return this.pages > 300;
}
}
// SOLUTION 2:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height;
}
set area(value) {
const side = Math.sqrt(value);
this.width = side;
this.height = side;
}
get perimeter() {
return 2 * (this.width + this.height);
}
}
// SOLUTION 3:
class Calculator {
constructor(initial = 0) {
this.value = initial;
}
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) throw new Error("Division by zero");
this.value /= n;
return this;
}
getResult() {
return this.value;
}
reset() {
this.value = 0;
return this;
}
}
// SOLUTION 4:
class Email {
constructor(address) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address)) {
throw new Error("Invalid email format");
}
this._address = address.toLowerCase();
}
get address() {
return this._address;
}
get domain() {
return this._address.split("@")[1];
}
get username() {
return this._address.split("@")[0];
}
}
// SOLUTION 5:
class Countdown {
constructor(start) {
this.start = start;
}
*[Symbol.iterator]() {
for (let i = this.start; i >= 0; i--) {
yield i;
}
}
reset(n) {
this.start = n;
}
}
// SOLUTION 6:
class Color {
constructor(r, g, b) {
this.r = Math.max(0, Math.min(255, r));
this.g = Math.max(0, Math.min(255, g));
this.b = Math.max(0, Math.min(255, b));
}
static fromHex(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) throw new Error("Invalid hex color");
return new Color(
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
);
}
static fromHSL(h, s, l) {
s /= 100;
l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r, g, b;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
return new Color(
Math.round((r + m) * 255),
Math.round((g + m) * 255),
Math.round((b + m) * 255)
);
}
toHex() {
const toHex = n => n.toString(16).padStart(2, "0");
return `#${toHex(this.r)}${toHex(this.g)}${toHex(this.b)}`;
}
toRGB() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
// SOLUTION 7:
class Queue {
constructor() {
this.items = [];
}
enqueue(item) {
this.items.push(item);
return this;
}
dequeue() {
return this.items.shift();
}
peek() {
return this.items[0];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
clear() {
this.items = [];
return this;
}
*[Symbol.iterator]() {
yield* this.items;
}
}
// SOLUTION 8:
class Timer {
constructor() {
this._startTime = null;
this._elapsed = 0;
this._running = false;
}
start() {
if (!this._running) {
this._startTime = Date.now();
this._running = true;
}
return this;
}
stop() {
if (this._running) {
this._elapsed += Date.now() - this._startTime;
this._running = false;
}
return this;
}
reset() {
this._elapsed = 0;
this._startTime = this._running ? Date.now() : null;
return this;
}
lap() {
return this.elapsed;
}
get elapsed() {
if (this._running) {
return this._elapsed + (Date.now() - this._startTime);
}
return this._elapsed;
}
get running() {
return this._running;
}
}
// SOLUTION 9:
class TodoList {
constructor() {
this._todos = [];
this._nextId = 1;
}
add(text, priority = "normal") {
const id = this._nextId++;
this._todos.push({
id,
text,
priority,
completed: false,
createdAt: new Date()
});
return id;
}
complete(id) {
const todo = this._todos.find(t => t.id === id);
if (todo) {
todo.completed = true;
todo.completedAt = new Date();
}
return todo;
}
remove(id) {
const index = this._todos.findIndex(t => t.id === id);
if (index > -1) {
return this._todos.splice(index, 1)[0];
}
return null;
}
get pending() {
return this._todos.filter(t => !t.completed);
}
get completed() {
return this._todos.filter(t => t.completed);
}
get all() {
return [...this._todos];
}
*[Symbol.iterator]() {
yield* this.pending;
}
}
// SOLUTION 10:
class EventEmitter {
constructor() {
this._events = new Map();
}
on(event, callback) {
if (!this._events.has(event)) {
this._events.set(event, []);
}
this._events.get(event).push(callback);
return this;
}
off(event, callback) {
if (this._events.has(event)) {
const callbacks = this._events.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
}
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
emit(event, ...args) {
if (this._events.has(event)) {
for (const callback of this._events.get(event)) {
callback(...args);
}
}
return this;
}
listenerCount(event) {
return this._events.has(event) ? this._events.get(event).length : 0;
}
}
// SOLUTION 11:
class Validator {
constructor() {
this._rules = new Map();
// Built-in rules
this.addRule("required", val =>
!val || val.length === 0 ? "This field is required" : null
);
this.addRule("email", val =>
val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) ? "Invalid email" : null
);
this.addRule("number", val =>
val && isNaN(Number(val)) ? "Must be a number" : null
);
}
addRule(name, validatorFn) {
this._rules.set(name, validatorFn);
return this;
}
validate(value, rules) {
const errors = [];
for (const rule of rules) {
const validator = this._rules.get(rule);
if (validator) {
const error = validator(value);
if (error) errors.push(error);
}
}
return { valid: errors.length === 0, errors };
}
}
// SOLUTION 12:
class Cache {
constructor(maxSize = 100) {
this._maxSize = maxSize;
this._cache = new Map();
}
set(key, value, ttl = null) {
if (this._cache.size >= this._maxSize) {
const oldestKey = this._cache.keys().next().value;
this._cache.delete(oldestKey);
}
this._cache.set(key, {
value,
expires: ttl ? Date.now() + ttl : null
});
return this;
}
get(key) {
const entry = this._cache.get(key);
if (!entry) return undefined;
if (entry.expires && Date.now() > entry.expires) {
this._cache.delete(key);
return undefined;
}
return entry.value;
}
has(key) {
return this.get(key) !== undefined;
}
delete(key) {
return this._cache.delete(key);
}
clear() {
this._cache.clear();
return this;
}
get size() {
return this._cache.size;
}
}
// SOLUTION 13:
class StateMachine {
constructor(initialState, transitions) {
this._state = initialState;
this._transitions = transitions;
this._enterCallbacks = {};
this._leaveCallbacks = {};
}
get state() {
return this._state;
}
can(event) {
return !!(this._transitions[this._state] &&
this._transitions[this._state][event]);
}
transition(event) {
if (!this.can(event)) {
throw new Error(`Invalid transition: ${event} from ${this._state}`);
}
const newState = this._transitions[this._state][event];
// Leave callbacks
if (this._leaveCallbacks[this._state]) {
for (const cb of this._leaveCallbacks[this._state]) {
cb();
}
}
this._state = newState;
// Enter callbacks
if (this._enterCallbacks[this._state]) {
for (const cb of this._enterCallbacks[this._state]) {
cb();
}
}
return this;
}
onEnter(state, callback) {
if (!this._enterCallbacks[state]) {
this._enterCallbacks[state] = [];
}
this._enterCallbacks[state].push(callback);
return this;
}
onLeave(state, callback) {
if (!this._leaveCallbacks[state]) {
this._leaveCallbacks[state] = [];
}
this._leaveCallbacks[state].push(callback);
return this;
}
}
// SOLUTION 14:
class Observable {
constructor(initialValue) {
this._value = initialValue;
this._subscribers = [];
}
get value() {
return this._value;
}
set value(newValue) {
const oldValue = this._value;
if (newValue !== oldValue) {
this._value = newValue;
for (const subscriber of this._subscribers) {
subscriber(newValue, oldValue);
}
}
}
subscribe(callback) {
this._subscribers.push(callback);
return () => {
const index = this._subscribers.indexOf(callback);
if (index > -1) {
this._subscribers.splice(index, 1);
}
};
}
map(fn) {
const mapped = new Observable(fn(this._value));
this.subscribe((newVal) => {
mapped.value = fn(newVal);
});
return mapped;
}
}
// SOLUTION 15:
class Model {
static instances = [];
static fields = {};
constructor(data = {}) {
this._data = {};
for (const [field, config] of Object.entries(this.constructor.fields)) {
Object.defineProperty(this, field, {
get: () => this._data[field],
set: (value) => { this._data[field] = value; },
enumerable: true
});
if (data[field] !== undefined) {
this._data[field] = data[field];
}
}
this.constructor.instances.push(this);
}
validate() {
const errors = {};
let valid = true;
for (const [field, config] of Object.entries(this.constructor.fields)) {
const value = this._data[field];
const fieldErrors = [];
if (config.required && !value) {
fieldErrors.push("Required");
}
if (config.minLength && value && value.length < config.minLength) {
fieldErrors.push(`Min length: ${config.minLength}`);
}
if (config.type === "email" && value &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
fieldErrors.push("Invalid email");
}
if (config.type === "number" && value !== undefined && isNaN(value)) {
fieldErrors.push("Must be a number");
}
if (config.min !== undefined && value < config.min) {
fieldErrors.push(`Min value: ${config.min}`);
}
if (fieldErrors.length > 0) {
errors[field] = fieldErrors;
valid = false;
}
}
return { valid, errors };
}
toJSON() {
return { ...this._data };
}
static create(data) {
return new this(data);
}
static find(predicate) {
return this.instances.filter(predicate);
}
}
*/
console.log('========================================');
console.log('Class Basics Exercises Loaded!');
console.log('Uncomment the test code to check your solutions.');
console.log('========================================');