javascript

exercises

exercises.js
/**
 * ============================================
 * 6.5 MEMORY MANAGEMENT - EXERCISES
 * ============================================
 *
 * Practice understanding memory allocation,
 * garbage collection, and avoiding memory leaks.
 */

/**
 * EXERCISE 1: Stack vs Heap
 * -------------------------
 * Predict the outputs.
 */

console.log('=== Exercise 1: Stack vs Heap ===');

let num1 = 5;
let num2 = num1;
num2 = 10;

let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);

console.log('num1:', num1);
console.log('arr1:', arr1);

// YOUR PREDICTION:
// num1: ___
// arr1: ___

/*
 * SOLUTION:
 * num1: 5 (primitives are copied by value)
 * arr1: [1, 2, 3, 4] (arrays are passed by reference)
 */

/**
 * EXERCISE 2: Object Reference
 * ----------------------------
 * Will the original object be modified?
 */

console.log('\n=== Exercise 2: Object Reference ===');

function modifyObject(obj) {
  obj.value = 100;
}

const myObj = { value: 1 };
modifyObject(myObj);
console.log('myObj.value:', myObj.value);

// YOUR PREDICTION: ___

/*
 * SOLUTION:
 * myObj.value: 100
 * Objects are passed by reference, so modifications affect the original
 */

/**
 * EXERCISE 3: Create a True Copy
 * ------------------------------
 * Modify this code so arr2 doesn't affect arr1.
 */

console.log('\n=== Exercise 3: Create True Copy ===');

const arr1Copy = [1, 2, 3];
// Change this line to create a true copy:
const arr2Copy = arr1Copy; // YOUR FIX HERE

arr2Copy.push(4);

// GOAL: arr1Copy should still be [1, 2, 3]

/*
 * SOLUTION:
 * const arr2Copy = [...arr1Copy];
 * // OR
 * const arr2Copy = arr1Copy.slice();
 * // OR
 * const arr2Copy = Array.from(arr1Copy);
 */

// Test with correct solution:
const arr1Fixed = [1, 2, 3];
const arr2Fixed = [...arr1Fixed];
arr2Fixed.push(4);
console.log('arr1Fixed:', arr1Fixed); // [1, 2, 3]
console.log('arr2Fixed:', arr2Fixed); // [1, 2, 3, 4]

/**
 * EXERCISE 4: Identify the Memory Leak
 * ------------------------------------
 * What's wrong with this code?
 */

console.log('\n=== Exercise 4: Identify the Leak ===');

function createLeak() {
  const elements = [];

  function addElement() {
    const el = { data: new Array(10000).fill('x') };
    elements.push(el);
  }

  setInterval(addElement, 100);
}

// createLeak(); // DON'T RUN - will consume memory!

// QUESTION: What causes the memory leak?
// YOUR ANSWER: ___

/*
 * SOLUTION:
 * The 'elements' array keeps growing forever because:
 * 1. setInterval runs indefinitely
 * 2. Each call adds a large object to the array
 * 3. Nothing ever clears the array or stops the interval
 *
 * FIX: Return the interval ID and clear it, or limit array size
 */

/**
 * EXERCISE 5: Fix the Memory Leak
 * -------------------------------
 * Rewrite Exercise 4 to prevent the leak.
 */

console.log('\n=== Exercise 5: Fix the Leak ===');

function createNoLeak() {
  // YOUR CODE HERE
  // Requirements:
  // 1. Limit the array to max 10 elements
  // 2. Provide a way to stop the interval
}

/*
 * SOLUTION:
 */
function createNoLeakSolution() {
  const elements = [];
  const MAX_ELEMENTS = 10;

  function addElement() {
    if (elements.length >= MAX_ELEMENTS) {
      elements.shift(); // Remove oldest
    }
    const el = { data: new Array(100).fill('x') };
    elements.push(el);
    console.log('Elements count:', elements.length);
  }

  const intervalId = setInterval(addElement, 100);

  // Return cleanup function
  return () => {
    clearInterval(intervalId);
    console.log('Interval stopped, leak prevented');
  };
}

const stop = createNoLeakSolution();
setTimeout(stop, 600); // Stop after a few iterations

/**
 * EXERCISE 6: Closure Leak
 * ------------------------
 * How much memory does each closure hold?
 */

console.log('\n=== Exercise 6: Closure Memory ===');

function createHandlers() {
  const largeData = new Array(100000).fill('x');

  return {
    handler1: () => largeData.length,
    handler2: () => largeData[0],
    handler3: () => 'static value',
  };
}

const handlers = createHandlers();

// QUESTION: Which handlers keep largeData in memory?
// YOUR ANSWER: ___

/*
 * SOLUTION:
 * handler1 and handler2 both keep largeData in memory
 * because they reference it.
 *
 * handler3 doesn't reference largeData but it's still kept
 * because all closures from the same scope share the same
 * variable environment.
 *
 * In practice, modern JS engines may optimize this.
 */

/**
 * EXERCISE 7: WeakMap Use Case
 * ----------------------------
 * Implement a function to cache expensive calculations
 * without preventing garbage collection.
 */

console.log('\n=== Exercise 7: WeakMap Cache ===');

// Implement a cache that doesn't prevent GC of objects
function createObjectCache() {
  // YOUR CODE HERE
  // Return an object with:
  // - compute(obj): Returns cached result or computes new
}

/*
 * SOLUTION:
 */
function createObjectCacheSolution() {
  const cache = new WeakMap();

  return {
    compute(obj) {
      if (cache.has(obj)) {
        console.log('Cache hit!');
        return cache.get(obj);
      }

      console.log('Computing...');
      // Expensive computation
      const result = JSON.stringify(obj).length;
      cache.set(obj, result);
      return result;
    },
  };
}

const cache = createObjectCacheSolution();
const testObj = { name: 'test', values: [1, 2, 3] };
console.log(cache.compute(testObj)); // Computing...
console.log(cache.compute(testObj)); // Cache hit!

/**
 * EXERCISE 8: Event Listener Leak
 * -------------------------------
 * This component leaks memory. Fix it.
 */

console.log('\n=== Exercise 8: Event Listener Leak ===');

// LEAKY VERSION:
class LeakyCounter {
  constructor() {
    this.count = 0;
    this.data = new Array(10000).fill('x');

    // In browser: window.addEventListener('click', () => this.count++);
    console.log('LeakyCounter: Listener added (simulated)');
  }
  // Missing: No way to remove the listener!
}

// YOUR FIX:
class FixedCounter {
  constructor() {
    this.count = 0;
    this.data = new Array(10000).fill('x');
    // YOUR CODE HERE
  }

  // Add cleanup method
}

/*
 * SOLUTION:
 */
class FixedCounterSolution {
  constructor() {
    this.count = 0;
    this.data = new Array(10000).fill('x');

    // Bind the handler
    this.handleClick = this.handleClick.bind(this);
    // In browser: window.addEventListener('click', this.handleClick);
    console.log('FixedCounter: Listener added (simulated)');
  }

  handleClick() {
    this.count++;
  }

  destroy() {
    // In browser: window.removeEventListener('click', this.handleClick);
    this.data = null;
    console.log('FixedCounter: Cleaned up');
  }
}

const counter = new FixedCounterSolution();
counter.destroy();

/**
 * EXERCISE 9: Circular Reference
 * ------------------------------
 * Will these objects be garbage collected?
 */

console.log('\n=== Exercise 9: Circular Reference ===');

function createCircularRef() {
  const a = { name: 'A' };
  const b = { name: 'B' };

  a.ref = b;
  b.ref = a;

  return null; // Return nothing
}

createCircularRef();

// QUESTION: Will a and b be garbage collected?
// YOUR ANSWER: ___

/*
 * SOLUTION:
 * YES - Modern JavaScript engines use mark-and-sweep garbage collection.
 *
 * After createCircularRef() returns, there are no external references
 * to either a or b. The GC starts from "roots" (global, stack) and
 * marks reachable objects. Since a and b are not reachable from any
 * root, they will both be collected, despite referencing each other.
 */

/**
 * EXERCISE 10: Optimize String Operations
 * ---------------------------------------
 * This code is memory-inefficient. Optimize it.
 */

console.log('\n=== Exercise 10: String Optimization ===');

// INEFFICIENT:
function buildStringInefficient(n) {
  let result = '';
  for (let i = 0; i < n; i++) {
    result += `item${i},`; // Creates new string each iteration!
  }
  return result;
}

// YOUR OPTIMIZED VERSION:
function buildStringEfficient(n) {
  // YOUR CODE HERE
}

/*
 * SOLUTION:
 */
function buildStringEfficientSolution(n) {
  const parts = [];
  for (let i = 0; i < n; i++) {
    parts.push(`item${i}`);
  }
  return parts.join(',');
}

console.time('Inefficient');
buildStringInefficient(1000);
console.timeEnd('Inefficient');

console.time('Efficient');
buildStringEfficientSolution(1000);
console.timeEnd('Efficient');

/**
 * EXERCISE 11: Implement Object Pool
 * -----------------------------------
 * Create a simple object pool for reuse.
 */

console.log('\n=== Exercise 11: Object Pool ===');

class ObjectPool {
  constructor(createFn, initialSize = 5) {
    // YOUR CODE HERE
    // - Create initial pool of objects
    // - Implement acquire() and release()
  }

  acquire() {
    // Return an available object or null
  }

  release(obj) {
    // Return object to pool
  }
}

/*
 * SOLUTION:
 */
class ObjectPoolSolution {
  constructor(createFn, initialSize = 5) {
    this.createFn = createFn;
    this.available = [];
    this.inUse = new Set();

    // Pre-create objects
    for (let i = 0; i < initialSize; i++) {
      this.available.push(this.createFn());
    }
  }

  acquire() {
    let obj;
    if (this.available.length > 0) {
      obj = this.available.pop();
    } else {
      obj = this.createFn();
    }
    this.inUse.add(obj);
    return obj;
  }

  release(obj) {
    if (this.inUse.has(obj)) {
      this.inUse.delete(obj);
      this.available.push(obj);
    }
  }

  get stats() {
    return {
      available: this.available.length,
      inUse: this.inUse.size,
    };
  }
}

const pool = new ObjectPoolSolution(() => ({ x: 0, y: 0 }));
console.log('Initial:', pool.stats);

const obj1 = pool.acquire();
const obj2 = pool.acquire();
console.log('After acquiring 2:', pool.stats);

pool.release(obj1);
console.log('After releasing 1:', pool.stats);

/**
 * EXERCISE 12: Find the Leak
 * --------------------------
 * This function has a subtle memory leak. Find it.
 */

console.log('\n=== Exercise 12: Find the Leak ===');

const globalHandlers = [];

function registerHandler(callback) {
  const wrapper = {
    data: new Array(1000).fill('x'),
    callback: callback,
    id: Date.now(),
  };

  globalHandlers.push(wrapper);

  return wrapper.id;
}

// QUESTION: What's the memory leak here?
// YOUR ANSWER: ___

// How would you fix it?
// YOUR FIX: ___

/*
 * SOLUTION:
 * LEAK: globalHandlers array grows indefinitely. Every call to
 * registerHandler adds an object that's never removed.
 *
 * FIX: Provide an unregister function:
 */

function unregisterHandler(id) {
  const index = globalHandlers.findIndex((h) => h.id === id);
  if (index !== -1) {
    globalHandlers.splice(index, 1);
  }
}

// Or use WeakMap if callbacks should be GC'd with their owners

/**
 * EXERCISE 13: Memory-Conscious Data Processing
 * ----------------------------------------------
 * Process a large array without holding all results in memory.
 */

console.log('\n=== Exercise 13: Generator Processing ===');

// Process items one at a time using a generator
function* processLargeDataset(items) {
  // YOUR CODE HERE
  // Yield processed items one at a time
}

/*
 * SOLUTION:
 */
function* processLargeDatasetSolution(items) {
  for (const item of items) {
    // Process each item
    yield item * 2;
    // Previous item can be GC'd before next is processed
  }
}

const largeDataset = Array.from({ length: 10 }, (_, i) => i);
const processor = processLargeDatasetSolution(largeDataset);

console.log('Processing one at a time:');
for (const result of processor) {
  console.log('Result:', result);
  // Each iteration, only one result is in memory
}

/**
 * EXERCISE 14: WeakSet for Visited Tracking
 * -----------------------------------------
 * Track visited nodes without preventing GC.
 */

console.log('\n=== Exercise 14: WeakSet Tracking ===');

function processTree(root) {
  // Track visited nodes to avoid cycles
  // Use WeakSet so nodes can be GC'd when tree is discarded
  // YOUR CODE HERE
}

/*
 * SOLUTION:
 */
function processTreeSolution(root) {
  const visited = new WeakSet();

  function visit(node) {
    if (!node || visited.has(node)) return;

    visited.add(node);
    console.log('Visiting:', node.value);

    for (const child of node.children || []) {
      visit(child);
    }
  }

  visit(root);
}

const tree = {
  value: 'root',
  children: [
    { value: 'child1', children: [] },
    { value: 'child2', children: [{ value: 'grandchild', children: [] }] },
  ],
};

processTreeSolution(tree);

/**
 * EXERCISE 15: Memory Profiling Exercise
 * --------------------------------------
 * Write code to estimate memory usage of different structures.
 */

console.log('\n=== Exercise 15: Memory Estimation ===');

function estimateSize(label, createFn, count) {
  if (typeof process === 'undefined' || !process.memoryUsage) {
    console.log('Memory API not available');
    return;
  }

  // Force GC if available
  if (global.gc) global.gc();

  const before = process.memoryUsage().heapUsed;

  const items = [];
  for (let i = 0; i < count; i++) {
    items.push(createFn(i));
  }

  const after = process.memoryUsage().heapUsed;
  const perItem = (after - before) / count;

  console.log(`${label}: ~${Math.round(perItem)} bytes per item`);

  return items; // Return to prevent optimization
}

// Compare different data structures
// Run with: node --expose-gc exercises.js
estimateSize('Empty object', () => ({}), 10000);
estimateSize('Small object', (i) => ({ id: i, name: 'test' }), 10000);
estimateSize('Array', (i) => [i, i + 1, i + 2], 10000);
estimateSize('Map entry', (i) => new Map([[i, i]]), 10000);

console.log('\n=== Exercises Complete ===');
Exercises - JavaScript Tutorial | DeepML