Primitive Types:
String: Represents textual data, e.g., "Hello"
.
Number: Represents numerical values, e.g., 42
or 3.14
.
Boolean: Represents logical values, either true
or false
.
Undefined: Represents an uninitialized variable, e.g., let x;
.
Null: Represents the absence of any value, e.g., let y = null;
.
Symbol: Represents a unique and immutable value, often used as object keys.
BigInt: Represents integers larger than Number.MAX_SAFE_INTEGER
.
Non-Primitive Types:
Object: Represents collections of key-value pairs, e.g., {name: "John", age: 30}
.
Function: A special type of object that can be invoked, e.g., function() {}
.
==
and ===
?==
operator (loose equality) compares two values for equality after converting both values to a common type.===
operator (strict equality) compares two values for equality without performing type conversion. Both the value and the type must be the same.null
and undefined
?null
is an assignment value that represents the intentional absence of any object value.undefined
means a variable has been declared but has not yet been assigned a value.let
and const
?let
allows you to reassign values to the variable.const
does not allow reassignment of the variable value and must be initialized at the time of declaration.typeof
in JavaScript?The typeof
operator is used to determine the type of a given variable or expression. It returns a string indicating the type of the operand.
NaN
stands for "Not-a-Number" and represents a value that is not a legal number. It typically occurs when a mathematical operation fails or when trying to convert a non-numeric string to a number.
Use Array.isArray()
to check if a value is an array.
const arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true
Adding Elements:
push()
: Adds elements to the end of an array.
jsCopy codearr.push(4); // [1, 2, 3, 4]
unshift()
: Adds elements to the beginning of an array.
jsCopy codearr.unshift(0); // [0, 1, 2, 3, 4]
Removing Elements:
pop()
: Removes the last element.
jsCopy codearr.pop(); // [0, 1, 2, 3]
shift()
: Removes the first element.
jsCopy codearr.shift(); // [1, 2, 3]
this
keyword in JavaScript?The this
keyword refers to the object from which the function was called. Its value depends on the context in which the function is called:
In a method, this
refers to the object that owns the method.
Alone, this
refers to the global object (in browsers, it's window
).
In a function, this
refers to the global object (in strict mode, this
is undefined
).
In an event, this
refers to the element that received the event.
In an arrow function, this
retains the value of the enclosing lexical context's this
.
const persona = {
nombre : 'Alice' ,
saludo : función () {
console.log( ' Hola , ' + this.name );
}
}
person.greet(); // "Hola, Alice"
Some of the most commonly used ES6 features include let
and const
, arrow functions, template literals, destructuring, default parameters, rest and spread operators, classes, and modules.
Errors in JavaScript can be handled using try
, catch
, finally
, and throw
statements.
try
: Wraps a block of code that might throw an error.
catch
: Executes a block of code if an error is thrown in the try
block.
finally
: Executes a block of code after the try
and catch
blocks, regardless of whether an error was thrown.
throw
: Throws a custom error.
try {
let result = riskyOperation();
} catch (error) {
console.error('An error occurred:', error.message);
} finally {
console.log('This code runs regardless of success or error.');
}
function riskyOperation() {
throw new Error('Something went wrong!');
}
if-else
and the ternary operator?The if-else
statement is used for conditional branching where multiple lines of code can be executed based on the condition.
The ternary operator is a shorthand for if-else
that can be used when you need to assign a value based on a condition. It is more concise but is typically used for simple expressions.
Hoisting is JavaScript’s behavior of moving declarations to the top of the current scope (global or function scope). This means variables and function declarations can be used before they are declared.
Some common built-in methods for string manipulation include:
charAt()
concat()
includes()
indexOf()
lastIndexOf()
replace()
split()
substring()
toLowerCase()
toUpperCase()
trim()
Some array methods include push
, pop
, shift
, unshift
, map
, filter
, reduce
, find
, forEach
, some
, every
, concat
, slice
, and splice
.
map
method? How about find
and filter
?map
creates a new array with the results of calling a function on every element in the calling array.find
returns the first element in the array that satisfies the provided testing function.filter
creates a new array with all elements that pass the test implemented by the provided function.slice()
and splice()
in arrays?slice()
:
const arr = [1, 2, 3, 4, 5];
const sliced = arr.slice(1, 3); // [2, 3]
console.log(arr); // [1, 2, 3, 4, 5]
splice()
:
const arr = [1, 2, 3, 4, 5];
const spliced = arr.splice(1, 2); // Removes 2 and 3
console.log(arr); // [1, 4, 5]
map
and reduce
?map
creates a new array with the results of calling a provided function on every element in the calling array.
reduce
executes a reducer function on each element of the array, resulting in a single output value.
for
, for...of
, and for...in
loops.for
: General-purpose, often used for numeric and index-based iteration.
for...of
: Iterates over values of an iterable (arrays, strings, etc.).
for...in
: Iterates over object keys or property names. Be cautious with arrays, as it iterates over index keys, not values.
Callback functions are used to handle asynchronous operations. They are passed as arguments to other functions and are invoked after an operation is completed, allowing you to execute code in response to the outcome of the asynchronous operation.
Arrow functions are a concise way to write function expressions in JavaScript. They use the =>
syntax and do not have their own this
context, which makes them useful in situations where you want to preserve the context of this
from the enclosing scope.
Callbacks are functions passed into other functions as arguments to be executed later.
Promises are objects representing the eventual completion or failure of an asynchronous operation, allowing you to chain operations and handle errors more gracefully.
You can handle errors when fetching data by using try...catch
blocks or handling the promise rejections using .catch()
method.
An IIFE is a function that is executed immediately after its definition. It is commonly used to create a local scope and avoid polluting the global namespace.
( función () {
const message = 'IIFE ejecutado' ;
console . log (mensaje);
})(); // "IIFE ejecutado"
Promises are objects representing the eventual completion or failure of an asynchronous operation. They provide a way to handle asynchronous operations more gracefully by allowing you to chain .then()
and .catch()
methods to handle successful outcomes and errors, respectively.
setTimeout
and setInterval
?setTimeout
executes a function once after a specified delay.setInterval
executes a function repeatedly at specified intervals.The spread operator (...
) allows an iterable such as an array or string to be expanded in places where zero or more arguments (for function calls) or elements (for array literals) are expected, or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected.
The rest operator (`…`) is used in function parameters to collect all remaining arguments into an array, and in array and object destructuring to collect the rest of the elements/properties.
Template literals are string literals enclosed in backticks (`
) that allow for multi-line strings and interpolation of expressions.
const name = 'John';
const greeting = `Hello, ${name}!`;
console.log(greeting); // Output: Hello, John!
JSON.stringify()
and JSON.parse()
?JSON.stringify()
: Converts a JavaScript object or array into a JSON string.
jsCopy codeconst obj = { name: 'John' };
const jsonString = JSON.stringify(obj); // '{"name":"John"}'
JSON.parse()
: Converts a JSON string back into a JavaScript object.
jsCopy codeconst jsonString = '{"name":"John"}';
const obj = JSON.parse(jsonString); // { name: 'John' }
Purpose:
The Observer pattern is a design pattern where an object (called the subject) maintains a list of observers that are notified of any state changes. When the subject's state changes, all registered observers automatically get updated.
Real-world use case: In JavaScript, the Observer pattern is useful in scenarios like event handling. For example, in a chat application, when a new message arrives (subject changes), all the chat windows (observers) need to update with the new message.
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notifyObservers(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log(`Received update: ${data}`);
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers("New message"); // Both observers get the update
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It's appropriate when you need to maintain a single global instance of a class (e.g., a database connection or a configuration manager).
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.data = "Singleton instance";
}
getData() {
return this.data;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
Use cases: Logging, configuration management, or managing a single instance of database connections.
Data streaming refers to continuously receiving or sending data in small chunks, rather than waiting for the entire dataset to be available. This is especially useful for handling large data (like video, audio, or file downloads).
Difference from traditional methods:
In JavaScript, streaming is often implemented using Streams API (ReadableStream
, WritableStream
).
Unit Testing: Focuses on testing individual units of code, such as functions or components, in isolation. The goal is to verify that each unit works correctly on its own.
Integration Testing: Tests how different units work together. It ensures that interactions between components or systems (like an API call and its response handling) behave as expected.
End-to-End (E2E) Testing: Simulates real user scenarios, testing the entire application workflow from the UI to the backend. Tools like Cypress or Puppeteer are used for E2E testing.
To mock API calls in unit tests, you can use libraries like Jest with jest.mock()
or axios-mock-adapter to simulate HTTP responses, ensuring your tests remain independent from actual API behavior.
import axios from 'axios';
import MyComponent from './MyComponent';
import { render, screen } from '@testing-library/react';
jest.mock('axios');
test('fetches data and renders component', async () => {
axios.get.mockResolvedValue({ data: { name: 'John' } });
render(<MyComponent />);
const element = await screen.findByText('John');
expect(element).toBeInTheDocument();
});
Write tests that mimic real user behavior: Focus on how users interact with your application rather than testing internal implementation details.
Use screen
for queries: It makes tests more readable and avoids dependency on implementation details.
Use await findBy...
for asynchronous elements: Wait for elements that load asynchronously, like fetched data or delayed UI changes.
Mock external services: Use mocking to simulate external API calls and avoid testing network-related issues.
Use snapshot testing sparingly: While snapshot tests are useful for detecting UI changes, rely more on tests that check actual DOM structure and behavior.
const fun = () => {
for (let i = 1; i <= 50; i++) {
if (i % 3 === 0 && i % 5 === 0) {
console.log(i, 'FIZBUZZ');
} else if (i % 5 === 0) {
console.log(i, 'BUZZ');
} else if (i % 3 === 0) {
console.log(i, 'FIZ');
}
}
};
function reverseString(str) {
return str.split('').reverse().join('');
}
function reverseString(str) {
let reversed = '';
for (let i = str.length - 1; i >= 0; i--) {
reversed += str[i];
}
return reversed;
}
findLargest(arr) {
return Math.max(...arr);
}
console.log(findLargest([1, 3, 7, 2, 9])); // Output: 9
function isPalindrome(str) {
return str === str.split('').reverse().join('');
}
console.log(isPalindrome("racecar")); // Output: true
function factorial(n) {
if (n === 0 || n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
function longestWord(str) {
const words = str.split(' ');
let longest = '';
for (let word of words) {
if (word.length > longest.length) {
longest = word;
}
}
return longest;
}
let a={name: 'Suman', age:22}
let b={name: 'Suman', age:22}
console.log(a === b)
// false
console.log(a)
console.log(b)
var a = b = 5
// undefined
// ReferenceError: b is not defined
for(var i = 0; i<5; i++) {
setTimeout(() =>{
console.log(i)
},1000)
}
// 5 5 5 5 5
for(let i = 0; i<5; i++) {
setTimeout(() =>{
console.log(i)
},1000)
}
// 0 1 2 3 4
console.log('6' + 5 * 6)
console.log('6' * 5 + 6)
console.log('5' - '3' + 6)
// 630
// 36
// 8
isNAN("Hello")
// true
function fun1 ( ){
setTimeout(() => {
console.log(x)
console.log(y)
},3000)
var x = 2
let y = 12
}
fun1()
// 2
// 12
var a = 5
console.log(a++)
console.log(a)
// 5
// 6
console.log(++a)
console.log(a)
// 6
// 6
console.log(1<2<3)
console.log(3>2>1)
// true
// false
Synchronous code is executed in a sequential manner. Each operation waits for the previous one to complete before moving on to the next one.
Asynchronous code allows multiple operations to run concurrently. An asynchronous operation does not block the execution of subsequent code. Instead, it delegates the task to the environment (e.g., the browser or Node.js) and continues executing the rest of the code. Once the asynchronous operation completes, it notifies the main thread through callbacks, promises, or async/await.
Promise.all()
method?The Promise.all()
method is used to run multiple promises concurrently and wait until all of them have resolved or at least one has rejected. It takes an iterable (like an array) of promises as an input and returns a single promise that:
This method is useful when you need to perform multiple asynchronous operations concurrently and want to proceed only when all of them are completed.
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"]
})
.catch((error) => {
console.error(error);
});
Promise.all()
and Promise.race()
.Promise.all()
: Takes an array of promises and resolves when all promises resolve. If any promise rejects, the entire Promise.all()
rejects.Promise.race()
: Takes an array of promises and resolves/rejects as soon as the first promise resolves or rejects.function declaration
and function expression
?Function Declaration: Declared with the function
keyword, it is hoisted, meaning it can be used before it's defined.
function sayHello() {
console.log('Hello');
}
Function Expression: Assigned to a variable, and it's not hoisted, meaning it can only be used after the line where it is defined.
const sayHello = function() {
console.log('Hello');
};
Scope: Refers to the accessibility of variables. JavaScript has function scope and block scope (using let
and const
).
Lexical Scoping: A function's scope is determined by its position in the code during definition, not where it's called. Inner functions can access variables from their outer functions.
function outer() {
const name = "John";
function inner() {
console.log(name); // Lexical scoping allows access to 'name'
}
inner();
}
Currying is a technique where a function is transformed into a series of nested functions that take one argument at a time.
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
Currying allows partial application of functions and can be useful in functional programming.
Debounce: Delays a function call until a certain period has passed since the last time it was invoked. Useful for limiting how often a function is executed (e.g., input events).
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
Throttle: Ensures a function is only called once in a specified period, regardless of how many times it's triggered (e.g., scroll events).
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
Synchronous Iteration: Loops like for
or for...of
execute iteratively and block the next iteration until the current one finishes.
Asynchronous Iteration: Introduced with for await...of
, this allows you to iterate over asynchronous data sources (like Promises) where each iteration waits for the asynchronous task to complete.
async function fetchData() {
const promises = [promise1, promise2];
for await (const result of promises) {
console.log(result);
}
}
The Temporal Dead Zone (TDZ) refers to the period between the start of a block and the moment a variable is declared. Accessing the variable during this time results in a ReferenceError
.
It occurs with variables declared using let
and const
.
{
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;
}
Destructuring allows you to unpack values from arrays or properties from objects into distinct variables.
Array Destructuring:
const [a, b] = [1, 2];
console.log(a, b); // 1, 2
Object Destructuring:
jsCopy codeconst { name, age } = { name: 'John', age: 30 };
console.log(name, age); // John, 30
You can also provide default values, rename variables, and extract nested properties.
Object.freeze()
and Object.seal()
do.Object.freeze()
: Makes an object immutable, meaning you cannot add, remove, or modify properties.
const obj = Object.freeze({ name: 'John' });
obj.name = 'Doe'; // Does not change
Object.seal()
: Prevents adding or deleting properties, but allows modifying existing properties.
const obj = Object.seal({ name: 'John' });
obj.name = 'Doe'; // Modifies name
obj.age = 30; // Cannot add new property
deep copy
and shallow copy
in JavaScript?Shallow Copy: Only copies the top-level properties of an object. Nested objects are still referenced, so changes to nested objects in the copy affect the original object.
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
shallowCopy.b.c = 3; // Modifies the original object's nested property
Deep Copy: Recursively copies all levels of the object, creating entirely independent objects.
const original = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.b.c = 3; // Does not affect the original object
A Symbol is a unique, immutable primitive value used to create property keys that won’t collide with any other keys (even those with the same name).
Usage: Symbols are often used for adding metadata to objects or creating private properties that won’t interfere with other code or libraries.
jsCopy codeconst sym = Symbol('description');
const obj = { [sym]: 'value' };
console.log(obj[sym]); // 'value'
Symbols are often used in JavaScript frameworks or libraries, such as iterators (e.g., Symbol.iterator
).
Events in JavaScript are actions or occurrences that happen in the system you are programming, which the system tells you about so you can respond to them. They are used to handle user interactions and other activities that occur in a web page, such as clicking a button, hovering over an element, or submitting a form.
Common event types include:
click
, dblclick
, mouseover
, mouseout
, mousemove
keydown
, keypress
, keyup
submit
, change
, focus
, blur
load
, resize
, scroll
, unload
call()
, apply()
, and bind()
?call()
, apply()
, and bind()
are methods available on JavaScript functions that allow you to control the this
value and pass arguments to the function.
The call()
method calls a function with a given this
value and arguments provided individually.
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
greet.call(person, 'Hello', '!'); // Hello, Alice!
apply()
method calls a function with a given this
value and arguments provided as an array (or an array-like object).
función saludar ( saludo, puntuación ) {
console.log (saludo + ', ' + this.nombre + puntuación);
}
const persona = { nombre : 'Bob' };
saludar.apply (persona, [ ' Hola' , ' .' ] ); // Hola, Bob.
The bind()
method creates a new function that, when called, has its this
value set to the value provided, with a given sequence of arguments preceding any value provided when the new function is called. Unlike call()
and apply()
, bind()
does not execute the function immediately it returns a new function.
función saludar ( saludo , puntuación ) {
console.log (saludo + ', ' + this.nombre + puntuación );
}
const persona = { nombre : 'Charlie' };
const personaSaludo = saludar.bind ( persona, 'Hola' , '?' );
personaSaludo (); // Se puede llamar más tarde
The event loop is a mechanism that allows JavaScript to perform non-blocking I/O operations despite being single-threaded. It continuously checks the call stack and the task queue, executing functions from the task queue when the call stack is empty.
Promises are executed before setTimeout
. When both a promise and a setTimeout
are scheduled to run at the same time, the .then()
callback of the promise will execute first because promises are part of the microtask queue, which has higher priority than the macrotask queue where setTimeout
resides.
JavaScript is single-threaded to simplify the execution model and avoid concurrency issues like race conditions. It uses an event loop to manage asynchronous operations, allowing it to perform non-blocking I/O operations despite being single-threaded.
DOMContentLoaded
event.The DOMContentLoaded
event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. This event is useful for executing JavaScript code as soon as the DOM is ready.
Memoization is an optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs are provided again. It helps improve the performance of functions by avoiding redundant calculations.
JavaScript modules allow you to break up your code into smaller, reusable chunks, which can be imported and exported between files. Modules help maintain modularity and encapsulation, making code more organized and manageable.
Exporting a Module: You can export functions, objects, or variables from one file.
jsCopy codeexport const greet = () => console.log('Hello, World!');
Importing a Module: You can import the exported module into another file.
Example (main.js):
jsCopy codeimport { greet } from './module.js';
greet(); // Output: 'Hello, World!'
Modern JavaScript (ES6+) uses import
and export
for module implementation. You can also use CommonJS (Node.js modules) with require
and module.exports
.
Immutability refers to the inability to change an object after it has been created. In JavaScript, immutability can be achieved by using methods that do not modify the original object but instead return a new object with the desired changes. Examples include Object.freeze()
for objects and methods like concat()
, slice()
, and map()
for arrays.
No, arrow functions are not hoisted. They behave like function expressions, meaning they are not available before their declaration
A closure is a feature in JavaScript where an inner function has access to the outer (enclosing) function’s variables, even after the outer function has finished executing. This allows the inner function to “remember” the environment in which it was created.
function outerFunction() {
let outerVariable = "I am outside!";
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const myClosure = outerFunction();
myClosure(); // Output: "I am outside!"
Event delegation is a powerful pattern in web development that allows you to manage events more efficiently by taking advantage of the event bubbling (or propagation) feature in the DOM. Instead of attaching an event listener to each individual element, event delegation involves attaching a single event listener to a parent element. This listener can then detect events triggered by any of its child elements.
loading
Attribute: Simplest way to lazy load images and iframes if supported by the browser.A higher-order function is a function that either:
Higher-order functions are commonly used in JavaScript for functional programming techniques like callbacks, map, filter, or reduce.
function higherOrder(fn) {
return function() {
return fn();
};
}
function sayHello() {
return 'Hello!';
}
const greet = higherOrder(sayHello);
console.log(greet()); // Output: 'Hello!'
In JavaScript, every object has a hidden property called [[Prototype]]
, which refers to another object, known as its prototype. Prototype inheritance allows objects to inherit properties and methods from their prototype chain.
Inheritance works by delegating property or method lookups to the prototype object if they are not found on the instance itself.
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person('John');
person1.sayHello(); // Output: 'Hello, my name is John'
In this example, person1
inherits the sayHello
method from Person.prototype
.
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
const debouncedFunction = debounce(() => console.log('Debounced!'), 300);
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
if (!lastRan) {
func.apply(this, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if (Date.now() - lastRan >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
const throttledFunction = throttle(() => console.log('Throttled!'), 300);
Dependency Injection (DI) is a design pattern where an object’s dependencies are injected rather than being hard-coded within the object. This promotes flexibility, reusability, and testing.
class Logger {
log(message) {
console.log(message);
}
}
class UserService {
constructor(logger) {
this.logger = logger;
}
createUser(user) {
this.logger.log(`User created: ${user.name}`);
}
}
// Inject the dependency (logger) into UserService
const logger = new Logger();
const userService = new UserService(logger);
userService.createUser({ name: 'John' });
In this example, the UserService
class depends on the Logger
class, but instead of creating a Logger
object inside UserService
, it's passed in.
Dependency Injection (DI) is a design pattern where an object’s dependencies are injected rather than being hard-coded within the object. This promotes flexibility, reusability, and testing.
class Logger {
log(message) {
console.log(message);
}
}
class UserService {
constructor(logger) {
this.logger = logger;
}
createUser(user) {
this.logger.log(`User created: ${user.name}`);
}
}
// Inject the dependency (logger) into UserService
const logger = new Logger();
const userService = new UserService(logger);
userService.createUser({ name: 'John' });
In this example, the UserService
class depends on the Logger
class, but instead of creating a Logger
object inside UserService
, it's passed in.
ReadableStream
and WritableStream
APIs work in JavaScript.ReadableStream: Represents a source of data that can be read in small chunks (streams). It allows you to handle data asynchronously as it arrives, without waiting for the entire data set to be available.
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hello, ');
controller.enqueue('World!');
controller.close();
}
});
const reader = stream.getReader();
reader.read().then(({ value, done }) => console.log(value)); // Output: 'Hello, '
WritableStream: Represents a destination where data can be written, chunk by chunk.
const writableStream = new WritableStream({
write(chunk) {
console.log('Writing:', chunk);
}
});
const writer = writableStream.getWriter();
writer.write('Streaming data');
These APIs are commonly used in handling files, network responses, or large data processing in small chunks.
The Observer Pattern is a behavioral design pattern where an object (called the subject) maintains a list of dependents (observers) and notifies them of any state changes.
Use Case: The Observer Pattern is useful in event-driven architectures, like user interface updates or real-time data feeds.
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log('Observer received:', data);
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('New data available!'); // Both observers will be notified
In a real-world scenario, you could use the Observer Pattern for notifications in a chat app or real-time updates in a collaborative document editing tool.
The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. Instead of directly instantiating objects, you delegate this responsibility to a separate factory function or class, which decides what specific instance to create.
Use Case: The Factory Pattern is useful when you have multiple subclasses of an object or need dynamic object creation based on certain conditions.
class Car {
constructor(model) {
this.model = model;
}
}
class Bike {
constructor(model) {
this.model = model;
}
}
class VehicleFactory {
static createVehicle(type, model) {
if (type === 'car') {
return new Car(model);
} else if (type === 'bike') {
return new Bike(model);
}
}
}
// Usage
const car = VehicleFactory.createVehicle('car', 'Sedan');
const bike = VehicleFactory.createVehicle('bike', 'Mountain Bike');
console.log(car instanceof Car); // true
console.log(bike instanceof Bike); // true
Benefit: The Factory Pattern is beneficial in scenarios where the exact type of object needs to be determined dynamically at runtime, such as in a vehicle or game entity creation system.
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms and makes them interchangeable. The strategy pattern lets the algorithm vary independently from the clients that use it. It is often used when you have multiple ways to achieve a task, and you want to switch between them at runtime.
Use Case: The Strategy Pattern is useful in scenarios where you want to swap between different business rules, such as different payment methods or sorting algorithms.
class PayPal {
pay(amount) {
console.log(`Paid ${amount} using PayPal`);
}
}
class CreditCard {
pay(amount) {
console.log(`Paid ${amount} using Credit Card`);
}
}
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy(amount) {
this.strategy.pay(amount);
}
}
// Usage
const payment = new PaymentContext(new PayPal());
payment.executeStrategy(100); // Output: Paid 100 using PayPal
payment.setStrategy(new CreditCard());
payment.executeStrategy(200); // Output: Paid 200 using Credit Card
Benefit: The Strategy Pattern is beneficial when you need to switch between multiple algorithms or behaviors at runtime, making it great for payment processing or data sorting.
Testing asynchronous code in JavaScript can be done using Jest or similar testing frameworks. For promises and async/await
, you can use the following approaches:
Using done()
: Call the done
callback for manual completion of async tests.
Using .resolves
and .rejects
: For testing promises that resolve or reject.
Using async/await
: Jest allows you to write tests in an async
function directly.
Example of Promise Test (Using async/await
):
jsCopy code// Function to test
const fetchData = () => {
return new Promise(resolve => setTimeout(() => resolve('data'), 100));
};
// Jest Test
test('fetches data asynchronously', async () => {
const data = await fetchData();
expect(data).toBe('data');
});
Example of Promise Test (Using .resolves
):
jsCopy codetest('fetches data resolves', () => {
return expect(fetchData()).resolves.toBe('data');
});
Testing Error Handling in Promises:
jsCopy codeconst fetchWithError = () => Promise.reject('Error occurred');
test('fetch fails with an error', () => {
return expect(fetchWithError()).rejects.toBe('Error occurred');
});
Benefits: Jest automatically handles async code, and these techniques ensure that tests handle promises and async/await cleanly, providing flexibility for testing asynchronous operations, including API calls and timers.
Snapshot Testing in Jest is a way to ensure that a component's output (usually its rendered UI) matches a previously saved snapshot of the component. If the output differs, Jest will flag the test as failed, which may indicate that the UI has changed unintentionally.
How Snapshot Testing Works:
import React from 'react';
import renderer from 'react-test-renderer';
import MyComponent from './MyComponent';
test('renders correctly', () => {
const tree = renderer.create(<MyComponent />).toJSON();
expect(tree).toMatchSnapshot();
});
Advantages:
Limitations:
Overuse: If overused, snapshot tests can be brittle and may fail frequently due to small, intentional changes in the UI.
Lack of Precision: Snapshots are more of a "broad" test, verifying the entire component output. They don't provide much insight into specific behaviors or changes, making them less helpful for more granular logic testing.
Manual Updates: Developers need to manually update snapshots when legitimate changes are made, which can sometimes lead to updating snapshots without realizing an unintended change has occurred.
Snapshot testing is best for UI regression testing but should be combined with other testing methods (unit, integration) for full test coverage.
When testing React components that rely on the Context API for global state, you need to simulate the context value by wrapping the component in its corresponding Provider
during testing.
import React from 'react';
import { render, screen } from '@testing-library/react';
import UserContext from './UserContext'; // Your context
import UserComponent from './UserComponent'; // Component that uses the context
test('renders user name from context', () => {
const mockUser = { name: 'John Doe' };
// Render the component wrapped in the context provider
render(
<UserContext.Provider value={mockUser}>
<UserComponent />
</UserContext.Provider>
);
// Assert that the component correctly displays the context value
expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
});
UserContext.Provider
and pass a mock context value (mockUser
).For Redux, testing involves wrapping the component with the Redux Provider
and passing a mock store to it. This allows you to simulate Redux state and actions during tests.
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import MyComponent from './MyComponent'; // Component connected to Redux
const mockStore = configureStore([]);
test('renders value from Redux state', () => {
const store = mockStore({
myState: { value: 'Hello World' }, // Mock Redux state
});
// Render component wrapped in the Redux provider
render(
<Provider store={store}>
<MyComponent />
</Provider>
);
// Assert that the component correctly renders data from the mock state
expect(screen.getByText(/Hello World/i)).toBeInTheDocument();
});
redux-mock-store
.Provider
component wraps the component and provides the mock store.render
and screen
, making it easy to interact with and test component outputs.// Subject Class
class Subject {
constructor() {
this.observers = []; // List to store observers (subscribers)
}
// Subscribe an observer
subscribe(observer) {
this.observers.push(observer);
}
// Unsubscribe an observer
unsubscribe(observer) {
this.observers = this.observers.filter(sub => sub !== observer);
}
// Notify all observers
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// Observer Class
class Observer {
constructor(name) {
this.name = name;
}
// Update method called by the subject when changes occur
update(data) {
console.log(`${this.name} received update:`, data);
}
}
// Example usage:
// Create a subject (e.g., weather station)
const weatherStation = new Subject();
// Create observers (e.g., weather displays)
const display1 = new Observer('Display 1');
const display2 = new Observer('Display 2');
// Subscribe displays to the weather station
weatherStation.subscribe(display1);
weatherStation.subscribe(display2);
// Notify observers with data (e.g., temperature update)
weatherStation.notify('Temperature is 25°C');
// Unsubscribe one observer
weatherStation.unsubscribe(display1);
// Notify again
weatherStation.notify('Temperature is 28°C');
Explanation:
subscribe
), remove (unsubscribe
), and notify (notify
) observers.notify()
to inform all observers.update()
method is called when the subject notifies them, and it can handle the received data.Example Output:
Display 1 received update: Temperature is 25°C
Display 2 received update: Temperature is 25°C
Display 2 received update: Temperature is 28°C
Use Case:
function flattenArray(arr) {
for (let i = 0; i < arr.length; i++) {
const value = arr[i];
if (Array.isArray(value)) {
// If the value is an array, recursively call the function flatten
result = result.concat(flatten(value));
} else {
// If it is not an array, simply add the value to the result
result.push(value);
}
}
return result;
};
function flattenArray(arr) {
return arr.reduce((flat, toFlatten) => {
return flat.concat(Array.isArray(toFlatten) ? flattenArray(toFlatten) : toFlatten);
}, []);
}
const arr = [1, [2, [3, [4]], 5]];
const flattened = flattenArray(arr);
console.log(flattened); // [1, 2, 3, 4, 5]
// using flat()
const arr1 = [1, [2, [3, [4]], 5]];
const flatArr1 = arr1.flat(Infinity);
console.log(flatArr1); // [1, 2, 3, 4, 5]
function memoize ( fn ) {
const cache = {};
return function ( ...args ) {
const key = JSON.stringify ( args);
if (cache[key]) {
return cache[key];
} else {
const result = fn ( ...args ) ;
cache[key] = result;
return result;
}
};
}
The Temporal Dead Zone (TDZ) is the period between the start of a scope (e.g., function or block scope) and when the variable is declared. During this time, variables declared with let
and const
cannot be accessed. If you try to access them before they are initialized, a ReferenceError will occur. The TDZ ensures that let
and const
declarations are block-scoped and prevents hoisting issues.
Memoization is a programming technique used to improve the performance of functions by caching the results of expensive function calls and returning the cached result when the same inputs occur again.
Reduces Duplicate Calculations: By storing the results of function calls, subsequent calls with the same arguments avoid recomputation, reducing redundant processing.
Useful in Recursion: Memoization is particularly helpful in recursive algorithms (like Fibonacci, or dynamic programming problems) where the same sub-problems are solved multiple times.
A Proxy object in JavaScript allows you to intercept and customize operations performed on an object, such as property access, assignment, function invocation, etc.
const target = {
name: 'John',
age: 25
};
const handler = {
get: (obj, prop) => {
return prop in obj ? obj[prop] : `Property "${prop}" does not exist`;
},
set: (obj, prop, value) => {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
// Intercepted property access
console.log(proxy.name); // John
console.log(proxy.height); // Property "height" does not exist
// Intercepted property assignment
proxy.age = 30;
console.log(proxy.age); // 30
// Error when setting invalid type
proxy.age = 'thirty'; // Throws TypeError
Validation: Ensuring certain properties are of the correct type or format.
Data Binding: Implement reactive frameworks or state management systems.
Object Access Control: Restrict or log access to certain object properties.
Using a large number of event listeners can have a negative impact on the performance of a web application, especially when attached to many elements or when handling complex events.
// Instead of attaching listeners to each button, we use one listener on the parent
document.getElementById('parent').addEventListener('click', (event) => {
if (event.target.tagName === 'BUTTON') {
console.log('Button clicked:', event.target.textContent);
}
});
WebAssembly (Wasm) is a low-level binary instruction format designed to run in web browsers alongside JavaScript. It allows developers to write code in languages like C, C++, or Rust and compile it to WebAssembly for use on the web.
Performance: WebAssembly is faster than JavaScript for computation-heavy tasks, as it is optimized for execution speed. It runs closer to the hardware and can handle tasks that would be slow in JavaScript.
Portability: Code written in other languages can be reused in web applications without rewriting it in JavaScript.
Complements JavaScript: WebAssembly is not meant to replace JavaScript but to complement it for use cases like video editing, game engines, and data visualization, where performance is critical.
When an event is triggered on a DOM element, there are two main phases through which the event travels: event capturing and event bubbling.
<div>
, the event will first trigger the button's click handler, then the <div>
’s click handler, and continue to the root.{ capture: true }
) will trigger first, starting from the outermost element and moving inward.<div id="parent">
<button id="child">Click Me</button>
</div>
<script>
document.getElementById('parent').addEventListener('click', () => {
console.log('Parent clicked');
});
document.getElementById('child').addEventListener('click', () => {
console.log('Child clicked');
});
</script>
Child clicked
Parent clicked
(because of event bubbling)document.getElementById('parent').addEventListener('click', () => {
console.log('Parent clicked');
}, true); // Capture mode
document.getElementById('child').addEventListener('click', () => {
console.log('Child clicked');
});
Parent clicked
(first, due to capturing)Child clicked
(second)You can prevent an event from propagating up the DOM by using event.stopPropagation()
.
document.getElementById('child').addEventListener('click', (event) => {
event.stopPropagation(); // Stops the event from bubbling up
console.log('Child clicked');
});
JavaScript uses automatic memory management and relies on a process called garbage collection to reclaim memory that is no longer in use.
Global variables
Forgotten timers or callbacks
Closures holding onto large objects
Generators are a special type of function in JavaScript that can be paused and resumed, allowing for the creation of iterators in a more straightforward manner.
Key Features:
Function Declaration: Generators are defined using the function*
syntax and yield values using the yield
keyword.
Statefulness: Generators maintain their state between calls, allowing for complex iteration patterns.
Defining a Generator:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const iterator = myGenerator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Use Case: Generators are useful for implementing iterators, managing asynchronous code with async/await
, and creating sequences.
WeakMap and WeakSet are collections in JavaScript that hold "weak" references to objects, meaning that they do not prevent garbage collection if there are no other references to the objects.
WeakMap:
WeakMap
is a collection of key-value pairs where the keys are objects and the values can be any value.WeakMap
.WeakMap
does not provide methods to iterate over its elements, and you cannot obtain the size of the map.let obj = {};
let weakMap = new WeakMap();
weakMap.set(obj, 'value');
console.log(weakMap.get(obj)); // 'value'
obj = null; // The object can be garbage collected
WeakSet
is a collection of unique objects. You can add or check for the presence of objects, but the objects are held weakly.WeakMap
, if there are no other references to an object in a WeakSet
, it can be garbage collected.WeakSet
does not allow iteration or size checking.let obj = {};
let weakSet = new WeakSet();
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
obj = null; // The object can be garbage collected
Key Differences:
WeakMap
and WeakSet
allow for garbage collection of their keys/values if there are no other references, whereas standard Map
and Set
do not.Map
and Set
provide methods for iteration, size checking, and other utilities, while WeakMap
and WeakSet
do not support these features.Stack: The stack is a place where primitive data types (e.g., numbers, booleans) and function calls are stored. It follows the LIFO (Last In, First Out) principle. Memory in the stack is limited and gets allocated and deallocated quickly.
Heap: The heap is where objects and reference types are stored. It’s an unordered memory pool used for dynamically allocated memory. The heap can store large amounts of data, but it requires garbage collection to free up unused memory.
Service Workers are scripts that run in the background in the browser, independent of the web page, enabling features like offline support, push notifications, and background sync.
Key Features:
Caching: Service workers can intercept network requests and cache assets, allowing for offline access to a website. They enable the creation of Progressive Web Apps (PWAs).
Background Tasks: Service workers can run tasks in the background, like syncing data or sending push notifications.
Non-blocking: They run on a separate thread from the main JavaScript thread, so they don't block the UI or user interactions.
Usage:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then((registration) => {
console.log('Service Worker registered:', registration);
}).catch((error) => {
console.log('Registration failed:', error);
});
}
sw.js
(Service Worker File):self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('static-cache').then((cache) => {
return cache.addAll(['/index.html', '/styles.css', '/app.js']);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Use Case: Service workers are commonly used to improve the performance and reliability of web apps by allowing them to work offline and load faster using cached content.
The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class. It provides a flexible alternative to subclassing for extending functionality.
Basic Object:
class Car {
getDescription() {
return "Basic car";
}
cost() {
return 10000;
}
}
Decorator Class:
class CarDecorator {
constructor(car) {
this.car = car;
}
getDescription() {
return this.car.getDescription();
}
cost() {
return this.car.cost();
}
}
class SportsPackage extends CarDecorator {
getDescription() {
return this.car.getDescription() + ', Sports Package';
}
cost() {
return this.car.cost() + 5000;
}
}
class Sunroof extends CarDecorator {
getDescription() {
return this.car.getDescription() + ', Sunroof';
}
cost() {
return this.car.cost() + 1500;
}
}
Using the Decorators:
let myCar = new Car();
myCar = new SportsPackage(myCar);
myCar = new Sunroof(myCar);
console.log(myCar.getDescription()); // "Basic car, Sports Package, Sunroof"
console.log(myCar.cost()); // 16500
Use Case: This pattern is beneficial when you need to add responsibilities to objects dynamically. For example, in a UI framework, decorators can be used to add extra functionality (like adding borders or shadows) to components without altering their base implementation.
WebSockets: Allows for bidirectional communication between a client and server. It’s useful for chat applications, live notifications, or online gaming.
Server-Sent Events (SSE): Enables a server to push updates to the client, suitable for one-way data streams like live scores or news updates.
Streams API: Works with ReadableStream and WritableStream objects to process data chunks on the fly. This is useful for handling large datasets or real-time media (audio/video) streaming.
Test-Driven Development (TDD) is a software development approach where tests are written before the actual code. The development process follows a Red-Green-Refactor cycle:
Write a Test:
// sum.test.js
const sum = require('./sum');
test('adds 2 + 3 to equal 5', () => {
expect(sum(2, 3)).toBe(5); // Test will fail because sum function doesn't exist yet.
});
Write the Minimum Code:
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
Refactor:
Repeat the Cycle:
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
const clone = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
// Example:
const obj = { a: 1, b: { c: 2, d: [3, 4] }};
const clonedObj = deepClone(obj);
console.log(clonedObj);
structuredClone()
is a built-in function in modern JavaScript (introduced in ECMAScript 2021) that creates a deep clone of a given value. It supports cloning of many built-in types, including objects, arrays, maps, sets, dates, and more, without many of the limitations.
const original = {
a: 1,
b: {
c: 2,
d: [3, 4, 5]
},
e: new Date(),
f: new Map([['key', 'value']])
};
const deepCopy = structuredClone(original);
console.log(deepCopy); // { a: 1, b: { c: 2, d: [3, 4, 5] }, e: Date, f: Map }
console.log(deepCopy !== original); // true
console.log(deepCopy.b !== original.b); // true
console.log(deepCopy.b.d !== original.b.d); // true
console.log(deepCopy.e !== original.e); // true
console.log(deepCopy.f !== original.f); // true
// Modifying the copy does not affect the original
deepCopy.b.c = 10;
deepCopy.b.d.push(6);
deepCopy.e.setFullYear(2000);
deepCopy.f.set('key', 'newValue');
console.log(original.b.c); // 2
console.log(original.b.d); // [3, 4, 5]
console.log(original.e.getFullYear()); // Current year
console.log(original.f.get('key')); // "value"
Shallow comparison involves comparing the objects’ own properties and values but not nested objects.
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || obj1 === null ||
typeof obj2 !== 'object' || obj2 === null) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
// Example usage:
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
const obj3 = { a: 1, b: 3 };
console.log(shallowEqual(obj1, obj2)); // true
console.log(shallowEqual(obj1, obj3)); // false
Deep comparison involves recursively comparing all nested objects and arrays.
function deepEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || obj1 === null ||
typeof obj2 !== 'object' || obj2 === null) {
return false;
}
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!keys2.includes(key)) return false;
if (!deepEqual(obj1[key], obj2[key])) return false;
}
return true;
}
// Example usage:
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };
console.log(deepEqual(obj1, obj2)); // true
console.log(deepEqual(obj1, obj3)); // false
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
function slowFunction(num) {
console.log("Computing...");
return num * 2;
}
const memoizedFn = memoize(slowFunction);
console.log(memoizedFn(5)); // Output: "Computing..." 10
console.log(memoizedFn(5)); // Output: 10 (cached result)
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(sub => sub !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log('Observer received:', data);
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Event 1');
subject.unsubscribe(observer2);
subject.notify('Event 2');
Promise.all()
that resolves when all promises have resolved or rejects if any promise rejects.function customPromiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(result => {
results[index] = result;
completed++;
if (completed === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
}
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.resolve(42);
customPromiseAll([promise1, promise2, promise3]).then((results) => {
console.log(results); // Output: [3, "foo", 42]
});