JavaScript Developer I Interview Questions

25 expert-curated JavaScript Developer I interview questions — covering ES6+, Promises, async/await, closures, the event loop, fetch API, LWC, and Jest testing.

JavaScript Developer I Interview Questions Content

var: Function-scoped (or global if outside a function). Hoisted to the top of its scope and initialised to undefined before code runs — you can reference a var before its declaration without a ReferenceError (you get undefined instead). Can be re-declared and re-assigned. let: Block-scoped (limited to the nearest {}). Hoisted but NOT initialised — accessing before declaration throws a ReferenceError (the Temporal Dead Zone). Cannot be re-declared in the same scope. Can be re-assigned. const: Block-scoped like let. Must be initialised at declaration. Cannot be re-assigned (the binding is constant), but the value it references can still be mutated (e.g., pushing to a const array is allowed). Best practice: use const by default; use let when re-assignment is needed; avoid var.
Arrow functions (const fn = (x) => x * 2;) are concise function expressions with key differences: (1) Lexical this: Arrow functions do not have their own this — they inherit this from the enclosing lexical scope. This is the most important difference. Regular functions bind this dynamically based on how they are called (call-site). (2) No arguments object: Arrow functions do not have an arguments object — use rest parameters (...args) instead. (3) Cannot be used as constructors: Cannot use new with an arrow function. (4) No prototype property. Lexical this makes arrow functions ideal for callbacks in class methods and event handlers where you want to preserve the class instance's this.
Destructuring extracts values from arrays or objects into variables. Array destructuring: const [a, b, c = 10] = [1, 2]; — c defaults to 10. Skip elements with commas: const [, second] = [1, 2];. Object destructuring: const { name, age = 25 } = person; — defaults for missing properties. Renaming: const { name: fullName } = person; — extracts name as fullName. Nested: const { address: { city } } = person;. Rest in destructuring: const { a, ...rest } = obj; — rest contains all remaining properties. In function parameters: function greet({ name, age = 18 }) {...} — destructure directly in the parameter list. Destructuring is extensively used in LWC to handle wire service results and event detail objects.
Both use the ... syntax but in opposite directions. Spread: Expands an iterable (array, string, object) into individual elements. Array spread: const merged = [...arr1, ...arr2]; — shallow copy and merge. Object spread: const updated = { ...original, name: 'New' }; — immutable update pattern (later keys overwrite earlier). Function call: Math.max(...numbers);. Rest: Collects multiple arguments into an array. Function parameters: function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }. Destructuring rest: const [first, ...remaining] = arr;. Key distinction: spread is used where values are consumed (right-hand side, function arguments); rest is used where values are collected (parameter lists, destructuring left-hand side).
Template literals use backticks and allow embedded expressions: `Hello, ${name}! You are ${age} years old.`. Support multi-line strings without \n. Expressions can be any valid JS (function calls, ternaries, arithmetic). Tagged templates: A function (tag) is placed before the template literal: tag`Hello ${name}`. The tag function receives: (1) An array of string segments between expressions. (2) The evaluated expression values as separate arguments. Example: function highlight(strings, ...values) { return strings.reduce((result, str, i) => result + str + (values[i] ? `<strong>${values[i]}</strong>` : ''), ''); } Tagged templates are used for: SQL/SOQL query builders that prevent injection, styled-components (CSS-in-JS), internationalisation libraries, and sanitising HTML.
A Promise represents the eventual completion or failure of an async operation. States: pending, fulfilled (resolved), rejected. Creating: new Promise((resolve, reject) => { /* async work */ });. Consuming: .then(result => ...) (fulfilled), .catch(err => ...) (rejected), .finally(() => ...) (always). Chaining: each .then() returns a new Promise — the chain is sequential. Promise.all(promises): Waits for all to resolve; rejects immediately if any rejects. Promise.race(promises): Resolves/rejects with the first to settle. Promise.allSettled(promises): Waits for all to settle regardless of outcome — returns array of {status, value/reason} objects. Promise.any(promises): Resolves with the first fulfilled; rejects only if all reject (AggregateError).
async functions always return a Promise. await pauses execution within an async function until the awaited Promise settles. Error handling: wrap in try/catch: try { const data = await fetchData(); } catch (err) { console.error(err); } finally { cleanup(); }. Uncaught rejections in async functions become rejected Promises — handle them at the call site or with .catch(). Sequential vs Parallel: Sequential: const a = await fetch1(); const b = await fetch2(); (fetch2 starts after fetch1 completes). Parallel: const [a, b] = await Promise.all([fetch1(), fetch2()]); (both start simultaneously — faster). In LWC, Apex method calls return Promises and are called with async/await in event handlers: async handleClick() { this.data = await getRecords(); }.
ES modules provide a native JavaScript module system. Named exports: export const PI = 3.14; export function add(a, b) { return a + b; } — imported with exact name: import { PI, add } from './utils.js';. Default export: One per module: export default class MyClass {...} — imported with any name: import MyClass from './myClass.js';. Renaming imports: import { add as sum } from './utils.js';. Namespace import: import * as utils from './utils.js';. Dynamic import: const module = await import('./utils.js'); — lazy-loads modules at runtime. In LWC, every JavaScript file is a module. Salesforce-scoped imports use special paths: import { LightningElement } from 'lwc';, import getAccount from '@salesforce/apex/AccountCtrl.getAccount';.
A closure is a function that retains access to its outer (lexical) scope's variables even after the outer function has returned. This happens because functions in JavaScript form a closure over the variables in scope at the time of their creation. IIFE (Immediately Invoked Function Expression): (function() { /* private scope */ })(); — creates a private scope in pre-ES6 code. Counter example: function makeCounter() { let count = 0; return () => ++count; } const counter = makeCounter(); — each call to counter() increments the private count variable. Module pattern: Using closures to create private state with public methods. Common pitfall: closures in loops — all callback functions share the same variable reference. Fix with let (block-scoped per iteration) or IIFE to capture the value. LWC class properties are not closures, but event handlers using arrow functions close over the class instance.
JavaScript uses prototypal inheritance. Every object has an internal [[Prototype]] link to another object. When a property is accessed, JS traverses the prototype chain until found or reaches null. Object.getPrototypeOf(obj) returns the prototype. Functions have a prototype property — objects created with new Fn() have Fn.prototype as their prototype. Class syntax is syntactic sugar over the prototype chain — it does not introduce a new inheritance model. class Animal { speak() {...} } class Dog extends Animal { speak() { super.speak(); } } — Dog.prototype inherits from Animal.prototype. extends sets up the prototype chain. super() must be called in a subclass constructor before accessing this. In LWC, all components extend LightningElement which provides the component lifecycle APIs.
JavaScript is single-threaded with an event loop. The call stack executes synchronous code. When an async operation completes, its callback is queued. Two queues: (1) Microtask queue (higher priority): Promise callbacks (.then/.catch), queueMicrotask(), MutationObserver. (2) Task queue (Macro-task queue) (lower priority): setTimeout, setInterval, I/O callbacks, UI events. Event loop rule: after each task completes, the event loop drains the entire microtask queue before picking the next task. Order example: console.log('1'); setTimeout(() => console.log('timeout'), 0); Promise.resolve().then(() => console.log('promise')); console.log('2'); Output: 1, 2, promise, timeout. The promise callback (microtask) runs before the setTimeout (task) even with 0ms delay.
DOM selection: document.querySelector('.my-class') (first match), document.querySelectorAll('div') (NodeList). Manipulation: element.textContent = 'text';, element.innerHTML = '<span>...</span>';, element.setAttribute('class', 'active');, element.classList.add('active');. Event listeners: element.addEventListener('click', handler, useCapture); Remove: element.removeEventListener('click', handler);. Event bubbling vs capturing: Bubbling (default, useCapture=false): event propagates from target up to document. Capturing (useCapture=true): event propagates from document down to target first. stopPropagation(): Stops further propagation. stopImmediatePropagation(): Also prevents other listeners on the same element. preventDefault(): Prevents the browser's default action (e.g., form submission, link navigation). In LWC, use this.template.querySelector() for shadow DOM access.
fetch(url, options) returns a Promise that resolves with a Response object. Key: fetch only rejects on network failure, not on HTTP error statuses (404, 500). Always check response.ok or response.status. Pattern: const response = await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token' }, body: JSON.stringify(data) }); if (!response.ok) throw new Error(`HTTP error: ${response.status}`); const json = await response.json();. Other body methods: response.text(), response.blob(), response.formData(). The Request object: new Request(url, options) — reusable request object. In LWC, Salesforce's locker service restricts direct fetch calls to same-origin or approved endpoints — use Named Credentials and Apex for cross-origin callouts.
@api properties: Public, reactive, and accessible by parent components and Lightning App Builder. When a parent changes an @api property, the child re-renders. Primitive @api properties are passed by value; objects/arrays are passed by reference (but the child should not mutate them — use a local copy). Methods decorated with @api can be called from parents via template refs. Private properties (no decorator, or @track for internal reactivity): Only accessible within the component. In modern LWC (API version 39+), all primitive properties are reactive by default — the component re-renders when they change. For nested object/array mutations, assign a new reference to trigger re-render: this.data = { ...this.data, name: 'New' }; or use @track for the property. @track forces reactivity tracking for deep object changes.
@wire connects a component to a data source (Apex method or wire adapter). The wire service calls the adapter whenever reactive parameters change. Syntax: @wire(adapterFunction, { param: '$reactiveProp' }) wiredData; The $ prefix means: when this.reactiveProp changes, re-invoke the wire adapter. The result can be assigned to a property (as an object with data and error properties) or to a function: @wire(getRecord, { recordId: '$recordId', fields }) wiredRecord({ data, error }) { if (data) this.record = data; }. Wire calls are lazy and cached by the LDS cache for platform adapters. A wire parameter without $ is treated as a static value. If a reactive parameter is initially undefined, the wire call is deferred until it has a value.
LWC lifecycle hooks in order: (1) constructor(): Component creation. Call super() first. Cannot access child elements or external DOM. (2) connectedCallback(): Component inserted into the DOM. Access to this.template for the component's own shadow root. Safe for data loading and event subscriptions. (3) render() (internal): The framework calls the template's render. (4) renderedCallback(): After every render (initial and re-renders). Can access the rendered DOM. Avoid state changes that cause re-renders (infinite loop risk). (5) disconnectedCallback(): Component removed from DOM. Clean up: unsubscribe from LMS channels, cancel timers. (6) errorCallback(error, stack): Catches errors thrown by child components. Acts as an error boundary. For parent-child relationships: parent constructor → parent connectedCallback → child constructor → child connectedCallback → child renderedCallback → parent renderedCallback.
Child dispatches: this.dispatchEvent(new CustomEvent('selectitem', { detail: { itemId: this.id, name: this.name }, bubbles: true, composed: false })); Event name convention: all lowercase, no hyphens. Parent handles: In HTML: <c-child onselectitem={handleSelect}></c-child>. In JS: handleSelect(event) { const { itemId, name } = event.detail; }. bubbles: If true, the event bubbles up through the DOM tree. composed: If true, the event crosses shadow boundaries (needed to propagate out of shadow DOM). For LWC within another LWC, typically bubbles: true, composed: false is sufficient for parent to catch the event. For communication across unrelated components, use Lightning Message Service instead of relying on event bubbling.
Parent components can call methods on child components using template refs. Child declares a public method: @api focusInput() { this.template.querySelector('input').focus(); }. Parent HTML: <c-my-input lwc:ref="inputCmp"></c-my-input>. Parent JS: this.refs.inputCmp.focusInput(); (using lwc:ref — available in newer API versions). Older pattern using querySelector: this.template.querySelector('c-my-input').focusInput();. Important rules: (1) @api methods cannot be called in the constructor (child not yet created). (2) Call in connectedCallback, renderedCallback, or event handlers. (3) Only call synchronously — do not await @api method calls. (4) Do not expose @api methods that allow parents to directly mutate child state — prefer event-driven patterns.
LWC Jest tests use @salesforce/sfdx-lwc-jest. Structure: import { createElement } from 'lwc'; import MyComponent from 'c/myComponent'; describe('c-my-component', () => { afterEach(() => { while (document.body.firstChild) document.body.removeChild(document.body.firstChild); }); it('renders correctly', () => { const el = createElement('c-my-component', { is: MyComponent }); document.body.appendChild(el); expect(el.shadowRoot.querySelector('h1').textContent).toBe('Hello'); }); });. Mocking @wire: Import registerApexTestWireAdapter from @salesforce/sfdx-lwc-jest. Mock the wire: const adapter = registerApexTestWireAdapter(getRecord); adapter.emit(mockData);. Mocking Apex: jest.mock('@salesforce/apex/AccountCtrl.getAccounts', () => jest.fn(), { virtual: true }); getAccounts.mockResolvedValue(mockData);. Test events by spying on dispatchEvent.
Higher-order array methods take callbacks and return new arrays (non-mutating): map(): Transforms every element: const doubled = nums.map(n => n * 2);. filter(): Returns elements passing the test: const evens = nums.filter(n => n % 2 === 0);. reduce(): Accumulates to a single value: const sum = nums.reduce((acc, n) => acc + n, 0);. find(): Returns first match (or undefined). findIndex(): Returns index of first match (or -1). some(): Returns true if any element passes. every(): Returns true if all pass. flat(): Flattens nested arrays. flatMap(): map then flat in one step. In LWC, these are used extensively for transforming data from wire service results into display-ready formats: this.options = records.map(r => ({ label: r.Name, value: r.Id }));
WeakMap: A Map where keys must be objects (not primitives). Keys are held weakly — if no other reference to the key object exists, the key-value pair is garbage collected automatically. Not iterable (no .forEach, .keys(), .size). Use case: storing private data associated with DOM elements or class instances without preventing garbage collection: const privateData = new WeakMap(); class MyClass { constructor() { privateData.set(this, { secret: 42 }); } }. WeakSet: A Set of objects held weakly. Not iterable. Use case: tracking which objects have been processed without preventing GC (e.g., marking visited nodes in a graph traversal). Regular Map and Set hold strong references — objects stored as keys/values will not be GC'd even if nothing else references them. WeakMap/WeakSet are useful for memory-sensitive caching.
Iterator protocol: An object is iterable if it has a [Symbol.iterator]() method returning an iterator with next(). next() returns {value, done}. Built-in iterables: arrays, strings, Maps, Sets. Used by for...of, spread, destructuring. Generator functions (function*): Return a Generator object (which is both an iterable and an iterator). yield pauses execution and returns a value; next() resumes. Example: function* range(start, end) { for (let i = start; i <= end; i++) yield i; }. Generators produce values lazily — useful for large sequences without creating arrays. yield* delegates to another iterable. Generators can also receive values via next(value). Async generators (async function*) work with for await...of for async data streams.
A Proxy wraps an object and intercepts fundamental operations (get, set, delete, etc.) via trap functions. Syntax: const proxy = new Proxy(target, handler);. Common traps: get(target, prop): Intercept property access. set(target, prop, value): Intercept property assignment. has(target, prop): Intercept the in operator. deleteProperty(target, prop): Intercept delete. Use cases: (1) Validation: Throw errors when invalid values are set on an object. (2) Logging/debugging: Log all property accesses. (3) Default values: Return a default when a property doesn't exist (avoid undefined). (4) Reactive systems: Vue.js 3's reactivity system is built on Proxy. (5) Immutable objects: Prevent writes in the set trap. LWC's internal reactivity for @track uses Proxy-like mechanisms under the hood.
=== (strict equality): Compares both value and type — no type coercion. 1 === '1' is false (number vs string). null === undefined is false. == (loose equality): Performs type coercion before comparison. Follows the Abstract Equality Comparison algorithm. Examples of counterintuitive results: 0 == '' is true (both coerce to 0 vs false, then number 0), null == undefined is true, false == '0' is true. Always use === to avoid coercion surprises. Similarly, !== vs !=. Special case: NaN === NaN is false — use Number.isNaN(value) to check for NaN. Object.is(a, b) handles NaN and +0/-0 correctly, unlike ===. In LWC and Apex, always use strict equality comparisons in JavaScript.