Back to cheat sheets

Languages

JavaScript

A to-the-point review of the JavaScript an SDET gets asked about: types & coercion, var/let/const, scope, closures, this, prototypes & classes, the event loop, promises & async/await, and array methods — plus the Jest / Playwright testing stack. Short explanations, short examples.

01Types & Variables

JavaScript is dynamically typed: variables don’t have types, values do. There are 7 primitives and everything else is an object.

  • Primitives (immutable, copied by value): string, number, boolean, null, undefined, bigint, symbol.
  • Objects (arrays, functions, plain objects) are reference types — assigning copies the reference, so both names point at the same object.
  • typeof reports the type — with two classic quirks: typeof null === "object" (a historic bug) and typeof function(){} === "function".
let a = 1; let b = a; b = 9;          // a stays 1 (value copied)

let x = [1]; let y = x; y[0] = 9;     // x[0] is now 9 (same array)

typeof undefined  // "undefined"
typeof null       // "object"  (quirk)
NaN === NaN       // false — use Number.isNaN()
null vs undefined: undefined = a variable was declared but never assigned (the engine’s default); null = you deliberately set “no value”.

02var, let & const

KeywordScopeReassign?Hoisting
varfunctionyeshoisted, init undefined
letblockyeshoisted but in the TDZ
constblocknohoisted but in the TDZ
  • Default to const; use let only when you reassign; avoid var.
  • const stops reassignment, not mutationconst o = {}; o.x = 1 is fine.
  • TDZ (temporal dead zone): a let/const exists but can’t be read until its declaration runs — touching it early throws, unlike var which reads as undefined.
const cfg = { retries: 3 };
cfg.retries = 5;     // OK — mutating
cfg = {};            // TypeError — reassigning a const

03Scope, Hoisting & Closures

Hoisting: declarations are processed before code runs. function declarations are fully hoisted (callable above their definition); var is hoisted as undefined; let/const are hoisted into the TDZ.

A closure is a function that remembers the variables from the scope where it was created, even after that scope has returned. It’s the backbone of private state and the single most-asked JS concept.

function counter() {
  let n = 0;                 // captured by the closure
  return () => ++n;          // remembers n after counter() returns
}
const next = counter();
next(); next();              // 1, then 2

// classic interview bug — var is function-scoped:
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));  // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i));  // 0 1 2
Why let fixes the loop: let creates a fresh binding per iteration, so each callback closes over its own i; var shares one binding.

04this & Function Binding

this is decided by how a function is called, not where it’s defined — except arrow functions.

  • Method call obj.fn()this is obj.
  • Plain call fn()this is undefined in strict mode (else the global object).
  • Arrow function — has no own this; it inherits from the enclosing scope. Ideal for callbacks.
  • call/apply invoke with an explicit this; bind returns a new function permanently bound.
const user = {
  name: "Ada",
  greetLater() {
    setTimeout(() => console.log(this.name), 0);  // arrow keeps `this` = user
  }
};
user.greetLater();   // "Ada"

const loose = user.greetLater;
// loose();          // a plain `function` callback here would lose `this`

05Prototypes & Classes

JS inheritance is prototype-based: every object has a hidden link to a prototype object, and property lookups walk up that prototype chain until found or it hits null.

class (ES6) is syntactic sugar over prototypes — cleaner syntax, same mechanism.

class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
  speak() { return `${this.name} barks`; }   // override
}
new Dog("Rex").speak();   // "Rex barks"

// equivalent prototype lookup:
Object.getPrototypeOf(new Dog("Rex")) === Dog.prototype;  // true
Say this: classes don’t add a new inheritance model — they’re sugar over the prototype chain that already existed.

06Arrays, Objects & Modern Syntax

  • Immutable iterationmap, filter, reduce, find, some/every return new values without mutating the source.
  • Destructuring — pull fields out of objects/arrays in one line.
  • Spread / rest (...) — copy & merge, or gather arguments.
  • Optional chaining ?. and nullish coalescing ?? for safe access and defaults (?? only falls back on null/undefined, unlike ||).
const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
const admins = users.filter(u => u.role === "admin").map(u => u.id);

const { id, role = "guest" } = users[0];      // destructure + default
const merged = { ...users[0], active: true };  // spread copy

const city = order?.address?.city ?? "unknown";  // safe + fallback
?? vs ||: 0 || 5 is 5 (0 is falsy) but 0 ?? 5 is 0 — use ?? when 0/""/false are valid values.

07The Event Loop

JS is single-threaded. The event loop is how it does non-blocking work: synchronous code runs on the call stack; async callbacks wait in queues and run only when the stack is empty.

  • Call stack — runs synchronous code now.
  • Microtask queue — resolved promise callbacks (.then, await continuations). Drained fully after each task, before any macrotask.
  • Macrotask queuesetTimeout, I/O, events. One per loop turn.
console.log(1);
setTimeout(() => console.log(2), 0);   // macrotask
Promise.resolve().then(() => console.log(3)); // microtask
console.log(4);
// prints 1, 4, 3, 2  — microtasks beat the timer
Why 3 before 2: the promise callback is a microtask and runs before the next macrotask (the timer), even with a 0 ms delay.

08Promises & async / await

A Promise represents a future value: pending → fulfilled | rejected. async/await is syntax over promises that reads like synchronous code.

  • await pauses the async function until the promise settles, without blocking the thread.
  • Wrap await in try/catch to handle rejections (the equivalent of .catch).
  • Promise.all runs work concurrently and fails fast; Promise.allSettled waits for all regardless; Promise.race takes the first to settle.
  • Don’t await in a loop when calls are independent — fire them together with Promise.all.
async function loadUser(id) {
  try {
    const res = await fetch(`/users/${id}`);
    if (!res.ok) throw new Error(res.status);
    return await res.json();
  } catch (e) {
    console.error("load failed", e);
    throw e;
  }
}

// concurrent, not sequential:
const [a, b] = await Promise.all([loadUser(1), loadUser(2)]);

09Test Stack for SDETs

For a Playwright/SDET role the JS testing surface is small and worth naming:

  • Playwright Test — the runner you’ll use most: test/expect, fixtures, auto-waiting web-first assertions, parallel projects, trace viewer.
  • Jest / Vitest — unit testing: describe/it/expect, mocks & spies (jest.fn(), jest.mock()), snapshots.
  • Cypress — alternative E2E framework; in-browser, good DX, retry-able assertions.
  • TypeScript — most modern test suites are TS for type-safe page objects and fixtures.
import { test, expect } from '@playwright/test';

test('user can sign in', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('ada@test.dev');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Welcome')).toBeVisible(); // auto-retries
});

10Rapid-Fire Q&A

Reveal each answer to self-check, then test yourself with the quiz.

== vs ===?

=== compares value and type with no coercion (always prefer it); == coerces types first, giving surprises like 0 == "" and null == undefined being true.

var vs let vs const?

var is function-scoped and hoisted as undefined; let/const are block-scoped with a temporal dead zone; const can’t be reassigned (but objects can still mutate).

What is a closure?

A function that retains access to variables from the scope where it was created, even after that scope returns — the basis for private state.

How is `this` determined?

By the call site: obj.fn() → obj; plain fn() → undefined in strict mode; arrow functions have no own this and inherit it from the enclosing scope.

null vs undefined?

undefined = declared but unassigned (engine default); null = an intentional 'no value' you assign.

What is the event loop?

Single thread runs sync code on the call stack; when empty it drains all microtasks (promises) then takes one macrotask (setTimeout/IO) per turn.

Microtask vs macrotask?

Promise callbacks are microtasks and run before the next macrotask (timers, IO) — so a resolved promise beats setTimeout(…, 0).

Promise.all vs allSettled?

all runs concurrently and rejects on the first failure; allSettled waits for every promise and reports each outcome.

?? vs ||?

|| falls back on any falsy value (0, '', false); ?? falls back only on null/undefined — use ?? when 0/''/false are valid.

Are JS classes real classes?

No — syntactic sugar over the prototype chain; extends/super just wire up prototype links.

How do you avoid the classic loop-var bug?

Use let (fresh binding per iteration) instead of var, so each callback closes over its own value.

Shallow vs deep copy?

Spread/Object.assign copy one level (nested objects stay shared); deep copy needs structuredClone() or a recursive clone.