Top JavaScript interview questions and answers for Frontend Engineers (updated for 2026)
MDX
10,081
134 commits
updated Aug 16, 2026
Updated for 2026! This repository contains a few hundred curated JavaScript interview questions with high quality answers for acing your Front End Engineer interviews, brought to you by GreatFrontEnd.
Looking for more? Read our blog post 50+ must-know JavaScript interview questions and answers compiled from ex-FAANG interviewers.
[!NOTE] This repository is a public mirror. GreatFrontEnd's main repository is the source of truth for question content; use this repository's Issues to report problems instead of opening content pull requests.
The questions are divided into three sections:
This list contains the top essential questions that are frequently-asked during Front End Engineer interviews. Concise versions of the answers are presented here with links to elaborate versions for further reading. This section is great if you need a quick refresher of the most important concepts.
This list contains a longer list of important JavaScript questions. Not all of them will be asked during Front End Engineer interviews but they provide a well-rounded review of JavaScript concepts ranging from beginner to advanced topics.
"Hoisting" is informal shorthand for how JavaScript creates and initializes bindings while it instantiates a scope, before evaluating that scope's statements. The engine does not move source text.
var): The binding is created and initialized to undefined before statements run. Its assignment still happens at the declaration's source location.let and const): The binding is created but remains uninitialized in the temporal dead zone (TDZ) until evaluation reaches the declaration. Accessing it earlier throws ReferenceError.var): The var binding initially contains undefined; the function is created only when the expression is evaluated.function): The binding is initialized with the function during scope setup, so it can be called before its declaration appears in source order.class): The binding exists during scope setup but remains in the TDZ until the class declaration is evaluated.import): Imported bindings are linked before module evaluation. Dependencies normally evaluate before the importing module's body, although cycles can expose an uninitialized binding.The following behavior summarizes the result of accessing the variables before they are declared.
| Declaration | Accessing before declaration |
|---|---|
var foo | undefined |
let foo | ReferenceError |
const foo | ReferenceError |
class Foo | ReferenceError |
var foo = function() { ... } | undefined |
function foo() { ... } | Normal |
import | Normal, except some cycles |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
let, var or const?In JavaScript, let, var, and const are all keywords used to declare variables, but they differ significantly in terms of scope, initialization rules, whether they can be redeclared or reassigned, and the behavior when they are accessed before declaration:
| Behavior | var | let | const |
|---|---|---|---|
| Scope | Function or Global | Block | Block |
| Initialization | Optional | Optional | Required |
| Redeclaration | Yes | No | No |
| Reassignment | Yes | Yes | No |
| Accessing before declaration | undefined | ReferenceError | ReferenceError |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
== and === in JavaScript?== is the abstract equality operator while === is the strict equality operator. == performs type coercion before comparing, following the Abstract Equality Comparison algorithm defined in the ECMAScript specification. === does not perform coercion and returns false whenever the operand types differ. === is generally preferred in application code because it eliminates a class of bugs caused by unexpected coercion. The most common exception is x == null, which checks for both null and undefined in a single comparison.
| Operator | == | === |
|---|---|---|
| Name | Loose (abstract) equality operator | Strict equality operator |
| Type coercion | Yes — per the Abstract Equality Comparison algorithm | No |
| Comparison behavior | Types may be coerced before the value comparison | Types are compared first |
Don't confuse
=with==and===.=is the assignment operator — it sets a variable's value (x = 5) and does not compare anything.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
The event loop is the host runtime's scheduling mechanism for coordinating JavaScript jobs, asynchronous operations, and—in browsers—rendering. A simplified browser turn works like this:
queueMicrotask() callbacks, and other microtasks, including microtasks added while the checkpoint is running.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Event delegation is a technique in JavaScript where a single event listener is attached to a parent element instead of attaching event listeners to multiple child elements. When an event occurs on a child element, the event bubbles up the DOM tree, and the parent element's event listener handles the event based on the target element.
Event delegation provides the following benefits:
However, do note that:
focus/blur and mouseenter/mouseleave need capture-phase handling or bubbling alternatives such as focusin/focusout and mouseover/mouseout. Element scroll and resize generally need direct listeners.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
this works in JavaScriptThere's no simple explanation for this; it is one of the most confusing concepts in JavaScript because its behavior differs from many other programming languages. The one-liner explanation of the this keyword is that it is a dynamic reference to the context in which a function is executed.
A longer explanation is that this follows these rules:
new keyword is used when calling the function, meaning the function was used as a function constructor, the this inside the function is the newly-created object instance.this is used in a class constructor, the this inside the constructor is the newly-created object instance.apply(), call(), or bind() is used to call/create a function, this inside the function is the object that is passed in as the argument.obj.method()) — this is the object that the function is a property of.this is the global object. In the browser, the global object is the window object. If in strict mode ('use strict';), this will be undefined instead of the global object.this value.this value of its surrounding scope at the time it is created.For an in-depth explanation, do check out Arnav Aggrawal's article on Medium.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
sessionStorage and localStorage in browsersCookies, localStorage, and sessionStorage all store data in the browser, but they differ in lifetime, scope, server interaction, and security controls. localStorage and sessionStorage implement the Web Storage API.
HttpOnly, Secure, and SameSite.localStorage: Origin-scoped string storage that persists until it is cleared and is shared by same-origin tabs and windows.sessionStorage: Origin- and tab-scoped string storage for a page session. It survives reloads but is normally cleared when the tab or window closes.Here's a table summarizing the 3 client storage mechanisms.
| Property | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| Initiator | Client or server. Server can use Set-Cookie header | Client | Client |
| Lifespan | As specified | Until deleted | Until tab is closed |
| Persistent across browser sessions | If a future expiry date is set | Yes | No |
| Sent to server with matching HTTP requests | Yes, via the Cookie header | No | No |
| Typical storage limit | About 4 KB per cookie | Browser-dependent quota, commonly several MiB per origin | Browser-dependent quota, commonly several MiB per origin |
| Access | Across windows/tabs | Across windows/tabs | Same tab |
| Security | JavaScript cannot access HttpOnly cookies | None | None |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
<script>, <script async> and <script defer>All of these ways (<script>, <script async>, and <script defer>) are used to load and execute JavaScript files in an HTML document, but they differ in how the browser handles loading and execution of the script:
<script> is the default way of including JavaScript. The browser blocks HTML parsing while the script is being downloaded and executed. The browser will not continue rendering the page until the script has finished executing.<script async> downloads the script asynchronously, in parallel with parsing the HTML. Executes the script as soon as it is available, potentially interrupting the HTML parsing. Multiple <script async> tags do not wait for each other and execute in no particular order.<script defer> downloads the script asynchronously, in parallel with parsing the HTML. However, the execution of the script is deferred until HTML parsing is complete, in the order they appear in the HTML.Here's a table summarizing the 4 ways of loading <script>s in an HTML document. Modern apps almost always use modules, which deserve their own row.
| Feature | <script> | <script async> | <script defer> | <script type="module"> |
|---|---|---|---|---|
| Parsing behavior | Blocks HTML parsing | Downloads in parallel; execution still blocks parsing | Downloads in parallel; execution deferred until after parsing | Downloads in parallel; execution deferred until after parsing |
| Execution order | In order of appearance | Not guaranteed | In order of appearance | In order of appearance, with each script's import dependencies resolved first |
| DOM state at execution | Only earlier markup is parsed | Depends on download timing | Document parsing is complete | Document parsing is complete |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
null, undefined or undeclared?| Trait | null | undefined | Undeclared |
|---|---|---|---|
| Meaning | Explicitly set by the developer to indicate that a variable has no value | Variable has been declared but not assigned a value | Variable has not been declared at all |
Type (via typeof operator) | 'object' | 'undefined' | 'undefined' |
| Direct access/comparison | Can compare with value === null | Can compare with value === undefined | Direct access throws ReferenceError; typeof undeclaredName returns 'undefined' |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
.call and .apply in JavaScript?.call and .apply are both used to invoke functions with a specific this context and arguments. The primary difference lies in how they accept arguments:
.call(thisArg, arg1, arg2, ...): Takes arguments individually..apply(thisArg, [argsArray]): Takes arguments as an array.Assuming we have a function add, the function can be invoked using .call and .apply in the following manner:
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Function.prototype.bind in JavaScriptFunction.prototype.bind is a method in JavaScript that allows you to create a new function with a specific this value and optional initial arguments. Its primary purpose is to:
this value to preserve context: The primary purpose of bind is to bind the this value of a function to a specific object. When you call func.bind(thisArg), it creates a new function with the same body as func, but with this permanently bound to thisArg.bind also allows you to pre-specify arguments for the new function. Any arguments passed to bind after thisArg will be prepended to the arguments list when the new function is called.bind allows you to borrow methods from one object and apply them to another object, even if they were not originally designed to work with that object.The bind method is particularly useful in scenarios where you need to ensure that a function is called with a specific this context, such as in event handlers, callbacks, or method borrowing.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
The main advantage of using an arrow function as a method inside a constructor is that the value of this gets set at the time of the function creation and can't change after that. When the constructor is used to create a new object, this will always refer to that object.
For example, let's say we have a Person constructor that takes a first name as an argument and has two methods to console.log() that name, one as a regular function and one as an arrow function:
const Person = function (name) {
this.firstName = name;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// The regular function can have its `this` value changed, but the arrow function cannot
john.sayName1.call(dave); // Dave (because `this` is now the dave object)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (because `this` is now the dave object)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (because `this` is now the dave object)
john.sayName2.bind(dave)(); // John
const detachedRegularMethod = john.sayName1;
try {
detachedRegularMethod();
} catch (error) {
console.log(error.name); // TypeError in strict mode because `this` is undefined
}
const detachedArrowMethod = john.sayName2;
detachedArrowMethod(); // John
The main takeaway is that a regular function receives this from its call site, while an arrow captures it from the constructor call. The tradeoff is that this arrow function is created separately for every instance instead of being shared through the prototype, and callers cannot deliberately rebind it.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Prototypal inheritance is property lookup by delegation. An object's internal [[Prototype]] is another object or null; when an own property is missing, lookup continues along that chain. Inspect it with Object.getPrototypeOf() and create a deliberate link with Object.create() or class / extends. Avoid the legacy __proto__ accessor in application code, and avoid changing prototypes of existing objects in hot code because it can invalidate engine optimizations.
When a property is accessed on an object and if the property is not found on that object, the JavaScript engine looks at the object's __proto__, and the __proto__'s __proto__ and so on, until it finds the property defined on one of the __proto__s or until it reaches the end of the prototype chain.
This behavior simulates classical inheritance, but it is really more of delegation than inheritance.
Here's an example of prototypal inheritance:
// Parent object constructor.
function Animal(name) {
this.name = name;
}
// Add a method to the parent object's prototype.
Animal.prototype.makeSound = function () {
console.log('The ' + this.constructor.name + ' makes a sound.');
};
// Child object constructor.
function Dog(name) {
Animal.call(this, name); // Call the parent constructor.
}
// Set the child object's prototype to be the parent's prototype.
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// Add a method to the child object's prototype.
Dog.prototype.bark = function () {
console.log('Woof!');
};
// Create a new instance of Dog.
const bolt = new Dog('Bolt');
// Call methods on the child object.
console.log(bolt.name); // "Bolt"
bolt.makeSound(); // "The Dog makes a sound."
bolt.bark(); // "Woof!"
Things to note are:
.makeSound is not defined on Dog, so the JavaScript engine goes up the prototype chain and finds .makeSound on the inherited Animal.class and extends are the preferred way to set up inheritance; they are syntactic sugar over this prototype mechanism.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
function Person(){}, const person = Person(), and const person = new Person() in JavaScript?function Person(){}: A function declaration in JavaScript. It can be used as a regular function or as a constructor.const person = Person(): Calls Person as a regular function, not a constructor. If Person is intended to be a constructor, this will lead to unexpected behavior.const person = new Person(): Creates a new instance of Person, correctly using the constructor function to initialize the new object.| Aspect | function Person(){} | const person = Person() | const person = new Person() |
|---|---|---|---|
| Type | Function declaration | Function call | Constructor call |
| Usage | Defines a function | Invokes Person as a regular function | Creates a new instance of Person |
| Instance Creation | No instance created | No instance created | New instance created |
| Common Mistake | N/A | Misusing as constructor leading to undefined | None (when used correctly) |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
foo between function foo() {} and var foo = function() {} in JavaScriptfunction foo() {} is a function declaration while var foo = function() {} is a function expression. The key difference is that function declarations have their bodies hoisted but the bodies of function expressions are not (they have the same hoisting behavior as var-declared variables).
If you try to invoke a function expression before it is declared, you will get an Uncaught TypeError: XXX is not a function error.
Function declarations can be called in the enclosing scope even before they are declared.
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
Function expressions if called before they are declared will result in an error.
foo(); // Uncaught TypeError: foo is not a function
var foo = function () {
console.log('FOOOOO');
};
Another key difference is in the scope of the function name. Function expressions can be named by defining a name after the function keyword and before the parentheses. However, when using named function expressions, the function name is only accessible within the function itself. Trying to access it outside will result in a ReferenceError.
const myFunc = function namedFunc() {
console.log(namedFunc); // Works
};
myFunc(); // Runs the function and logs the function reference
console.log(namedFunc); // ReferenceError: namedFunc is not defined
Note: The examples use var due to legacy reasons. Function expressions can be defined using let and const, and the key difference is in the hoisting behavior of those keywords.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
An anonymous function in JavaScript is a function that does not have any name associated with it. They are typically used as arguments to other functions or assigned to variables.
const arr = [-1, 0, 5, 6];
// The filter method is passed an anonymous function.
arr.filter((x) => x > 1); // [5, 6]
They are often used as arguments to other functions, known as higher-order functions, which can take functions as input and return a function as output. Anonymous functions can access variables from the outer scope, a concept known as closures, allowing them to "close over" and remember the environment in which they were created.
// Encapsulating Code
(function () {
// Some code here.
})();
// Callbacks
setTimeout(function () {
console.log('Hello world!');
}, 1000);
// Functional programming constructs
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Creating objects in JavaScript offers several methods:
{}): Simplest and most popular approach. Define key-value pairs within curly braces.Object() constructor: Use new Object() with dot notation to add properties.Object.create(): Create new objects using existing objects as prototypes, inheriting properties and methods.new.class and constructor keywords.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In the book "You Don't Know JS" (YDKJS) by Kyle Simpson, a closure is defined as follows:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope
In simple terms, functions have access to variables that were in their scope at the time of their creation. This is what we call the function's lexical scope. A closure is a function that retains access to these variables even after the outer function has finished executing. It is as if the function has a memory of its original environment.
function outerFunction() {
const outerVar = 'I am outside of innerFunction';
function innerFunction() {
console.log(outerVar); // `innerFunction` can still access `outerVar`.
}
return innerFunction;
}
const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`.
inner(); // "I am outside of innerFunction"
// Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`.
Key points to remember:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
A higher-order function is any function that takes one or more functions as arguments, which it uses to operate on some data, and/or returns a function as a result.
Higher-order functions are meant to abstract some operation that is performed repeatedly. The classic example of this is Array.prototype.map(), which takes an array and a function as arguments. Array.prototype.map() then uses this function to transform each item in the array, returning a new array with the transformed data. Other popular examples in JavaScript are Array.prototype.forEach(), Array.prototype.filter(), and Array.prototype.reduce(). A higher-order function doesn't just need to be manipulating arrays as there are many use cases for returning a function from another function. Function.prototype.bind() is an example that returns another function.
Imagine a scenario where we have an array of names that we need to transform to uppercase. The imperative way will be as such:
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
Using Array.prototype.map(transformerFn) makes the code shorter and more declarative.
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
return names.map((name) => name.toUpperCase());
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
ES2015 introduces a new way of creating classes, which provides a more intuitive and concise way to define and work with objects and inheritance compared to the ES5 function constructor syntax. Here's an example of each:
// ES5 function constructor
function Person(name) {
this.name = name;
}
// ES2015 Class
class Person {
constructor(name) {
this.name = name;
}
}
For simple constructors, they look pretty similar. The main difference in the constructor comes when using inheritance. If we want to create a Student class that subclasses Person and adds a studentId field, this is what we have to do.
// ES5 inheritance
// Superclass
function Person1(name) {
this.name = name;
}
// Subclass
function Student1(name, studentId) {
// Call constructor of superclass to initialize superclass-derived members.
Person1.call(this, name);
// Initialize subclass's own members.
this.studentId = studentId;
}
Student1.prototype = Object.create(Person1.prototype);
Student1.prototype.constructor = Student1;
const student1 = new Student1('John', 1234);
console.log(student1.name, student1.studentId); // "John" 1234
// ES2015 inheritance
// Superclass
class Person2 {
constructor(name) {
this.name = name;
}
}
// Subclass
class Student2 extends Person2 {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
const student2 = new Student2('Alice', 5678);
console.log(student2.name, student2.studentId); // "Alice" 5678
It's much more verbose to use inheritance in ES5, and the ES2015 version is easier to understand and remember.
Comparison of ES5 function constructors vs ES2015 classes
| Feature | ES5 Function Constructor | ES2015 Class |
|---|---|---|
| Syntax | Uses function constructors and prototypes | Uses class keyword |
| Constructor | Function with properties assigned using this | constructor method inside the class |
| Method Definition | Defined on the prototype | Defined inside the class body |
| Static Methods | Added directly to the constructor function | Defined using the static keyword |
| Inheritance | Uses Object.create() and manually sets prototype chain | Uses extends keyword and super |
| Readability | Less intuitive and more verbose | More concise and intuitive |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Event bubbling is a DOM event propagation mechanism where an event (e.g. a click) starts at the target element and bubbles up to the root of the document. This allows ancestor elements to also respond to the event.
Event bubbling is essential for event delegation, where a single event handler manages events for multiple child elements, enhancing performance and code simplicity. While convenient, failing to manage event propagation properly can lead to unintended behavior, such as multiple handlers firing for a single event.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Event capturing is a lesser-used counterpart to event bubbling in the DOM event propagation mechanism. During capture, the event travels along its event path from ancestors toward the target, and capture listeners run in that order.
Event capturing is used less often than event bubbling, but it is useful when an ancestor needs to observe an event before it reaches the target. DOM events still travel through the capture phase; what defaults to false is the capture option when registering a listener with addEventListener().
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
mouseenter and mouseover event in JavaScript and browsers?The main difference lies in the bubbling behavior of mouseenter and mouseover events. mouseenter does not bubble while mouseover bubbles.
mouseenter events do not bubble. The mouseenter event is triggered only when the mouse pointer enters the element itself, not its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseenter event will not be triggered on the parent element again; it is only triggered once upon entry of the parent element, without regard for its contents. If both parent and child have mouseenter listeners attached and the mouse pointer moves from the parent element to the child element, mouseenter will only fire for the child.
mouseover events bubble up the DOM tree. The mouseover event is triggered when the mouse pointer enters the element or one of its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseover event will be triggered on the parent element again as well. If the parent element has multiple child elements, this can result in multiple event callbacks fired. If there are child elements, and the mouse pointer moves from the parent element to the child element, mouseover will fire for both the parent and the child.
| Property | mouseenter | mouseover |
|---|---|---|
| Bubbling | No | Yes |
| Trigger | Only when entering itself | When entering itself and when entering descendants |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
'use strict'; (strict mode) in JavaScript for?'use strict' is a statement used to enable strict mode to entire scripts or individual functions. Strict mode is a way to opt into a restricted variant of JavaScript.
Advantages
this === undefined instead of coercing it to the global object.Disadvantages
with, arguments.callee, and access to function.caller are unavailable.ES modules and class bodies are already strict. Use the directive for legacy scripts or functions that are otherwise in sloppy mode; do not add a redundant directive merely to code that is already an ES module.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Synchronous code runs to completion on the current call stack before later statements can run. Asynchronous APIs arrange for a result to be handled later through a callback, promise, or event, allowing the current stack to finish while the host waits for I/O or a timer. Asynchronous does not mean “runs on another thread”: an async function runs synchronously until its first suspension point, and CPU-heavy JavaScript still blocks its thread.
function sum(a, b) {
console.log('Inside sum function');
return a + b;
}
const result = sum(2, 3); // The program waits for sum() to complete before assigning the result
console.log('Result: ', result); // Output: 5
Asynchronous APIs commonly expose callbacks, promises, or events. Once the operation can make progress, its continuation is scheduled according to the host's event loop. This works especially well for I/O such as network and database requests; CPU-intensive work must instead be split up or moved to a worker to keep a browser UI responsive.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data); // Calling the callback function with data
}, 2000); // Simulating a 2-second delay
}
console.log('Fetching data...');
fetchData((data) => {
console.log(data); // Output: { name: 'John', age: 30 } (after 2 seconds)
});
console.log('Call made to fetch data'); // This will print before the data is fetched
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Promises standardize one eventual outcome and make sequential, parallel, and error flows composable with .then(), async/await, and combinators such as Promise.all(). They avoid many callback-contract ambiguities, but they do not cancel work, represent repeated events, or guarantee settlement. A Promise chain can still become unreadable or leak an unhandled rejection when callers forget to return or await it.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
AJAX (Asynchronous JavaScript and XML) facilitates asynchronous communication between the client and server, enabling dynamic updates to web pages without reloading. It uses techniques like XMLHttpRequest or the fetch() API to send and receive data in the background. In modern web applications, the fetch() API is more commonly used to implement AJAX.
Using XMLHttpRequest
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('Request failed: ' + xhr.status);
}
}
};
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.send();
Using fetch()
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error('Fetch error:', error));
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
AJAX (Asynchronous JavaScript and XML) is a technique in JavaScript that allows web pages to send and retrieve data asynchronously from servers without refreshing or reloading the entire page.
Advantages
Disadvantages
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
XMLHttpRequest and fetch() in JavaScript and browsers?XMLHttpRequest (XHR) and fetch() API are both used for asynchronous HTTP requests in JavaScript (AJAX). fetch() offers a cleaner syntax, promise-based approach, and more modern feature set compared to XHR. However, there are some differences:
XMLHttpRequest uses event callbacks, while fetch() utilizes promise chaining.Headers, Request, and Response abstractions and integrates with streams.response.ok or response.status.cache request option.fetch() requires an AbortController for cancelation, while XMLHttpRequest provides an abort() method.These days fetch() is preferred for its cleaner syntax and modern features.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
AbortController in JavaScript?Create an AbortController, pass its signal to fetch(), and call controller.abort() when the result is no longer needed. fetch() and response-body consumption reject when aborted. Treat cancellation as an expected control-flow outcome, clean up any related timers or listeners, and create a new controller for the next operation because an aborted signal stays aborted.
Aborting releases the client from waiting and may cancel network activity, but it does not guarantee that the server stops or rolls back work already started. Make important writes idempotent or provide an application-level cancellation protocol.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Polyfills implement a missing JavaScript or Web API in environments that do not provide it. Choose them from an explicit support matrix and feature tests, load only the required modules, and prefer maintained implementations because matching specification edge cases is difficult. A transpiler rewrites syntax; it does not by itself add runtime objects such as Promise, URL, or new array methods.
They can be implemented manually or included through libraries and are often used in conjunction with feature detection.
Common use cases include:
Array.prototype.includes(), Object.assign(), etc.fetch(), Promise, IntersectionObserver, etc. Modern browsers support these now, but for a long time they had to be polyfilled.A commonly used polyfill library is:
core-js: A modular standard library for JavaScript which includes polyfills for a wide range of ECMAScript features.
import 'core-js/actual/array/flat-map'; // With this, Array.prototype.flatMap is available to be used.
[1, 2].flatMap((it) => [it, it]); // => [1, 1, 2, 2]
Prefer installing maintained polyfills as project dependencies and bundling them with your application instead of loading executable code from a third-party polyfill service at runtime.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Extending a built-in/native JavaScript object means adding properties/functions to its prototype. While this may seem like a good idea at first, it is dangerous in practice. Imagine your code uses two libraries that both extend the Array.prototype by adding the same contains method; the implementations will overwrite each other and your code will have unpredictable behavior if these two methods do not work the same way.
The only time you may want to extend a native object is when you want to create a polyfill, providing your own implementation for a method that is part of the JavaScript specification but might not exist in the user's browser due to it being an older browser.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
JavaScript that is executed in the browser has access to the global scope (the window object). In general it's a good software engineering practice to not pollute the global namespace unless you are working on a feature that truly needs to be global – it is needed by the entire page. Several reasons to avoid touching the global scope:
Follow these best practices to avoid global scope pollution:
var, let, or const to limit their scope.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, modules are reusable pieces of code that encapsulate functionality, making it easier to manage, maintain, and structure your applications. Modules allow you to break down your code into smaller, manageable parts, each with its own scope.
CommonJS is an older module system that was initially designed for server-side JavaScript development with Node.js. It uses the require() function to load modules and the module.exports or exports object to define the exports of a module.
// my-module.js
const value = 42;
module.exports = { value };
// main.js
const myModule = require('./my-module.js');
console.log(myModule.value); // 42
ES Modules (ECMAScript Modules) are the standardized module system introduced in ES6 (ECMAScript 2015). They use the import and export statements to handle module dependencies.
// my-module.js
export const value = 42;
// main.js
import { value } from './my-module.js';
console.log(value); // 42
CommonJS vs ES modules
| Feature | CommonJS | ES modules |
|---|---|---|
| Module Syntax | require() for importing module.exports for exporting | import for importing export for exporting |
| Environment | Primarily used in Node.js for server-side development | Designed for both browser and server-side JavaScript (Node.js) |
| Loading and linking | require() is synchronous | Static imports are linked before evaluation; import() returns a promise, and top-level await can make evaluation asynchronous |
| Structure | require() calls can be conditional | Static import/export declarations are top-level; dynamic loading uses import() |
| File extensions | .js (default) | .mjs or .js (with type: "module" in package.json) |
| Browser support | Not natively supported in browsers | Natively supported in modern browsers |
| Optimization | Limited optimization due to dynamic nature | Allows for optimizations like tree-shaking due to static structure |
| Compatibility | Widely used in existing Node.js codebases and libraries | JavaScript standard supported by browsers and modern server runtimes |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, data types can be categorized into primitive and non-primitive types:
Primitive data types
true or false values.Symbols.The remaining ECMAScript language type is Object. Arrays, functions, dates, regular expressions, maps, and sets are all kinds of objects rather than additional language types. Functions are callable objects and receive the special typeof result "function".
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
There are multiple ways to iterate over object properties as well as arrays in JavaScript:
for...in loop
The for...in loop iterates over all enumerable properties of an object, including inherited enumerable properties. So it is important to have a check if you only want to iterate over the object's own properties.
const obj = {
a: 1,
b: 2,
c: 3,
};
for (const key in obj) {
// To avoid iterating over inherited properties
if (Object.hasOwn(obj, key)) {
console.log(`${key}: ${obj[key]}`);
}
}
Object.keys()
Object.keys() returns an array of the object's own enumerable property names. You can then use a for...of loop or forEach to iterate over this array.
const obj = {
a: 1,
b: 2,
c: 3,
};
Object.keys(obj).forEach((key) => {
console.log(`${key}: ${obj[key]}`);
});
The most common ways to iterate over an array are using a for loop and the Array.prototype.forEach method.
Using for loop
let array = [1, 2, 3, 4, 5, 6];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
Using Array.prototype.forEach method
let array = [1, 2, 3, 4, 5, 6];
array.forEach((number, index) => {
console.log(`${number} at index ${index}`);
});
Using for...of
This method is the newest and most convenient way to iterate over arrays. It automatically iterates over each element without requiring you to manage the index.
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
There are also other built-in methods available which are suitable for specific scenarios, for example:
Array.prototype.filter: You can use the filter method to create a new array containing only the elements that satisfy a certain condition.Array.prototype.map: You can use the map method to create a new array based on the existing one, transforming each element with a provided function.Array.prototype.reduce: You can use the reduce method to combine all elements into a single value by repeatedly calling a function that takes two arguments: the accumulated value and the current element.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Spread syntax (...) allows an iterable (like an array or string) to be expanded into individual elements. This is often used as a convenient and modern way to create new arrays or objects by combining existing ones.
| Operation | Traditional | Spread |
|---|---|---|
| Array cloning | arr.slice() | [...arr] |
| Array merging | arr1.concat(arr2) | [...arr1, ...arr2] |
| Object cloning | Object.assign({}, obj) | { ...obj } |
| Object merging | Object.assign({}, obj1, obj2) | { ...obj1, ...obj2 } |
Rest syntax is the opposite of what spread syntax does. It collects a variable number of arguments into an array. This is often used in function parameters to handle a dynamic number of arguments.
// Using rest syntax in a function
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Output: 6
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, iterators and generators are tools for managing sequences of data and controlling the flow of execution in a more flexible way.
Iterators are objects that define a sequence and potentially a return value upon its termination. They adhere to a specific interface:
next() method.next() method returns an object with two properties:
value: The next value in the sequence.done: A boolean that is true if the iterator has finished its sequence, otherwise false.Here's an example of an object implementing the iterator interface.
const iterator = {
current: 0,
last: 5,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
let result = iterator.next();
while (!result.done) {
console.log(result.value); // Logs 0, 1, 2, 3, 4, 5
result = iterator.next();
}
Generators are special functions that can pause execution and resume at a later point. They use the function* syntax and the yield keyword to control the flow of execution. When you call a generator function, it doesn't execute completely like a regular function. Instead, it returns an iterator object. Calling the next() method on the returned iterator advances the generator to the next yield statement, and the value after yield becomes the return value of next().
function* numberGenerator() {
let num = 0;
while (num <= 5) {
yield num++;
}
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generators are useful for creating iterators on demand, especially for infinite sequences or complex iteration logic. They can be used for:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Mutable objects allow for modification of properties and values after creation, which is the default behavior for most objects.
const mutableObject = {
name: 'John',
age: 30,
};
// Modify the object
mutableObject.name = 'Jane';
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }
Immutable values cannot be changed after creation. JavaScript objects and arrays are mutable by default; an application can enforce shallow immutability with Object.freeze() or follow an immutable-update convention that creates a new object instead of mutating the existing one.
const immutableObject = Object.freeze({
name: 'John',
age: 30,
});
// Attempt to modify the object
immutableObject.name = 'Jane';
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }
Object.freeze() is shallow, so nested objects remain mutable unless they are frozen separately. Failed writes throw in strict mode and otherwise usually fail silently.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Map object and a plain object in JavaScript?Both Map objects and plain objects in JavaScript can store key-value pairs, but they have several key differences:
| Feature | Map | Plain object |
|---|---|---|
| Key type | Any data type | String (or Symbol) |
| Key order | Insertion order | Defined own-key order; integer-index keys come first, then other strings by insertion order, then symbols |
| Size property | Yes (size) | None |
| Iteration | forEach, keys(), values(), entries() | for...in, Object.keys(), etc. |
| Prototype interaction | User keys do not collide with Map.prototype methods | Object literals inherit from Object.prototype unless created with a null prototype |
| Performance | Designed for frequent keyed additions/removals; measure for the actual workload | Often convenient for fixed records; measure for the actual workload |
| JSON | Entries need an explicit conversion or replacer | Own enumerable string-keyed data is handled by JSON.stringify(), subject to JSON's normal limitations |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Map/Set and WeakMap/WeakSet in JavaScript?The primary difference between Map/Set and WeakMap/WeakSet in JavaScript lies in how they handle keys. Here's a breakdown:
Map vs. WeakMap
Maps allow any JavaScript value as a key and hold their keys and values strongly while the Map remains reachable. They are suitable for general-purpose key-value storage and are iterable in insertion order.
WeakMaps allow objects and non-registered symbols as keys. These keys are held weakly: an entry does not by itself keep its key reachable. WeakMaps are useful for associating metadata with a key without controlling that key's lifetime. Garbage collection timing is not observable or guaranteed.
Set vs. WeakSet
Similar to Map, Sets allow any data type as elements. The elements within a Set must be unique. Sets are useful for storing unique values and checking for membership efficiently. Common use cases include removing duplicates from arrays or keeping track of completed tasks.
WeakSet allows objects and non-registered symbols as elements. Like WeakMap keys, they are held weakly. WeakSets are useful when membership should not keep an object alive.
Here's a table summarizing the key differences:
| Feature | Map | WeakMap | Set | WeakSet |
|---|---|---|---|---|
| Key Types | Any JavaScript value | Objects and non-registered symbols | Any JavaScript value (unique) | Objects and non-registered symbols (unique) |
| References | Strong keys and values | Weak keys; values are associated with the key's lifetime | Strong elements | Weak elements |
| Use Cases | General-purpose key-value storage | Caching, private DOM node data | Removing duplicates, membership checks | Object weak references, custom use cases |
Choosing between them
Map and Set for most scenarios where you need to store key-value pairs or unique elements and want to maintain references to both the keys/elements and the values.WeakMap and WeakSet when membership should not keep an object or non-registered symbol alive. They are intentionally non-iterable, so use Map or Set if entries must be listed or counted.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Static fields and methods belong to the class constructor rather than to each instance. Use them for behavior or data conceptually associated with the type as a whole: named factories, validation helpers, registries, constants, or counters. Access them as ClassName.member (or through this inside a static method), not through an instance.
Do not use static mutable state for request/user data or browser secrets. It is shared within that constructor's runtime scope, complicates isolation and concurrency, and is duplicated across processes, workers, realms, or separate module copies. A module-level function or dependency-injected object is often simpler when no class abstraction is needed.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Symbols used for in JavaScript?Symbols are primitive values commonly used as collision-resistant object property keys and protocol hooks such as Symbol.iterator. Each call to Symbol(description) creates a distinct value, while Symbol.for(key) uses a shared registry. Symbol-keyed properties are skipped by for...in, Object.keys(), and JSON serialization, but they are not private: code can retrieve them with Object.getOwnPropertySymbols() or Reflect.ownKeys().
let sym1 = Symbol();
let sym2 = Symbol('myKey');
console.log(typeof sym1); // "symbol"
console.log(sym1 === sym2); // false, because each symbol is unique
let obj = {};
let sym = Symbol('uniqueKey');
obj[sym] = 'value';
console.log(obj[sym]); // "value"
Note: The Symbol() function must be called without the new keyword. It is not exactly a constructor because it can only be called as a function instead of with new Symbol().
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Server-sent events (SSE) is the text/event-stream format for a server-to-client stream over HTTP. Browsers expose the EventSource API for a long-lived GET connection with built-in reconnection and last-event-ID support. A Fetch response body can also consume an SSE-formatted stream when an application needs a different HTTP method, request body, or custom headers, but reconnection and parsing then become application responsibilities.
| Property | WebSocket | EventSource |
|---|---|---|
| Direction | Bi-directional – both client and server can exchange messages | Unidirectional – only server sends data |
| Data type | Binary and text data | Only text |
| Protocol | WebSocket protocol (ws://) | Regular HTTP (http://) |
| Browser API request shape | Custom handshake | EventSource issues GET; no request body and limited request-header control |
Creating an event source
const eventSource = new EventSource('/sse-stream');
Listening for events
// Fired when the connection is established.
eventSource.addEventListener('open', () => {
console.log('Connection opened');
});
// Fired when a message is received from the server.
eventSource.addEventListener('message', (event) => {
console.log('Received message:', event.data);
});
// Fired when an error occurs.
eventSource.addEventListener('error', (error) => {
console.error('Error occurred:', error);
});
Sending events from server
const express = require('express');
const app = express();
app.get('/sse-stream', (req, res) => {
// `Content-Type` need to be set to `text/event-stream`.
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Each message should be prefixed with data.
const sendEvent = (data) => res.write(`data: ${data}\n\n`);
sendEvent('Hello from server');
const intervalId = setInterval(() => sendEvent(new Date().toString()), 1000);
res.on('close', () => {
console.log('Client closed connection');
clearInterval(intervalId);
});
});
app.listen(3000, () => console.log('Server started on port 3000'));
In this example, the server sends a "Hello from server" message initially, and then sends the current date every second. The connection is kept alive until the client closes it.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, property flags and descriptors manage the behavior and attributes of object properties.
Property flags
Property flags are used to specify the behavior of a property on an object. Here are the available flags:
writable: Specifies whether the property can be written to.enumerable: Specifies whether the property is enumerable.configurable: Controls whether the property can be deleted and whether most parts of its descriptor can be reconfigured.Property descriptors
These provide detailed information about an object's property, including its value and flags. They are retrieved using Object.getOwnPropertyDescriptor() and set using Object.defineProperty().
The use cases of property descriptors are as follows:
writable: false to ensure data consistency.enumerable: false.configurable: false. A writable data property's value can still change, and writable can still change from true to false.Object.seal() or Object.freeze(). Both are shallow, and sealing still permits writes to writable data properties.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
JavaScript object getters and setters are used to control access to an object's properties. They provide a way to encapsulate the implementation details of a property and define custom behavior when getting or setting its value.
Getters and setters are defined using the get and set keywords, respectively, followed by a function that is executed when the property is accessed or assigned a new value.
Here's a code example demonstrating the use of getters and setters:
const person = {
_name: 'John Doe', // Underscore is a naming convention, not privacy
get name() {
// Getter
return this._name;
},
set name(newName) {
// Setter
if (newName.trim().length > 0) {
this._name = newName;
} else {
console.log('Invalid name');
}
},
};
// Accessing the name property using the getter
console.log(person.name); // Output: 'John Doe'
// Setting the name property using the setter
person.name = 'Jane Smith'; // Setter is called
console.log(person.name); // Output: 'Jane Smith'
person.name = ''; // Setter is called, but the value is not set due to validation
console.log(person.name); // Output: 'Jane Smith'
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, a proxy is an object that acts as an intermediary between an object and the code. Proxies are used to intercept and customize the fundamental operations of JavaScript objects, such as property access, assignment, function invocation, and more.
Here's a basic example of using a Proxy to log every property access:
const myObject = {
name: 'John',
age: 42,
};
const handler = {
get: function (target, prop, receiver) {
console.log(`Someone accessed property "${prop}"`);
return Reflect.get(target, prop, receiver);
},
};
const proxiedObject = new Proxy(myObject, handler);
console.log(proxiedObject.name);
// Someone accessed property "name"
// 'John'
console.log(proxiedObject.age);
// Someone accessed property "age"
// 42
Use cases include:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Reproduce the failure reliably, reduce it to the smallest useful scenario, form a specific hypothesis, and inspect the program at the boundary where expected and actual behavior diverge. Use breakpoints and the call stack for control flow, the Network panel for request failures, source maps for transformed code, the Performance and Memory panels for measured performance problems, and framework-specific tools only when the failure is inside that framework's state or render model.
Prefer a debugger, conditional breakpoint, or logpoint over scattering permanent console.log() calls. Preserve the failing input and add a regression test after finding the cause.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Workers in JavaScript are background threads that allow you to run scripts in parallel with the main execution thread, without blocking or interfering with the user interface. Their key features include:
postMessage() and onmessage/'message' event for messaging.fetch(), IndexedDB, Cache, Web Crypto, and timers. They do not expose the Window-only localStorage or sessionStorage APIs.There are three main types of workers in JavaScript:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
JavaScript engines automatically reclaim objects that are no longer reachable from roots such as the current call stack, global objects, and live host objects. Modern engines combine tracing collectors with optimizations such as generations, incremental work, and compaction; the exact strategy is an engine implementation detail.
Mark-and-sweep
The most common garbage collection algorithm used in JavaScript is the Mark-and-sweep algorithm. It operates in two phases:
This algorithm effectively identifies and removes objects that have become unreachable, freeing up memory for new allocations.
Generational garbage collection
Used by modern JavaScript engines, objects are divided into different generations based on their age. Objects start in the young generation, and those that survive several collections are promoted to the old generation. This optimization reduces the overhead of garbage collection by focusing on the younger generation, where most objects are short-lived.
Garbage collection does not prevent memory leaks: a listener, timer, closure, DOM reference, or unbounded cache can keep data reachable even when the application no longer needs it. Diagnose a suspected leak by repeating the problematic action, comparing heap snapshots, and following retaining paths. Do not try to force garbage collection in normal application code.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, data types can be categorized into primitive and non-primitive types:
Primitive data types
true or false values.Symbols.The remaining ECMAScript language type is Object. Arrays, functions, dates, regular expressions, maps, and sets are all kinds of objects rather than additional language types. Functions are callable objects and receive the special typeof result "function".
Use typeof for primitive categories and functions. Its possible results include "undefined", "boolean", "number", "bigint", "string", "symbol", "function", and "object". Because typeof null is historically "object" and arrays are objects, check those with value === null and Array.isArray(value). For specific object kinds, prefer purpose-built checks and be careful with instanceof across realms.
null, undefined or undeclared?| Trait | null | undefined | Undeclared |
|---|---|---|---|
| Meaning | Explicitly set by the developer to indicate that a variable has no value | Variable has been declared but not assigned a value | Variable has not been declared at all |
Type (via typeof operator) | 'object' | 'undefined' | 'undefined' |
| Direct access/comparison | Can compare with value === null | Can compare with value === undefined | Direct access throws ReferenceError; typeof undeclaredName returns 'undefined' |
let, var or const?In JavaScript, let, var, and const are all keywords used to declare variables, but they differ significantly in terms of scope, initialization rules, whether they can be redeclared or reassigned, and the behavior when they are accessed before declaration:
| Behavior | var | let | const |
|---|---|---|---|
| Scope | Function or Global | Block | Block |
| Initialization | Optional | Optional | Required |
| Redeclaration | Yes | No | No |
| Reassignment | Yes | Yes | No |
| Accessing before declaration | undefined | ReferenceError | ReferenceError |
JavaScript that is executed in the browser has access to the global scope (the window object). In general it's a good software engineering practice to not pollute the global namespace unless you are working on a feature that truly needs to be global – it is needed by the entire page. Several reasons to avoid touching the global scope:
Follow these best practices to avoid global scope pollution:
var, let, or const to limit their scope.Use Number(value) when the whole string must represent a number. Use parseInt(value, radix) or parseFloat(value) when intentionally accepting a numeric prefix such as '12px'; they stop at the first invalid character. Check the result with Number.isNaN() or, for finite application values, Number.isFinite(). Remember that Number('') and Number(' ') are 0, so validate required input before conversion.
Template literals are a feature in JavaScript that allow for easier string interpolation and multi-line strings. They are enclosed by backticks (`) instead of single or double quotes. You can embed expressions within template literals using ${expression} syntax.
Example:
const myName = 'John';
const greeting = `Hello, ${myName}!`;
console.log(greeting); // Output: Hello, John!
Tagged templates in JavaScript allow you to parse template literals with a function. The function receives the literal strings and the values as arguments, enabling custom processing of the template. For example:
function tag(strings, ...values) {
return strings[0] + values[0] + strings[1] + values[1] + strings[2];
}
const result = tag`Hello ${'world'}! How are ${'you'}?`;
console.log(result); // "Hello world! How are you?"
The spread operator, represented by three dots (...), is used in JavaScript to expand iterable objects like arrays or strings into individual elements. It can also be used to spread object properties. For example, you can use it to combine arrays, copy arrays, or pass array elements as arguments to a function.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combinedObj = { ...obj1, ...obj2 };
console.log(combinedObj); // { a: 1, b: 2, c: 3, d: 4 }
Symbols used for in JavaScript?Symbols are primitive values commonly used as collision-resistant object property keys and protocol hooks such as Symbol.iterator. Each call to Symbol(description) creates a distinct value, while Symbol.for(key) uses a shared registry. Symbol-keyed properties are skipped by for...in, Object.keys(), and JSON serialization, but they are not private: code can retrieve them with Object.getOwnPropertySymbols() or Reflect.ownKeys().
let sym1 = Symbol();
let sym2 = Symbol('myKey');
console.log(typeof sym1); // "symbol"
console.log(sym1 === sym2); // false, because each symbol is unique
let obj = {};
let sym = Symbol('uniqueKey');
obj[sym] = 'value';
console.log(obj[sym]); // "value"
Note: The Symbol() function must be called without the new keyword. It is not exactly a constructor because it can only be called as a function instead of with new Symbol().
In JavaScript, a proxy is an object that acts as an intermediary between an object and the code. Proxies are used to intercept and customize the fundamental operations of JavaScript objects, such as property access, assignment, function invocation, and more.
Here's a basic example of using a Proxy to log every property access:
const myObject = {
name: 'John',
age: 42,
};
const handler = {
get: function (target, prop, receiver) {
console.log(`Someone accessed property "${prop}"`);
return Reflect.get(target, prop, receiver);
},
};
const proxiedObject = new Proxy(myObject, handler);
console.log(proxiedObject.name);
// Someone accessed property "name"
// 'John'
console.log(proxiedObject.age);
// Someone accessed property "age"
// 42
Use cases include:
"Hoisting" is informal shorthand for how JavaScript creates and initializes bindings while it instantiates a scope, before evaluating that scope's statements. The engine does not move source text.
var): The binding is created and initialized to undefined before statements run. Its assignment still happens at the declaration's source location.let and const): The binding is created but remains uninitialized in the temporal dead zone (TDZ) until evaluation reaches the declaration. Accessing it earlier throws ReferenceError.var): The var binding initially contains undefined; the function is created only when the expression is evaluated.function): The binding is initialized with the function during scope setup, so it can be called before its declaration appears in source order.class): The binding exists during scope setup but remains in the TDZ until the class declaration is evaluated.import): Imported bindings are linked before module evaluation. Dependencies normally evaluate before the importing module's body, although cycles can expose an uninitialized binding.The following behavior summarizes the result of accessing the variables before they are declared.
| Declaration | Accessing before declaration |
|---|---|
var foo | undefined |
let foo | ReferenceError |
const foo | ReferenceError |
class Foo | ReferenceError |
var foo = function() { ... } | undefined |
function foo() { ... } | Normal |
import | Normal, except some cycles |
var, let, and constBefore a scope's statements run, JavaScript creates its var, let, and const bindings; it does not move source text. A var binding is initialized to undefined, so it can be read before its declaration appears. A let or const binding remains uninitialized in the temporal dead zone (TDZ), so reading it before evaluation reaches the declaration throws ReferenceError. const also requires an initializer and cannot be reassigned.
Function declarations are initialized when JavaScript instantiates their scope, so they can be called before their source declaration. Function expressions are created only when evaluation reaches the expression. Before then, a var binding contains undefined and calling it throws TypeError; a let or const binding is in the temporal dead zone and accessing it throws ReferenceError. "Hoisting" describes these binding rules; the engine does not move source text.
// Function declaration
console.log(foo()); // Works fine
function foo() {
return 'Hello';
}
// Function expression
console.log(bar()); // Throws TypeError: bar is not a function
var bar = function () {
return 'Hello';
};
JavaScript creates bindings before executing a scope's statements, but initializes different declaration forms differently. An early read of var produces undefined; an early read of let, const, or class throws because the binding is in the temporal dead zone; and function declarations are already callable. These differences—not literal source-code movement—can produce confusing bugs.
console.log(a); // undefined
var a = 5;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
Use const by default and let when reassignment is required, keep declarations close to their first use, and never read a binding before it is initialized. let and const are still hoisted—their bindings exist from the start of the block—but they remain in the temporal dead zone until evaluation reaches the declaration. Static-analysis rules such as no-use-before-define and no-undef catch the risky patterns.
// Use let or const
let x = 10;
const y = 20;
console.log(x, y); // Output: 10 20
// Declare functions before calling them
function myFunction() {
console.log('Hello, world!');
}
myFunction(); // Output: 'Hello, world!'
== and === in JavaScript?== is the abstract equality operator while === is the strict equality operator. == performs type coercion before comparing, following the Abstract Equality Comparison algorithm defined in the ECMAScript specification. === does not perform coercion and returns false whenever the operand types differ. === is generally preferred in application code because it eliminates a class of bugs caused by unexpected coercion. The most common exception is x == null, which checks for both null and undefined in a single comparison.
| Operator | == | === |
|---|---|---|
| Name | Loose (abstract) equality operator | Strict equality operator |
| Type coercion | Yes — per the Abstract Equality Comparison algorithm | No |
| Comparison behavior | Types may be coerced before the value comparison | Types are compared first |
Don't confuse
=with==and===.=is the assignment operator — it sets a variable's value (x = 5) and does not compare anything.
There are multiple ways to iterate over object properties as well as arrays in JavaScript:
for...in loop
The for...in loop iterates over all enumerable properties of an object, including inherited enumerable properties. So it is important to have a check if you only want to iterate over the object's own properties.
const obj = {
a: 1,
b: 2,
c: 3,
};
for (const key in obj) {
// To avoid iterating over inherited properties
if (Object.hasOwn(obj, key)) {
console.log(`${key}: ${obj[key]}`);
}
}
Object.keys()
Object.keys() returns an array of the object's own enumerable property names. You can then use a for...of loop or forEach to iterate over this array.
const obj = {
a: 1,
b: 2,
c: 3,
};
Object.keys(obj).forEach((key) => {
console.log(`${key}: ${obj[key]}`);
});
The most common ways to iterate over an array are using a for loop and the Array.prototype.forEach method.
Using for loop
let array = [1, 2, 3, 4, 5, 6];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
Using Array.prototype.forEach method
let array = [1, 2, 3, 4, 5, 6];
array.forEach((number, index) => {
console.log(`${number} at index ${index}`);
});
Using for...of
This method is the newest and most convenient way to iterate over arrays. It automatically iterates over each element without requiring you to manage the index.
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
There are also other built-in methods available which are suitable for specific scenarios, for example:
Array.prototype.filter: You can use the filter method to create a new array containing only the elements that satisfy a certain condition.Array.prototype.map: You can use the map method to create a new array based on the existing one, transforming each element with a provided function.Array.prototype.reduce: You can use the reduce method to combine all elements into a single value by repeatedly calling a function that takes two arguments: the accumulated value and the current element.break and continue statements?The break statement is used to exit a loop or switch statement prematurely, while the continue statement skips the current iteration of a loop and proceeds to the next iteration. For example, in a for loop, break will stop the loop entirely, and continue will skip to the next iteration.
for (let i = 0; i < 10; i++) {
if (i === 5) break; // exits the loop when i is 5
console.log(i);
}
for (let i = 0; i < 10; i++) {
if (i === 5) continue; // skips the iteration when i is 5
console.log(i);
}
The ternary operator is a shorthand for an if-else statement in JavaScript. It takes three operands: a condition, a result for true, and a result for false. The syntax is condition ? expr1 : expr2. For example, let result = (a > b) ? 'a is greater' : 'b is greater'; assigns 'a is greater' to result if a is greater than b, otherwise it assigns 'b is greater'.
To access the index of an element in an array during iteration, you can use methods like forEach, map, for...of with entries, or a traditional for loop. For example, using forEach:
const array = ['a', 'b', 'c'];
array.forEach((element, index) => {
console.log(index, element);
});
switch statement?The switch statement is used to execute one block of code among many based on the value of an expression. It is an alternative to using multiple if...else if statements. The switch statement evaluates an expression, matches the expression's value to a case label, and executes the associated block of code. If no case matches, the default block is executed.
switch (expression) {
case value1:
// code to be executed if expression === value1
break;
case value2:
// code to be executed if expression === value2
break;
default:
// code to be executed if no case matches
}
Rest parameters in JavaScript allow a function to accept an indefinite number of arguments as an array. They are denoted by three dots (...) followed by the name of the array. This feature is useful for functions that need to handle multiple arguments without knowing the exact number in advance.
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // Output: 10
The spread operator (...) in JavaScript expands the elements of an iterable (like an array) into individual elements, and copies the own enumerable properties of an object into a new object. It is commonly used for copying arrays or objects, merging arrays or objects, and passing elements of an array as arguments to a function.
// Copying an array
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
console.log(arr2); // Output: [1, 2, 3]
// Merging arrays
const arr3 = [4, 5, 6];
const mergedArray = [...arr1, ...arr3];
console.log(mergedArray); // Output: [1, 2, 3, 4, 5, 6]
// Copying an object
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1 };
console.log(obj2); // Output: { a: 1, b: 2 }
// Merging objects
const obj3 = { c: 3, d: 4 };
const mergedObject = { ...obj1, ...obj3 };
console.log(mergedObject); // Output: { a: 1, b: 2, c: 3, d: 4 }
// Passing array elements as function arguments
const sum = (x, y, z) => x + y + z;
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // Output: 6
Spread syntax (...) allows an iterable (like an array or string) to be expanded into individual elements. This is often used as a convenient and modern way to create new arrays or objects by combining existing ones.
| Operation | Traditional | Spread |
|---|---|---|
| Array cloning | arr.slice() | [...arr] |
| Array merging | arr1.concat(arr2) | [...arr1, ...arr2] |
| Object cloning | Object.assign({}, obj) | { ...obj } |
| Object merging | Object.assign({}, obj1, obj2) | { ...obj1, ...obj2 } |
Rest syntax is the opposite of what spread syntax does. It collects a variable number of arguments into an array. This is often used in function parameters to handle a dynamic number of arguments.
// Using rest syntax in a function
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Output: 6
In JavaScript, iterators and generators are tools for managing sequences of data and controlling the flow of execution in a more flexible way.
Iterators are objects that define a sequence and potentially a return value upon its termination. They adhere to a specific interface:
next() method.next() method returns an object with two properties:
value: The next value in the sequence.done: A boolean that is true if the iterator has finished its sequence, otherwise false.Here's an example of an object implementing the iterator interface.
const iterator = {
current: 0,
last: 5,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
let result = iterator.next();
while (!result.done) {
console.log(result.value); // Logs 0, 1, 2, 3, 4, 5
result = iterator.next();
}
Generators are special functions that can pause execution and resume at a later point. They use the function* syntax and the yield keyword to control the flow of execution. When you call a generator function, it doesn't execute completely like a regular function. Instead, it returns an iterator object. Calling the next() method on the returned iterator advances the generator to the next yield statement, and the value after yield becomes the return value of next().
function* numberGenerator() {
let num = 0;
while (num <= 5) {
yield num++;
}
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generators are useful for creating iterators on demand, especially for infinite sequences or complex iteration logic. They can be used for:
foo between function foo() {} and var foo = function() {} in JavaScriptfunction foo() {} is a function declaration while var foo = function() {} is a function expression. The key difference is that function declarations have their bodies hoisted but the bodies of function expressions are not (they have the same hoisting behavior as var-declared variables).
If you try to invoke a function expression before it is declared, you will get an Uncaught TypeError: XXX is not a function error.
Function declarations can be called in the enclosing scope even before they are declared.
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
Function expressions if called before they are declared will result in an error.
foo(); // Uncaught TypeError: foo is not a function
var foo = function () {
console.log('FOOOOO');
};
Another key difference is in the scope of the function name. Function expressions can be named by defining a name after the function keyword and before the parentheses. However, when using named function expressions, the function name is only accessible within the function itself. Trying to access it outside will result in a ReferenceError.
const myFunc = function namedFunc() {
console.log(namedFunc); // Works
};
myFunc(); // Runs the function and logs the function reference
console.log(namedFunc); // ReferenceError: namedFunc is not defined
Note: The examples use var due to legacy reasons. Function expressions can be defined using let and const, and the key difference is in the hoisting behavior of those keywords.
A parameter is a variable in the declaration of a function, while an argument is the actual value passed to the function when it is called. For example, in the function function add(a, b) { return a + b; }, a and b are parameters. When you call add(2, 3), 2 and 3 are arguments.
"Hoisting" describes the observable result of declarations being instantiated before a scope's statements execute; the engine does not move source text. A function declaration's binding is initialized with the function during scope setup, so it can be called earlier in that scope. A function or arrow expression is created only when evaluation reaches the expression. Its variable binding follows the rules for var, let, or const.
// Function declaration
hoistedFunction(); // Works fine
function hoistedFunction() {
console.log('This function is hoisted');
}
// Function expression
nonHoistedFunction(); // Throws an error
var nonHoistedFunction = function () {
console.log('This function is not hoisted');
};
.call and .apply in JavaScript?.call and .apply are both used to invoke functions with a specific this context and arguments. The primary difference lies in how they accept arguments:
.call(thisArg, arg1, arg2, ...): Takes arguments individually..apply(thisArg, [argsArray]): Takes arguments as an array.Assuming we have a function add, the function can be invoked using .call and .apply in the following manner:
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3
Arrow functions provide concise syntax and capture this and arguments from their surrounding scope. They work well for array transformations and for callbacks created inside a method or constructor that need the surrounding receiver. An arrow used directly as an object method does not receive the object as this; use a regular method when the caller's receiver should determine this. Arrow functions also cannot be called with new, do not have a prototype, and cannot be generators.
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6]
function Person(){}, const person = Person(), and const person = new Person() in JavaScript?function Person(){}: A function declaration in JavaScript. It can be used as a regular function or as a constructor.const person = Person(): Calls Person as a regular function, not a constructor. If Person is intended to be a constructor, this will lead to unexpected behavior.const person = new Person(): Creates a new instance of Person, correctly using the constructor function to initialize the new object.| Aspect | function Person(){} | const person = Person() | const person = new Person() |
|---|---|---|---|
| Type | Function declaration | Function call | Constructor call |
| Usage | Defines a function | Invokes Person as a regular function | Creates a new instance of Person |
| Instance Creation | No instance created | No instance created | New instance created |
| Common Mistake | N/A | Misusing as constructor leading to undefined | None (when used correctly) |
A higher-order function is any function that takes one or more functions as arguments, which it uses to operate on some data, and/or returns a function as a result.
Higher-order functions are meant to abstract some operation that is performed repeatedly. The classic example of this is Array.prototype.map(), which takes an array and a function as arguments. Array.prototype.map() then uses this function to transform each item in the array, returning a new array with the transformed data. Other popular examples in JavaScript are Array.prototype.forEach(), Array.prototype.filter(), and Array.prototype.reduce(). A higher-order function doesn't just need to be manipulating arrays as there are many use cases for returning a function from another function. Function.prototype.bind() is an example that returns another function.
Imagine a scenario where we have an array of names that we need to transform to uppercase. The imperative way will be as such:
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
Using Array.prototype.map(transformerFn) makes the code shorter and more declarative.
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
return names.map((name) => name.toUpperCase());
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
A callback is a function supplied to other code to be invoked according to that API's contract. Some callbacks run synchronously, such as an Array.prototype.map() callback; others run later, such as timer, event, or I/O callbacks. Passing a callback does not by itself make an operation asynchronous.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John Doe' };
callback(null, data);
}, 0);
}
function handleData(error, data) {
if (error) throw error;
console.log(data);
}
fetchData(handleData);
An anonymous function in JavaScript is a function that does not have any name associated with it. They are typically used as arguments to other functions or assigned to variables.
const arr = [-1, 0, 5, 6];
// The filter method is passed an anonymous function.
arr.filter((x) => x > 1); // [5, 6]
They are often used as arguments to other functions, known as higher-order functions, which can take functions as input and return a function as output. Anonymous functions can access variables from the outer scope, a concept known as closures, allowing them to "close over" and remember the environment in which they were created.
// Encapsulating Code
(function () {
// Some code here.
})();
// Callbacks
setTimeout(function () {
console.log('Hello world!');
}, 1000);
// Functional programming constructs
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
Recursion solves a problem by calling the same function on smaller inputs until a base case is reached. It naturally fits recursive structures such as trees and divide-and-conquer algorithms, but each ordinary call consumes stack space. Validate that the input moves toward the base case, and use an explicit stack or loop when depth can be large or user-controlled.
function factorial(n) {
if (!Number.isInteger(n) || n < 0) {
throw new RangeError('n must be a non-negative integer');
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(4)); // Output: 24
Default parameters supply a value when an argument is omitted or explicitly undefined; they do not apply to null, 0, false, or ''. The expression is evaluated at call time, so it can use earlier parameters and create a fresh object per call. Use defaults for genuine optional arguments, but validate values separately when null or another sentinel is invalid.
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
greet(); // Output: Hello, Guest!
greet('Alice'); // Output: Hello, Alice!
function foo(){}();. What needs to be changed to properly make it an IIFE?The code function foo(){}(); doesn't work as an Immediately Invoked Function Expression (IIFE) because the JavaScript parser treats function foo(){} as a function declaration, not an expression. To make it an IIFE, you need to wrap the function in parentheses to turn it into a function expression: (function foo(){})();.
Creating objects in JavaScript offers several methods:
{}): Simplest and most popular approach. Define key-value pairs within curly braces.Object() constructor: Use new Object() with dot notation to add properties.Object.create(): Create new objects using existing objects as prototypes, inheriting properties and methods.new.class and constructor keywords.Dot notation and bracket notation are two ways to access properties of an object in JavaScript. Dot notation is more concise and readable but can only be used with valid JavaScript identifiers. Bracket notation is more flexible and can be used with property names that are not valid identifiers, such as those containing spaces or special characters.
const obj = { name: 'Alice', 'favorite color': 'blue' };
// Dot notation
console.log(obj.name); // Alice
// Bracket notation
console.log(obj['favorite color']); // blue
Choose an iteration construct by the result and control flow you need: map() transforms into a new array, filter() selects, reduce() combines, some() / every() answer conditions, and find() locates one item. Use for...of or an indexed for loop when you need break, continue, sequential await, or precise index control. Use forEach() for synchronous side effects only; it ignores returned Promises and cannot be stopped early.
For in-place changes, use push() / unshift() to add, pop() / shift() to remove at the ends, splice() for arbitrary ranges, and index assignment to update. These mutate the original array. When callers rely on immutable updates, create a new array with spread, slice(), filter(), map(), toSpliced(), or with() instead. Choose based on ownership, not a universal performance rule.
let arr = [1, 2, 3];
// Add elements
arr.push(4); // [1, 2, 3, 4]
arr.unshift(0); // [0, 1, 2, 3, 4]
arr.splice(2, 0, 1.5); // [0, 1, 1.5, 2, 3, 4]
// Remove elements
arr.pop(); // [0, 1, 1.5, 2, 3]
arr.shift(); // [1, 1.5, 2, 3]
arr.splice(1, 1); // [1, 2, 3]
// Update elements
arr[1] = 5; // [1, 5, 3]
console.log(arr); // Final state: [1, 5, 3]
Note: If you try to console.log(arr) after each operation in some environments (like Chrome DevTools), you may only see the final state of arr. This happens because the console sometimes keeps a live reference to the array instead of logging its state at the exact moment. To see intermediate states properly, store snapshots using console.log([...arr]) or print values immediately after each operation.
Use spread syntax or Object.assign() for a shallow copy. Use structuredClone() for values supported by the structured clone algorithm, including nested arrays and objects, cycles, Map, Set, and many built-in types. A JSON stringify/parse round trip is only a lossy conversion for JSON-shaped data, not a general deep-clone algorithm. A library or domain-specific copy routine may be needed for custom classes and semantics.
// Shallow copy of an array
const originalArray = [1, 2, 3];
const shallowCopyArray = [...originalArray];
console.log(shallowCopyArray); // [1, 2, 3]
// Shallow copy of an object
const originalObject = { a: 1, b: 2 };
const shallowCopyObject = { ...originalObject };
console.log(shallowCopyObject); // { a: 1, b: 2 }
// Deep copy using JSON methods
const deepCopyObject = JSON.parse(JSON.stringify(originalObject));
console.log(deepCopyObject); // { a: 1, b: 2 }
A shallow copy duplicates the top-level properties of an object, but nested objects are still referenced. A deep copy duplicates all levels of an object, creating entirely new instances of nested objects. Object.assign() and the spread operator (...) create shallow copies. structuredClone() is the modern built-in for deep copies. JSON.parse(JSON.stringify()) and Lodash's _.cloneDeep are other common approaches, each with different tradeoffs around which values they can faithfully clone.
// Shallow copy — nested object is shared
let obj1 = { a: 1, b: { c: 2 } };
let shallowCopy = { ...obj1 };
shallowCopy.b.c = 3;
console.log(obj1.b.c); // 3 — original mutated too
// Deep copy — fully independent
let obj2 = { a: 1, b: { c: 2 } };
let deepCopy = structuredClone(obj2);
deepCopy.b.c = 4;
console.log(obj2.b.c); // 2 — original unchanged
The spread operator (...) in JavaScript allows you to easily copy arrays and objects, merge them, and add new elements or properties. It simplifies syntax and improves readability. For arrays, it can be used to concatenate or clone arrays. For objects, it can be used to merge objects or add new properties.
// Arrays
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]
// Objects
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };
console.log(obj2); // { a: 1, b: 2, c: 3 }
Use the in operator when inherited properties should count, and Object.hasOwn() when only the object's own properties should count. Avoid calling obj.hasOwnProperty() directly because the object can shadow that method or have a null prototype.
const obj = { key: 'value' };
// Using the `in` operator
if ('key' in obj) {
console.log('Property exists');
}
// Checking only own properties
if (Object.hasOwn(obj, 'key')) {
console.log('Property exists');
}
Mutable objects allow for modification of properties and values after creation, which is the default behavior for most objects.
const mutableObject = {
name: 'John',
age: 30,
};
// Modify the object
mutableObject.name = 'Jane';
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }
Immutable values cannot be changed after creation. JavaScript objects and arrays are mutable by default; an application can enforce shallow immutability with Object.freeze() or follow an immutable-update convention that creates a new object instead of mutating the existing one.
const immutableObject = Object.freeze({
name: 'John',
age: 30,
});
// Attempt to modify the object
immutableObject.name = 'Jane';
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }
Object.freeze() is shallow, so nested objects remain mutable unless they are frozen separately. Failed writes throw in strict mode and otherwise usually fail silently.
Destructuring assignment is a syntax in JavaScript that allows you to unpack values from arrays or properties from objects into distinct variables. For arrays, you use square brackets, and for objects, you use curly braces. For example:
// Array destructuring
const [a, b] = [1, 2];
// Object destructuring
const { name, age } = { name: 'John', age: 30 };
Object.freeze() for?Object.freeze() makes an object non-extensible and changes its own properties so they cannot be removed or reconfigured; data properties also become non-writable. This is a shallow operation: nested objects and internal state such as a Map's entries can still change. Invalid writes throw in strict mode and otherwise usually fail silently.
const obj = { name: 'John' };
Object.freeze(obj);
obj.name = 'Doe'; // This will not change the name property
console.log(obj); // { name: 'John' }
Object.seal() for?Object.seal() prevents extensions and makes every existing own property non-configurable. Existing data-property values can still change when their descriptors are writable. The operation is shallow: nested objects are unaffected. Invalid additions and deletions throw in strict mode and may fail silently otherwise. Sealing is useful for catching accidental shape changes, not for enforcing authorization or protecting secrets.
// 'use strict'
const obj = { name: 'John' };
Object.seal(obj);
obj.name = 'Jane'; // Allowed
obj.age = 30; // Not allowed, throws an error in strict mode
delete obj.name; // Not allowed, throws an error in strict mode
console.log(obj); // { name: 'Jane' } (age was not added, name was not deleted)
Object.preventExtensions() for?Object.preventExtensions(object) makes that object non-extensible: new own properties cannot be added and its prototype cannot be changed, while existing properties may still be changed or deleted according to their descriptors. The operation is shallow and irreversible for that object. In strict mode, invalid additions throw; otherwise ordinary assignment may fail silently.
const obj = { name: 'John' };
Object.preventExtensions(obj);
obj.age = 30; // This will not work, as the object is not extensible
console.log(obj.age); // undefined
JavaScript object getters and setters are used to control access to an object's properties. They provide a way to encapsulate the implementation details of a property and define custom behavior when getting or setting its value.
Getters and setters are defined using the get and set keywords, respectively, followed by a function that is executed when the property is accessed or assigned a new value.
Here's a code example demonstrating the use of getters and setters:
const person = {
_name: 'John Doe', // Underscore is a naming convention, not privacy
get name() {
// Getter
return this._name;
},
set name(newName) {
// Setter
if (newName.trim().length > 0) {
this._name = newName;
} else {
console.log('Invalid name');
}
},
};
// Accessing the name property using the getter
console.log(person.name); // Output: 'John Doe'
// Setting the name property using the setter
person.name = 'Jane Smith'; // Setter is called
console.log(person.name); // Output: 'Jane Smith'
person.name = ''; // Setter is called, but the value is not set due to validation
console.log(person.name); // Output: 'Jane Smith'
In JavaScript, property flags and descriptors manage the behavior and attributes of object properties.
Property flags
Property flags are used to specify the behavior of a property on an object. Here are the available flags:
writable: Specifies whether the property can be written to.enumerable: Specifies whether the property is enumerable.configurable: Controls whether the property can be deleted and whether most parts of its descriptor can be reconfigured.Property descriptors
These provide detailed information about an object's property, including its value and flags. They are retrieved using Object.getOwnPropertyDescriptor() and set using Object.defineProperty().
The use cases of property descriptors are as follows:
writable: false to ensure data consistency.enumerable: false.configurable: false. A writable data property's value can still change, and writable can still change from true to false.Object.seal() or Object.freeze(). Both are shallow, and sealing still permits writes to writable data properties.First define what “empty” means. For the common case “no own enumerable string-keyed properties,” use Object.keys(obj).length === 0. If symbol or non-enumerable own keys should count, use Reflect.ownKeys(obj).length === 0. Validate the input separately if null, primitives, arrays, maps, or sets are not valid inputs.
const isEmpty = (obj) => Object.keys(obj).length === 0;
const obj = {};
console.log(isEmpty(obj)); // true
The event loop is the host runtime's scheduling mechanism for coordinating JavaScript jobs, asynchronous operations, and—in browsers—rendering. A simplified browser turn works like this:
queueMicrotask() callbacks, and other microtasks, including microtasks added while the checkpoint is running.Synchronous code runs to completion on the current call stack before later statements can run. Asynchronous APIs arrange for a result to be handled later through a callback, promise, or event, allowing the current stack to finish while the host waits for I/O or a timer. Asynchronous does not mean “runs on another thread”: an async function runs synchronously until its first suspension point, and CPU-heavy JavaScript still blocks its thread.
function sum(a, b) {
console.log('Inside sum function');
return a + b;
}
const result = sum(2, 3); // The program waits for sum() to complete before assigning the result
console.log('Result: ', result); // Output: 5
Asynchronous APIs commonly expose callbacks, promises, or events. Once the operation can make progress, its continuation is scheduled according to the host's event loop. This works especially well for I/O such as network and database requests; CPU-intensive work must instead be split up or moved to a worker to keep a browser UI responsive.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data); // Calling the callback function with data
}, 2000); // Simulating a 2-second delay
}
console.log('Fetching data...');
fetchData((data) => {
console.log(data); // Output: { name: 'John', age: 30 } (after 2 seconds)
});
console.log('Call made to fetch data'); // This will print before the data is fetched
A callback is a function passed to another API to invoke later or during its operation. In asynchronous APIs, the contract must define when and how often it runs, how errors are reported, and how to cancel or unsubscribe. Scheduling a callback lets the current stack continue, but the callback's own JavaScript still occupies the thread when it eventually runs.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data);
});
Promises represent the eventual outcome of an asynchronous operation. They start pending and settle once as fulfilled with a value or rejected with a reason. .then(), .catch(), and .finally() return new Promises, so returned values and thrown errors flow through a chain. Reactions run as microtasks even when the original Promise is already settled.
A Promise coordinates an outcome; it does not automatically start work on another thread or cancel the underlying operation. Expose an AbortSignal or another explicit cancellation mechanism when work can be stopped.
let promise = new Promise((resolve, reject) => {
// asynchronous operation
const success = true;
if (success) {
resolve('Success!');
} else {
reject(new Error('Operation failed'));
}
});
promise
.then((result) => {
console.log(result); // 'Success!' (this will print)
})
.catch((error) => {
console.error(error); // 'Error!'
});
A Promise has three mutually exclusive states:
Error.Fulfilled and rejected Promises are settled. Once settled, a Promise cannot change state; later attempts to fulfill or reject it are ignored. “Resolved” is not always synonymous with “fulfilled”: a Promise can be resolved to another still-pending Promise and adopt its eventual state.
Promises standardize one eventual outcome and make sequential, parallel, and error flows composable with .then(), async/await, and combinators such as Promise.all(). They avoid many callback-contract ambiguities, but they do not cancel work, represent repeated events, or guarantee settlement. A Promise chain can still become unreadable or leak an unhandled rejection when callers forget to return or await it.
Promise.all()Promise.all() accepts an iterable of promises or values and returns a promise fulfilled with results in input order once every input fulfills. It rejects as soon as an input rejects. It does not start the operations or cancel the remaining work; create the promises first, and use AbortController or an operation-specific mechanism when cancellation is required.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // [3, 42, 'foo']
});
Promise.all() different from Promise.allSettled()?Promise.all() and Promise.allSettled() are both methods for handling multiple promises in JavaScript, but they behave differently. Promise.all() waits for all promises to resolve and fails fast if any promise rejects, returning a single rejected promise. Promise.allSettled(), on the other hand, waits for all promises to settle (either resolve or reject) and returns an array of objects describing the outcome of each promise.
async/await is a modern syntax in JavaScript that simplifies working with promises. By using the async keyword before a function, you can use the await keyword inside that function to pause execution until a promise is resolved. This makes asynchronous code look and behave more like synchronous code, making it easier to read and maintain.
async function fetchData() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/1',
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData();
Handle promise rejections with try...catch around await or with .catch() on a promise chain. Remember that Fetch fulfills its promise for HTTP errors such as 404 and 500, so check response.ok and throw an application error yourself.
async function fetchData() {
try {
// This request will fail (example domain)
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData(); // Error fetching data: ....
With Promises, you can use the .catch() method:
fetch('https://api.example.com/data') // This request will fail (example domain)
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error('Error fetching data:', error));
The microtask queue holds callbacks such as promise reactions, queueMicrotask() callbacks, and MutationObserver notifications. At a microtask checkpoint—normally after the current task or callback finishes and the JavaScript stack is empty—the runtime drains the queue before it may render and select another task. Microtasks queued by other microtasks are drained in the same checkpoint, so recursively adding them can starve rendering and other work.
setTimeout(), setImmediate(), and process.nextTick()?In Node.js, setTimeout() schedules a timer callback after a minimum delay, setImmediate() schedules a callback for the event loop's check phase after I/O callbacks, and process.nextTick() queues work to be drained after the current JavaScript operation before the event loop continues. Node now marks process.nextTick() as legacy and recommends queueMicrotask() for most userland deferral.
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
In this example, process.nextTick() will execute first, followed by either setTimeout() or setImmediate() depending on the environment.
Prototypal inheritance is property lookup by delegation. An object's internal [[Prototype]] is another object or null; when an own property is missing, lookup continues along that chain. Inspect it with Object.getPrototypeOf() and create a deliberate link with Object.create() or class / extends. Avoid the legacy __proto__ accessor in application code, and avoid changing prototypes of existing objects in hot code because it can invalidate engine optimizations.
When a property is accessed on an object and if the property is not found on that object, the JavaScript engine looks at the object's __proto__, and the __proto__'s __proto__ and so on, until it finds the property defined on one of the __proto__s or until it reaches the end of the prototype chain.
This behavior simulates classical inheritance, but it is really more of delegation than inheritance.
Here's an example of prototypal inheritance:
// Parent object constructor.
function Animal(name) {
this.name = name;
}
// Add a method to the parent object's prototype.
Animal.prototype.makeSound = function () {
console.log('The ' + this.constructor.name + ' makes a sound.');
};
// Child object constructor.
function Dog(name) {
Animal.call(this, name); // Call the parent constructor.
}
// Set the child object's prototype to be the parent's prototype.
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// Add a method to the child object's prototype.
Dog.prototype.bark = function () {
console.log('Woof!');
};
// Create a new instance of Dog.
const bolt = new Dog('Bolt');
// Call methods on the child object.
console.log(bolt.name); // "Bolt"
bolt.makeSound(); // "The Dog makes a sound."
bolt.bark(); // "Woof!"
Things to note are:
.makeSound is not defined on Dog, so the JavaScript engine goes up the prototype chain and finds .makeSound on the inherited Animal.class and extends are the preferred way to set up inheritance; they are syntactic sugar over this prototype mechanism.The prototype chain is a mechanism in JavaScript that allows objects to inherit properties and methods from other objects. When you try to access a property on an object, JavaScript will first look for the property on the object itself. If it doesn't find it, it will look at the object's prototype, and then the prototype's prototype, and so on, until it either finds the property or reaches the end of the chain, which is null.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person('Alice');
alice.greet(); // "Hello, my name is Alice"
In this example, alice inherits the greet method from Person.prototype.
Classical inheritance is a model where classes inherit from other classes, typically seen in languages like Java and C++. Prototypal inheritance, used in JavaScript, involves objects inheriting directly from other objects. In classical inheritance, you define a class and create instances from it. In prototypal inheritance, you create an object and use it as a prototype for other objects.
JavaScript class inheritance uses extends to link constructor and prototype chains. A derived constructor must call super() before accessing this, and super.method() invokes parent behavior. Use inheritance when the child is genuinely substitutable for the parent and the contract is stable. Prefer composition for optional capabilities or when subclasses would need to override many unrelated methods.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex', 'German Shepherd');
dog.speak(); // Rex barks.
new keyword?Calling a constructable function with new creates a fresh object, links its prototype to the constructor's prototype, calls the constructor with this set to that object, and normally returns the object. If the constructor explicitly returns another non-primitive object, that returned object wins; an explicit primitive return is ignored. Arrow functions and some other callables are not constructable.
function Person(name) {
this.name = name;
}
const person1 = new Person('Alice');
console.log(person1.name); // Alice
To create a constructor function in JavaScript, define a regular function with a capitalized name to indicate it's a constructor. Use the this keyword to set properties and methods. When creating an instance, use the new keyword.
function Person(name, age) {
this.name = name;
this.age = age;
}
const john = new Person('John', 30);
console.log(john.age); // 30
ES2015 introduces a new way of creating classes, which provides a more intuitive and concise way to define and work with objects and inheritance compared to the ES5 function constructor syntax. Here's an example of each:
// ES5 function constructor
function Person(name) {
this.name = name;
}
// ES2015 Class
class Person {
constructor(name) {
this.name = name;
}
}
For simple constructors, they look pretty similar. The main difference in the constructor comes when using inheritance. If we want to create a Student class that subclasses Person and adds a studentId field, this is what we have to do.
// ES5 inheritance
// Superclass
function Person1(name) {
this.name = name;
}
// Subclass
function Student1(name, studentId) {
// Call constructor of superclass to initialize superclass-derived members.
Person1.call(this, name);
// Initialize subclass's own members.
this.studentId = studentId;
}
Student1.prototype = Object.create(Person1.prototype);
Student1.prototype.constructor = Student1;
const student1 = new Student1('John', 1234);
console.log(student1.name, student1.studentId); // "John" 1234
// ES2015 inheritance
// Superclass
class Person2 {
constructor(name) {
this.name = name;
}
}
// Subclass
class Student2 extends Person2 {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
const student2 = new Student2('Alice', 5678);
console.log(student2.name, student2.studentId); // "Alice" 5678
It's much more verbose to use inheritance in ES5, and the ES2015 version is easier to understand and remember.
Comparison of ES5 function constructors vs ES2015 classes
| Feature | ES5 Function Constructor | ES2015 Class |
|---|---|---|
| Syntax | Uses function constructors and prototypes | Uses class keyword |
| Constructor | Function with properties assigned using this | constructor method inside the class |
| Method Definition | Defined on the prototype | Defined inside the class body |
| Static Methods | Added directly to the constructor function | Defined using the static keyword |
| Inheritance | Uses Object.create() and manually sets prototype chain | Uses extends keyword and super |
| Readability | Less intuitive and more verbose | More concise and intuitive |
The main advantage of using an arrow function as a method inside a constructor is that the value of this gets set at the time of the function creation and can't change after that. When the constructor is used to create a new object, this will always refer to that object.
For example, let's say we have a Person constructor that takes a first name as an argument and has two methods to console.log() that name, one as a regular function and one as an arrow function:
const Person = function (name) {
this.firstName = name;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// The regular function can have its `this` value changed, but the arrow function cannot
john.sayName1.call(dave); // Dave (because `this` is now the dave object)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (because `this` is now the dave object)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (because `this` is now the dave object)
john.sayName2.bind(dave)(); // John
const detachedRegularMethod = john.sayName1;
try {
detachedRegularMethod();
} catch (error) {
console.log(error.name); // TypeError in strict mode because `this` is undefined
}
const detachedArrowMethod = john.sayName2;
detachedArrowMethod(); // John
The main takeaway is that a regular function receives this from its call site, while an arrow captures it from the constructor call. The tradeoff is that this arrow function is created separately for every instance instead of being shared through the prototype, and callers cannot deliberately rebind it.
Static fields and methods belong to the class constructor rather than to each instance. Use them for behavior or data conceptually associated with the type as a whole: named factories, validation helpers, registries, constants, or counters. Access them as ClassName.member (or through this inside a static method), not through an instance.
Do not use sta
Truncated — view the full README on GitHub.
(top 24 of 29)
38 followers · starred Mar 2025
634 followers · starred Nov 2024
60 followers · starred Oct 2025
13 followers · starred Jan 2025
Top JavaScript interview questions and answers for Frontend Engineers (updated for 2026)
MDX
10,081
134 commits
updated Aug 16, 2026
Updated for 2026! This repository contains a few hundred curated JavaScript interview questions with high quality answers for acing your Front End Engineer interviews, brought to you by GreatFrontEnd.
Looking for more? Read our blog post 50+ must-know JavaScript interview questions and answers compiled from ex-FAANG interviewers.
[!NOTE] This repository is a public mirror. GreatFrontEnd's main repository is the source of truth for question content; use this repository's Issues to report problems instead of opening content pull requests.
The questions are divided into three sections:
This list contains the top essential questions that are frequently-asked during Front End Engineer interviews. Concise versions of the answers are presented here with links to elaborate versions for further reading. This section is great if you need a quick refresher of the most important concepts.
This list contains a longer list of important JavaScript questions. Not all of them will be asked during Front End Engineer interviews but they provide a well-rounded review of JavaScript concepts ranging from beginner to advanced topics.
"Hoisting" is informal shorthand for how JavaScript creates and initializes bindings while it instantiates a scope, before evaluating that scope's statements. The engine does not move source text.
var): The binding is created and initialized to undefined before statements run. Its assignment still happens at the declaration's source location.let and const): The binding is created but remains uninitialized in the temporal dead zone (TDZ) until evaluation reaches the declaration. Accessing it earlier throws ReferenceError.var): The var binding initially contains undefined; the function is created only when the expression is evaluated.function): The binding is initialized with the function during scope setup, so it can be called before its declaration appears in source order.class): The binding exists during scope setup but remains in the TDZ until the class declaration is evaluated.import): Imported bindings are linked before module evaluation. Dependencies normally evaluate before the importing module's body, although cycles can expose an uninitialized binding.The following behavior summarizes the result of accessing the variables before they are declared.
| Declaration | Accessing before declaration |
|---|---|
var foo | undefined |
let foo | ReferenceError |
const foo | ReferenceError |
class Foo | ReferenceError |
var foo = function() { ... } | undefined |
function foo() { ... } | Normal |
import | Normal, except some cycles |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
let, var or const?In JavaScript, let, var, and const are all keywords used to declare variables, but they differ significantly in terms of scope, initialization rules, whether they can be redeclared or reassigned, and the behavior when they are accessed before declaration:
| Behavior | var | let | const |
|---|---|---|---|
| Scope | Function or Global | Block | Block |
| Initialization | Optional | Optional | Required |
| Redeclaration | Yes | No | No |
| Reassignment | Yes | Yes | No |
| Accessing before declaration | undefined | ReferenceError | ReferenceError |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
== and === in JavaScript?== is the abstract equality operator while === is the strict equality operator. == performs type coercion before comparing, following the Abstract Equality Comparison algorithm defined in the ECMAScript specification. === does not perform coercion and returns false whenever the operand types differ. === is generally preferred in application code because it eliminates a class of bugs caused by unexpected coercion. The most common exception is x == null, which checks for both null and undefined in a single comparison.
| Operator | == | === |
|---|---|---|
| Name | Loose (abstract) equality operator | Strict equality operator |
| Type coercion | Yes — per the Abstract Equality Comparison algorithm | No |
| Comparison behavior | Types may be coerced before the value comparison | Types are compared first |
Don't confuse
=with==and===.=is the assignment operator — it sets a variable's value (x = 5) and does not compare anything.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
The event loop is the host runtime's scheduling mechanism for coordinating JavaScript jobs, asynchronous operations, and—in browsers—rendering. A simplified browser turn works like this:
queueMicrotask() callbacks, and other microtasks, including microtasks added while the checkpoint is running.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Event delegation is a technique in JavaScript where a single event listener is attached to a parent element instead of attaching event listeners to multiple child elements. When an event occurs on a child element, the event bubbles up the DOM tree, and the parent element's event listener handles the event based on the target element.
Event delegation provides the following benefits:
However, do note that:
focus/blur and mouseenter/mouseleave need capture-phase handling or bubbling alternatives such as focusin/focusout and mouseover/mouseout. Element scroll and resize generally need direct listeners.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
this works in JavaScriptThere's no simple explanation for this; it is one of the most confusing concepts in JavaScript because its behavior differs from many other programming languages. The one-liner explanation of the this keyword is that it is a dynamic reference to the context in which a function is executed.
A longer explanation is that this follows these rules:
new keyword is used when calling the function, meaning the function was used as a function constructor, the this inside the function is the newly-created object instance.this is used in a class constructor, the this inside the constructor is the newly-created object instance.apply(), call(), or bind() is used to call/create a function, this inside the function is the object that is passed in as the argument.obj.method()) — this is the object that the function is a property of.this is the global object. In the browser, the global object is the window object. If in strict mode ('use strict';), this will be undefined instead of the global object.this value.this value of its surrounding scope at the time it is created.For an in-depth explanation, do check out Arnav Aggrawal's article on Medium.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
sessionStorage and localStorage in browsersCookies, localStorage, and sessionStorage all store data in the browser, but they differ in lifetime, scope, server interaction, and security controls. localStorage and sessionStorage implement the Web Storage API.
HttpOnly, Secure, and SameSite.localStorage: Origin-scoped string storage that persists until it is cleared and is shared by same-origin tabs and windows.sessionStorage: Origin- and tab-scoped string storage for a page session. It survives reloads but is normally cleared when the tab or window closes.Here's a table summarizing the 3 client storage mechanisms.
| Property | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| Initiator | Client or server. Server can use Set-Cookie header | Client | Client |
| Lifespan | As specified | Until deleted | Until tab is closed |
| Persistent across browser sessions | If a future expiry date is set | Yes | No |
| Sent to server with matching HTTP requests | Yes, via the Cookie header | No | No |
| Typical storage limit | About 4 KB per cookie | Browser-dependent quota, commonly several MiB per origin | Browser-dependent quota, commonly several MiB per origin |
| Access | Across windows/tabs | Across windows/tabs | Same tab |
| Security | JavaScript cannot access HttpOnly cookies | None | None |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
<script>, <script async> and <script defer>All of these ways (<script>, <script async>, and <script defer>) are used to load and execute JavaScript files in an HTML document, but they differ in how the browser handles loading and execution of the script:
<script> is the default way of including JavaScript. The browser blocks HTML parsing while the script is being downloaded and executed. The browser will not continue rendering the page until the script has finished executing.<script async> downloads the script asynchronously, in parallel with parsing the HTML. Executes the script as soon as it is available, potentially interrupting the HTML parsing. Multiple <script async> tags do not wait for each other and execute in no particular order.<script defer> downloads the script asynchronously, in parallel with parsing the HTML. However, the execution of the script is deferred until HTML parsing is complete, in the order they appear in the HTML.Here's a table summarizing the 4 ways of loading <script>s in an HTML document. Modern apps almost always use modules, which deserve their own row.
| Feature | <script> | <script async> | <script defer> | <script type="module"> |
|---|---|---|---|---|
| Parsing behavior | Blocks HTML parsing | Downloads in parallel; execution still blocks parsing | Downloads in parallel; execution deferred until after parsing | Downloads in parallel; execution deferred until after parsing |
| Execution order | In order of appearance | Not guaranteed | In order of appearance | In order of appearance, with each script's import dependencies resolved first |
| DOM state at execution | Only earlier markup is parsed | Depends on download timing | Document parsing is complete | Document parsing is complete |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
null, undefined or undeclared?| Trait | null | undefined | Undeclared |
|---|---|---|---|
| Meaning | Explicitly set by the developer to indicate that a variable has no value | Variable has been declared but not assigned a value | Variable has not been declared at all |
Type (via typeof operator) | 'object' | 'undefined' | 'undefined' |
| Direct access/comparison | Can compare with value === null | Can compare with value === undefined | Direct access throws ReferenceError; typeof undeclaredName returns 'undefined' |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
.call and .apply in JavaScript?.call and .apply are both used to invoke functions with a specific this context and arguments. The primary difference lies in how they accept arguments:
.call(thisArg, arg1, arg2, ...): Takes arguments individually..apply(thisArg, [argsArray]): Takes arguments as an array.Assuming we have a function add, the function can be invoked using .call and .apply in the following manner:
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Function.prototype.bind in JavaScriptFunction.prototype.bind is a method in JavaScript that allows you to create a new function with a specific this value and optional initial arguments. Its primary purpose is to:
this value to preserve context: The primary purpose of bind is to bind the this value of a function to a specific object. When you call func.bind(thisArg), it creates a new function with the same body as func, but with this permanently bound to thisArg.bind also allows you to pre-specify arguments for the new function. Any arguments passed to bind after thisArg will be prepended to the arguments list when the new function is called.bind allows you to borrow methods from one object and apply them to another object, even if they were not originally designed to work with that object.The bind method is particularly useful in scenarios where you need to ensure that a function is called with a specific this context, such as in event handlers, callbacks, or method borrowing.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
The main advantage of using an arrow function as a method inside a constructor is that the value of this gets set at the time of the function creation and can't change after that. When the constructor is used to create a new object, this will always refer to that object.
For example, let's say we have a Person constructor that takes a first name as an argument and has two methods to console.log() that name, one as a regular function and one as an arrow function:
const Person = function (name) {
this.firstName = name;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// The regular function can have its `this` value changed, but the arrow function cannot
john.sayName1.call(dave); // Dave (because `this` is now the dave object)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (because `this` is now the dave object)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (because `this` is now the dave object)
john.sayName2.bind(dave)(); // John
const detachedRegularMethod = john.sayName1;
try {
detachedRegularMethod();
} catch (error) {
console.log(error.name); // TypeError in strict mode because `this` is undefined
}
const detachedArrowMethod = john.sayName2;
detachedArrowMethod(); // John
The main takeaway is that a regular function receives this from its call site, while an arrow captures it from the constructor call. The tradeoff is that this arrow function is created separately for every instance instead of being shared through the prototype, and callers cannot deliberately rebind it.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Prototypal inheritance is property lookup by delegation. An object's internal [[Prototype]] is another object or null; when an own property is missing, lookup continues along that chain. Inspect it with Object.getPrototypeOf() and create a deliberate link with Object.create() or class / extends. Avoid the legacy __proto__ accessor in application code, and avoid changing prototypes of existing objects in hot code because it can invalidate engine optimizations.
When a property is accessed on an object and if the property is not found on that object, the JavaScript engine looks at the object's __proto__, and the __proto__'s __proto__ and so on, until it finds the property defined on one of the __proto__s or until it reaches the end of the prototype chain.
This behavior simulates classical inheritance, but it is really more of delegation than inheritance.
Here's an example of prototypal inheritance:
// Parent object constructor.
function Animal(name) {
this.name = name;
}
// Add a method to the parent object's prototype.
Animal.prototype.makeSound = function () {
console.log('The ' + this.constructor.name + ' makes a sound.');
};
// Child object constructor.
function Dog(name) {
Animal.call(this, name); // Call the parent constructor.
}
// Set the child object's prototype to be the parent's prototype.
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// Add a method to the child object's prototype.
Dog.prototype.bark = function () {
console.log('Woof!');
};
// Create a new instance of Dog.
const bolt = new Dog('Bolt');
// Call methods on the child object.
console.log(bolt.name); // "Bolt"
bolt.makeSound(); // "The Dog makes a sound."
bolt.bark(); // "Woof!"
Things to note are:
.makeSound is not defined on Dog, so the JavaScript engine goes up the prototype chain and finds .makeSound on the inherited Animal.class and extends are the preferred way to set up inheritance; they are syntactic sugar over this prototype mechanism.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
function Person(){}, const person = Person(), and const person = new Person() in JavaScript?function Person(){}: A function declaration in JavaScript. It can be used as a regular function or as a constructor.const person = Person(): Calls Person as a regular function, not a constructor. If Person is intended to be a constructor, this will lead to unexpected behavior.const person = new Person(): Creates a new instance of Person, correctly using the constructor function to initialize the new object.| Aspect | function Person(){} | const person = Person() | const person = new Person() |
|---|---|---|---|
| Type | Function declaration | Function call | Constructor call |
| Usage | Defines a function | Invokes Person as a regular function | Creates a new instance of Person |
| Instance Creation | No instance created | No instance created | New instance created |
| Common Mistake | N/A | Misusing as constructor leading to undefined | None (when used correctly) |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
foo between function foo() {} and var foo = function() {} in JavaScriptfunction foo() {} is a function declaration while var foo = function() {} is a function expression. The key difference is that function declarations have their bodies hoisted but the bodies of function expressions are not (they have the same hoisting behavior as var-declared variables).
If you try to invoke a function expression before it is declared, you will get an Uncaught TypeError: XXX is not a function error.
Function declarations can be called in the enclosing scope even before they are declared.
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
Function expressions if called before they are declared will result in an error.
foo(); // Uncaught TypeError: foo is not a function
var foo = function () {
console.log('FOOOOO');
};
Another key difference is in the scope of the function name. Function expressions can be named by defining a name after the function keyword and before the parentheses. However, when using named function expressions, the function name is only accessible within the function itself. Trying to access it outside will result in a ReferenceError.
const myFunc = function namedFunc() {
console.log(namedFunc); // Works
};
myFunc(); // Runs the function and logs the function reference
console.log(namedFunc); // ReferenceError: namedFunc is not defined
Note: The examples use var due to legacy reasons. Function expressions can be defined using let and const, and the key difference is in the hoisting behavior of those keywords.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
An anonymous function in JavaScript is a function that does not have any name associated with it. They are typically used as arguments to other functions or assigned to variables.
const arr = [-1, 0, 5, 6];
// The filter method is passed an anonymous function.
arr.filter((x) => x > 1); // [5, 6]
They are often used as arguments to other functions, known as higher-order functions, which can take functions as input and return a function as output. Anonymous functions can access variables from the outer scope, a concept known as closures, allowing them to "close over" and remember the environment in which they were created.
// Encapsulating Code
(function () {
// Some code here.
})();
// Callbacks
setTimeout(function () {
console.log('Hello world!');
}, 1000);
// Functional programming constructs
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Creating objects in JavaScript offers several methods:
{}): Simplest and most popular approach. Define key-value pairs within curly braces.Object() constructor: Use new Object() with dot notation to add properties.Object.create(): Create new objects using existing objects as prototypes, inheriting properties and methods.new.class and constructor keywords.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In the book "You Don't Know JS" (YDKJS) by Kyle Simpson, a closure is defined as follows:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope
In simple terms, functions have access to variables that were in their scope at the time of their creation. This is what we call the function's lexical scope. A closure is a function that retains access to these variables even after the outer function has finished executing. It is as if the function has a memory of its original environment.
function outerFunction() {
const outerVar = 'I am outside of innerFunction';
function innerFunction() {
console.log(outerVar); // `innerFunction` can still access `outerVar`.
}
return innerFunction;
}
const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`.
inner(); // "I am outside of innerFunction"
// Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`.
Key points to remember:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
A higher-order function is any function that takes one or more functions as arguments, which it uses to operate on some data, and/or returns a function as a result.
Higher-order functions are meant to abstract some operation that is performed repeatedly. The classic example of this is Array.prototype.map(), which takes an array and a function as arguments. Array.prototype.map() then uses this function to transform each item in the array, returning a new array with the transformed data. Other popular examples in JavaScript are Array.prototype.forEach(), Array.prototype.filter(), and Array.prototype.reduce(). A higher-order function doesn't just need to be manipulating arrays as there are many use cases for returning a function from another function. Function.prototype.bind() is an example that returns another function.
Imagine a scenario where we have an array of names that we need to transform to uppercase. The imperative way will be as such:
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
Using Array.prototype.map(transformerFn) makes the code shorter and more declarative.
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
return names.map((name) => name.toUpperCase());
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
ES2015 introduces a new way of creating classes, which provides a more intuitive and concise way to define and work with objects and inheritance compared to the ES5 function constructor syntax. Here's an example of each:
// ES5 function constructor
function Person(name) {
this.name = name;
}
// ES2015 Class
class Person {
constructor(name) {
this.name = name;
}
}
For simple constructors, they look pretty similar. The main difference in the constructor comes when using inheritance. If we want to create a Student class that subclasses Person and adds a studentId field, this is what we have to do.
// ES5 inheritance
// Superclass
function Person1(name) {
this.name = name;
}
// Subclass
function Student1(name, studentId) {
// Call constructor of superclass to initialize superclass-derived members.
Person1.call(this, name);
// Initialize subclass's own members.
this.studentId = studentId;
}
Student1.prototype = Object.create(Person1.prototype);
Student1.prototype.constructor = Student1;
const student1 = new Student1('John', 1234);
console.log(student1.name, student1.studentId); // "John" 1234
// ES2015 inheritance
// Superclass
class Person2 {
constructor(name) {
this.name = name;
}
}
// Subclass
class Student2 extends Person2 {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
const student2 = new Student2('Alice', 5678);
console.log(student2.name, student2.studentId); // "Alice" 5678
It's much more verbose to use inheritance in ES5, and the ES2015 version is easier to understand and remember.
Comparison of ES5 function constructors vs ES2015 classes
| Feature | ES5 Function Constructor | ES2015 Class |
|---|---|---|
| Syntax | Uses function constructors and prototypes | Uses class keyword |
| Constructor | Function with properties assigned using this | constructor method inside the class |
| Method Definition | Defined on the prototype | Defined inside the class body |
| Static Methods | Added directly to the constructor function | Defined using the static keyword |
| Inheritance | Uses Object.create() and manually sets prototype chain | Uses extends keyword and super |
| Readability | Less intuitive and more verbose | More concise and intuitive |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Event bubbling is a DOM event propagation mechanism where an event (e.g. a click) starts at the target element and bubbles up to the root of the document. This allows ancestor elements to also respond to the event.
Event bubbling is essential for event delegation, where a single event handler manages events for multiple child elements, enhancing performance and code simplicity. While convenient, failing to manage event propagation properly can lead to unintended behavior, such as multiple handlers firing for a single event.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Event capturing is a lesser-used counterpart to event bubbling in the DOM event propagation mechanism. During capture, the event travels along its event path from ancestors toward the target, and capture listeners run in that order.
Event capturing is used less often than event bubbling, but it is useful when an ancestor needs to observe an event before it reaches the target. DOM events still travel through the capture phase; what defaults to false is the capture option when registering a listener with addEventListener().
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
mouseenter and mouseover event in JavaScript and browsers?The main difference lies in the bubbling behavior of mouseenter and mouseover events. mouseenter does not bubble while mouseover bubbles.
mouseenter events do not bubble. The mouseenter event is triggered only when the mouse pointer enters the element itself, not its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseenter event will not be triggered on the parent element again; it is only triggered once upon entry of the parent element, without regard for its contents. If both parent and child have mouseenter listeners attached and the mouse pointer moves from the parent element to the child element, mouseenter will only fire for the child.
mouseover events bubble up the DOM tree. The mouseover event is triggered when the mouse pointer enters the element or one of its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseover event will be triggered on the parent element again as well. If the parent element has multiple child elements, this can result in multiple event callbacks fired. If there are child elements, and the mouse pointer moves from the parent element to the child element, mouseover will fire for both the parent and the child.
| Property | mouseenter | mouseover |
|---|---|---|
| Bubbling | No | Yes |
| Trigger | Only when entering itself | When entering itself and when entering descendants |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
'use strict'; (strict mode) in JavaScript for?'use strict' is a statement used to enable strict mode to entire scripts or individual functions. Strict mode is a way to opt into a restricted variant of JavaScript.
Advantages
this === undefined instead of coercing it to the global object.Disadvantages
with, arguments.callee, and access to function.caller are unavailable.ES modules and class bodies are already strict. Use the directive for legacy scripts or functions that are otherwise in sloppy mode; do not add a redundant directive merely to code that is already an ES module.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Synchronous code runs to completion on the current call stack before later statements can run. Asynchronous APIs arrange for a result to be handled later through a callback, promise, or event, allowing the current stack to finish while the host waits for I/O or a timer. Asynchronous does not mean “runs on another thread”: an async function runs synchronously until its first suspension point, and CPU-heavy JavaScript still blocks its thread.
function sum(a, b) {
console.log('Inside sum function');
return a + b;
}
const result = sum(2, 3); // The program waits for sum() to complete before assigning the result
console.log('Result: ', result); // Output: 5
Asynchronous APIs commonly expose callbacks, promises, or events. Once the operation can make progress, its continuation is scheduled according to the host's event loop. This works especially well for I/O such as network and database requests; CPU-intensive work must instead be split up or moved to a worker to keep a browser UI responsive.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data); // Calling the callback function with data
}, 2000); // Simulating a 2-second delay
}
console.log('Fetching data...');
fetchData((data) => {
console.log(data); // Output: { name: 'John', age: 30 } (after 2 seconds)
});
console.log('Call made to fetch data'); // This will print before the data is fetched
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Promises standardize one eventual outcome and make sequential, parallel, and error flows composable with .then(), async/await, and combinators such as Promise.all(). They avoid many callback-contract ambiguities, but they do not cancel work, represent repeated events, or guarantee settlement. A Promise chain can still become unreadable or leak an unhandled rejection when callers forget to return or await it.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
AJAX (Asynchronous JavaScript and XML) facilitates asynchronous communication between the client and server, enabling dynamic updates to web pages without reloading. It uses techniques like XMLHttpRequest or the fetch() API to send and receive data in the background. In modern web applications, the fetch() API is more commonly used to implement AJAX.
Using XMLHttpRequest
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('Request failed: ' + xhr.status);
}
}
};
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.send();
Using fetch()
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error('Fetch error:', error));
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
AJAX (Asynchronous JavaScript and XML) is a technique in JavaScript that allows web pages to send and retrieve data asynchronously from servers without refreshing or reloading the entire page.
Advantages
Disadvantages
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
XMLHttpRequest and fetch() in JavaScript and browsers?XMLHttpRequest (XHR) and fetch() API are both used for asynchronous HTTP requests in JavaScript (AJAX). fetch() offers a cleaner syntax, promise-based approach, and more modern feature set compared to XHR. However, there are some differences:
XMLHttpRequest uses event callbacks, while fetch() utilizes promise chaining.Headers, Request, and Response abstractions and integrates with streams.response.ok or response.status.cache request option.fetch() requires an AbortController for cancelation, while XMLHttpRequest provides an abort() method.These days fetch() is preferred for its cleaner syntax and modern features.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
AbortController in JavaScript?Create an AbortController, pass its signal to fetch(), and call controller.abort() when the result is no longer needed. fetch() and response-body consumption reject when aborted. Treat cancellation as an expected control-flow outcome, clean up any related timers or listeners, and create a new controller for the next operation because an aborted signal stays aborted.
Aborting releases the client from waiting and may cancel network activity, but it does not guarantee that the server stops or rolls back work already started. Make important writes idempotent or provide an application-level cancellation protocol.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Polyfills implement a missing JavaScript or Web API in environments that do not provide it. Choose them from an explicit support matrix and feature tests, load only the required modules, and prefer maintained implementations because matching specification edge cases is difficult. A transpiler rewrites syntax; it does not by itself add runtime objects such as Promise, URL, or new array methods.
They can be implemented manually or included through libraries and are often used in conjunction with feature detection.
Common use cases include:
Array.prototype.includes(), Object.assign(), etc.fetch(), Promise, IntersectionObserver, etc. Modern browsers support these now, but for a long time they had to be polyfilled.A commonly used polyfill library is:
core-js: A modular standard library for JavaScript which includes polyfills for a wide range of ECMAScript features.
import 'core-js/actual/array/flat-map'; // With this, Array.prototype.flatMap is available to be used.
[1, 2].flatMap((it) => [it, it]); // => [1, 1, 2, 2]
Prefer installing maintained polyfills as project dependencies and bundling them with your application instead of loading executable code from a third-party polyfill service at runtime.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Extending a built-in/native JavaScript object means adding properties/functions to its prototype. While this may seem like a good idea at first, it is dangerous in practice. Imagine your code uses two libraries that both extend the Array.prototype by adding the same contains method; the implementations will overwrite each other and your code will have unpredictable behavior if these two methods do not work the same way.
The only time you may want to extend a native object is when you want to create a polyfill, providing your own implementation for a method that is part of the JavaScript specification but might not exist in the user's browser due to it being an older browser.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
JavaScript that is executed in the browser has access to the global scope (the window object). In general it's a good software engineering practice to not pollute the global namespace unless you are working on a feature that truly needs to be global – it is needed by the entire page. Several reasons to avoid touching the global scope:
Follow these best practices to avoid global scope pollution:
var, let, or const to limit their scope.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, modules are reusable pieces of code that encapsulate functionality, making it easier to manage, maintain, and structure your applications. Modules allow you to break down your code into smaller, manageable parts, each with its own scope.
CommonJS is an older module system that was initially designed for server-side JavaScript development with Node.js. It uses the require() function to load modules and the module.exports or exports object to define the exports of a module.
// my-module.js
const value = 42;
module.exports = { value };
// main.js
const myModule = require('./my-module.js');
console.log(myModule.value); // 42
ES Modules (ECMAScript Modules) are the standardized module system introduced in ES6 (ECMAScript 2015). They use the import and export statements to handle module dependencies.
// my-module.js
export const value = 42;
// main.js
import { value } from './my-module.js';
console.log(value); // 42
CommonJS vs ES modules
| Feature | CommonJS | ES modules |
|---|---|---|
| Module Syntax | require() for importing module.exports for exporting | import for importing export for exporting |
| Environment | Primarily used in Node.js for server-side development | Designed for both browser and server-side JavaScript (Node.js) |
| Loading and linking | require() is synchronous | Static imports are linked before evaluation; import() returns a promise, and top-level await can make evaluation asynchronous |
| Structure | require() calls can be conditional | Static import/export declarations are top-level; dynamic loading uses import() |
| File extensions | .js (default) | .mjs or .js (with type: "module" in package.json) |
| Browser support | Not natively supported in browsers | Natively supported in modern browsers |
| Optimization | Limited optimization due to dynamic nature | Allows for optimizations like tree-shaking due to static structure |
| Compatibility | Widely used in existing Node.js codebases and libraries | JavaScript standard supported by browsers and modern server runtimes |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, data types can be categorized into primitive and non-primitive types:
Primitive data types
true or false values.Symbols.The remaining ECMAScript language type is Object. Arrays, functions, dates, regular expressions, maps, and sets are all kinds of objects rather than additional language types. Functions are callable objects and receive the special typeof result "function".
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
There are multiple ways to iterate over object properties as well as arrays in JavaScript:
for...in loop
The for...in loop iterates over all enumerable properties of an object, including inherited enumerable properties. So it is important to have a check if you only want to iterate over the object's own properties.
const obj = {
a: 1,
b: 2,
c: 3,
};
for (const key in obj) {
// To avoid iterating over inherited properties
if (Object.hasOwn(obj, key)) {
console.log(`${key}: ${obj[key]}`);
}
}
Object.keys()
Object.keys() returns an array of the object's own enumerable property names. You can then use a for...of loop or forEach to iterate over this array.
const obj = {
a: 1,
b: 2,
c: 3,
};
Object.keys(obj).forEach((key) => {
console.log(`${key}: ${obj[key]}`);
});
The most common ways to iterate over an array are using a for loop and the Array.prototype.forEach method.
Using for loop
let array = [1, 2, 3, 4, 5, 6];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
Using Array.prototype.forEach method
let array = [1, 2, 3, 4, 5, 6];
array.forEach((number, index) => {
console.log(`${number} at index ${index}`);
});
Using for...of
This method is the newest and most convenient way to iterate over arrays. It automatically iterates over each element without requiring you to manage the index.
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
There are also other built-in methods available which are suitable for specific scenarios, for example:
Array.prototype.filter: You can use the filter method to create a new array containing only the elements that satisfy a certain condition.Array.prototype.map: You can use the map method to create a new array based on the existing one, transforming each element with a provided function.Array.prototype.reduce: You can use the reduce method to combine all elements into a single value by repeatedly calling a function that takes two arguments: the accumulated value and the current element.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Spread syntax (...) allows an iterable (like an array or string) to be expanded into individual elements. This is often used as a convenient and modern way to create new arrays or objects by combining existing ones.
| Operation | Traditional | Spread |
|---|---|---|
| Array cloning | arr.slice() | [...arr] |
| Array merging | arr1.concat(arr2) | [...arr1, ...arr2] |
| Object cloning | Object.assign({}, obj) | { ...obj } |
| Object merging | Object.assign({}, obj1, obj2) | { ...obj1, ...obj2 } |
Rest syntax is the opposite of what spread syntax does. It collects a variable number of arguments into an array. This is often used in function parameters to handle a dynamic number of arguments.
// Using rest syntax in a function
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Output: 6
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, iterators and generators are tools for managing sequences of data and controlling the flow of execution in a more flexible way.
Iterators are objects that define a sequence and potentially a return value upon its termination. They adhere to a specific interface:
next() method.next() method returns an object with two properties:
value: The next value in the sequence.done: A boolean that is true if the iterator has finished its sequence, otherwise false.Here's an example of an object implementing the iterator interface.
const iterator = {
current: 0,
last: 5,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
let result = iterator.next();
while (!result.done) {
console.log(result.value); // Logs 0, 1, 2, 3, 4, 5
result = iterator.next();
}
Generators are special functions that can pause execution and resume at a later point. They use the function* syntax and the yield keyword to control the flow of execution. When you call a generator function, it doesn't execute completely like a regular function. Instead, it returns an iterator object. Calling the next() method on the returned iterator advances the generator to the next yield statement, and the value after yield becomes the return value of next().
function* numberGenerator() {
let num = 0;
while (num <= 5) {
yield num++;
}
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generators are useful for creating iterators on demand, especially for infinite sequences or complex iteration logic. They can be used for:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Mutable objects allow for modification of properties and values after creation, which is the default behavior for most objects.
const mutableObject = {
name: 'John',
age: 30,
};
// Modify the object
mutableObject.name = 'Jane';
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }
Immutable values cannot be changed after creation. JavaScript objects and arrays are mutable by default; an application can enforce shallow immutability with Object.freeze() or follow an immutable-update convention that creates a new object instead of mutating the existing one.
const immutableObject = Object.freeze({
name: 'John',
age: 30,
});
// Attempt to modify the object
immutableObject.name = 'Jane';
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }
Object.freeze() is shallow, so nested objects remain mutable unless they are frozen separately. Failed writes throw in strict mode and otherwise usually fail silently.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Map object and a plain object in JavaScript?Both Map objects and plain objects in JavaScript can store key-value pairs, but they have several key differences:
| Feature | Map | Plain object |
|---|---|---|
| Key type | Any data type | String (or Symbol) |
| Key order | Insertion order | Defined own-key order; integer-index keys come first, then other strings by insertion order, then symbols |
| Size property | Yes (size) | None |
| Iteration | forEach, keys(), values(), entries() | for...in, Object.keys(), etc. |
| Prototype interaction | User keys do not collide with Map.prototype methods | Object literals inherit from Object.prototype unless created with a null prototype |
| Performance | Designed for frequent keyed additions/removals; measure for the actual workload | Often convenient for fixed records; measure for the actual workload |
| JSON | Entries need an explicit conversion or replacer | Own enumerable string-keyed data is handled by JSON.stringify(), subject to JSON's normal limitations |
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Map/Set and WeakMap/WeakSet in JavaScript?The primary difference between Map/Set and WeakMap/WeakSet in JavaScript lies in how they handle keys. Here's a breakdown:
Map vs. WeakMap
Maps allow any JavaScript value as a key and hold their keys and values strongly while the Map remains reachable. They are suitable for general-purpose key-value storage and are iterable in insertion order.
WeakMaps allow objects and non-registered symbols as keys. These keys are held weakly: an entry does not by itself keep its key reachable. WeakMaps are useful for associating metadata with a key without controlling that key's lifetime. Garbage collection timing is not observable or guaranteed.
Set vs. WeakSet
Similar to Map, Sets allow any data type as elements. The elements within a Set must be unique. Sets are useful for storing unique values and checking for membership efficiently. Common use cases include removing duplicates from arrays or keeping track of completed tasks.
WeakSet allows objects and non-registered symbols as elements. Like WeakMap keys, they are held weakly. WeakSets are useful when membership should not keep an object alive.
Here's a table summarizing the key differences:
| Feature | Map | WeakMap | Set | WeakSet |
|---|---|---|---|---|
| Key Types | Any JavaScript value | Objects and non-registered symbols | Any JavaScript value (unique) | Objects and non-registered symbols (unique) |
| References | Strong keys and values | Weak keys; values are associated with the key's lifetime | Strong elements | Weak elements |
| Use Cases | General-purpose key-value storage | Caching, private DOM node data | Removing duplicates, membership checks | Object weak references, custom use cases |
Choosing between them
Map and Set for most scenarios where you need to store key-value pairs or unique elements and want to maintain references to both the keys/elements and the values.WeakMap and WeakSet when membership should not keep an object or non-registered symbol alive. They are intentionally non-iterable, so use Map or Set if entries must be listed or counted.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Static fields and methods belong to the class constructor rather than to each instance. Use them for behavior or data conceptually associated with the type as a whole: named factories, validation helpers, registries, constants, or counters. Access them as ClassName.member (or through this inside a static method), not through an instance.
Do not use static mutable state for request/user data or browser secrets. It is shared within that constructor's runtime scope, complicates isolation and concurrency, and is duplicated across processes, workers, realms, or separate module copies. A module-level function or dependency-injected object is often simpler when no class abstraction is needed.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Symbols used for in JavaScript?Symbols are primitive values commonly used as collision-resistant object property keys and protocol hooks such as Symbol.iterator. Each call to Symbol(description) creates a distinct value, while Symbol.for(key) uses a shared registry. Symbol-keyed properties are skipped by for...in, Object.keys(), and JSON serialization, but they are not private: code can retrieve them with Object.getOwnPropertySymbols() or Reflect.ownKeys().
let sym1 = Symbol();
let sym2 = Symbol('myKey');
console.log(typeof sym1); // "symbol"
console.log(sym1 === sym2); // false, because each symbol is unique
let obj = {};
let sym = Symbol('uniqueKey');
obj[sym] = 'value';
console.log(obj[sym]); // "value"
Note: The Symbol() function must be called without the new keyword. It is not exactly a constructor because it can only be called as a function instead of with new Symbol().
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Server-sent events (SSE) is the text/event-stream format for a server-to-client stream over HTTP. Browsers expose the EventSource API for a long-lived GET connection with built-in reconnection and last-event-ID support. A Fetch response body can also consume an SSE-formatted stream when an application needs a different HTTP method, request body, or custom headers, but reconnection and parsing then become application responsibilities.
| Property | WebSocket | EventSource |
|---|---|---|
| Direction | Bi-directional – both client and server can exchange messages | Unidirectional – only server sends data |
| Data type | Binary and text data | Only text |
| Protocol | WebSocket protocol (ws://) | Regular HTTP (http://) |
| Browser API request shape | Custom handshake | EventSource issues GET; no request body and limited request-header control |
Creating an event source
const eventSource = new EventSource('/sse-stream');
Listening for events
// Fired when the connection is established.
eventSource.addEventListener('open', () => {
console.log('Connection opened');
});
// Fired when a message is received from the server.
eventSource.addEventListener('message', (event) => {
console.log('Received message:', event.data);
});
// Fired when an error occurs.
eventSource.addEventListener('error', (error) => {
console.error('Error occurred:', error);
});
Sending events from server
const express = require('express');
const app = express();
app.get('/sse-stream', (req, res) => {
// `Content-Type` need to be set to `text/event-stream`.
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Each message should be prefixed with data.
const sendEvent = (data) => res.write(`data: ${data}\n\n`);
sendEvent('Hello from server');
const intervalId = setInterval(() => sendEvent(new Date().toString()), 1000);
res.on('close', () => {
console.log('Client closed connection');
clearInterval(intervalId);
});
});
app.listen(3000, () => console.log('Server started on port 3000'));
In this example, the server sends a "Hello from server" message initially, and then sends the current date every second. The connection is kept alive until the client closes it.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, property flags and descriptors manage the behavior and attributes of object properties.
Property flags
Property flags are used to specify the behavior of a property on an object. Here are the available flags:
writable: Specifies whether the property can be written to.enumerable: Specifies whether the property is enumerable.configurable: Controls whether the property can be deleted and whether most parts of its descriptor can be reconfigured.Property descriptors
These provide detailed information about an object's property, including its value and flags. They are retrieved using Object.getOwnPropertyDescriptor() and set using Object.defineProperty().
The use cases of property descriptors are as follows:
writable: false to ensure data consistency.enumerable: false.configurable: false. A writable data property's value can still change, and writable can still change from true to false.Object.seal() or Object.freeze(). Both are shallow, and sealing still permits writes to writable data properties.Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
JavaScript object getters and setters are used to control access to an object's properties. They provide a way to encapsulate the implementation details of a property and define custom behavior when getting or setting its value.
Getters and setters are defined using the get and set keywords, respectively, followed by a function that is executed when the property is accessed or assigned a new value.
Here's a code example demonstrating the use of getters and setters:
const person = {
_name: 'John Doe', // Underscore is a naming convention, not privacy
get name() {
// Getter
return this._name;
},
set name(newName) {
// Setter
if (newName.trim().length > 0) {
this._name = newName;
} else {
console.log('Invalid name');
}
},
};
// Accessing the name property using the getter
console.log(person.name); // Output: 'John Doe'
// Setting the name property using the setter
person.name = 'Jane Smith'; // Setter is called
console.log(person.name); // Output: 'Jane Smith'
person.name = ''; // Setter is called, but the value is not set due to validation
console.log(person.name); // Output: 'Jane Smith'
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, a proxy is an object that acts as an intermediary between an object and the code. Proxies are used to intercept and customize the fundamental operations of JavaScript objects, such as property access, assignment, function invocation, and more.
Here's a basic example of using a Proxy to log every property access:
const myObject = {
name: 'John',
age: 42,
};
const handler = {
get: function (target, prop, receiver) {
console.log(`Someone accessed property "${prop}"`);
return Reflect.get(target, prop, receiver);
},
};
const proxiedObject = new Proxy(myObject, handler);
console.log(proxiedObject.name);
// Someone accessed property "name"
// 'John'
console.log(proxiedObject.age);
// Someone accessed property "age"
// 42
Use cases include:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Reproduce the failure reliably, reduce it to the smallest useful scenario, form a specific hypothesis, and inspect the program at the boundary where expected and actual behavior diverge. Use breakpoints and the call stack for control flow, the Network panel for request failures, source maps for transformed code, the Performance and Memory panels for measured performance problems, and framework-specific tools only when the failure is inside that framework's state or render model.
Prefer a debugger, conditional breakpoint, or logpoint over scattering permanent console.log() calls. Preserve the failing input and add a regression test after finding the cause.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
Workers in JavaScript are background threads that allow you to run scripts in parallel with the main execution thread, without blocking or interfering with the user interface. Their key features include:
postMessage() and onmessage/'message' event for messaging.fetch(), IndexedDB, Cache, Web Crypto, and timers. They do not expose the Window-only localStorage or sessionStorage APIs.There are three main types of workers in JavaScript:
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
JavaScript engines automatically reclaim objects that are no longer reachable from roots such as the current call stack, global objects, and live host objects. Modern engines combine tracing collectors with optimizations such as generations, incremental work, and compaction; the exact strategy is an engine implementation detail.
Mark-and-sweep
The most common garbage collection algorithm used in JavaScript is the Mark-and-sweep algorithm. It operates in two phases:
This algorithm effectively identifies and removes objects that have become unreachable, freeing up memory for new allocations.
Generational garbage collection
Used by modern JavaScript engines, objects are divided into different generations based on their age. Objects start in the young generation, and those that survive several collections are promoted to the old generation. This optimization reduces the overhead of garbage collection by focusing on the younger generation, where most objects are short-lived.
Garbage collection does not prevent memory leaks: a listener, timer, closure, DOM reference, or unbounded cache can keep data reachable even when the application no longer needs it. Diagnose a suspected leak by repeating the problematic action, comparing heap snapshots, and following retaining paths. Do not try to force garbage collection in normal application code.
Read the detailed answer on GreatFrontEnd which allows progress tracking, contains more code samples, and useful resources.
In JavaScript, data types can be categorized into primitive and non-primitive types:
Primitive data types
true or false values.Symbols.The remaining ECMAScript language type is Object. Arrays, functions, dates, regular expressions, maps, and sets are all kinds of objects rather than additional language types. Functions are callable objects and receive the special typeof result "function".
Use typeof for primitive categories and functions. Its possible results include "undefined", "boolean", "number", "bigint", "string", "symbol", "function", and "object". Because typeof null is historically "object" and arrays are objects, check those with value === null and Array.isArray(value). For specific object kinds, prefer purpose-built checks and be careful with instanceof across realms.
null, undefined or undeclared?| Trait | null | undefined | Undeclared |
|---|---|---|---|
| Meaning | Explicitly set by the developer to indicate that a variable has no value | Variable has been declared but not assigned a value | Variable has not been declared at all |
Type (via typeof operator) | 'object' | 'undefined' | 'undefined' |
| Direct access/comparison | Can compare with value === null | Can compare with value === undefined | Direct access throws ReferenceError; typeof undeclaredName returns 'undefined' |
let, var or const?In JavaScript, let, var, and const are all keywords used to declare variables, but they differ significantly in terms of scope, initialization rules, whether they can be redeclared or reassigned, and the behavior when they are accessed before declaration:
| Behavior | var | let | const |
|---|---|---|---|
| Scope | Function or Global | Block | Block |
| Initialization | Optional | Optional | Required |
| Redeclaration | Yes | No | No |
| Reassignment | Yes | Yes | No |
| Accessing before declaration | undefined | ReferenceError | ReferenceError |
JavaScript that is executed in the browser has access to the global scope (the window object). In general it's a good software engineering practice to not pollute the global namespace unless you are working on a feature that truly needs to be global – it is needed by the entire page. Several reasons to avoid touching the global scope:
Follow these best practices to avoid global scope pollution:
var, let, or const to limit their scope.Use Number(value) when the whole string must represent a number. Use parseInt(value, radix) or parseFloat(value) when intentionally accepting a numeric prefix such as '12px'; they stop at the first invalid character. Check the result with Number.isNaN() or, for finite application values, Number.isFinite(). Remember that Number('') and Number(' ') are 0, so validate required input before conversion.
Template literals are a feature in JavaScript that allow for easier string interpolation and multi-line strings. They are enclosed by backticks (`) instead of single or double quotes. You can embed expressions within template literals using ${expression} syntax.
Example:
const myName = 'John';
const greeting = `Hello, ${myName}!`;
console.log(greeting); // Output: Hello, John!
Tagged templates in JavaScript allow you to parse template literals with a function. The function receives the literal strings and the values as arguments, enabling custom processing of the template. For example:
function tag(strings, ...values) {
return strings[0] + values[0] + strings[1] + values[1] + strings[2];
}
const result = tag`Hello ${'world'}! How are ${'you'}?`;
console.log(result); // "Hello world! How are you?"
The spread operator, represented by three dots (...), is used in JavaScript to expand iterable objects like arrays or strings into individual elements. It can also be used to spread object properties. For example, you can use it to combine arrays, copy arrays, or pass array elements as arguments to a function.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combinedObj = { ...obj1, ...obj2 };
console.log(combinedObj); // { a: 1, b: 2, c: 3, d: 4 }
Symbols used for in JavaScript?Symbols are primitive values commonly used as collision-resistant object property keys and protocol hooks such as Symbol.iterator. Each call to Symbol(description) creates a distinct value, while Symbol.for(key) uses a shared registry. Symbol-keyed properties are skipped by for...in, Object.keys(), and JSON serialization, but they are not private: code can retrieve them with Object.getOwnPropertySymbols() or Reflect.ownKeys().
let sym1 = Symbol();
let sym2 = Symbol('myKey');
console.log(typeof sym1); // "symbol"
console.log(sym1 === sym2); // false, because each symbol is unique
let obj = {};
let sym = Symbol('uniqueKey');
obj[sym] = 'value';
console.log(obj[sym]); // "value"
Note: The Symbol() function must be called without the new keyword. It is not exactly a constructor because it can only be called as a function instead of with new Symbol().
In JavaScript, a proxy is an object that acts as an intermediary between an object and the code. Proxies are used to intercept and customize the fundamental operations of JavaScript objects, such as property access, assignment, function invocation, and more.
Here's a basic example of using a Proxy to log every property access:
const myObject = {
name: 'John',
age: 42,
};
const handler = {
get: function (target, prop, receiver) {
console.log(`Someone accessed property "${prop}"`);
return Reflect.get(target, prop, receiver);
},
};
const proxiedObject = new Proxy(myObject, handler);
console.log(proxiedObject.name);
// Someone accessed property "name"
// 'John'
console.log(proxiedObject.age);
// Someone accessed property "age"
// 42
Use cases include:
"Hoisting" is informal shorthand for how JavaScript creates and initializes bindings while it instantiates a scope, before evaluating that scope's statements. The engine does not move source text.
var): The binding is created and initialized to undefined before statements run. Its assignment still happens at the declaration's source location.let and const): The binding is created but remains uninitialized in the temporal dead zone (TDZ) until evaluation reaches the declaration. Accessing it earlier throws ReferenceError.var): The var binding initially contains undefined; the function is created only when the expression is evaluated.function): The binding is initialized with the function during scope setup, so it can be called before its declaration appears in source order.class): The binding exists during scope setup but remains in the TDZ until the class declaration is evaluated.import): Imported bindings are linked before module evaluation. Dependencies normally evaluate before the importing module's body, although cycles can expose an uninitialized binding.The following behavior summarizes the result of accessing the variables before they are declared.
| Declaration | Accessing before declaration |
|---|---|
var foo | undefined |
let foo | ReferenceError |
const foo | ReferenceError |
class Foo | ReferenceError |
var foo = function() { ... } | undefined |
function foo() { ... } | Normal |
import | Normal, except some cycles |
var, let, and constBefore a scope's statements run, JavaScript creates its var, let, and const bindings; it does not move source text. A var binding is initialized to undefined, so it can be read before its declaration appears. A let or const binding remains uninitialized in the temporal dead zone (TDZ), so reading it before evaluation reaches the declaration throws ReferenceError. const also requires an initializer and cannot be reassigned.
Function declarations are initialized when JavaScript instantiates their scope, so they can be called before their source declaration. Function expressions are created only when evaluation reaches the expression. Before then, a var binding contains undefined and calling it throws TypeError; a let or const binding is in the temporal dead zone and accessing it throws ReferenceError. "Hoisting" describes these binding rules; the engine does not move source text.
// Function declaration
console.log(foo()); // Works fine
function foo() {
return 'Hello';
}
// Function expression
console.log(bar()); // Throws TypeError: bar is not a function
var bar = function () {
return 'Hello';
};
JavaScript creates bindings before executing a scope's statements, but initializes different declaration forms differently. An early read of var produces undefined; an early read of let, const, or class throws because the binding is in the temporal dead zone; and function declarations are already callable. These differences—not literal source-code movement—can produce confusing bugs.
console.log(a); // undefined
var a = 5;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
Use const by default and let when reassignment is required, keep declarations close to their first use, and never read a binding before it is initialized. let and const are still hoisted—their bindings exist from the start of the block—but they remain in the temporal dead zone until evaluation reaches the declaration. Static-analysis rules such as no-use-before-define and no-undef catch the risky patterns.
// Use let or const
let x = 10;
const y = 20;
console.log(x, y); // Output: 10 20
// Declare functions before calling them
function myFunction() {
console.log('Hello, world!');
}
myFunction(); // Output: 'Hello, world!'
== and === in JavaScript?== is the abstract equality operator while === is the strict equality operator. == performs type coercion before comparing, following the Abstract Equality Comparison algorithm defined in the ECMAScript specification. === does not perform coercion and returns false whenever the operand types differ. === is generally preferred in application code because it eliminates a class of bugs caused by unexpected coercion. The most common exception is x == null, which checks for both null and undefined in a single comparison.
| Operator | == | === |
|---|---|---|
| Name | Loose (abstract) equality operator | Strict equality operator |
| Type coercion | Yes — per the Abstract Equality Comparison algorithm | No |
| Comparison behavior | Types may be coerced before the value comparison | Types are compared first |
Don't confuse
=with==and===.=is the assignment operator — it sets a variable's value (x = 5) and does not compare anything.
There are multiple ways to iterate over object properties as well as arrays in JavaScript:
for...in loop
The for...in loop iterates over all enumerable properties of an object, including inherited enumerable properties. So it is important to have a check if you only want to iterate over the object's own properties.
const obj = {
a: 1,
b: 2,
c: 3,
};
for (const key in obj) {
// To avoid iterating over inherited properties
if (Object.hasOwn(obj, key)) {
console.log(`${key}: ${obj[key]}`);
}
}
Object.keys()
Object.keys() returns an array of the object's own enumerable property names. You can then use a for...of loop or forEach to iterate over this array.
const obj = {
a: 1,
b: 2,
c: 3,
};
Object.keys(obj).forEach((key) => {
console.log(`${key}: ${obj[key]}`);
});
The most common ways to iterate over an array are using a for loop and the Array.prototype.forEach method.
Using for loop
let array = [1, 2, 3, 4, 5, 6];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
Using Array.prototype.forEach method
let array = [1, 2, 3, 4, 5, 6];
array.forEach((number, index) => {
console.log(`${number} at index ${index}`);
});
Using for...of
This method is the newest and most convenient way to iterate over arrays. It automatically iterates over each element without requiring you to manage the index.
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
There are also other built-in methods available which are suitable for specific scenarios, for example:
Array.prototype.filter: You can use the filter method to create a new array containing only the elements that satisfy a certain condition.Array.prototype.map: You can use the map method to create a new array based on the existing one, transforming each element with a provided function.Array.prototype.reduce: You can use the reduce method to combine all elements into a single value by repeatedly calling a function that takes two arguments: the accumulated value and the current element.break and continue statements?The break statement is used to exit a loop or switch statement prematurely, while the continue statement skips the current iteration of a loop and proceeds to the next iteration. For example, in a for loop, break will stop the loop entirely, and continue will skip to the next iteration.
for (let i = 0; i < 10; i++) {
if (i === 5) break; // exits the loop when i is 5
console.log(i);
}
for (let i = 0; i < 10; i++) {
if (i === 5) continue; // skips the iteration when i is 5
console.log(i);
}
The ternary operator is a shorthand for an if-else statement in JavaScript. It takes three operands: a condition, a result for true, and a result for false. The syntax is condition ? expr1 : expr2. For example, let result = (a > b) ? 'a is greater' : 'b is greater'; assigns 'a is greater' to result if a is greater than b, otherwise it assigns 'b is greater'.
To access the index of an element in an array during iteration, you can use methods like forEach, map, for...of with entries, or a traditional for loop. For example, using forEach:
const array = ['a', 'b', 'c'];
array.forEach((element, index) => {
console.log(index, element);
});
switch statement?The switch statement is used to execute one block of code among many based on the value of an expression. It is an alternative to using multiple if...else if statements. The switch statement evaluates an expression, matches the expression's value to a case label, and executes the associated block of code. If no case matches, the default block is executed.
switch (expression) {
case value1:
// code to be executed if expression === value1
break;
case value2:
// code to be executed if expression === value2
break;
default:
// code to be executed if no case matches
}
Rest parameters in JavaScript allow a function to accept an indefinite number of arguments as an array. They are denoted by three dots (...) followed by the name of the array. This feature is useful for functions that need to handle multiple arguments without knowing the exact number in advance.
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // Output: 10
The spread operator (...) in JavaScript expands the elements of an iterable (like an array) into individual elements, and copies the own enumerable properties of an object into a new object. It is commonly used for copying arrays or objects, merging arrays or objects, and passing elements of an array as arguments to a function.
// Copying an array
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
console.log(arr2); // Output: [1, 2, 3]
// Merging arrays
const arr3 = [4, 5, 6];
const mergedArray = [...arr1, ...arr3];
console.log(mergedArray); // Output: [1, 2, 3, 4, 5, 6]
// Copying an object
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1 };
console.log(obj2); // Output: { a: 1, b: 2 }
// Merging objects
const obj3 = { c: 3, d: 4 };
const mergedObject = { ...obj1, ...obj3 };
console.log(mergedObject); // Output: { a: 1, b: 2, c: 3, d: 4 }
// Passing array elements as function arguments
const sum = (x, y, z) => x + y + z;
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // Output: 6
Spread syntax (...) allows an iterable (like an array or string) to be expanded into individual elements. This is often used as a convenient and modern way to create new arrays or objects by combining existing ones.
| Operation | Traditional | Spread |
|---|---|---|
| Array cloning | arr.slice() | [...arr] |
| Array merging | arr1.concat(arr2) | [...arr1, ...arr2] |
| Object cloning | Object.assign({}, obj) | { ...obj } |
| Object merging | Object.assign({}, obj1, obj2) | { ...obj1, ...obj2 } |
Rest syntax is the opposite of what spread syntax does. It collects a variable number of arguments into an array. This is often used in function parameters to handle a dynamic number of arguments.
// Using rest syntax in a function
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Output: 6
In JavaScript, iterators and generators are tools for managing sequences of data and controlling the flow of execution in a more flexible way.
Iterators are objects that define a sequence and potentially a return value upon its termination. They adhere to a specific interface:
next() method.next() method returns an object with two properties:
value: The next value in the sequence.done: A boolean that is true if the iterator has finished its sequence, otherwise false.Here's an example of an object implementing the iterator interface.
const iterator = {
current: 0,
last: 5,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
let result = iterator.next();
while (!result.done) {
console.log(result.value); // Logs 0, 1, 2, 3, 4, 5
result = iterator.next();
}
Generators are special functions that can pause execution and resume at a later point. They use the function* syntax and the yield keyword to control the flow of execution. When you call a generator function, it doesn't execute completely like a regular function. Instead, it returns an iterator object. Calling the next() method on the returned iterator advances the generator to the next yield statement, and the value after yield becomes the return value of next().
function* numberGenerator() {
let num = 0;
while (num <= 5) {
yield num++;
}
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Generators are useful for creating iterators on demand, especially for infinite sequences or complex iteration logic. They can be used for:
foo between function foo() {} and var foo = function() {} in JavaScriptfunction foo() {} is a function declaration while var foo = function() {} is a function expression. The key difference is that function declarations have their bodies hoisted but the bodies of function expressions are not (they have the same hoisting behavior as var-declared variables).
If you try to invoke a function expression before it is declared, you will get an Uncaught TypeError: XXX is not a function error.
Function declarations can be called in the enclosing scope even before they are declared.
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
Function expressions if called before they are declared will result in an error.
foo(); // Uncaught TypeError: foo is not a function
var foo = function () {
console.log('FOOOOO');
};
Another key difference is in the scope of the function name. Function expressions can be named by defining a name after the function keyword and before the parentheses. However, when using named function expressions, the function name is only accessible within the function itself. Trying to access it outside will result in a ReferenceError.
const myFunc = function namedFunc() {
console.log(namedFunc); // Works
};
myFunc(); // Runs the function and logs the function reference
console.log(namedFunc); // ReferenceError: namedFunc is not defined
Note: The examples use var due to legacy reasons. Function expressions can be defined using let and const, and the key difference is in the hoisting behavior of those keywords.
A parameter is a variable in the declaration of a function, while an argument is the actual value passed to the function when it is called. For example, in the function function add(a, b) { return a + b; }, a and b are parameters. When you call add(2, 3), 2 and 3 are arguments.
"Hoisting" describes the observable result of declarations being instantiated before a scope's statements execute; the engine does not move source text. A function declaration's binding is initialized with the function during scope setup, so it can be called earlier in that scope. A function or arrow expression is created only when evaluation reaches the expression. Its variable binding follows the rules for var, let, or const.
// Function declaration
hoistedFunction(); // Works fine
function hoistedFunction() {
console.log('This function is hoisted');
}
// Function expression
nonHoistedFunction(); // Throws an error
var nonHoistedFunction = function () {
console.log('This function is not hoisted');
};
.call and .apply in JavaScript?.call and .apply are both used to invoke functions with a specific this context and arguments. The primary difference lies in how they accept arguments:
.call(thisArg, arg1, arg2, ...): Takes arguments individually..apply(thisArg, [argsArray]): Takes arguments as an array.Assuming we have a function add, the function can be invoked using .call and .apply in the following manner:
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3
Arrow functions provide concise syntax and capture this and arguments from their surrounding scope. They work well for array transformations and for callbacks created inside a method or constructor that need the surrounding receiver. An arrow used directly as an object method does not receive the object as this; use a regular method when the caller's receiver should determine this. Arrow functions also cannot be called with new, do not have a prototype, and cannot be generators.
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
console.log(doubled); // [2, 4, 6]
function Person(){}, const person = Person(), and const person = new Person() in JavaScript?function Person(){}: A function declaration in JavaScript. It can be used as a regular function or as a constructor.const person = Person(): Calls Person as a regular function, not a constructor. If Person is intended to be a constructor, this will lead to unexpected behavior.const person = new Person(): Creates a new instance of Person, correctly using the constructor function to initialize the new object.| Aspect | function Person(){} | const person = Person() | const person = new Person() |
|---|---|---|---|
| Type | Function declaration | Function call | Constructor call |
| Usage | Defines a function | Invokes Person as a regular function | Creates a new instance of Person |
| Instance Creation | No instance created | No instance created | New instance created |
| Common Mistake | N/A | Misusing as constructor leading to undefined | None (when used correctly) |
A higher-order function is any function that takes one or more functions as arguments, which it uses to operate on some data, and/or returns a function as a result.
Higher-order functions are meant to abstract some operation that is performed repeatedly. The classic example of this is Array.prototype.map(), which takes an array and a function as arguments. Array.prototype.map() then uses this function to transform each item in the array, returning a new array with the transformed data. Other popular examples in JavaScript are Array.prototype.forEach(), Array.prototype.filter(), and Array.prototype.reduce(). A higher-order function doesn't just need to be manipulating arrays as there are many use cases for returning a function from another function. Function.prototype.bind() is an example that returns another function.
Imagine a scenario where we have an array of names that we need to transform to uppercase. The imperative way will be as such:
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
Using Array.prototype.map(transformerFn) makes the code shorter and more declarative.
const names = ['irish', 'daisy', 'anna'];
function transformNamesToUppercase(names) {
return names.map((name) => name.toUpperCase());
}
console.log(transformNamesToUppercase(names)); // ['IRISH', 'DAISY', 'ANNA']
A callback is a function supplied to other code to be invoked according to that API's contract. Some callbacks run synchronously, such as an Array.prototype.map() callback; others run later, such as timer, event, or I/O callbacks. Passing a callback does not by itself make an operation asynchronous.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John Doe' };
callback(null, data);
}, 0);
}
function handleData(error, data) {
if (error) throw error;
console.log(data);
}
fetchData(handleData);
An anonymous function in JavaScript is a function that does not have any name associated with it. They are typically used as arguments to other functions or assigned to variables.
const arr = [-1, 0, 5, 6];
// The filter method is passed an anonymous function.
arr.filter((x) => x > 1); // [5, 6]
They are often used as arguments to other functions, known as higher-order functions, which can take functions as input and return a function as output. Anonymous functions can access variables from the outer scope, a concept known as closures, allowing them to "close over" and remember the environment in which they were created.
// Encapsulating Code
(function () {
// Some code here.
})();
// Callbacks
setTimeout(function () {
console.log('Hello world!');
}, 1000);
// Functional programming constructs
const arr = [1, 2, 3];
const double = arr.map(function (el) {
return el * 2;
});
console.log(double); // [2, 4, 6]
Recursion solves a problem by calling the same function on smaller inputs until a base case is reached. It naturally fits recursive structures such as trees and divide-and-conquer algorithms, but each ordinary call consumes stack space. Validate that the input moves toward the base case, and use an explicit stack or loop when depth can be large or user-controlled.
function factorial(n) {
if (!Number.isInteger(n) || n < 0) {
throw new RangeError('n must be a non-negative integer');
}
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(4)); // Output: 24
Default parameters supply a value when an argument is omitted or explicitly undefined; they do not apply to null, 0, false, or ''. The expression is evaluated at call time, so it can use earlier parameters and create a fresh object per call. Use defaults for genuine optional arguments, but validate values separately when null or another sentinel is invalid.
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
greet(); // Output: Hello, Guest!
greet('Alice'); // Output: Hello, Alice!
function foo(){}();. What needs to be changed to properly make it an IIFE?The code function foo(){}(); doesn't work as an Immediately Invoked Function Expression (IIFE) because the JavaScript parser treats function foo(){} as a function declaration, not an expression. To make it an IIFE, you need to wrap the function in parentheses to turn it into a function expression: (function foo(){})();.
Creating objects in JavaScript offers several methods:
{}): Simplest and most popular approach. Define key-value pairs within curly braces.Object() constructor: Use new Object() with dot notation to add properties.Object.create(): Create new objects using existing objects as prototypes, inheriting properties and methods.new.class and constructor keywords.Dot notation and bracket notation are two ways to access properties of an object in JavaScript. Dot notation is more concise and readable but can only be used with valid JavaScript identifiers. Bracket notation is more flexible and can be used with property names that are not valid identifiers, such as those containing spaces or special characters.
const obj = { name: 'Alice', 'favorite color': 'blue' };
// Dot notation
console.log(obj.name); // Alice
// Bracket notation
console.log(obj['favorite color']); // blue
Choose an iteration construct by the result and control flow you need: map() transforms into a new array, filter() selects, reduce() combines, some() / every() answer conditions, and find() locates one item. Use for...of or an indexed for loop when you need break, continue, sequential await, or precise index control. Use forEach() for synchronous side effects only; it ignores returned Promises and cannot be stopped early.
For in-place changes, use push() / unshift() to add, pop() / shift() to remove at the ends, splice() for arbitrary ranges, and index assignment to update. These mutate the original array. When callers rely on immutable updates, create a new array with spread, slice(), filter(), map(), toSpliced(), or with() instead. Choose based on ownership, not a universal performance rule.
let arr = [1, 2, 3];
// Add elements
arr.push(4); // [1, 2, 3, 4]
arr.unshift(0); // [0, 1, 2, 3, 4]
arr.splice(2, 0, 1.5); // [0, 1, 1.5, 2, 3, 4]
// Remove elements
arr.pop(); // [0, 1, 1.5, 2, 3]
arr.shift(); // [1, 1.5, 2, 3]
arr.splice(1, 1); // [1, 2, 3]
// Update elements
arr[1] = 5; // [1, 5, 3]
console.log(arr); // Final state: [1, 5, 3]
Note: If you try to console.log(arr) after each operation in some environments (like Chrome DevTools), you may only see the final state of arr. This happens because the console sometimes keeps a live reference to the array instead of logging its state at the exact moment. To see intermediate states properly, store snapshots using console.log([...arr]) or print values immediately after each operation.
Use spread syntax or Object.assign() for a shallow copy. Use structuredClone() for values supported by the structured clone algorithm, including nested arrays and objects, cycles, Map, Set, and many built-in types. A JSON stringify/parse round trip is only a lossy conversion for JSON-shaped data, not a general deep-clone algorithm. A library or domain-specific copy routine may be needed for custom classes and semantics.
// Shallow copy of an array
const originalArray = [1, 2, 3];
const shallowCopyArray = [...originalArray];
console.log(shallowCopyArray); // [1, 2, 3]
// Shallow copy of an object
const originalObject = { a: 1, b: 2 };
const shallowCopyObject = { ...originalObject };
console.log(shallowCopyObject); // { a: 1, b: 2 }
// Deep copy using JSON methods
const deepCopyObject = JSON.parse(JSON.stringify(originalObject));
console.log(deepCopyObject); // { a: 1, b: 2 }
A shallow copy duplicates the top-level properties of an object, but nested objects are still referenced. A deep copy duplicates all levels of an object, creating entirely new instances of nested objects. Object.assign() and the spread operator (...) create shallow copies. structuredClone() is the modern built-in for deep copies. JSON.parse(JSON.stringify()) and Lodash's _.cloneDeep are other common approaches, each with different tradeoffs around which values they can faithfully clone.
// Shallow copy — nested object is shared
let obj1 = { a: 1, b: { c: 2 } };
let shallowCopy = { ...obj1 };
shallowCopy.b.c = 3;
console.log(obj1.b.c); // 3 — original mutated too
// Deep copy — fully independent
let obj2 = { a: 1, b: { c: 2 } };
let deepCopy = structuredClone(obj2);
deepCopy.b.c = 4;
console.log(obj2.b.c); // 2 — original unchanged
The spread operator (...) in JavaScript allows you to easily copy arrays and objects, merge them, and add new elements or properties. It simplifies syntax and improves readability. For arrays, it can be used to concatenate or clone arrays. For objects, it can be used to merge objects or add new properties.
// Arrays
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]
// Objects
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };
console.log(obj2); // { a: 1, b: 2, c: 3 }
Use the in operator when inherited properties should count, and Object.hasOwn() when only the object's own properties should count. Avoid calling obj.hasOwnProperty() directly because the object can shadow that method or have a null prototype.
const obj = { key: 'value' };
// Using the `in` operator
if ('key' in obj) {
console.log('Property exists');
}
// Checking only own properties
if (Object.hasOwn(obj, 'key')) {
console.log('Property exists');
}
Mutable objects allow for modification of properties and values after creation, which is the default behavior for most objects.
const mutableObject = {
name: 'John',
age: 30,
};
// Modify the object
mutableObject.name = 'Jane';
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }
Immutable values cannot be changed after creation. JavaScript objects and arrays are mutable by default; an application can enforce shallow immutability with Object.freeze() or follow an immutable-update convention that creates a new object instead of mutating the existing one.
const immutableObject = Object.freeze({
name: 'John',
age: 30,
});
// Attempt to modify the object
immutableObject.name = 'Jane';
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }
Object.freeze() is shallow, so nested objects remain mutable unless they are frozen separately. Failed writes throw in strict mode and otherwise usually fail silently.
Destructuring assignment is a syntax in JavaScript that allows you to unpack values from arrays or properties from objects into distinct variables. For arrays, you use square brackets, and for objects, you use curly braces. For example:
// Array destructuring
const [a, b] = [1, 2];
// Object destructuring
const { name, age } = { name: 'John', age: 30 };
Object.freeze() for?Object.freeze() makes an object non-extensible and changes its own properties so they cannot be removed or reconfigured; data properties also become non-writable. This is a shallow operation: nested objects and internal state such as a Map's entries can still change. Invalid writes throw in strict mode and otherwise usually fail silently.
const obj = { name: 'John' };
Object.freeze(obj);
obj.name = 'Doe'; // This will not change the name property
console.log(obj); // { name: 'John' }
Object.seal() for?Object.seal() prevents extensions and makes every existing own property non-configurable. Existing data-property values can still change when their descriptors are writable. The operation is shallow: nested objects are unaffected. Invalid additions and deletions throw in strict mode and may fail silently otherwise. Sealing is useful for catching accidental shape changes, not for enforcing authorization or protecting secrets.
// 'use strict'
const obj = { name: 'John' };
Object.seal(obj);
obj.name = 'Jane'; // Allowed
obj.age = 30; // Not allowed, throws an error in strict mode
delete obj.name; // Not allowed, throws an error in strict mode
console.log(obj); // { name: 'Jane' } (age was not added, name was not deleted)
Object.preventExtensions() for?Object.preventExtensions(object) makes that object non-extensible: new own properties cannot be added and its prototype cannot be changed, while existing properties may still be changed or deleted according to their descriptors. The operation is shallow and irreversible for that object. In strict mode, invalid additions throw; otherwise ordinary assignment may fail silently.
const obj = { name: 'John' };
Object.preventExtensions(obj);
obj.age = 30; // This will not work, as the object is not extensible
console.log(obj.age); // undefined
JavaScript object getters and setters are used to control access to an object's properties. They provide a way to encapsulate the implementation details of a property and define custom behavior when getting or setting its value.
Getters and setters are defined using the get and set keywords, respectively, followed by a function that is executed when the property is accessed or assigned a new value.
Here's a code example demonstrating the use of getters and setters:
const person = {
_name: 'John Doe', // Underscore is a naming convention, not privacy
get name() {
// Getter
return this._name;
},
set name(newName) {
// Setter
if (newName.trim().length > 0) {
this._name = newName;
} else {
console.log('Invalid name');
}
},
};
// Accessing the name property using the getter
console.log(person.name); // Output: 'John Doe'
// Setting the name property using the setter
person.name = 'Jane Smith'; // Setter is called
console.log(person.name); // Output: 'Jane Smith'
person.name = ''; // Setter is called, but the value is not set due to validation
console.log(person.name); // Output: 'Jane Smith'
In JavaScript, property flags and descriptors manage the behavior and attributes of object properties.
Property flags
Property flags are used to specify the behavior of a property on an object. Here are the available flags:
writable: Specifies whether the property can be written to.enumerable: Specifies whether the property is enumerable.configurable: Controls whether the property can be deleted and whether most parts of its descriptor can be reconfigured.Property descriptors
These provide detailed information about an object's property, including its value and flags. They are retrieved using Object.getOwnPropertyDescriptor() and set using Object.defineProperty().
The use cases of property descriptors are as follows:
writable: false to ensure data consistency.enumerable: false.configurable: false. A writable data property's value can still change, and writable can still change from true to false.Object.seal() or Object.freeze(). Both are shallow, and sealing still permits writes to writable data properties.First define what “empty” means. For the common case “no own enumerable string-keyed properties,” use Object.keys(obj).length === 0. If symbol or non-enumerable own keys should count, use Reflect.ownKeys(obj).length === 0. Validate the input separately if null, primitives, arrays, maps, or sets are not valid inputs.
const isEmpty = (obj) => Object.keys(obj).length === 0;
const obj = {};
console.log(isEmpty(obj)); // true
The event loop is the host runtime's scheduling mechanism for coordinating JavaScript jobs, asynchronous operations, and—in browsers—rendering. A simplified browser turn works like this:
queueMicrotask() callbacks, and other microtasks, including microtasks added while the checkpoint is running.Synchronous code runs to completion on the current call stack before later statements can run. Asynchronous APIs arrange for a result to be handled later through a callback, promise, or event, allowing the current stack to finish while the host waits for I/O or a timer. Asynchronous does not mean “runs on another thread”: an async function runs synchronously until its first suspension point, and CPU-heavy JavaScript still blocks its thread.
function sum(a, b) {
console.log('Inside sum function');
return a + b;
}
const result = sum(2, 3); // The program waits for sum() to complete before assigning the result
console.log('Result: ', result); // Output: 5
Asynchronous APIs commonly expose callbacks, promises, or events. Once the operation can make progress, its continuation is scheduled according to the host's event loop. This works especially well for I/O such as network and database requests; CPU-intensive work must instead be split up or moved to a worker to keep a browser UI responsive.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data); // Calling the callback function with data
}, 2000); // Simulating a 2-second delay
}
console.log('Fetching data...');
fetchData((data) => {
console.log(data); // Output: { name: 'John', age: 30 } (after 2 seconds)
});
console.log('Call made to fetch data'); // This will print before the data is fetched
A callback is a function passed to another API to invoke later or during its operation. In asynchronous APIs, the contract must define when and how often it runs, how errors are reported, and how to cancel or unsubscribe. Scheduling a callback lets the current stack continue, but the callback's own JavaScript still occupies the thread when it eventually runs.
function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data);
});
Promises represent the eventual outcome of an asynchronous operation. They start pending and settle once as fulfilled with a value or rejected with a reason. .then(), .catch(), and .finally() return new Promises, so returned values and thrown errors flow through a chain. Reactions run as microtasks even when the original Promise is already settled.
A Promise coordinates an outcome; it does not automatically start work on another thread or cancel the underlying operation. Expose an AbortSignal or another explicit cancellation mechanism when work can be stopped.
let promise = new Promise((resolve, reject) => {
// asynchronous operation
const success = true;
if (success) {
resolve('Success!');
} else {
reject(new Error('Operation failed'));
}
});
promise
.then((result) => {
console.log(result); // 'Success!' (this will print)
})
.catch((error) => {
console.error(error); // 'Error!'
});
A Promise has three mutually exclusive states:
Error.Fulfilled and rejected Promises are settled. Once settled, a Promise cannot change state; later attempts to fulfill or reject it are ignored. “Resolved” is not always synonymous with “fulfilled”: a Promise can be resolved to another still-pending Promise and adopt its eventual state.
Promises standardize one eventual outcome and make sequential, parallel, and error flows composable with .then(), async/await, and combinators such as Promise.all(). They avoid many callback-contract ambiguities, but they do not cancel work, represent repeated events, or guarantee settlement. A Promise chain can still become unreadable or leak an unhandled rejection when callers forget to return or await it.
Promise.all()Promise.all() accepts an iterable of promises or values and returns a promise fulfilled with results in input order once every input fulfills. It rejects as soon as an input rejects. It does not start the operations or cancel the remaining work; create the promises first, and use AbortController or an operation-specific mechanism when cancellation is required.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values); // [3, 42, 'foo']
});
Promise.all() different from Promise.allSettled()?Promise.all() and Promise.allSettled() are both methods for handling multiple promises in JavaScript, but they behave differently. Promise.all() waits for all promises to resolve and fails fast if any promise rejects, returning a single rejected promise. Promise.allSettled(), on the other hand, waits for all promises to settle (either resolve or reject) and returns an array of objects describing the outcome of each promise.
async/await is a modern syntax in JavaScript that simplifies working with promises. By using the async keyword before a function, you can use the await keyword inside that function to pause execution until a promise is resolved. This makes asynchronous code look and behave more like synchronous code, making it easier to read and maintain.
async function fetchData() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/1',
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData();
Handle promise rejections with try...catch around await or with .catch() on a promise chain. Remember that Fetch fulfills its promise for HTTP errors such as 404 and 500, so check response.ok and throw an application error yourself.
async function fetchData() {
try {
// This request will fail (example domain)
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
}
fetchData(); // Error fetching data: ....
With Promises, you can use the .catch() method:
fetch('https://api.example.com/data') // This request will fail (example domain)
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error('Error fetching data:', error));
The microtask queue holds callbacks such as promise reactions, queueMicrotask() callbacks, and MutationObserver notifications. At a microtask checkpoint—normally after the current task or callback finishes and the JavaScript stack is empty—the runtime drains the queue before it may render and select another task. Microtasks queued by other microtasks are drained in the same checkpoint, so recursively adding them can starve rendering and other work.
setTimeout(), setImmediate(), and process.nextTick()?In Node.js, setTimeout() schedules a timer callback after a minimum delay, setImmediate() schedules a callback for the event loop's check phase after I/O callbacks, and process.nextTick() queues work to be drained after the current JavaScript operation before the event loop continues. Node now marks process.nextTick() as legacy and recommends queueMicrotask() for most userland deferral.
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
In this example, process.nextTick() will execute first, followed by either setTimeout() or setImmediate() depending on the environment.
Prototypal inheritance is property lookup by delegation. An object's internal [[Prototype]] is another object or null; when an own property is missing, lookup continues along that chain. Inspect it with Object.getPrototypeOf() and create a deliberate link with Object.create() or class / extends. Avoid the legacy __proto__ accessor in application code, and avoid changing prototypes of existing objects in hot code because it can invalidate engine optimizations.
When a property is accessed on an object and if the property is not found on that object, the JavaScript engine looks at the object's __proto__, and the __proto__'s __proto__ and so on, until it finds the property defined on one of the __proto__s or until it reaches the end of the prototype chain.
This behavior simulates classical inheritance, but it is really more of delegation than inheritance.
Here's an example of prototypal inheritance:
// Parent object constructor.
function Animal(name) {
this.name = name;
}
// Add a method to the parent object's prototype.
Animal.prototype.makeSound = function () {
console.log('The ' + this.constructor.name + ' makes a sound.');
};
// Child object constructor.
function Dog(name) {
Animal.call(this, name); // Call the parent constructor.
}
// Set the child object's prototype to be the parent's prototype.
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// Add a method to the child object's prototype.
Dog.prototype.bark = function () {
console.log('Woof!');
};
// Create a new instance of Dog.
const bolt = new Dog('Bolt');
// Call methods on the child object.
console.log(bolt.name); // "Bolt"
bolt.makeSound(); // "The Dog makes a sound."
bolt.bark(); // "Woof!"
Things to note are:
.makeSound is not defined on Dog, so the JavaScript engine goes up the prototype chain and finds .makeSound on the inherited Animal.class and extends are the preferred way to set up inheritance; they are syntactic sugar over this prototype mechanism.The prototype chain is a mechanism in JavaScript that allows objects to inherit properties and methods from other objects. When you try to access a property on an object, JavaScript will first look for the property on the object itself. If it doesn't find it, it will look at the object's prototype, and then the prototype's prototype, and so on, until it either finds the property or reaches the end of the chain, which is null.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person('Alice');
alice.greet(); // "Hello, my name is Alice"
In this example, alice inherits the greet method from Person.prototype.
Classical inheritance is a model where classes inherit from other classes, typically seen in languages like Java and C++. Prototypal inheritance, used in JavaScript, involves objects inheriting directly from other objects. In classical inheritance, you define a class and create instances from it. In prototypal inheritance, you create an object and use it as a prototype for other objects.
JavaScript class inheritance uses extends to link constructor and prototype chains. A derived constructor must call super() before accessing this, and super.method() invokes parent behavior. Use inheritance when the child is genuinely substitutable for the parent and the contract is stable. Prefer composition for optional capabilities or when subclasses would need to override many unrelated methods.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex', 'German Shepherd');
dog.speak(); // Rex barks.
new keyword?Calling a constructable function with new creates a fresh object, links its prototype to the constructor's prototype, calls the constructor with this set to that object, and normally returns the object. If the constructor explicitly returns another non-primitive object, that returned object wins; an explicit primitive return is ignored. Arrow functions and some other callables are not constructable.
function Person(name) {
this.name = name;
}
const person1 = new Person('Alice');
console.log(person1.name); // Alice
To create a constructor function in JavaScript, define a regular function with a capitalized name to indicate it's a constructor. Use the this keyword to set properties and methods. When creating an instance, use the new keyword.
function Person(name, age) {
this.name = name;
this.age = age;
}
const john = new Person('John', 30);
console.log(john.age); // 30
ES2015 introduces a new way of creating classes, which provides a more intuitive and concise way to define and work with objects and inheritance compared to the ES5 function constructor syntax. Here's an example of each:
// ES5 function constructor
function Person(name) {
this.name = name;
}
// ES2015 Class
class Person {
constructor(name) {
this.name = name;
}
}
For simple constructors, they look pretty similar. The main difference in the constructor comes when using inheritance. If we want to create a Student class that subclasses Person and adds a studentId field, this is what we have to do.
// ES5 inheritance
// Superclass
function Person1(name) {
this.name = name;
}
// Subclass
function Student1(name, studentId) {
// Call constructor of superclass to initialize superclass-derived members.
Person1.call(this, name);
// Initialize subclass's own members.
this.studentId = studentId;
}
Student1.prototype = Object.create(Person1.prototype);
Student1.prototype.constructor = Student1;
const student1 = new Student1('John', 1234);
console.log(student1.name, student1.studentId); // "John" 1234
// ES2015 inheritance
// Superclass
class Person2 {
constructor(name) {
this.name = name;
}
}
// Subclass
class Student2 extends Person2 {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
const student2 = new Student2('Alice', 5678);
console.log(student2.name, student2.studentId); // "Alice" 5678
It's much more verbose to use inheritance in ES5, and the ES2015 version is easier to understand and remember.
Comparison of ES5 function constructors vs ES2015 classes
| Feature | ES5 Function Constructor | ES2015 Class |
|---|---|---|
| Syntax | Uses function constructors and prototypes | Uses class keyword |
| Constructor | Function with properties assigned using this | constructor method inside the class |
| Method Definition | Defined on the prototype | Defined inside the class body |
| Static Methods | Added directly to the constructor function | Defined using the static keyword |
| Inheritance | Uses Object.create() and manually sets prototype chain | Uses extends keyword and super |
| Readability | Less intuitive and more verbose | More concise and intuitive |
The main advantage of using an arrow function as a method inside a constructor is that the value of this gets set at the time of the function creation and can't change after that. When the constructor is used to create a new object, this will always refer to that object.
For example, let's say we have a Person constructor that takes a first name as an argument and has two methods to console.log() that name, one as a regular function and one as an arrow function:
const Person = function (name) {
this.firstName = name;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// The regular function can have its `this` value changed, but the arrow function cannot
john.sayName1.call(dave); // Dave (because `this` is now the dave object)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (because `this` is now the dave object)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (because `this` is now the dave object)
john.sayName2.bind(dave)(); // John
const detachedRegularMethod = john.sayName1;
try {
detachedRegularMethod();
} catch (error) {
console.log(error.name); // TypeError in strict mode because `this` is undefined
}
const detachedArrowMethod = john.sayName2;
detachedArrowMethod(); // John
The main takeaway is that a regular function receives this from its call site, while an arrow captures it from the constructor call. The tradeoff is that this arrow function is created separately for every instance instead of being shared through the prototype, and callers cannot deliberately rebind it.
Static fields and methods belong to the class constructor rather than to each instance. Use them for behavior or data conceptually associated with the type as a whole: named factories, validation helpers, registries, constants, or counters. Access them as ClassName.member (or through this inside a static method), not through an instance.
Do not use sta
Truncated — view the full README on GitHub.
(top 24 of 29)
38 followers · starred Mar 2025
634 followers · starred Nov 2024
60 followers · starred Oct 2025
13 followers · starred Jan 2025