Irbisa · Cheatsheet

Advanced JavaScript

Type coercion, call, apply and bind, the new operator, Map, Set and WeakMap, iterators and generators, Proxy, garbage collection, output puzzles, writing Promise.all, curry and an event emitter

Middle Developer · 18 questions

  1. 01

    What is the difference between Map and a plain object, and between Set and an array?

    A Map takes keys of any type, keeps insertion order, knows its size and is iterable; a plain object turns every key into a string or symbol and suits records with known fields.

    const user = { id: 7 }; const plain = {};plain[user] = 'Ada'; // the key becomes the string "[object Object]"plain[2] = 'two';plain.b = 'b';…
  2. 02

    What are WeakMap, WeakSet and WeakRef, and when would you use them instead of Map and Set?

    WeakMap and WeakSet hold their keys weakly: an entry does not keep its key object alive and disappears once that object is collected.

    // derived data per state object: entries go away together with old statesconst totals = new WeakMap(); function getTotal(cart) {  if (!totals.has(cart)) {    console.log('computing');…
  3. 03

    What does each of these expressions evaluate to, and which coercion rules explain the results?

    console.log([] + {});console.log('5' - 2);console.log('5' + 2);console.log(true + 1);console.log(null + 1);console.log(undefined + 1);console.log([] == ![]);console.log('b' + 'a' + +'a' + 'a');

    They log [object Object], 3, 52, 2, 1, NaN, true and baNaNa.

  4. 04

    What do call, apply and bind do, and how would you write a polyfill for Function.prototype.bind?

    All three set this for a regular function.

    Function.prototype.myBind = function (thisArg, ...preset) {  const fn = this;  return function bound(...args) {    if (new.target) return new fn(...preset, ...args); // new ignores the bound this    return fn.apply(thisArg, [...preset, ...args]);  };…
  5. 05

    What does this code log, and why does this change between the calls?

    class Counter {  count = 0;  inc() {    return ++this.count;  }  incArrow = () => ++this.count;} const c = new Counter();const other = { count: 10, inc: c.inc, incArrow: c.incArrow };console.log(c.inc());console.log(other.inc());console.log(other.incArrow());console.log(c.inc.bind(other).call(c));const { inc } = c;inc();

    It logs 1, 11, 2 and 12, then throws TypeError: Cannot read properties of undefined (reading 'count').

  6. 06

    What does the new operator do step by step, and how would you reimplement it as a plain function?

    new Fn(args) creates an empty object whose prototype is Fn.prototype, runs Fn with this set to that object and returns the object, unless Fn itself returns an object, which then replaces it.

    function myNew(Constructor, ...args) {  const obj = Object.create(Constructor.prototype); // 1. link the prototype  const result = Constructor.apply(obj, args); // 2. run the body with this = obj  const isObject = result !== null && (typeof result === 'object' || typeof result === 'function');  return isObject ? result : obj; // 3. an object returned by the body wins}…
  7. 07

    What do Object.freeze, Object.seal and property descriptors do, and why is a frozen object not deeply immutable?

    Every property has a descriptor: a value and a writable flag (or a getter and setter instead), plus enumerable and configurable.

    const config = Object.freeze({ api: '/v1', retry: { count: 3 } });config.retry.count = 5; // allowed: freeze is shallowtry {  config.api = '/v2'; // modules and classes are strict, so this throws} catch (error) {  console.log(error.message); // Cannot assign to read only property 'api' of object '#<Object>'…
  8. 08

    What makes an object iterable, how does Symbol.iterator work, and how does for...of differ from for...in?

    An object is iterable when it has a [Symbol.iterator]() method that returns an iterator: an object whose next() returns { value, done }.

    class Range {  constructor(from, to) {    this.from = from;    this.to = to;  }…
  9. 09

    What are generator functions, and where are they still useful in frontend code?

    A generator function (function*) returns an iterator instead of running its body; each next() runs the body up to the next yield and pauses there with all local state intact.

    function* idGenerator(prefix) {  let n = 1;  while (true) yield `${prefix}-${n++}`; // infinite, but lazy: runs only on next()} const ids = idGenerator('row');…
  10. 10

    What does this promise chain log, and which then or catch handles each error?

    Promise.resolve(1)  .then((x) => {    throw new Error(`boom ${x}`);  })  .then((x) => console.log('a', x))  .catch((err) => err.message.toUpperCase())  .then(    (x) => {      console.log('b', x);      throw new Error('again');    },    (err) => console.log('c', err.message),  )  .finally(() => console.log('d'))  .catch((err) => console.log('e', err.message));

    It logs b BOOM 1, then d, then e again.

  11. 11

    What is currying, and how would you implement a curry function that supports both sum(1)(2)(3) and sum(1, 2)(3)?

    Currying turns a function of several arguments into a chain of calls that collects them until there are enough.

    function curry(fn) {  return function curried(...args) {    if (args.length >= fn.length) return fn.apply(this, args);    return (...more) => curried.apply(this, [...args, ...more]);  };}…
  12. 12

    How would you write a memoize function, and how do you stop its cache from growing forever?

    Wrap the function, build a key from its arguments, return the cached result when the key is known, and otherwise compute it and store it.

    function memoize(fn, { maxSize = 100, key = (...args) => JSON.stringify(args) } = {}) {  const cache = new Map(); // a Map remembers insertion order: the first key is the oldest   return function (...args) {    const k = key(...args);    if (cache.has(k)) {…
  13. 13

    How would you implement an EventEmitter with on, off, emit and once?

    Keep a Map from event name to a list of listeners: on appends, off removes, emit calls every listener with the arguments, and once registers a wrapper that removes itself before calling the real listener.

    class EventEmitter {  #listeners = new Map(); // event name -> array of listeners   on(event, listener) {    this.#listeners.set(event, [...(this.#listeners.get(event) ?? []), listener]);    return () => this.off(event, listener); // handy as a useEffect cleanup…
  14. 14

    How would you implement Promise.all from scratch, keeping the result order and rejecting on the first error?

    Return a new promise, wrap each input in Promise.resolve, store each value at its own index and count the fulfilled ones: resolve when the counter reaches the total, reject as soon as any input rejects.

    function promiseAll(iterable) {  return new Promise((resolve, reject) => {    const items = Array.from(iterable);    const results = new Array(items.length);    let pending = items.length;    if (pending === 0) return resolve(results);…
  15. 15

    How would you write retry(fn, attempts) that retries a failing async call with exponential backoff?

    Call fn in a loop inside try/catch; on an error worth retrying, wait baseDelay * 2 ** attempt milliseconds with random jitter and try again, and after the last attempt rethrow the error.

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function retry(fn, { attempts = 3, baseDelay = 200, maxDelay = 5000, shouldRetry = () => true } = {}) {  for (let attempt = 1; ; attempt++) {    try {      return await fn(attempt); // without await, a rejection would skip this catch…
  16. 16

    How would you run a list of async tasks with at most N in flight at once (a promise pool) and keep the results in order?

    Start N workers that share one cursor into the task list: each worker takes the next index, awaits that task, stores the result at the same index and takes the next one until the list runs out.

    async function runPool(tasks, limit) {  const results = new Array(tasks.length);  let next = 0; // shared cursor: safe, because next++ runs without an await in between   async function worker() {    while (next < tasks.length) {…
  17. 17

    What is a Proxy, how do libraries like Immer, MobX and Vue use it, and what does it cost?

    A Proxy wraps an object and sends operations on it (reading, writing, deleting, in, listing keys) through trap functions you write, with Reflect forwarding the default behaviour.

    // a tiny Vue-style reactive object: record reads, react to writesfunction reactive(target, onChange) {  return new Proxy(target, {    get(obj, key, receiver) {      console.log('read', String(key)); // Vue's track(): remember who depends on key      return Reflect.get(obj, key, receiver);…
  18. 18

    How does garbage collection work in a JavaScript engine like V8, and how can a closure keep a large object alive by accident?

    The engine frees objects that can no longer be reached from the roots (globals, the call stack, live handles), not objects that are merely unused.

    // run with: node --expose-gc gc.jsconst handlers = []; function setup() {  const report = new Array(1_000_000).fill(0); // about 8 MB  const total = () => report.length; // the only closure that reads report…