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.
typeofreports the type — with two classic quirks:typeof null === "object"(a historic bug) andtypeof 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()undefined = a variable was declared but never assigned (the engine’s default); null = you deliberately set “no value”.02var, let & const
| Keyword | Scope | Reassign? | Hoisting |
|---|---|---|---|
| var | function | yes | hoisted, init undefined |
| let | block | yes | hoisted but in the TDZ |
| const | block | no | hoisted but in the TDZ |
- Default to
const; useletonly when you reassign; avoidvar. conststops reassignment, not mutation —const o = {}; o.x = 1is fine.- TDZ (temporal dead zone): a
let/constexists but can’t be read until its declaration runs — touching it early throws, unlikevarwhich reads asundefined.
const cfg = { retries: 3 };
cfg.retries = 5; // OK — mutating
cfg = {}; // TypeError — reassigning a const03Scope, 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 2let 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()—thisisobj. - Plain call
fn()—thisisundefinedin strict mode (else the global object). - Arrow function — has no own
this; it inherits from the enclosing scope. Ideal for callbacks. call/applyinvoke with an explicitthis;bindreturns 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; // true06Arrays, Objects & Modern Syntax
- Immutable iteration —
map,filter,reduce,find,some/everyreturn 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 onnull/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,awaitcontinuations). Drained fully after each task, before any macrotask. - Macrotask queue —
setTimeout, 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 timer08Promises & async / await
A Promise represents a future value: pending → fulfilled | rejected. async/await is syntax over promises that reads like synchronous code.
awaitpauses the async function until the promise settles, without blocking the thread.- Wrap
awaitintry/catchto handle rejections (the equivalent of.catch). Promise.allruns work concurrently and fails fast;Promise.allSettledwaits for all regardless;Promise.racetakes the first to settle.- Don’t
awaitin a loop when calls are independent — fire them together withPromise.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.