Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
429 views

javaScript for interview

Uploaded by

fexid98123
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
429 views

javaScript for interview

Uploaded by

fexid98123
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 110

1

Data Types in JavaScript


JavaScript has two categories of data types:
1. Primitive Types
2. Non-Primitive Types
1. Primitive Data Types
These are immutable (cannot be altered) and directly hold values.
 Number: Represents numeric values.
Examples:
let age = 25; // Integer
let price = 99.99; // Decimal
 String: Represents text enclosed in quotes ('', "", or backticks ````).
Examples:
let name = "John";
let message = 'Hello, World!';
let templateLiteral = `Hello, ${name}!`; // Using template literals
 Boolean: Represents logical values (true or false).
Example:
let isActive = true;
let isVerified = false;
 Undefined: A variable declared but not assigned any value.
Example:
let uninitialized;
console.log(uninitialized); // Output: undefined
 Null: Represents an intentional absence of value.
Example:
let empty = null;
 BigInt: Used for integers larger than the safe limit (2^53 - 1).
Example:
let bigNumber = 123456789012345678901234567890n; // 'n' denotes BigInt
 Symbol: Represents unique identifiers.
Example:
let symbol1 = Symbol('id');
2. Non-Primitive Data Types
These are mutable and store collections of data or more complex entities.
2

 Object: Used to store key-value pairs or structured data.


Example:
let person = { name: "Alice", age: 30 };
 Array: A special type of object used to store ordered collections.
Example:
let colors = ["red", "green", "blue"];
 Function: A reusable block of code.
Example:
function greet() {
return "Hello!";
}

Difference Between var, let, and const


1. var
 Scope: Function-scoped.
 Can be re-declared and updated.
 Hoisted: Variables declared with var are hoisted, but their value is undefined
until execution.
 Example:
function exampleVar() {
console.log(a); // Output: undefined (hoisting)
var a = 10;
console.log(a); // Output: 10
}
2. let
 Scope: Block-scoped.
 Cannot be re-declared in the same scope but can be updated.
 Hoisted: Variables declared with let are hoisted but not initialized, causing a
"Temporal Dead Zone" if accessed before declaration.
 Example:
let x = 5;
{
let x = 10; // Block-scoped
3

console.log(x); // Output: 10
}
console.log(x); // Output: 5
3. const
 Scope: Block-scoped.
 Cannot be re-declared or updated (but objects and arrays can have their
contents modified).
 Hoisted: Behaves like let with the "Temporal Dead Zone."
 Example:
const PI = 3.14;
// PI = 3.15; // Error: Assignment to constant variable
const obj = { name: "John" };
obj.name = "Alice"; // Allowed

Difference Between undefined and null


Aspect Undefined Null

A variable that has been declared but Represents an intentional


Definition
not assigned. absence of value.

Type undefined is a primitive type. null is a primitive type.

Use Case Unintentional absence of a value. Intentional absence of a value.

Example let a; (default value is undefined). let b = null; (explicitly assigned).

Comparis undefined == null is true (loose undefined === null is false


on equality). (strict equality).

Key Notes for Interviews


1. Primitive vs. Non-Primitive: Be ready to explain the difference and
examples of each.
2. Hoisting: Explain hoisting behavior for var, let, and const.
3. Temporal Dead Zone: For let and const, illustrate with code examples.
4. Use Cases: Highlight when to use let (e.g., in loops) and const (e.g.,
constants or objects).
5. Practical Scenarios: Demonstrate understanding of undefined vs. null in
debugging or API design.
4

Operators in JavaScript
JavaScript operators are used to perform operations on variables and values. Here's
a classification of key operator types:
1. Arithmetic Operators
Used for mathematical calculations.
 Examples: +, -, *, /, %, ++ (increment), -- (decrement).
let a = 10, b = 5;
console.log(a + b); // Output: 15
console.log(a % b); // Output: 0
2. Assignment Operators
Used to assign values.
 Examples: =, +=, -=, *=, /=.
let x = 10;
x += 5; // Equivalent to x = x + 5
console.log(x); // Output: 15
3. Comparison Operators
Used to compare values.
 Examples: ==, ===, !=, !==, <, <=, >, >=.
console.log(5 == "5"); // Output: true (type coercion)
console.log(5 === "5"); // Output: false (strict equality)
4. Logical Operators
Used to combine conditions.
 Examples: && (AND), || (OR), ! (NOT).
console.log(true && false); // Output: false
console.log(!true); // Output: false
5. Bitwise Operators
Used to manipulate bits of binary numbers.
 Examples: &, |, ^, ~, <<, >>.
6. Ternary Operator
A shorthand for if-else.
 Syntax: condition ? exprIfTrue : exprIfFalse
5

let age = 18;


let isAdult = age >= 18 ? "Yes" : "No";
console.log(isAdult); // Output: Yes

How Does == Differ from ===?


== (Equality Operator)
 Performs type coercion, converting one operand’s type to match the other
before comparison.
 Examples:
console.log(5 == "5"); // true (string "5" is coerced to number 5)
console.log(null == undefined); // true (both are treated as no value)
console.log(true == 1); // true (boolean true is coerced to number 1)
=== (Strict Equality Operator)
 Does not perform type coercion; values and types must both match.
 Examples:
console.log(5 === "5"); // false (different types)
console.log(null === undefined); // false (different types)
console.log(true === 1); // false (different types)
Key Differences
Feature == (Equality) === (Strict Equality)

Type Coercion Yes No

Comparison Compares values after Compares values and types


Behavior coercion directly

Slightly slower due to


Performance Faster as no coercion is done
coercion

Type Coercion in JavaScript


Type coercion refers to the automatic or implicit conversion of values from one data
type to another.
Types of Coercion
1. Implicit Coercion: Done by JavaScript automatically.
2. Explicit Coercion: Done manually by the developer.
6

Implicit Coercion Examples


 String Concatenation:
console.log("5" + 1); // Output: "51" (number 1 is coerced to string)
console.log("5" - 1); // Output: 4 (string "5" is coerced to number)
 Boolean Conversion:
console.log(Boolean(0)); // Output: false
console.log(Boolean("")); // Output: false
console.log(Boolean("Hello")); // Output: true
 Comparisons:
console.log("5" == 5); // true (string coerced to number)
console.log(null == 0); // false (no coercion between null and 0)
Explicit Coercion Examples
 String to Number:
let num = "42";
console.log(Number(num)); // Output: 42
console.log(+num); // Output: 42 (unary plus operator)
 Number to String:
let num = 42;
console.log(String(num)); // Output: "42"
console.log(num + ""); // Output: "42"
 Boolean Conversion:
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false

Common Pitfalls with Type Coercion


1. Falsy and Truthy Values
o Falsy: false, 0, "", null, undefined, NaN.
o Truthy: Any value not falsy, including objects, arrays, and "0".
if ("0") console.log("Truthy!"); // Output: Truthy!
2. Unexpected Results
console.log(null == 0); // false
7

console.log([] + {}); // "[object Object]"


console.log([] == false); // true
3. Avoid using == when exact comparison is required. Use === to
prevent errors due to implicit coercion.

Extra Topics: Interview-Ready Tips


1. Object.is():
o A better alternative to === for certain edge cases:
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(+0, -0)); // false
2. typeof Pitfalls:
o typeof null returns "object" (legacy issue).
o Use Array.isArray() instead of typeof to check for arrays.
3. Practical Advice:
o Use === for comparisons whenever possible.
o Be cautious of implicit type coercion in conditions (if, loops, etc.).
4. Coding Challenge to Expect: Given a mixed array, separate numbers and
strings while ensuring the correct type:
let arr = [1, "2", 3, "4"];
let numbers = arr.filter(item => typeof item === "number");
let strings = arr.filter(item => typeof item === "string");
console.log(numbers); // [1, 3]
console.log(strings); // ["2", "4"]
Functions in JavaScript
Functions are reusable blocks of code that perform a specific task. JavaScript
supports several ways to define and use functions, offering flexibility and power.

Types of Function Definitions


1. Function Declaration
 Syntax:
function functionName(parameters) {
// function body
8

}
 Characteristics:
o Hoisted: Can be called before its definition because of hoisting.
o Named: Always has a name.
o Best for reusable named functions.
 Example:
console.log(add(2, 3)); // Output: 5 (works due to hoisting)

function add(a, b) {
return a + b;
}
2. Function Expression
 Syntax:
const functionName = function(parameters) {
// function body
};
 Characteristics:
o Not hoisted: Cannot be called before its definition.
o Can be anonymous (no name).
o Used for functions assigned to variables or passed as arguments.
 Example:
const multiply = function(a, b) {
return a * b;
};

console.log(multiply(4, 5)); // Output: 20


3. Arrow Function
 Syntax:
const functionName = (parameters) => {
// function body
};
9

 Characteristics:
o Concise syntax.
o Does not have its own this (inherits this from its surrounding context).
o Cannot be used as constructors (no new keyword).
o Implicit return for single-line expressions.
 Example:
const divide = (a, b) => a / b; // Implicit return
console.log(divide(10, 2)); // Output: 5

Key Differences Between Function Declarations, Function Expressions, and


Arrow Functions
Function
Feature Function Expression Arrow Function
Declaration

Hoisting Yes No No

this Inherits this from the


Own this Own this
Binding context

Syntax Verbose Verbose Concise

Reusable, named Anonymous or assigned to Short, lightweight


Use Case
functions variables functions

Constructo
Yes Yes No
rs

Default Parameters in JavaScript Functions


Default parameters allow you to set default values for function parameters.
Why Use Default Parameters?
1. Avoid undefined values when arguments are missing.
2. Simplify handling optional parameters.
Syntax:
function functionName(param1 = defaultValue, param2 = defaultValue) {
// function body
}
Examples:
10

1. Basic Example:
function greet(name = "Guest") {
return `Hello, ${name}!`;
}
console.log(greet()); // Output: Hello, Guest!
console.log(greet("Alice")); // Output: Hello, Alice!
2. Default Parameters with Expressions:
function calculateArea(length = 5, width = length) {
return length * width;
}
console.log(calculateArea()); // Output: 25
console.log(calculateArea(10)); // Output: 100
console.log(calculateArea(10, 20)); // Output: 200
Edge Case: Passing undefined vs. null
 undefined triggers the default parameter.
 null does not trigger the default parameter.
function checkValue(val = 10) {
return val;
}
console.log(checkValue(undefined)); // Output: 10
console.log(checkValue(null)); // Output: null

Higher-Order Functions
Definition:
A higher-order function is a function that:
1. Takes another function as an argument.
2. Returns a function as its output.
Why Important?
1. Core concept in functional programming.
2. Widely used in array manipulation (map, filter, reduce) and asynchronous
operations.
11

Examples:
1. Higher-Order Function with Callback:
function calculate(operation, a, b) {
return operation(a, b);
}

const add = (x, y) => x + y;


const subtract = (x, y) => x - y;

console.log(calculate(add, 5, 3)); // Output: 8


console.log(calculate(subtract, 5, 3)); // Output: 2
2. Using Array Methods (Higher-Order Functions):
o map: Transform array elements.
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // Output: [2, 4, 6]
o filter: Filter elements based on a condition.
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
o reduce: Aggregate array values.
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // Output: 10
3. Returning Functions:
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
12

const double = multiplier(2);


console.log(double(5)); // Output: 10

Bonus: Related Topics to Prepare


Closures
 Functions remember variables from their outer scope, even after the outer
function has returned.
 Example:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}

const counter = outer();


console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
Immediately Invoked Function Expressions (IIFE)
 Functions that are executed immediately after their definition.
 Example:
(function() {
console.log("IIFE executed!");
})();

Interview-Ready Tips
1. Common Use Cases: Explain when and why you'd use each type of function
definition.
2. Advanced Scenarios: Be ready to write and debug code using callbacks,
map, filter, and reduce.
3. Behavioral Insights: Highlight this behavior differences between arrow
functions and traditional functions.
13

4. Practical Coding Challenge: Implement a custom higher-order function,


such as a customMap.
Closures
A closure is created when a function retains access to variables from its lexical
scope, even when the outer function has finished executing.
Key Characteristics:
1. Functions "remember" their scope chain, even if they're executed outside
their defining context.
2. Closures are often used for data encapsulation, stateful functions, and
currying.
Example: Basic Closure
function outerFunction() {
let count = 0; // Variable within the outer function

return function innerFunction() {


count++;
return count;
};
}

const counter = outerFunction(); // outerFunction returns innerFunction


console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
console.log(counter()); // Output: 3
Practical Use Cases of Closures:
1. Data Encapsulation: Closures allow you to create private variables.
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
14

decrement() {
count--;
return count;
},
reset() {
count = 0;
return count;
},
};
}

const counter = createCounter();


console.log(counter.increment()); // Output: 1
console.log(counter.decrement()); // Output: 0
console.log(counter.reset()); // Output: 0
2. Currying: Breaking a function into a series of functions, each taking one
argument.
function multiply(a) {
return function (b) {
return a * b;
};
}

const double = multiply(2);


console.log(double(5)); // Output: 10
console.log(multiply(3)(4)); // Output: 12
Common Closure Interview Question:
Write a function to create a sequence of functions that log numbers incrementally.
function createLoggers(n) {
const loggers = [];
for (let i = 0; i < n; i++) {
15

loggers.push(() => console.log(i));


}
return loggers;
}

const loggers = createLoggers(3);


loggers[0](); // Output: 0
loggers[1](); // Output: 1
loggers[2](); // Output: 2

Recursion
A recursive function is one that calls itself until it reaches a base condition.
Key Concepts:
1. Base Case: Stops the recursion to prevent infinite calls.
2. Recursive Case: Function calls itself with modified parameters.
Example: Simple Recursion
Find the factorial of a number:
function factorial(n) {
if (n === 0) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}

console.log(factorial(5)); // Output: 120


Practical Use Cases of Recursion:
1. Searching in Trees:
const tree = {
value: 1,
children: [
{ value: 2, children: [] },
{ value: 3, children: [{ value: 4, children: [] }] },
],
16

};

function findValue(node, target) {


if (node.value === target) return true;
for (const child of node.children) {
if (findValue(child, target)) return true;
}
return false;
}

console.log(findValue(tree, 4)); // Output: true


console.log(findValue(tree, 5)); // Output: false
2. Flattening an Array:
function flattenArray(arr) {
return arr.reduce(
(acc, val) => acc.concat(Array.isArray(val) ? flattenArray(val) : val),
[]
);
}

console.log(flattenArray([1, [2, [3, 4], 5], 6])); // Output: [1, 2, 3, 4, 5, 6]


Tail-Call Optimization in JavaScript
Modern JavaScript engines optimize recursive functions using tail-call
optimization when the recursive call is the last statement.
 Example:
function factorialTailRecursive(n, acc = 1) {
if (n === 0) return acc;
return factorialTailRecursive(n - 1, acc * n);
}

console.log(factorialTailRecursive(5)); // Output: 120


17

Additional Topics Worth Exploring


1. Function Memoization: Improve recursive performance by caching results.
function memoize(fn) {
const cache = {};
return function (...args) {
const key = args.toString();
if (cache[key]) return cache[key];
const result = fn(...args);
cache[key] = result;
return result;
};
}

const fib = memoize(function (n) {


if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});

console.log(fib(40)); // Fast execution due to memoization


2. First-Class Functions: Functions in JavaScript can be assigned to variables,
passed as arguments, and returned from other functions.
const greet = (name) => `Hello, ${name}`;
const execute = (fn, arg) => fn(arg);
console.log(execute(greet, "Ritul")); // Output: Hello, Ritul
3. Debouncing and Throttling (Practical Applications):
o Debouncing: Ensures a function is called only after a specified delay.
o Throttling: Limits the function execution to once in a specified time.

Interview-Ready Tips
1. Highlight Problem-Solving Skills: Explain how closures or recursion help
solve real-world problems, such as data privacy or hierarchical data traversal.
18

2. Common Pitfalls to Discuss:


o Stack overflow in recursion (how to optimize with tail-recursion or
iteration).
o Debugging closures (scoping issues due to loops and asynchronous
code).
3. Coding Challenge Practice:
o Write a recursive function to calculate the Fibonacci sequence.
o Create a closure-based counter with reset and decrement functionality.
Scope in JavaScript
Scope determines the accessibility of variables, functions, and objects in a program.
1. Types of Scope
Global Scope
 Variables declared outside of any function or block are in the global scope.
 Accessible throughout the entire program.
 Downside: Can lead to namespace pollution if overused.
Example:
var globalVar = "I am global";

function showGlobal() {
console.log(globalVar); // Output: "I am global"
}
showGlobal();
Function Scope
 Variables declared inside a function using var are scoped to that function.
 Not accessible outside the function.
Example:
function testScope() {
var functionScoped = "I exist only here";
console.log(functionScoped); // Output: "I exist only here"
}
testScope();
console.log(functionScoped); // Error: functionScoped is not defined
19

Block Scope
 Variables declared with let or const are limited to the block ({}) in which they
are defined.
 Introduced in ES6, addressing var’s scoping issues.
Example:
{
let blockScoped = "I exist only in this block";
console.log(blockScoped); // Output: "I exist only in this block"
}
console.log(blockScoped); // Error: blockScoped is not defined
Lexical Scope
 Inner functions can access variables from their outer functions due to their
position in the code hierarchy.
Example:
function outer() {
let outerVar = "Outer";
function inner() {
console.log(outerVar); // Output: "Outer"
}
inner();
}
outer();

2. Closures
A closure is a function that "remembers" the variables from its lexical scope even
when the outer function has completed execution.
Key Points:
1. Persistence: Variables in closures persist in memory.
2. Access to Outer Scope: Inner functions can access variables of the outer
function.
Example:
function makeCounter() {
let count = 0;
20

return function () {
count++;
return count;
};
}

const counter = makeCounter();


console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
Use Cases of Closures
1. Data Privacy:
o Closures allow you to create private variables.
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
balance -= amount;
return balance;
},
};
}

const account = createBankAccount(1000);


console.log(account.deposit(500)); // Output: 1500
console.log(account.withdraw(200)); // Output: 1300
2. Event Handlers: Closures are commonly used in callback functions for event
handling.
21

function setup() {
let name = "Closure Example";
document.addEventListener("click", () => {
console.log(name);
});
}
setup();
3. Currying: Transforming a function so that it takes arguments one at a time.
function multiply(a) {
return function (b) {
return a * b;
};
}

const double = multiply(2);


console.log(double(5)); // Output: 10
4. Debouncing and Throttling (Advanced Use Cases): Closures are
foundational in implementing debouncing or throttling for performance
optimization in applications.

3. this Keyword in JavaScript


What is this?
The this keyword refers to the object that the function is a property of or the context
in which the function is called.
How this Differs Across Contexts:
Context Value of this

window object (browser) or global


Global Scope
(Node.js)

Object Method The object that owns the method

Arrow Function Inherits this from its lexical scope

Constructor The instance of the object created by


Function new
22

Examples of this Behavior


1. Global Scope:
console.log(this); // In a browser: Window object
2. Inside a Method:
const user = {
name: "Ritul",
greet() {
console.log(this.name);
},
};
user.greet(); // Output: "Ritul"
3. Inside an Arrow Function:
o Arrow functions do not have their own this. They inherit it from their
enclosing scope.
const user = {
name: "Ritul",
greet: () => {
console.log(this.name); // Output: undefined (depends on outer `this`)
},
};
user.greet();
4. Inside a Constructor Function:
function Person(name) {
this.name = name;
}

const person = new Person("Ritul");


console.log(person.name); // Output: "Ritul"
5. Explicit Binding (call, apply, bind):
o this can be explicitly set using call, apply, or bind.
function greet(greeting) {
23

console.log(`${greeting}, ${this.name}`);
}

const user = { name: "Ritul" };


greet.call(user, "Hello"); // Output: "Hello, Ritul"
greet.apply(user, ["Hi"]); // Output: "Hi, Ritul"

const boundGreet = greet.bind(user);


boundGreet("Hey"); // Output: "Hey, Ritul"

Interview-Ready Tips
1. Scope:
o Know how var, let, and const behave in different scopes.
o Be prepared to debug scope-related questions like variable shadowing.
2. Closures:
o Understand how closures are created and used.
o Be ready to write functions using closures for private variables or
currying.
3. this:
o Understand how this changes based on context (regular function vs.
arrow function).
o Be prepared for practical coding questions that require you to
manipulate this.
Debouncing and Throttling
1. What is Debouncing?
Debouncing ensures a function is executed only once after a specified time has
elapsed since the last invocation.
Use Case:
 Search Bar Autocomplete: Trigger API calls only when the user stops
typing.
 Resize Events: Prevent the function from being called excessively while
resizing the browser window.
How it Works:
24

1. A timer is reset every time the event is triggered.


2. The function executes only if the timer completes without being interrupted.
Implementation:
function debounce(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer); // Clear the previous timer
timer = setTimeout(() => func.apply(this, args), delay); // Set a new timer
};
}

// Example: Search input


const handleSearch = debounce((query) => {
console.log("Searching for:", query);
}, 300);

document.getElementById("search").addEventListener("input", (e) => {


handleSearch(e.target.value);
});
Key Points:
 clearTimeout(timer) ensures the function isn't called until the user stops
triggering the event.
 The apply method ensures the correct this context.

2. What is Throttling?
Throttling ensures a function is executed at most once in a specified time interval,
regardless of how many times the event is triggered.
Use Case:
 Scrolling: Improve performance by limiting frequent updates during scroll
events.
 Button Clicks: Prevent rapid firing of API requests.
How it Works:
25

1. The function executes immediately and then pauses until the specified
interval has elapsed.
2. Further calls during the interval are ignored.
Implementation:
function throttle(func, interval) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
func.apply(this, args);
}
};
}

// Example: Window scroll event


const handleScroll = throttle(() => {
console.log("Scroll event triggered");
}, 100);

window.addEventListener("scroll", handleScroll);
Key Points:
 The function is guaranteed to run at regular intervals, improving
responsiveness without overloading resources.

Debouncing vs. Throttling


Aspect Debouncing Throttling

Definitio Executes the function after a delay Executes the function at regular
n since the last call intervals

Frequen
Runs only once if triggered repeatedly Runs at most once per interval
cy
26

Aspect Debouncing Throttling

Use Scroll, resize, mouse movements,


Search, resize, text input
Cases button clicks

3. Combining Debouncing and Throttling


Sometimes, a combination of both is required, such as throttling during continuous
events but debouncing the final call.
Example:
function debounceAndThrottle(func, delay, interval) {
let timer;
let lastTime = 0;

return function (...args) {


const now = Date.now();
clearTimeout(timer);

if (now - lastTime >= interval) {


lastTime = now;
func.apply(this, args);
}

timer = setTimeout(() => func.apply(this, args), delay);


};
}

// Example: Text input + final API call


const handleInput = debounceAndThrottle((value) => {
console.log("Processed input:", value);
}, 300, 1000);

document.getElementById("input").addEventListener("input", (e) => {


handleInput(e.target.value);
27

});

4. Practical Use Cases in Frontend Development


1. Improving UX:
o Debouncing enhances input responsiveness without overwhelming
the backend with too many requests.
o Throttling ensures smooth scrolling or animation performance.
2. Real-World Examples:
o Angular apps often involve heavy DOM manipulation or API calls that
can benefit from these techniques:
 ngOnScroll handlers
 Autocomplete dropdowns

Interview Tips
1. Know the Basics:
o Understand the difference between debouncing and throttling.
o Be ready to explain them conceptually and implement them in vanilla
JavaScript.
2. Be Prepared for Scenarios:
o How would you debounce/throttle an Angular event like keyup or
scroll?
o Discuss real-world challenges, such as ensuring that the final API call
happens after a debounced event.
3. Write Clear Code:
o Ensure your code demonstrates reusability (e.g., using higher-order
functions for debounce/throttle logic).
4. Be Ready for Edge Cases:
o Handling this context issues in classes or components.
o Combining debouncing and throttling for optimal results.
Error Handling in JavaScript
Error handling is critical in any application to gracefully handle unexpected
situations without crashing the entire system. JavaScript provides mechanisms to
catch and manage errors, ensuring a smoother user experience and better
debugging.
28

1. How do you handle errors in JavaScript?


In JavaScript, errors can be handled using a combination of try-catch blocks,
throwing errors, and handling asynchronous errors (e.g., using Promise chaining
or async-await).
Basic Error Handling:
1. try-catch Block:
o try: Defines a block of code to be tested for errors.
o catch: Defines a block of code to handle errors.
try {
let result = riskyFunction(); // This might throw an error
console.log(result);
} catch (error) {
console.error("An error occurred:", error.message);
}
o catch can be used to capture any exception thrown inside the try
block, whether it is a syntax error or a runtime error.
2. Custom Errors with throw:
o You can throw your own errors using the throw statement, which can
be handled with try-catch.
try {
let age = -1;
if (age < 0) {
throw new Error("Age cannot be negative");
}
} catch (error) {
console.log("Caught error:", error.message); // "Age cannot be negative"
}
3. Asynchronous Error Handling:
o In asynchronous code (like Promise or async-await), errors can be
handled by chaining .catch() or using try-catch with async-await.
Using Promise:
fetchData()
29

.then(response => console.log(response))


.catch(error => console.error("Error fetching data:", error));
Using async-await:
async function getData() {
try {
let response = await fetchData();
console.log(response);
} catch (error) {
console.error("Error occurred while fetching:", error);
}
}

2. What is the difference between throw and try-catch?


throw and try-catch are often used together but have distinct roles in error
handling.
throw:
 The throw statement is used to explicitly create an error and raise it in the
current execution context.
 You can throw custom error messages or objects to indicate a failure.
Example:
function validateAge(age) {
if (age < 0) {
throw new Error("Age cannot be negative");
}
return true;
}

try {
validateAge(-5); // This will throw an error
} catch (error) {
console.log("Caught error:", error.message); // Output: "Caught error: Age cannot
be negative"
30

}
try-catch:
 The try-catch statement is used to catch errors and handle them
gracefully when executing code that could potentially fail.
 The catch block only executes if an error is thrown inside the try block.
Example:
try {
let result = riskyOperation(); // This might throw an error
console.log(result);
} catch (error) {
console.log("Error caught:", error.message); // Handling the error
}
Difference in Usage:
 throw is used to generate an error intentionally.
 try-catch is used to catch and handle an error that occurs in a block of
code.

3. Best Practices in Error Handling


 Use Custom Errors: Create specific error messages for different failure
scenarios to make debugging easier.
class InvalidAgeError extends Error {
constructor(message) {
super(message);
this.name = "InvalidAgeError";
}
}

throw new InvalidAgeError("Age cannot be negative");


 Avoid Silent Failures: Always handle errors appropriately. Don’t let errors
go unhandled, as they can cause silent failures that are hard to debug.
 Use finally Block: The finally block will execute after the try-catch,
regardless of whether an error was thrown or not. It’s useful for cleanup tasks
like closing files or database connections.
31

try {
// Code that might throw
} catch (error) {
// Error handling
} finally {
// Cleanup code, always runs
}
 Log Errors: Make sure to log the errors for easier debugging, especially for
production environments.
 Avoid Overuse of try-catch: try-catch should be used for error handling
and not for normal control flow. Instead, use conditions or validations where
appropriate.

Real-World Application for Angular Developers


As an Angular developer, handling errors efficiently is crucial for ensuring smooth
user experiences and proper application flow. Here's how it translates in real-world
scenarios:
1. HTTP Requests:
o Handle HTTP errors with try-catch and .catch() when using Angular
services to fetch data.
this.http.get('api/data').subscribe(
data => console.log(data),
error => this.handleHttpError(error)
);
2. Form Validation Errors:
o When dealing with form inputs and validations, use throw to explicitly
handle validation errors.
if (form.invalid) {
throw new Error('Form is invalid!');
}
3. Global Error Handling (Angular Services):
o Angular allows creating a global error handler by extending
ErrorHandler to catch unhandled errors globally.
import { ErrorHandler } from '@angular/core';
32

export class GlobalErrorHandler implements ErrorHandler {


handleError(error: any): void {
console.error('Global Error:', error);
}
}

Conclusion
 throw and try-catch are essential for managing errors in JavaScript, and as
an Angular developer, you'll often work with asynchronous code, API calls,
and form validations where error handling becomes crucial.
 Best Practices like using specific error classes, logging, and leveraging the
finally block can make your error handling much more robust.
1. Explain the JavaScript Event Loop and its Role in Asynchronous
Programming
JavaScript is a single-threaded language, meaning it can only execute one
operation at a time. However, it can handle multiple operations asynchronously by
leveraging the Event Loop, Call Stack, and Callback Queue.
Key Concepts:
1. Call Stack:
o This is where JavaScript keeps track of function calls. The call stack
operates on the Last In, First Out (LIFO) principle. When a function
is invoked, it is pushed onto the call stack. Once the function
completes, it is popped off.
Example:
function firstFunction() {
console.log('First Function');
}

function secondFunction() {
console.log('Second Function');
}

firstFunction(); // This gets pushed to the call stack


33

secondFunction(); // This also gets pushed after firstFunction completes


2. Web APIs (Browser Environment):
o JavaScript interacts with Web APIs (like setTimeout, fetch, DOM
events) in the browser. These APIs run independently of the JavaScript
thread and send callbacks to the Callback Queue once their
operations are completed.
3. Callback Queue (or Message Queue):
o This queue holds the callbacks that are waiting to be executed. Once
the call stack is empty, the event loop picks up these tasks and moves
them to the call stack for execution.
4. Event Loop:
o The Event Loop is a mechanism that continuously checks if the call
stack is empty. If it is, the event loop pushes tasks from the callback
queue onto the call stack for execution.
How It Works Together:
 When you execute a synchronous function, it is placed on the call stack and
executed immediately.
 For asynchronous functions, like setTimeout or fetch, the function is handed
over to the Web API, which runs it independently and places the callback into
the callback queue once done.
 The event loop ensures that when the call stack is empty, the next task from
the callback queue is pushed onto the call stack for execution.
Visual Explanation:
sql
Call Stack --> Event Loop --> Callback Queue
↑ ↑
Function Execution Check for tasks
Example:
console.log('Start'); // 1st: Synchronous code

setTimeout(() => {
console.log('Inside setTimeout'); // 3rd: Asynchronous callback
}, 0);

console.log('End'); // 2nd: Synchronous code


34

Output:
sql
Start
End
Inside setTimeout
 Explanation: The event loop allows setTimeout to run asynchronously, so
the 'End' message prints before the callback from setTimeout.

2. What is the difference between setTimeout, setInterval, and


requestAnimationFrame?
These three functions are often used for handling asynchronous behavior and
scheduling tasks, but they have different purposes and behaviors:
setTimeout
 Purpose: Executes a function after a specified delay (in milliseconds).
 Use Case: Used when you need to execute a function once after a delay.
 Behavior: Only runs once after the specified delay.
Example:
setTimeout(() => {
console.log('This will run after 1 second');
}, 1000);
setInterval
 Purpose: Executes a function repeatedly at specified intervals (in
milliseconds).
 Use Case: Used when you need a function to run repeatedly, such as polling
or creating an animation loop.
 Behavior: Runs repeatedly at the specified interval until cleared.
Example:
let count = 0;
const intervalId = setInterval(() => {
console.log('This runs every 1 second', ++count);
if (count >= 5) clearInterval(intervalId); // Stops the interval after 5 iterations
}, 1000);
requestAnimationFrame
35

 Purpose: Schedules the next function call to be executed before the next
repaint (next frame). It is specifically designed for animation.
 Use Case: Ideal for performing smooth animations, especially when
manipulating the DOM, as it aligns with the browser's refresh rate.
 Behavior: Runs before the next frame is rendered (typically 60 FPS), which is
more optimized than using setTimeout or setInterval for animations.
Example:
function animate() {
console.log('Animating...');
requestAnimationFrame(animate); // Keep calling itself
}

requestAnimationFrame(animate); // Starts the animation loop


Key Differences:
Feature setTimeout setInterval requestAnimationFrame

Executes
Execution Executes once after Executes before next repaint,
repeatedly at
Type delay optimized for animations
intervals

Repeated
Delayed one-time Smooth animations and
Use Case execution at set
execution rendering
intervals

Less precise in terms


Can accumulate Syncs with the browser's paint
Accuracy of time due to
drift over time cycle
browser load

Performan Not optimized for Not optimized for Optimized for smooth
ce animations animations animations

3. How the Event Loop Handles Asynchronous Code:


Async Function Execution Flow:
 When you use asynchronous functions like setTimeout, setInterval, or
requestAnimationFrame, JavaScript offloads the execution of these functions
to the browser's Web APIs.
 These APIs execute independently of the call stack and place their callbacks
in the callback queue once completed.
 The event loop picks up these tasks and pushes them onto the call stack for
execution.
36

Additional Points to Consider:


1. Microtask Queue vs. Macrotask Queue:
o The microtask queue (e.g., promises) is prioritized over the
macrotask queue (e.g., setTimeout and setInterval).
o The event loop will always process microtasks before macrotasks, even
if the macrotask is in the queue first.
Example:
console.log('Start');

Promise.resolve().then(() => console.log('Promise Resolved')); // Microtask


setTimeout(() => console.log('setTimeout'), 0); // Macrotask

console.log('End');
Output:
Start
End
Promise Resolved
setTimeout
2. Optimizing Performance with requestAnimationFrame:
o requestAnimationFrame is the most efficient method for creating
animations because it automatically aligns with the browser's repaint
cycle, ensuring smooth rendering without unnecessary computation.

Conclusion:
 The Event Loop is the backbone of asynchronous programming in
JavaScript, enabling non-blocking execution.
 setTimeout, setInterval, and requestAnimationFrame are essential tools
for managing asynchronous tasks, but each serves different purposes, with
requestAnimationFrame being the most optimized for animations.
 Understanding how the event loop interacts with these asynchronous
functions is critical for building efficient applications, especially for UI-heavy
or real-time applications.
1. How do Promises Work? What are resolve, reject, and then?
37

A Promise is a JavaScript object that represents the eventual completion or failure


of an asynchronous operation and its resulting value. Promises allow you to handle
asynchronous operations more effectively by avoiding "callback hell."
Basic Concept:
A Promise can be in one of three states:
1. Pending: The operation is still in progress.
2. Fulfilled: The operation was successful, and the promise is resolved.
3. Rejected: The operation failed, and the promise is rejected.
Key Methods:
 resolve(value): This method is called when the promise is successfully
completed. It indicates that the promise has been fulfilled, and it sends the
result (value) back.
 reject(reason): This method is called if the promise is rejected. It indicates
that the operation failed, and it sends a reason (typically an error).
 then(onFulfilled, onRejected): This method is used to specify what to do
when the promise is fulfilled or rejected. The onFulfilled callback runs when
the promise is resolved, and the onRejected callback runs when the promise
is rejected.
Example:
let promise = new Promise((resolve, reject) => {
let success = true;

if (success) {
resolve('Operation was successful!');
} else {
reject('Something went wrong!');
}
});

promise
.then(result => {
console.log(result); // If the promise is resolved
})
.catch(error => {
38

console.log(error); // If the promise is rejected


});
Explanation:
 If the promise is resolved (resolve is called), the message 'Operation was
successful!' is logged.
 If the promise is rejected (reject is called), the error 'Something went wrong!'
is logged.
Key Points:
 then: Used for handling a promise’s fulfillment or rejection.
 catch: Used for handling rejections (it is shorthand for then(null,
onRejected)).
 finally: Executes after the promise settles (either resolved or rejected),
useful for cleanup tasks.

2. Explain async/await with an Example


async and await are newer JavaScript features that make working with promises
easier and more readable, allowing asynchronous code to look and behave like
synchronous code.
async Function:
 The async keyword is used to define a function as asynchronous. An async
function always returns a promise. Even if the function returns a non-promise
value, it will be wrapped in a resolved promise.
await Keyword:
 The await keyword is used to wait for a promise to resolve or reject inside an
async function. It can only be used within async functions.
 await pauses the execution of the function until the promise settles and then
returns the result of the resolved promise.
Example:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('Data fetched successfully'), 2000);
});
}

async function displayData() {


39

try {
console.log('Fetching data...');
let result = await fetchData(); // Wait for the promise to resolve
console.log(result); // Logs 'Data fetched successfully' after 2 seconds
} catch (error) {
console.log('Error:', error); // If any error occurs
}
}

displayData();
Explanation:
 The fetchData function returns a promise that resolves after 2 seconds.
 The displayData function is marked as async and uses await to wait for the
fetchData promise to resolve before continuing with the code execution.
 The await keyword ensures that we handle the asynchronous operation more
synchronously, making the code easier to read and maintain.
Benefits of async/await:
 Makes asynchronous code look like synchronous code.
 More readable and reduces the need for chaining multiple .then() calls.
 Provides better error handling with try-catch.

3. What are Microtasks and Macrotasks?


In the JavaScript event loop, tasks are divided into two types: microtasks and
macrotasks. Both types are handled in different queues and have different
priorities.
Macrotasks:
 Macrotasks represent the tasks that are scheduled in the callback queue
(e.g., setTimeout, setInterval, I/O tasks).
 These tasks are executed when the call stack is empty, but they are executed
after the microtasks.
 Examples: setTimeout, setInterval, I/O operations, events (click, scroll, etc.).
Microtasks:
40

 Microtasks represent tasks that are typically related to promises and other
asynchronous operations that need to be processed immediately after the
current execution context (before the next rendering or macrotask).
 They have higher priority than macrotasks, meaning they are always
executed before any macrotasks, even if the macrotask was scheduled first.
 Examples: Promises (then, catch), MutationObserver.
Order of Execution:
 First, synchronous code is executed.
 Then, all microtasks in the microtask queue are executed.
 Finally, after all microtasks have been processed, macrotasks are executed.
Example of Microtasks and Macrotasks:
console.log('Start');

setTimeout(() => {
console.log('Macrotask - setTimeout');
}, 0);

Promise.resolve().then(() => {
console.log('Microtask - Promise resolved');
});

console.log('End');
Output:
Start
End
Microtask - Promise resolved
Macrotask - setTimeout
Explanation:
 Even though setTimeout is scheduled first, its callback is executed later
because it’s a macrotask.
 The promise callback (then) is a microtask and is executed before the
macrotask, even though it was scheduled later.
41

4. Important Considerations:
 Error Handling with async/await: Always use try-catch blocks within async
functions to handle promise rejections.
 Concurrency: While async/await looks synchronous, it’s non-blocking.
Multiple await calls can be handled concurrently by using Promise.all().
Example:
async function fetchData1() { return 'Data 1'; }
async function fetchData2() { return 'Data 2'; }

async function fetchAll() {


const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
console.log(data1, data2);
}

fetchAll();
 await only with promises: The await keyword only works with promises.
Using it with non-promises will throw a syntax error.

Conclusion:
 Promises are used to manage asynchronous operations in JavaScript,
making it easier to handle success or failure without nested callbacks.
 async/await simplifies working with promises, making asynchronous code
more readable and easier to understand.
 Understanding microtasks and macrotasks is crucial for understanding the
order in which asynchronous operations are executed, especially in complex
applications.
1. How do you traverse the DOM using JavaScript?
DOM Traversal refers to the process of navigating through the Document Object
Model (DOM) to access and manipulate HTML elements.
Common Methods for DOM Traversal:
1. Accessing Elements:
o getElementById(id): Returns an element with the specified id.
let element = document.getElementById('myElement');
42

o getElementsByClassName(className): Returns a live


HTMLCollection of elements with the specified class name.
let elements = document.getElementsByClassName('myClass');
o getElementsByTagName(tagName): Returns a live HTMLCollection
of elements with the specified tag name.
let elements = document.getElementsByTagName('div');
o querySelector(selector): Returns the first element that matches the
CSS selector.
let element = document.querySelector('.myClass');
o querySelectorAll(selector): Returns a NodeList of all elements that
match the CSS selector.
let elements = document.querySelectorAll('.myClass');
2. Navigating between Parent, Child, and Sibling Elements:
o parentNode: Returns the parent node of an element.
let parent = element.parentNode;
o childNodes: Returns a live NodeList of an element’s child nodes
(including text nodes).
let children = element.childNodes;
o firstChild / lastChild: Returns the first/last child node.
let firstChild = element.firstChild;
let lastChild = element.lastChild;
o nextSibling / previousSibling: Returns the next/previous sibling
node.
let nextSibling = element.nextSibling;
let prevSibling = element.previousSibling;
o children: Returns only the element children (not text nodes).
let children = element.children;
o firstElementChild / lastElementChild: Returns the first/last child
element (ignores text nodes).
let firstElement = element.firstElementChild;
let lastElement = element.lastElementChild;
o nextElementSibling / previousElementSibling: Returns the
next/previous sibling element (ignores text nodes).
let nextElement = element.nextElementSibling;
43

let prevElement = element.previousElementSibling;


3. Other Traversal Methods:
o closest(selector): Returns the closest ancestor (or the element itself)
that matches the specified selector.
let closestElement = element.closest('.parentClass');

2. What is Event Delegation, and Why is it Useful?


Event Delegation is a technique where instead of attaching event listeners to
individual elements, you attach a single event listener to a parent element and let
the events bubble up to the parent. The parent element can then determine which
child element triggered the event.
Why It’s Useful:
 Performance: If you have many child elements (like list items or dynamically
created elements), attaching event listeners to each one can be inefficient.
Using event delegation reduces the number of event listeners needed.
 Dynamic Content: Event delegation allows event handling on dynamically
added or removed elements. Without it, you would need to add event
listeners to new elements manually.
How It Works:
1. Add an event listener to a common parent element.
2. Use the event’s target property to identify which child element was clicked.
3. Handle the event based on the target element.
Example:
document.querySelector('#parent').addEventListener('click', function(event) {
if (event.target && event.target.matches('button.className')) {
console.log('Button clicked:', event.target);
}
});
Explanation:
 The event listener is added to the #parent element.
 When a button inside the #parent is clicked, the event bubbles up to the
parent, and we check if the target matches the button.className. If it does,
we handle the event.
Advantages:
 Fewer event listeners to manage.
44

 Handles dynamically created elements.


 Improves performance, especially with large numbers of elements.

3. What are the Differences Between addEventListener and Inline Event


Handlers?
addEventListener:
 Flexibility: Can add multiple event listeners to the same element, for the
same or different events.
 Separation of Concerns: Keeps JavaScript and HTML code separate,
promoting cleaner and more maintainable code.
 Control: Allows you to specify the type of event, as well as whether it should
be captured or bubbled (by using the capture parameter).
 Supports removeEventListener: You can remove event listeners later.
Example:
let button = document.querySelector('button');

button.addEventListener('click', function() {
console.log('Button clicked!');
});
Key Points:
 You can attach multiple listeners to the same element.
 It allows for more control over event propagation (e.g., capture phase vs.
bubble phase).
 You can remove event listeners with removeEventListener.
Inline Event Handlers:
 Limited Flexibility: Only one event handler can be attached to an element.
 Mixing HTML and JavaScript: Event handling is written directly in the
HTML, which can lead to messy code and difficulty in debugging.
 Less Control: You don't have control over event propagation or more
advanced features like capture.
 No Removal: It's difficult to remove an inline event handler once it’s
attached.
Example:
<button onclick="alert('Button clicked!')">Click Me</button>
45

Key Points:
 Event handling is directly written in HTML, which mixes content and behavior.
 You can only attach one event handler for a specific event.
 It’s harder to maintain as the code grows, especially in larger applications.

4. Important Considerations & Extra Notes


Event Propagation (Bubbling and Capturing)
 Bubbling: After an event is triggered on an element, it bubbles up through
its ancestors in the DOM tree. Most events bubble by default.
 Capturing: The event is captured by the parent elements before reaching
the target element. This is less common but can be specified when using
addEventListener.
Example with Bubbling and Capturing:
// Event Listener in the capturing phase
document.querySelector('#parent').addEventListener('click', function() {
console.log('Captured by parent');
}, true); // true here specifies capturing phase

// Event Listener in the bubbling phase


document.querySelector('#child').addEventListener('click', function() {
console.log('Event reached child');
});
Event Object
 The event object provides information about the event, including the target,
type, currentTarget, and defaultPrevented.
 You can call event.preventDefault() to stop the default action (like following a
link) or event.stopPropagation() to stop the event from bubbling.

Conclusion:
 DOM Traversal is fundamental for selecting and manipulating elements,
with various methods for navigating parent, child, and sibling elements.
 Event Delegation improves performance and simplifies event handling,
especially for dynamically added content.
46

 addEventListener vs. Inline Handlers: The former is more flexible,


maintainable, and offers better control over events and propagation, while
inline handlers are simpler but less scalable.
1. How does Prototypal Inheritance Work in JavaScript?
In JavaScript, inheritance is achieved through prototypes rather than classes (as
seen in languages like Java or C++). Every object in JavaScript has a prototype, and
an object can inherit properties and methods from another object’s prototype.
Key Points:
 Prototype Chain: When you try to access a property or method of an object,
JavaScript first looks for it in the object itself. If it doesn't find it, JavaScript
looks for it in the object's prototype, and if it's not there, it continues up the
prototype chain until it reaches Object.prototype.
 Creating Prototypal Inheritance: You can link objects to share properties
and methods using prototypes.
Example:
function Animal(name) {
this.name = name;
}

Animal.prototype.sayHello = function() {
console.log(`${this.name} says hello!`);
};

const dog = new Animal('Buddy');


dog.sayHello(); // Output: Buddy says hello!
Explanation:
 Animal is a constructor function, and it has a prototype property where you
can add shared methods like sayHello.
 The dog object inherits from Animal.prototype and can call sayHello even
though it’s not directly defined in dog.
Important Concepts:
 Prototype: Each function in JavaScript has a prototype property, which
points to an object that is shared by all instances of that function.
 Prototype Chain: Objects inherit from other objects via prototypes. This
chain is how JavaScript implements inheritance.
47

2. Explain ES6 Classes and Their Benefits Over Traditional Prototypes


ES6 introduced the class syntax, which makes it easier to work with inheritance and
object-oriented patterns. It is syntactic sugar over the existing prototype-based
inheritance but provides a more familiar structure for developers coming from other
object-oriented languages.
Class Syntax (ES6):
ES6 introduced the class syntax to define classes, constructors, and methods.
Example:
class Animal {
constructor(name) {
this.name = name;
}

sayHello() {
console.log(`${this.name} says hello!`);
}
}

const dog = new Animal('Buddy');


dog.sayHello(); // Output: Buddy says hello!
Explanation:
 The Animal class has a constructor method that initializes the name property.
 The sayHello method is shared across all instances of Animal.
Benefits Over Traditional Prototypes:
1. Cleaner Syntax: The class syntax makes it easier to define methods and
constructors without explicitly dealing with the prototype chain.
2. Inheritance Made Easy: You can use extends to inherit from another class,
making it simpler to work with inheritance.
3. Constructor and Method Syntax: No need to manually add methods to the
prototype object. Methods are defined inside the class, and they are
automatically added to the prototype.
4. Better Readability: ES6 classes provide a structure that looks similar to
other OOP languages (like Java or C#), making it easier for developers with
experience in those languages to transition to JavaScript.
Inheritance with ES6 Classes:
48

You can use the extends keyword to create subclasses, making inheritance more
straightforward.
Example:
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call the parent class constructor
this.breed = breed;
}

bark() {
console.log(`${this.name} barks!`);
}
}

const dog = new Dog('Buddy', 'Golden Retriever');


dog.sayHello(); // Output: Buddy says hello!
dog.bark(); // Output: Buddy barks!
Explanation:
 The Dog class inherits from Animal using the extends keyword.
 The super(name) calls the parent class's constructor.

3. What are Getter and Setter Methods in JavaScript?


Getters and Setters are special methods that allow you to define how a property
should be accessed or modified. They provide a way to run code when a property is
accessed or set, which is useful for validation or calculation.
Getter Method:
 A getter is a method that allows you to access a property’s value in a
controlled way.
Setter Method:
 A setter is a method that allows you to modify a property’s value in a
controlled way.
Example:
class Rectangle {
49

constructor(width, height) {
this._width = width;
this._height = height;
}

// Getter for width


get width() {
return this._width;
}

// Setter for width


set width(value) {
if (value > 0) {
this._width = value;
} else {
console.log('Width must be greater than 0');
}
}

// Getter for height


get height() {
return this._height;
}

// Setter for height


set height(value) {
if (value > 0) {
this._height = value;
} else {
console.log('Height must be greater than 0');
}
50

// Area method
get area() {
return this._width * this._height;
}
}

let rect = new Rectangle(10, 20);


console.log(rect.area); // Output: 200

rect.width = 15; // Calls the setter


console.log(rect.area); // Output: 300

rect.width = -5; // Invalid value, so the setter logs an error


Explanation:
 The getter and setter methods are defined for width and height to control
how these properties are accessed and modified.
 The area property is a getter that calculates the area based on the width and
height values.
Why Use Getters and Setters?
 Encapsulation: They allow you to hide the internal details of an object and
provide controlled access to properties.
 Validation: Setters allow you to validate inputs before updating a property
(e.g., ensure a value is positive).
 Computed Properties: Getters allow you to define computed properties
(like area in the example above), which are calculated on the fly.

Summary of Key Concepts:


 Prototypal Inheritance: Objects in JavaScript can inherit from other objects
via prototypes. This inheritance is dynamic and flexible.
 ES6 Classes: A more structured and easier-to-understand way to work with
prototypes and inheritance. Classes provide cleaner syntax and better
organization.
51

 Getters and Setters: Special methods to control access to object


properties, allowing for encapsulation, validation, and computed properties.
1. What is the Difference Between CommonJS and ES6 Modules?
JavaScript has different module systems for organizing code into reusable units, and
the two most common ones are CommonJS and ES6 Modules.
CommonJS Modules (Node.js Modules):
 Synchronous: CommonJS modules are designed to work synchronously,
which means they are loaded in a blocking manner. This is fine for server-side
environments like Node.js but not ideal for browsers.
 require() and module.exports: CommonJS uses require() to import
modules and module.exports or exports to export them.
 Used in Node.js: CommonJS is the default module system in Node.js.
Example:
// module1.js
module.exports = function greet(name) {
console.log(`Hello, ${name}!`);
};

// main.js
const greet = require('./module1');
greet('John'); // Output: Hello, John!
ES6 Modules:
 Asynchronous: ES6 modules are designed for asynchronous loading and can
be imported dynamically, which is better for modern web development
(browser and server-side).
 import and export: ES6 modules use import to bring in modules and export
to share functions, objects, or variables.
 Static: ES6 imports are static, meaning the dependencies are determined at
compile-time, which allows for better optimizations like tree shaking
(removing unused code).
 Universal: ES6 modules can be used both in the browser and Node.js (with
support from tools like Babel or newer versions of Node.js).
Example:
// module1.js
export function greet(name) {
52

console.log(`Hello, ${name}!`);
}

// main.js
import { greet } from './module1';
greet('John'); // Output: Hello, John!
Key Differences:
 Syntax: require() and module.exports (CommonJS) vs. import and export
(ES6).
 Loading Mechanism: CommonJS is synchronous, while ES6 modules are
asynchronous and support dynamic imports.
 Scope: ES6 modules have a block scope, meaning variables and functions
inside modules are scoped to the module itself, avoiding pollution of the
global namespace.
 Support for Named Exports: ES6 modules support both named exports
and default exports, while CommonJS generally exports a single entity.

2. How Do You Import and Export Functions or Objects in JavaScript?


With ES6 modules, you can export functions, variables, or objects from one module
and import them into another.
Named Exports:
 You can export multiple functions or variables from a module by using
named exports.
Example:
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// main.js
import { add, subtract } from './math';
console.log(add(5, 3)); // Output: 8
console.log(subtract(5, 3)); // Output: 2
Default Export:
53

 A module can have only one default export, which allows you to export a
single function, class, or object.
Example:
// greet.js
export default function greet(name) {
console.log(`Hello, ${name}!`);
}

// main.js
import greet from './greet';
greet('John'); // Output: Hello, John!
You can also combine named exports with a default export:
Example:
// utils.js
export const multiply = (a, b) => a * b;
export default function add(a, b) {
return a + b;
}

// main.js
import add, { multiply } from './utils';
console.log(add(2, 3)); // Output: 5
console.log(multiply(2, 3)); // Output: 6
Dynamic Import (ES6):
 You can also use dynamic imports to load modules asynchronously, which is
useful when you need to load a module on demand (lazy loading).
Example:
// main.js
function loadModule() {
import('./math.js').then(module => {
console.log(module.add(2, 3)); // Output: 5
});
54

}
Re-exporting Modules:
 You can re-export from other modules using export * or export
{ specificFunction }.
Example:
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// utils.js
export * from './math'; // Re-exports all exports from math.js

// main.js
import { add, subtract } from './utils';
console.log(add(5, 3)); // Output: 8
console.log(subtract(5, 3)); // Output: 2

3. Practical Tips for Working with Modules in JavaScript


 Use default for a Single Export: When your module has a primary entity
(e.g., a function or a class), use the default export to make it easier to import
without curly braces.
 Use Named Exports for Multiple Entities: If your module has several
utilities or helpers, use named exports to maintain clarity about what is being
imported.
 Avoid Mixing CommonJS and ES6 Imports: While mixing CommonJS and
ES6 modules is technically possible, it can lead to compatibility issues. Stick
to one module system within a project, preferably ES6 for modern JavaScript
development.
 Tools like Webpack or Babel: If you're using ES6 modules in a browser
environment, tools like Webpack or Babel can help you transpile and bundle
the modules for compatibility with older browsers.
 Tree Shaking: ES6 modules enable tree shaking, which removes unused
exports from the final bundle, improving performance. CommonJS does not
support tree shaking because it uses a dynamic require() system.

Conclusion:
55

 CommonJS: Older, synchronous module system, primarily used in Node.js.


Uses require() and module.exports.
 ES6 Modules: Newer, asynchronous, and static module system. Uses import
and export for cleaner, more maintainable code, and supports tree shaking
for optimized code.
 Import/Export Syntax: Learn the difference between named exports,
default exports, and how to dynamically import modules.
Understanding modules and how to work with them efficiently is crucial for building
maintainable and scalable applications. This knowledge is essential for interviews,
especially at top-tier product-based companies where you might work on large
codebases or collaborate in large teams.
1. How Does Garbage Collection Work in JavaScript?
Garbage Collection (GC) is a process that automatically frees up memory by
removing objects that are no longer in use. In JavaScript, this process is handled by
the JavaScript engine (like V8 in Chrome or SpiderMonkey in Firefox), so developers
don’t need to manually manage memory allocation and deallocation.
How Garbage Collection Works:
 Automatic Process: When objects are no longer referenced by any part of
your code, the garbage collector marks them as unreachable and removes
them from memory.
 Reachability: An object is considered "reachable" if it can be accessed by
the running code, starting from the root (global objects, local variables in the
current function call, etc.).
 Two Common Techniques Used in Garbage Collection:
1. Mark-and-Sweep: The garbage collector marks all reachable objects.
Then, it "sweeps" through the memory and deletes any objects that
were not marked.
2. Reference Counting: This approach counts the references to an
object. When the reference count drops to zero (meaning no part of the
code can access the object), it is removed. However, this technique
has a problem with circular references.
Garbage Collection Phases:
 Mark Phase: The garbage collector starts by marking all the objects that are
still reachable (i.e., objects that are accessible from the root).
 Sweep Phase: After marking, it sweeps through the heap, removing all
objects that were not marked as reachable.
Note: The garbage collection process is usually triggered when the JavaScript
engine detects that the system is running low on memory. This process is
automatic, and you don't need to explicitly call it.
Example:
56

function createObject() {
let obj = { name: 'John' }; // obj is reachable here
obj = null; // Now, obj is unreachable and can be garbage collected
}
createObject();
In this example, after setting obj to null, the object becomes unreachable and will
eventually be collected by the garbage collector.

2. What Are Memory Leaks, and How Can You Avoid Them?
A memory leak happens when an application retains more memory than
necessary, often due to objects that are no longer used but are still kept in memory.
Over time, memory leaks can cause an application to slow down or even crash, as
the program consumes more and more resources without releasing them.
Common Causes of Memory Leaks in JavaScript:
1. Global Variables: If you accidentally create global variables or fail to clean
up references, they stay in memory for the entire lifetime of the application.
o Solution: Minimize the use of global variables, and always declare
variables using let, const, or var in a local scope.
2. Unintentional Closures: JavaScript closures can unintentionally "capture"
variables that are no longer needed, causing memory to be held
unnecessarily.
o Solution: Be cautious when using closures. Avoid holding unnecessary
references inside closures that outlive their intended scope.
3. Detached DOM Nodes: If you remove DOM elements but still hold
references to them in JavaScript, the garbage collector won't be able to clean
them up.
o Solution: Ensure that event listeners and references to DOM elements
are cleaned up when they are removed from the DOM.
4. Event Listeners: If you attach event listeners to DOM elements but forget to
remove them when they are no longer needed, those event listeners will keep
references to the elements and prevent them from being garbage collected.
o Solution: Always remove event listeners when they are no longer
necessary using removeEventListener.
5. Timers and Intervals: Functions like setInterval or setTimeout can create
memory leaks if the intervals are never cleared, especially if they reference
large objects.
o Solution: Always clear intervals or timeouts using clearInterval or
clearTimeout when they are no longer needed.
57

Best Practices to Avoid Memory Leaks:


1. Use Weak References: Use WeakMap or WeakSet for references that do not
prevent garbage collection. These types of collections hold objects weakly,
meaning that if there are no other references to an object, it can be garbage
collected.
Example:
let weakMap = new WeakMap();
let obj = { name: 'Jane' };
weakMap.set(obj, 'data');

obj = null; // Now obj can be garbage collected


2. Remove References: Ensure that you clean up references to objects once
they are no longer needed. This is especially important for DOM elements,
event listeners, and intervals.
3. Monitor Memory Usage: In some browsers, you can use built-in tools like
the Chrome DevTools Memory panel to check for memory leaks. You can take
heap snapshots and look for detached DOM nodes or excessive memory
usage.
4. Use Modern Libraries: Modern JavaScript frameworks (like Angular, React,
etc.) and libraries have mechanisms to handle memory management
automatically, but it's still important to understand how they work to avoid
unnecessary memory usage.

Conclusion:
 Garbage Collection in JavaScript is an automatic process where the
JavaScript engine frees up memory by removing unreachable objects. It
typically uses techniques like mark-and-sweep or reference counting.
 Memory Leaks occur when objects are kept in memory even though they
are no longer needed. Common causes include global variables, unintentional
closures, detached DOM nodes, and unremoved event listeners.
 To avoid memory leaks, it’s crucial to manage scope effectively, clean up
references, and monitor memory usage.
Understanding memory management and how to avoid memory leaks is crucial,
especially in performance-sensitive applications like those built with Angular, where
improper memory management can lead to slow applications and bad user
experiences.
1. How Can You Optimize JavaScript Code for Performance?
Optimizing JavaScript is crucial for enhancing user experience by improving the
performance of applications. Below are some techniques that can be used:
58

1.1 Minimize DOM Manipulation


 DOM manipulation can be slow, especially if you're accessing or modifying
the DOM frequently. Try to reduce the number of DOM reads and writes.
 Use batch DOM updates and requestAnimationFrame for smooth
animations, reducing layout thrashing (when the browser has to recalculate
styles and re-render the page multiple times).
Example: Instead of updating the DOM one element at a time:
// Avoid multiple DOM manipulations
for (let i = 0; i < 1000; i++) {
let div = document.createElement('div');
div.textContent = 'Item ' + i;
document.body.appendChild(div);
}
You can batch DOM manipulations using innerHTML:
let items = '';
for (let i = 0; i < 1000; i++) {
items += `<div>Item ${i}</div>`;
}
document.body.innerHTML += items;
1.2 Minimize Reflows and Repaints
 Reflows and repaints are expensive operations in the browser. A reflow
occurs when the browser has to recalculate the layout (e.g., due to changes
in element size, position, or other properties). A repaint happens when the
style of an element changes but does not affect its layout.
 Avoid frequent changes to styles that can trigger reflows and repaints. Use
CSS transitions or animations when possible, instead of JavaScript-driven
animations.
1.3 Debouncing and Throttling
 Use debouncing to limit how often a function is called, especially for
functions triggered by events like scroll, resize, or keyup. Throttling is useful
when you want to limit the frequency of function calls over time.
Example (debouncing):
function debounce(func, delay) {
let timer;
return function(...args) {
59

clearTimeout(timer);
timer = setTimeout(() => func(...args), delay);
};
}
window.addEventListener('resize', debounce(() => console.log('Resized!'), 300));
1.4 Avoid Memory Leaks
 Memory leaks can degrade performance over time, so it’s essential to remove
event listeners, clean up intervals, and free up resources when they’re no
longer needed.
 Use WeakMap for storing data to avoid preventing garbage collection.
1.5 Optimize Loops
 Loops can be slow if they are not optimized. Use local variables in the loop,
avoid array.length in the loop condition (since array.length is recalculated on
every iteration), and use modern loop methods like forEach, map, or reduce
where appropriate.
Example:
let len = array.length;
for (let i = 0; i < len; i++) {
console.log(array[i]);
}
1.6 Lazy Loading
 Load resources only when they are needed. For example, you can load
images or modules only when they come into view (lazy loading), which can
greatly improve performance.
Example (Lazy Loading Images):
html
<img data-src="image.jpg" class="lazy" />
document.addEventListener('scroll', function() {
let images = document.querySelectorAll('.lazy');
images.forEach((img) => {
if (img.getBoundingClientRect().top < window.innerHeight) {
img.src = img.dataset.src;
img.classList.remove('lazy');
}
60

});
});

2. What Are the Best Practices to Improve the Load Time of a Web
Application?
Improving the load time of a web application is crucial to providing a fast and
smooth user experience. The following practices can help you reduce load times:
2.1 Minify and Compress Code
 Minify your JavaScript, CSS, and HTML files to remove unnecessary
whitespace, comments, and line breaks.
 Compress files using gzip or Brotli to reduce the size of the files being sent
over the network. This significantly reduces download time.
2.2 Bundle Files
 Instead of loading many individual JavaScript files, bundle your JavaScript
and CSS files into fewer, larger files. This reduces the number of HTTP
requests made during page load.
 Tools like Webpack, Rollup, and Parcel can help you bundle and optimize
files.
2.3 Implement Code Splitting
 Code splitting allows you to break your JavaScript code into smaller chunks
and load only the code that is necessary for a particular page or route,
instead of loading the entire application upfront.
Example (Code Splitting with Webpack):
import(/* webpackChunkName: "myModule" */ './myModule').then(module => {
// Use the module
});
2.4 Lazy Load Non-Essential Resources
 Load JavaScript, images, and other resources only when they are needed.
Lazy load non-critical resources such as images, videos, or even parts of the
JavaScript code for faster initial page load.
2.5 Use a Content Delivery Network (CDN)
 Serve static assets like images, CSS, and JavaScript from a Content Delivery
Network (CDN). CDNs have multiple servers across the globe, which helps
reduce latency by serving files from the server closest to the user.
2.6 Optimize Images
61

 Images can often be the largest asset on a web page. Use modern image
formats like WebP and compress images to reduce file size.
 Consider using responsive images (srcset) to load images based on the
device's resolution.
Example:
<img srcset="image-500w.jpg 500w, image-1000w.jpg 1000w" src="image.jpg"
alt="Responsive Image">
2.7 Reduce HTTP Requests
 Minimize the number of requests made to the server. This can be done by:
o Combining CSS and JavaScript files.
o Using SVG sprites for icons.
o Using data URIs for small images (like icons).
2.8 Use Service Workers for Caching
 Service workers allow you to cache resources on the client side, so
subsequent visits to the website will load faster. Service workers can
intercept network requests and serve cached resources even when the user is
offline.
2.9 Enable HTTP/2 or HTTP/3
 Use HTTP/2 or HTTP/3 to allow multiplexing, header compression, and other
improvements that can reduce latency and speed up loading.
2.10 Optimize JavaScript Execution
 Defer or async the loading of JavaScript that is not needed immediately.
 Use Web Workers to offload CPU-intensive tasks to background threads,
allowing the main thread to remain free and responsive.
Example:
<script src="script.js" defer></script>
2.11 Use Browser Caching
 Set cache-control headers for static resources to instruct the browser to
cache them locally. This way, files don’t need to be downloaded again on
subsequent page loads.

Conclusion:
To optimize JavaScript code for performance:
 Minimize DOM manipulations, use debouncing and throttling, avoid
unnecessary memory usage, and optimize loops.
62

 For improving web application load times:


o Minify and compress files, bundle resources, lazy load non-critical
assets, use CDNs, and leverage HTTP/2 or HTTP/3.
o Also, consider code splitting, image optimization, and service
workers for efficient caching.
By following these best practices, you can significantly improve both the
performance of JavaScript code and the load time of your web applications, which is
essential for delivering a fast, smooth experience, particularly in high-traffic or
performance-sensitive applications.
1. What Are Some Commonly Used Design Patterns in JavaScript?
Here are some of the most commonly used design patterns in JavaScript:
1.1 Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a
global point of access to it.
 Use Case: This pattern is used when you want to make sure that a certain
object or service is shared across your application, such as a logging service
or configuration object.
1.2 Observer Pattern
The Observer pattern allows one object (the "subject") to notify other objects (the
"observers") about changes in its state, typically by calling one of their methods.
 Use Case: It's commonly used in event-driven systems (like UI frameworks)
where various parts of an application need to respond to state changes or
events without tightly coupling them together.
1.3 Module Pattern
The Module pattern is used to create modular code by organizing functions and
variables into individual units, providing a well-defined public API while keeping
internal details private.
 Use Case: This pattern is widely used to encapsulate functionality in
JavaScript and avoid polluting the global scope. It's often used in combination
with the IIFE (Immediately Invoked Function Expression).
1.4 Factory Pattern
The Factory pattern allows for the creation of objects without specifying the exact
class of object that will be created. This pattern is often used to manage object
creation when there is a need to create different types of objects based on specific
conditions.
 Use Case: When you need to create objects from different classes but don’t
want to expose the specific class to the rest of your application.
1.5 Prototype Pattern
63

The Prototype pattern is used to create new objects by copying an existing


object, rather than creating new instances from scratch. It is particularly useful
when you need to create large numbers of similar objects.
 Use Case: When object creation is expensive or involves complex
initialization, the prototype pattern allows you to create a new object by
cloning an existing prototype.
1.6 Command Pattern
The Command pattern turns a request into a stand-alone object that contains all
the information about the request, including the action, the recipient, and the
parameters.
 Use Case: It's useful in situations where requests need to be handled in
different ways (e.g., undo/redo operations or event handling systems).
1.7 Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by creating a
wrapper around an existing object, converting the interface into one that is
compatible with the client.
 Use Case: When you want to integrate new functionality into an existing
system without changing the existing code.

2. Can You Explain the Singleton and Observer Patterns with Examples?
2.1 Singleton Pattern:
The Singleton pattern ensures that a class has only one instance and provides a
global point of access to it.
Example: Imagine you are building an application that uses a logging service. You
want to ensure that there is only one instance of the logger throughout the entire
application.
class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance; // Return the existing instance
}
this.logs = [];
Logger.instance = this; // Store the instance
}

log(message) {
64

this.logs.push(message);
console.log(message);
}

getLogs() {
return this.logs;
}
}

// Usage
const logger1 = new Logger();
const logger2 = new Logger();

logger1.log("This is a log message.");


logger2.log("This is another log message.");

console.log(logger1.getLogs()); // ["This is a log message.", "This is another log


message."]
console.log(logger1 === logger2); // true (Both references point to the same
instance)
In this example, the Logger class ensures that only one instance exists. When a
new instance is created, it checks if the instance already exists and returns it
instead of creating a new one.
2.2 Observer Pattern:
The Observer pattern allows a subject to notify its observers when its state changes.
It's useful when you need to broadcast changes to multiple objects without tightly
coupling them together.
Example: Imagine a scenario where multiple components need to be notified when
a user's data changes.
class User {
constructor(name) {
this.name = name;
this.observers = [];
}
65

addObserver(observer) {
this.observers.push(observer);
}

removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}

notifyObservers() {
this.observers.forEach(observer => observer.update(this));
}

setName(name) {
this.name = name;
this.notifyObservers(); // Notify all observers about the name change
}
}

class Logger {
update(user) {
console.log(`User's name has changed to: ${user.name}`);
}
}

class EmailService {
update(user) {
console.log(`Sending email about name change to: ${user.name}`);
}
}
66

// Usage
const user = new User("John");

const logger = new Logger();


const emailService = new EmailService();

user.addObserver(logger);
user.addObserver(emailService);

user.setName("Jane"); // Both the logger and email service are notified


In this example, the User class is the subject that holds a list of observers (Logger
and EmailService). When the user’s name changes, all observers are notified, and
they can react to this change.
 The Logger observer logs the change to the console.
 The EmailService observer might send an email notification.
This pattern is especially useful in event-driven programming or UI updates (for
instance, in Angular, where you might subscribe to a service that broadcasts state
changes).

Conclusion:
 Singleton Pattern: Ensures that a class has only one instance and provides
a global point of access to it. It's useful for shared resources like logging,
configuration settings, or database connections.
 Observer Pattern: Allows one object (subject) to notify other objects
(observers) about changes to its state. This pattern is great for implementing
event-driven systems and is widely used in UI frameworks where various
components need to react to changes in state.
Both of these patterns are essential tools in a developer's toolkit for designing
clean, scalable, and maintainable applications. By understanding these and other
design patterns, you'll be able to create better, more efficient code, which is crucial
when building complex applications, especially in product-based companies.
14. JavaScript Frameworks and Libraries
Understanding JavaScript frameworks and libraries is essential for any senior
developer. These tools can help streamline development by providing pre-built
solutions to common problems, and can significantly improve performance and
scalability. Below are key concepts regarding JavaScript frameworks and libraries,
focusing on the Virtual DOM and Angular's Change Detection and Zone.js.
67

1. What is the Virtual DOM, and How is it Different from the Real DOM?
The Virtual DOM (VDOM) is an abstraction of the Real DOM (RD) used in modern
JavaScript frameworks and libraries like React. Understanding the distinction
between the two is important for performance optimization and efficient rendering.
Virtual DOM:
 Definition: The Virtual DOM is an in-memory representation of the actual
DOM elements. It is essentially a lightweight copy of the real DOM.
 Purpose: The main goal of the Virtual DOM is to minimize direct
manipulation of the real DOM, which can be slow and inefficient,
especially in large applications. By using the VDOM, changes can be made
quickly in memory and then "patched" to the real DOM in a more optimized
and batch-processed way.
 How It Works:
o When the state of an application changes, the Virtual DOM is updated
first.
o A diffing algorithm compares the current version of the Virtual DOM
with a previous version, calculating the minimal set of changes
required.
o Only the necessary changes are then applied to the real DOM. This
process is known as reconciliation.
Example: In React, every time state or props change, the VDOM is updated and
compared to the previous version. The framework efficiently updates only the parts
of the actual DOM that have changed.
javascript
// Virtual DOM diffing in React
function App() {
const [counter, setCounter] = useState(0);

return (
<div>
<button onClick={() => setCounter(counter + 1)}>
Count: {counter}
</button>
</div>
);
68

}
In this example, the button’s text changes when the state counter updates. React’s
Virtual DOM helps ensure that only the button text is updated in the real DOM,
rather than re-rendering the entire page.
Real DOM:
 Definition: The Real DOM is the actual DOM that the browser uses to render
the page on screen.
 Performance: Manipulating the real DOM directly can be expensive because
it forces the browser to recalculate styles, reflow layouts, and repaint the
page. Frequent and large updates to the real DOM can result in poor
performance.
Key Differences:
 Virtual DOM is faster and more efficient because it minimizes direct
manipulations of the real DOM.
 The Real DOM is slower, especially with frequent updates, because the
browser must fully re-render the page or section of the page.

2. How Do Angular's Change Detection and Zone.js Work in JavaScript?


Angular is a powerful framework, and understanding its change detection
mechanism and how it works with Zone.js is essential, especially for performance
optimization and ensuring that Angular updates the UI efficiently.
2.1 Angular Change Detection:
Angular uses change detection to update the view (UI) when the model (data)
changes. The main objective of change detection is to ensure that the UI reflects the
latest state of the data in the application.
 How It Works: Angular performs change detection by checking the state of
the application's data model and comparing it with the view (UI) to see if they
are in sync. When changes occur in the model, Angular updates the view.
o Change Detection Strategies:
 Default: In this mode, Angular checks every component in the
component tree for changes. This can be inefficient for large
applications because Angular checks all components every time
a change occurs, even if some components don't require any
update.
 OnPush: With this strategy, Angular only checks a component
for changes when its inputs change or if an event inside the
component triggers a change. This improves performance by
limiting the number of checks.
69

 Example: If you update a property in your component, Angular triggers a


change detection cycle, ensuring the view reflects the new data.
javascript
@Component({
selector: 'app-counter',
template: `<button
(click)="increment()">Increment</button><p>{{ count }}</p>`,
})
export class CounterComponent {
count = 0;

increment() {
this.count++;
}
}
In this case, when the button is clicked, the increment method updates the count
property, and Angular triggers change detection to update the view.
2.2 Zone.js:
Zone.js is a library used in Angular to manage asynchronous operations (such as
HTTP requests, timers, etc.). It is integral to Angular's change detection system
because it helps track asynchronous activities and automatically triggers change
detection when the asynchronous code completes.
 How It Works:
o Zone.js creates an execution context (a zone) around asynchronous
tasks. This allows Angular to detect when tasks like HTTP requests or
timers have completed, and it triggers change detection in response to
these events.
o Zone.js "monitors" the execution flow and can hook into events such as
setTimeout, HTTP requests, or promise resolutions.
o When any asynchronous task completes, Zone.js informs Angular,
which triggers change detection to update the UI.
 Example: Angular uses Zone.js behind the scenes to automatically detect
when asynchronous tasks are completed. For example, when an HTTP request
returns, Zone.js helps Angular know when to check if the view needs to be
updated.
this.http.get('/api/data').subscribe(data => {
70

this.data = data; // Zone.js triggers change detection after the response is


received
});
In this example, after the HTTP request completes, Zone.js ensures Angular
performs change detection so that the view reflects the new data.
Key Points to Note:
 Change Detection is the process by which Angular updates the view when
the data model changes.
 Zone.js is responsible for tracking asynchronous operations and ensuring
that Angular’s change detection system is triggered when those operations
are completed.

Conclusion:
 Virtual DOM: The Virtual DOM is a lightweight representation of the real
DOM. It improves performance by minimizing direct updates to the real DOM
and allows for optimized rendering via diffing algorithms. It's used primarily in
React but is also part of many other modern frameworks.
 Angular’s Change Detection and Zone.js: Angular uses change detection
to update the view whenever the data model changes. The OnPush strategy
allows for more efficient checks. Zone.js tracks asynchronous operations and
triggers change detection when they complete, ensuring that the UI is in sync
with the application state.
Both concepts are foundational to building performant web applications.
Understanding how the Virtual DOM works and how Angular uses change detection
with Zone.js will help you write more efficient, scalable applications and optimize
your performance, which is a key concern in product-based companies.
15. Testing in JavaScript
Testing is a critical part of software development. It ensures that the application
works as expected, reduces bugs, and increases maintainability. In JavaScript, there
are various strategies and tools available for testing code. Unit testing, mocks, and
spies are essential concepts to understand, especially when preparing for interviews
at top product-based companies. Below are explanations of key testing concepts in
JavaScript:

1. What is Unit Testing in JavaScript, and How Do You Implement It?


Unit Testing:
Unit testing is a software testing technique where individual units or components of
a software are tested in isolation from the rest of the application. The purpose of
unit testing is to verify that a specific piece of code (usually a function or method)
behaves as expected under different conditions.
71

 Unit tests should be small, isolated tests that check the functionality of a
particular unit of code.
 The goal is to ensure that each unit of the program works independently and
correctly, which in turn guarantees that the overall application works as
intended.
Why Unit Testing is Important:
 Ensures Code Quality: Unit tests help ensure that the logic in your code
works as expected and can catch bugs early.
 Reduces Regression: By running tests whenever changes are made, unit
tests help prevent existing features from breaking when new code is added.
 Increases Maintainability: Well-tested code is easier to maintain and
refactor since the tests provide confidence that the code will continue to work
after changes.
How to Implement Unit Testing in JavaScript:
To implement unit testing, you need a testing framework and possibly a mocking
library. Some popular JavaScript testing frameworks are Jest, Mocha, and
Jasmine. Here's how you can implement unit tests using Jest:
 Install Jest:
If you're using npm (Node Package Manager), you can install Jest by running:
bash
npm install --save-dev jest
 Writing Unit Tests:
Unit tests typically go into a __tests__ or test directory. Here's an example of a
simple unit test:
// calculator.js (Function to be tested)
function add(a, b) {
return a + b;
}

module.exports = add;

// calculator.test.js (Unit test file)


const add = require('./calculator');

test('adds 1 + 2 to equal 3', () => {


72

expect(add(1, 2)).toBe(3);
});

test('adds -1 + 2 to equal 1', () => {


expect(add(-1, 2)).toBe(1);
});
In the example:
 add() is the function under test.
 test() defines a unit test. Inside the test, expect() is used to assert that the
output of the add() function matches the expected value.
Running Tests:
To run tests in Jest, add a script in your package.json file:
"scripts": {
"test": "jest"
}
Then, you can run your tests using:
npm test
This will execute all the tests and give you feedback on whether the tests pass or
fail.

2. What are Mocks and Spies in Testing?


Mocks:
A mock is an object or function that simulates the behavior of real objects or
functions. Mocks are typically used to isolate the unit under test and avoid
interactions with external systems such as APIs, databases, or other services during
testing. Mocks allow you to specify predefined behaviors and expectations for these
external dependencies.
 Use Cases for Mocks:
o When you want to isolate your unit tests from external dependencies
like API calls or database operations.
o When testing code that interacts with other services that are difficult or
time-consuming to test directly.
Example: Using Jest to mock a function call.
// api.js (A function that calls an external API)
73

function fetchData(url) {
return fetch(url).then(response => response.json());
}

module.exports = fetchData;

// api.test.js (Mocking fetch)


const fetchData = require('./api');
global.fetch = jest.fn().mockResolvedValue({ data: 'test' }); // Mock the fetch API

test('fetchData calls the API and returns data', async () => {


const data = await fetchData('https://api.example.com/data');
expect(data).toEqual({ data: 'test' });
expect(fetch).toHaveBeenCalledWith('https://api.example.com/data');
});
In this example:
 The fetch function is mocked to simulate an API call.
 The mockResolvedValue method is used to specify the value that the mock
will return when called.
 The test checks whether the function works as expected, and if fetch was
called with the correct URL.
Spies:
A spy is a function that allows you to track the calls to other functions. It doesn't
replace the function being tracked but "watches" it, so you can assert whether it
was called and with what arguments.
 Use Cases for Spies:
o To check if a function was called correctly (e.g., correct parameters,
number of calls).
o To test the interaction between components or modules.
Example: Using Jest to spy on a function.
// Example: spyOn in Jest
const fetchData = require('./api');
const logger = require('./logger');
74

// Spy on the logger function


jest.spyOn(logger, 'log');

test('logger logs the data after fetch', async () => {


await fetchData('https://api.example.com/data');
expect(logger.log).toHaveBeenCalledWith('Data fetched successfully');
});
In this example:
 The jest.spyOn function is used to spy on the logger.log method.
 The test ensures that after fetchData() is called, the logger's log method is
invoked with the expected string.
Key Differences:
 Mocks replace a real function or object and provide predefined behaviors,
while spies track and observe the behavior of real functions.
 Mocks are typically used to avoid interacting with external systems during
testing, while spies are useful for tracking function calls and ensuring that the
code behaves as expected.

Conclusion:
 Unit Testing is essential for ensuring that individual units of your application
work as expected. It helps catch bugs early and ensures code quality.
Frameworks like Jest, Mocha, and Jasmine make it easy to write and run
tests.
 Mocks are used to simulate external dependencies, allowing you to isolate
your code during testing, while spies are useful for tracking function calls
and verifying interactions between components.
For top product-based companies, proficiency in writing unit tests and
understanding how to use mocks and spies effectively is critical, as it demonstrates
a strong ability to maintain code quality, detect issues early, and ensure the
stability of the application.
16. TypeScript
TypeScript is a superset of JavaScript that introduces static typing, interfaces, and
advanced features to the language. Understanding TypeScript is crucial for Angular
developers because Angular is built with TypeScript. Here, we will cover the key
differences between JavaScript and TypeScript and how TypeScript improves the
development process in Angular.
75

1. What are the Key Differences Between JavaScript and TypeScript?


While JavaScript is a dynamic, interpreted language, TypeScript is a statically
typed superset of JavaScript that compiles down to JavaScript. Here are the key
differences between JavaScript and TypeScript:
1.1 Static Typing vs. Dynamic Typing:
 JavaScript: Dynamically typed, which means variables can be assigned
different types of values during runtime (e.g., a variable can hold both a
string and a number).
 TypeScript: Statically typed, which means types are defined at compile time
and enforced during development. This helps catch type-related errors early.
Example:
let message: string = "Hello, TypeScript!"; // message is explicitly a string
message = 10; // Error: Type 'number' is not assignable to type 'string'.
In JavaScript, there would be no type error until runtime.
1.2 Compilation:
 JavaScript: Interpreted language, meaning it doesn't require any compilation
step. The browser or Node.js directly runs the code.
 TypeScript: Needs to be compiled into JavaScript before it can be run.
TypeScript uses the TypeScript Compiler (tsc) to convert .ts files into .js
files that can be executed.
Command to compile TypeScript:
bash
tsc file.ts
1.3 Type Annotations:
 JavaScript: Does not support type annotations or type checking.
 TypeScript: Supports type annotations, allowing developers to explicitly
define the types of variables, function parameters, and return types. This
provides better type safety and improves code readability.
Example:
function add(a: number, b: number): number {
return a + b;
}

console.log(add(2, 3)); // Correct usage


76

console.log(add(2, "3")); // Error: Argument of type 'string' is not assignable to


parameter of type 'number'.
1.4 Interfaces and Type Aliases:
 JavaScript: Does not have built-in support for interfaces or type aliases.
 TypeScript: Introduces interfaces and type aliases, which allow you to
define custom types for objects, classes, and functions. This is especially
useful for defining the shape of complex data structures.
Example:
interface Person {
name: string;
age: number;
}

let user: Person = { name: "Alice", age: 30 };


1.5 Classes and Inheritance:
 JavaScript: Introduces classes in ECMAScript 6 (ES6), but doesn't offer
advanced features like abstract classes, visibility modifiers, etc.
 TypeScript: Adds enhanced class features like access modifiers (public,
private, protected), abstract classes, and method overloads.
Example:
class Person {
constructor(public name: string, private age: number) {}

greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}

const person = new Person("Alice", 30);


person.greet(); // Works fine
console.log(person.age); // Error: Property 'age' is private and only accessible within
class 'Person'.
1.6 Tooling and IDE Support:
77

 JavaScript: Provides basic tooling support like syntax highlighting and basic
code completion.
 TypeScript: Offers enhanced tooling support, including intellisense (auto-
completion), type checking, and error highlighting within IDEs like Visual
Studio Code, making development more efficient.
1.7 Error Detection:
 JavaScript: Errors are typically detected at runtime (e.g., when the code is
executed in the browser or Node.js).
 TypeScript: Errors are detected during compile time, allowing developers to
catch issues before running the application. This leads to fewer runtime
errors and more reliable code.

2. How Does TypeScript Improve the Development Process in Angular?


Angular is built with TypeScript, and using TypeScript in Angular development
provides several advantages, especially when building large-scale applications.
Below are key ways TypeScript improves the Angular development process:
2.1 Type Safety and Early Error Detection:
TypeScript helps catch common bugs by providing type checking. Since Angular is
a large framework, developers can define types for services, components,
models, and observables. Type safety helps prevent issues such as passing an
object with the wrong structure to a function, which could otherwise lead to runtime
errors.
Example: Without TypeScript, a developer might accidentally pass the wrong type
of data to a component:
// JavaScript (no type checking)
@Component({
selector: 'app-user',
template: `{{ user.name }}`
})
export class UserComponent {
user = { name: "Alice" };
}
In TypeScript, we can explicitly define the user as a specific type and catch any
errors early:
// TypeScript with type checking
interface User {
78

name: string;
age: number;
}

@Component({
selector: 'app-user',
template: `{{ user.name }}`
})
export class UserComponent {
user: User = { name: "Alice", age: 30 };
}
2.2 Enhanced IDE Support and Autocompletion:
TypeScript provides better autocompletion and intellisense support in IDEs like
Visual Studio Code, which is the most popular IDE for Angular development. It
automatically shows method signatures, properties, and interfaces, helping
developers write code faster and with fewer mistakes.
Example: While defining a new service, TypeScript suggests properties and
methods based on the type of the service, which accelerates development.
2.3 Interfaces and Dependency Injection:
In Angular, dependency injection (DI) is commonly used to inject services into
components, directives, and other services. With TypeScript, you can define
interfaces for these services, providing better documentation and ensuring the
correct service is injected.
Example:
typescript
interface AuthService {
login(username: string, password: string): Observable<boolean>;
}

@Injectable({
providedIn: 'root'
})
export class AuthServiceImpl implements AuthService {
login(username: string, password: string): Observable<boolean> {
79

// Implementation here
}
}
Here, TypeScript ensures that any class implementing AuthService must follow the
correct method signature (login method with specific parameters and return type).
2.4 Decorators and Type Inference:
Angular relies heavily on decorators like @Component(), @Injectable(), and
@Input(). TypeScript improves the development process by inferring types for
parameters and return values in methods, which reduces the need for explicit type
annotations and helps maintain type consistency across the application.
Example:
typescript
@Component({
selector: 'app-product',
template: '<p>{{ product.name }}</p>',
})
export class ProductComponent {
@Input() product: Product; // TypeScript infers the type of 'product'
}
2.5 Code Refactoring and Maintenance:
TypeScript's static typing makes refactoring safer and easier. When changing
function signatures, properties, or data models, the TypeScript compiler checks the
entire codebase for compatibility issues, preventing errors from propagating across
the application. This is especially important in large Angular projects.

Conclusion:
 Key Differences Between JavaScript and TypeScript:
o TypeScript introduces static typing, type annotations, interfaces,
and advanced OOP features.
o TypeScript requires a compilation step, while JavaScript is interpreted
directly by the browser.
o TypeScript helps detect errors at compile time, improving
development efficiency and reducing bugs.
 How TypeScript Improves Development in Angular:
80

o TypeScript provides type safety, better tooling, and IDE support


that enhances the overall development process.
o It enables features like dependency injection, interfaces,
decorators, and type inference, which improve the maintainability
and scalability of Angular applications.
o With TypeScript, developers can ensure early error detection, which
makes building complex Angular applications easier and more reliable.
Understanding TypeScript and its role in Angular development is crucial for
becoming a proficient Angular developer and succeeding in interviews at top
product-based companies.
17. Advanced Asynchronous Programming
In JavaScript, handling asynchronous operations is crucial for building efficient and
responsive applications. As you move into advanced concepts, understanding
generators, async/await, and advanced Promise handling is important. Here’s a
deep dive into these topics:

1. What Are Generators, and How Do They Differ from Async/Await?


1.1 Generators:
A generator is a special type of function that can pause and resume its execution.
It is defined using the function* syntax, and it uses the yield keyword to pause
execution and return a value. Generators allow you to create iterators, which can be
useful for implementing more complex control flows, such as lazy evaluation or
managing a sequence of asynchronous tasks.
Key Points about Generators:
 Pauses execution: When a yield expression is encountered, the function’s
execution is paused, and the value is returned.
 Resumes execution: Execution can be resumed from where it was paused
using the next() method.
 Can return multiple values: Unlike regular functions that return a single
value, generators can return multiple values over time, making them more
versatile.
Example:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
81

const gen = myGenerator();

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: undefined, done: true }
 yield: Pauses the function and returns a value to the caller.
 next(): Resumes the function and proceeds to the next yield.
1.2 Async/Await:
Async/Await is a syntax sugar built on top of Promises that makes asynchronous
code look and behave more like synchronous code, making it easier to read and
maintain.
 async: Marks a function as asynchronous, meaning it will always return a
Promise.
 await: Pauses the execution of the function until the Promise resolves or
rejects. It can only be used inside an async function.
Example:
async function fetchData() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}

fetchData().then(data => console.log(data));


Key Differences between Generators and Async/Await:
1. Execution Control:
o Generators provide more control over the flow of execution. You can
explicitly pause and resume the function using yield and next().
o Async/Await abstracts away the complexity of asynchronous calls and
provides a more synchronous flow for async operations.
2. Use Cases:
o Generators are ideal for handling sequences of values and managing
complex stateful control flows.
82

o Async/Await is used primarily for simplifying asynchronous code,


especially when you are working with Promises.
3. State Management:
o Generators can maintain the state across multiple yield statements
and allow you to pause/resume the execution at any point.
o Async/Await maintains a cleaner, linear flow of execution without
manual intervention.

2. How Does the Promise.all Method Work, and What Happens if One
Promise Fails?
2.1 Promise.all:
Promise.all is a method that takes an array of Promises and returns a single
Promise that resolves when all the Promises in the array resolve, or rejects as soon
as one of the Promises rejects.
 When all promises resolve: The result is an array containing the resolved
values of each promise in the same order they were passed in.
 When one promise rejects: If any of the promises reject, Promise.all
immediately rejects with the reason of the first rejected promise, and it
doesn’t wait for the other promises to settle.
Example:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])


.then(values => console.log(values)) // [1, 2, 3]
.catch(error => console.log(error));
2.2 Handling Promise Failure with Promise.all:
If any of the promises passed to Promise.all fail (i.e., they reject), the entire
Promise.all call fails, and the rejection reason of the first rejected promise is
returned.
Example (one promise fails):
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error in promise 2');
const promise3 = Promise.resolve(3);
83

Promise.all([promise1, promise2, promise3])


.then(values => console.log(values))
.catch(error => console.log('Error:', error)); // Error: Error in promise 2
In the above example, since promise2 is rejected, Promise.all rejects immediately,
and we get the rejection reason of promise2, which is 'Error in promise 2'.
2.3 Parallel vs. Sequential Execution:
Promise.all runs all promises in parallel, and it waits for all of them to resolve
before executing the .then() block. This is in contrast to waiting for each promise
sequentially using await or chaining promises.
 Parallel Execution: Promises are executed simultaneously, and Promise.all
will resolve when all of them are finished.
 Sequential Execution: If you need promises to resolve one after the other,
you should avoid using Promise.all and instead use async/await or
chained .then() calls.
Example of sequential promises:
async function fetchData() {
const data1 = await fetch('/data1');
const data2 = await fetch('/data2');
const data3 = await fetch('/data3');
return [data1, data2, data3];
}
2.4 Promise.allSettled (Bonus):
If you want to wait for all promises to settle (either fulfilled or rejected) and handle
each result, you can use Promise.allSettled, which is a more advanced method
introduced in ES2020. It returns an array of objects that describe the outcome of
each promise.
Example:
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject('Error in promise 2');
const promise3 = Promise.resolve(3);

Promise.allSettled([promise1, promise2, promise3])


.then(results => console.log(results));
// [
84

// { status: 'fulfilled', value: 1 },


// { status: 'rejected', reason: 'Error in promise 2' },
// { status: 'fulfilled', value: 3 }
// ]

Conclusion:
 Generators vs. Async/Await:
o Generators offer greater flexibility for pausing and resuming
execution, making them useful for more complex control flows and
iterators.
o Async/Await simplifies asynchronous programming by making the
code look synchronous and easier to maintain.
 Promise.all:
o Promise.all executes promises in parallel and waits for all promises to
resolve. It fails immediately when any promise rejects.
o If you need to wait for all promises to settle, including rejected ones,
use Promise.allSettled.
By understanding and mastering these advanced asynchronous programming
techniques, you'll be able to handle complex asynchronous workflows and improve
the performance and maintainability of your JavaScript applications, especially in
frameworks like Angular.
Meta-programming in JavaScript
Meta-programming refers to the practice of writing programs that manipulate other
programs (or themselves). In JavaScript, this concept is implemented through
features like proxies and the Reflect API. These allow developers to intercept and
manipulate fundamental operations, providing more control over objects and
functions.

1. What Are Proxies in JavaScript, and What Are Their Use Cases?
1.1 Proxies:
A Proxy in JavaScript is an object that wraps another object (called the target) and
allows you to intercept and redefine fundamental operations (such as property
lookup, assignment, enumeration, function invocation, etc.) on that object. Proxies
enable you to define custom behavior for basic operations.
Creating a Proxy: The Proxy constructor takes two arguments:
 Target: The original object you want to wrap.
85

 Handler: An object that defines traps for various operations (like get, set,
etc.).
const target = {
message: 'Hello'
};

const handler = {
get: function(target, prop, receiver) {
if (prop === 'message') {
return `${target[prop]}!`;
}
return target[prop];
}
};

const proxy = new Proxy(target, handler);

console.log(proxy.message); // Output: 'Hello!'


Key Features:
 Traps: The handler object can define traps for various operations. For
example, get is a trap for property access, set for setting properties, and
apply for function invocation.
 Custom behavior: Traps allow you to modify the default behavior of an
object (e.g., validation, logging, or even transforming data).
1.2 Use Cases for Proxies:
 Validation: You can validate values before setting properties on an object.
const validator = {
set: function(target, prop, value) {
if (prop === 'age' && value < 0) {
throw new Error('Age cannot be negative');
}
target[prop] = value;
return true;
86

}
};

const person = new Proxy({}, validator);


person.age = 30; // Valid
person.age = -5; // Throws Error: Age cannot be negative
 Logging/Monitoring: Proxies can log operations like getting or setting
properties.
const logger = {
get: function(target, prop, receiver) {
console.log(`Accessed property: ${prop}`);
return prop in target ? target[prop] : undefined;
}
};

const data = new Proxy({}, logger);


data.name = 'John';
console.log(data.name); // Logs: Accessed property: name
 Property Access Control: You can prevent access to certain properties or
modify them when accessed.
const handler = {
get: function(target, prop) {
if (prop === 'password') {
return '*****'; // Hide sensitive information
}
return prop in target ? target[prop] : undefined;
}
};

const user = new Proxy({ username: 'admin', password: 'secret' }, handler);


console.log(user.password); // Output: *****
87

 Memoization: You can use proxies for caching or memoizing results of


function calls.

2. How Does the Reflect API Work in JavaScript?


2.1 Reflect API:
The Reflect API in JavaScript provides a set of methods that perform operations
similar to the traps in a Proxy, but the key difference is that Reflect methods
perform the operations directly on the target object, rather than intercepting them.
This makes it useful for meta-programming tasks, where you want to manipulate
the behavior of objects but still retain control over the original operations.
The Reflect API is part of ES6 and provides methods for intercepting basic
operations like get, set, delete, and others. It is a utility object, and all methods
return a boolean value (for success/failure) or the resulting value of the operation.
2.2 Key Methods in Reflect API:
 Reflect.get(target, prop): Retrieves the value of the property prop from
the target object.
const obj = { name: 'John' };
console.log(Reflect.get(obj, 'name')); // Output: 'John'
 Reflect.set(target, prop, value): Sets the value of the property prop on
the target object.
const obj = { name: 'John' };
Reflect.set(obj, 'name', 'Doe');
console.log(obj.name); // Output: 'Doe'
 Reflect.deleteProperty(target, prop): Deletes a property from the target
object.
const obj = { name: 'John' };
Reflect.deleteProperty(obj, 'name');
console.log(obj.name); // Output: undefined
 Reflect.has(target, prop): Checks if a property exists in the target object.
const obj = { name: 'John' };
console.log(Reflect.has(obj, 'name')); // Output: true
 Reflect.apply(target, thisArgument, args): Calls a function (the target)
with specified arguments (args) and a this context (thisArgument).
function greet(name) {
return `Hello, ${name}`;
88

console.log(Reflect.apply(greet, null, ['John'])); // Output: 'Hello, John'


2.3 Use Cases for Reflect API:
 Simplified Meta-programming: While Proxies allow us to intercept
operations, Reflect provides a direct, low-level way to interact with objects. It
simplifies code when you need to perform meta-programming without
implementing custom proxy traps.
 Proxy Integration: The Reflect API is often used in combination with Proxies
to delegate operations from the handler back to the original target object. For
example, you might use Reflect.get in the get trap to forward property access
to the target object.
const handler = {
get(target, prop, receiver) {
return Reflect.get(...arguments); // Delegates to the target object
}
};

const proxy = new Proxy({ name: 'John' }, handler);


console.log(proxy.name); // Output: 'John'

Conclusion:
 Proxies allow you to define custom behavior for fundamental operations on
objects. They are powerful for cases where you need to intercept and
manipulate operations like property access, assignment, or method calls.
 The Reflect API provides a low-level, method-based approach to interact
with objects and perform operations like get, set, and delete. It can simplify
meta-programming tasks and is often used in combination with Proxies.
Mastering these features can greatly enhance your ability to write flexible,
maintainable, and powerful JavaScript code, especially when dealing with complex
or dynamic object manipulations.
19. Security in JavaScript
Security is a critical aspect of any web application, and JavaScript, being the
backbone of web interactivity, has its own security considerations. Two key
vulnerabilities that JavaScript developers need to be aware of are Cross-Site
Scripting (XSS) and Content Security Policy (CSP).
89

1. How Can You Prevent XSS (Cross-Site Scripting) in JavaScript?


1.1 What is XSS?
Cross-Site Scripting (XSS) is a security vulnerability that allows attackers to
inject malicious scripts into content that is executed by a user's browser. This can
happen when an application doesn't properly validate or escape user input, allowing
the injection of malicious JavaScript code into web pages that other users will view.
There are three main types of XSS:
 Stored XSS: Malicious script is stored on the server and served to users.
 Reflected XSS: Malicious script is reflected off a web server (e.g., through
query parameters or form submissions).
 DOM-based XSS: Malicious script is executed when the web page
dynamically updates the DOM with untrusted data.
1.2 How to Prevent XSS:
 Escape User Input: Ensure that any dynamic content generated from user
input is properly escaped. This means converting characters like <, >, &, and
" into their HTML entity equivalents (&lt;, &gt;, &amp;, and &quot;
respectively).
const userInput = '<script>alert("XSS Attack!")</script>';
const escapedInput = userInput.replace(/</g, "&lt;").replace(/>/g, "&gt;");
console.log(escapedInput); // Output: &lt;script&gt;alert("XSS
Attack!")&lt;/script&gt;
 Use JavaScript Frameworks with Built-in Security: Modern frameworks
like Angular, React, and Vue come with automatic protection against XSS
attacks by sanitizing user input. They escape user input when injecting it into
the DOM, reducing the risk of XSS.
 Content Security Policy (CSP): A strong CSP can be used to restrict the
types of content that are allowed to be loaded or executed on a webpage,
thereby reducing the risk of XSS. More on CSP below.
 Avoid Inline JavaScript: Avoid using inline event handlers (e.g., <button
onclick="alert('Hello')">Click me</button>) and instead use event listeners
that are added through JavaScript. This makes it harder for malicious scripts
to inject their code into the page.
// Instead of this:
<button onclick="alert('Hello')">Click me</button>

// Do this:
const button = document.querySelector('button');
button.addEventListener('click', () => alert('Hello'));
90

 Sanitize Input: Use input sanitization libraries like DOMPurify or


Sanitize.js to sanitize user-generated content before inserting it into the
DOM. These libraries strip out any malicious code.
const DOMPurify = require('dompurify');
const cleanHTML = DOMPurify.sanitize('<img src="x" onerror="alert(\'XSS\')">');
console.log(cleanHTML); // Output: <img src="x">

2. What is the Role of Content Security Policy (CSP) in Securing JavaScript


Applications?
2.1 What is CSP?
Content Security Policy (CSP) is a security standard that helps prevent a variety
of attacks, including XSS and data injection attacks. CSP allows you to control
which content (JavaScript, images, styles, etc.) can be loaded and executed on your
website, reducing the surface area for attacks.
CSP is implemented via HTTP headers or meta tags and specifies which domains are
allowed to load content, preventing potentially malicious content from being
executed.
2.2 How CSP Works:
CSP works by allowing you to define a policy that restricts the types of content that
can be loaded on your site. For example:
 Script Sources: Define where JavaScript can be loaded from.
 Style Sources: Define where styles can be loaded from.
 Mixed Content: Prevents loading of content over HTTP on HTTPS websites.
2.3 Key Directives in CSP:
 default-src: A fallback directive for other content types like scripts, images,
etc.
 script-src: Defines which sources can load JavaScript.
 style-src: Defines which sources can load CSS.
 img-src: Defines which sources can load images.
 connect-src: Defines which URLs the application can make requests to (e.g.,
for APIs or WebSockets).
Example of a CSP header that allows only content from your own domain and a
specific CDN:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
This policy means:
91

 default-src 'self': Only allows content to be loaded from the same domain
as the website.
 script-src 'self' https://cdn.example.com: JavaScript can only be loaded
from the same domain or from cdn.example.com.
 style-src 'self' https://fonts.googleapis.com: Stylesheets can only be
loaded from the same domain or from fonts.googleapis.com.
2.4 Advantages of Using CSP:
 Prevents Inline Script Execution: By blocking inline scripts (using unsafe-
inline), CSP ensures that even if an attacker manages to inject code into a
page, it won’t be executed.
 Limits External Resources: By specifying trusted sources, CSP helps to
reduce the risk of loading malicious external resources.
 Helps Mitigate Data Injection Attacks: CSP can prevent attackers from
injecting malicious scripts into the DOM by restricting where content can
come from.
2.5 Example of Adding CSP to Your Website:
 HTTP Header: Add the following CSP header to your server response:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com;
 Meta Tag: You can also define CSP using a meta tag in the <head> of your
HTML document:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src
'self' https://trusted-cdn.com;">
2.6 Challenges of Implementing CSP:
 Compatibility: Some older browsers may not fully support CSP or have
limited support for certain directives.
 Policy Tuning: Developing a strict CSP requires testing to ensure that
legitimate content and scripts are not blocked by the policy. This may take
time to fine-tune.
 Inline Scripts: If your website relies heavily on inline JavaScript,
implementing a strict CSP may require refactoring to externalize scripts or
use nonces (one-time tokens) for inline scripts.

Conclusion:
 XSS vulnerabilities can be mitigated by escaping user input, avoiding inline
scripts, and using modern frameworks that automatically sanitize data.
Additionally, using Content Security Policy (CSP) allows you to restrict
where resources can be loaded from, preventing malicious content from
being executed.
92

 By following these best practices, developers can enhance the security of


their JavaScript applications and make them more resilient against common
web attacks.
Compilation and Execution in JavaScript
JavaScript is an interpreted language, but modern engines optimize its execution by
compiling it into machine code before running it. Understanding how JavaScript is
compiled and executed can give you insights into performance and behavior,
especially during debugging and code optimization.

1. How Does the JavaScript Engine Compile and Execute Code?


1.1 Overview of JavaScript Engine
A JavaScript engine is responsible for interpreting and executing JavaScript code in
the browser or a server-side environment like Node.js. The engine converts high-
level JavaScript code into executable machine code, which can then be processed
by the CPU.
Common JavaScript engines include:
 V8 (used by Google Chrome and Node.js)
 SpiderMonkey (used by Firefox)
 JavaScriptCore (Nitro) (used by Safari)
 Chakra (used by Microsoft Edge before its Chromium-based version)
1.2 Compilation Process
JavaScript engines follow these basic steps to execute code:
 Parsing: The engine first reads the JavaScript code and converts it into an
Abstract Syntax Tree (AST). The AST represents the structure of the code
(functions, variables, expressions, etc.) in a tree-like format.
 Compilation:
o In just-in-time (JIT) compilation, the engine compiles the JavaScript
code into machine code just before execution. This allows the engine to
optimize the compiled code based on runtime information.
o Modern engines like V8 first parse the code and create an intermediate
bytecode (often called the "baseline code"). The bytecode is executed
by an interpreter, and if a piece of code is run frequently, the engine
optimizes it into machine code.
 Execution: Once the code is compiled or interpreted, it is executed. This
process may involve managing the call stack, memory heap, and managing
variables and function scopes during execution.
1.3 Optimizations in JavaScript Engines
Modern JavaScript engines employ several optimization techniques, including:
93

 Inline Caching: The engine caches certain method calls to optimize access
to object properties.
 Deoptimization: If a piece of code becomes too complex for optimization
(due to dynamic typing, for example), the engine may deoptimize it and
execute it in a slower but simpler manner.
 Garbage Collection: The engine automatically manages memory by
cleaning up unused objects and data, helping optimize performance and
avoid memory leaks.
1.4 Execution Context and the Call Stack
JavaScript engines use an execution context to track the code’s execution. Every
time a function is called, a new execution context is created, and the call stack
manages these contexts. The stack keeps track of function calls and ensures the
code runs in the correct order.

2. Explain the Concept of Hoisting in JavaScript


2.1 What is Hoisting?
Hoisting is a JavaScript mechanism where variables and function declarations are
moved to the top of their containing scope (global or function) during the compile
phase, before the code is executed.
Important points about hoisting:
 Only the declarations (not the initializations) are hoisted.
 JavaScript hoists variables declared with var, but not let or const.
 Function declarations are hoisted, but function expressions (assigned to
variables) are not.
2.2 Hoisting with var
When you declare a variable using var, its declaration is hoisted to the top of the
scope, but its initialization is not. This means that the variable can be referenced
before its initialization, but its value will be undefined until the code reaches the
point where it is assigned a value.
Example:
console.log(a); // undefined
var a = 5;
console.log(a); // 5
In the above code, the declaration var a is hoisted to the top, but the initialization (a
= 5) stays in place. The output is undefined on the first console.log, because the
variable is hoisted but not yet assigned a value.
2.3 Hoisting with let and const
94

Variables declared using let and const are also hoisted, but they remain in a
"temporal dead zone" (TDZ) until they are actually declared in the code. This means
that referencing them before the declaration results in a ReferenceError.
Example:
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;
In this case, the variable a is hoisted, but since it is in the TDZ, trying to access it
before the let a = 5 line causes an error.
2.4 Hoisting with Functions
Function declarations are hoisted to the top of their scope, meaning you can call a
function before its declaration.
Example:
myFunction(); // "Hello, world!"

function myFunction() {
console.log("Hello, world!");
}
In the above example, even though myFunction is called before its declaration, the
function works because the entire function declaration (both the name and the
body) is hoisted.
2.5 Hoisting with Function Expressions
If a function is assigned to a variable (i.e., a function expression), only the variable
declaration is hoisted, not the function definition. This means calling the function
before its declaration results in an error.
Example:
javascript
myFunction(); // TypeError: myFunction is not a function

var myFunction = function() {


console.log("Hello, world!");
};
Here, the declaration var myFunction is hoisted, but the function itself is not
assigned until the line var myFunction = function() {...} is executed. Therefore,
attempting to call it before this line results in a TypeError.
95

Conclusion
 Compilation and Execution: JavaScript engines compile code in stages,
starting with parsing and creating an AST, followed by bytecode compilation,
and eventually execution. Optimization techniques like inline caching and
garbage collection are used to improve performance.
 Hoisting: Variables and function declarations are hoisted in JavaScript, with
var behaving differently than let and const. Function declarations are hoisted
fully, while function expressions (assigned to variables) are not hoisted.
Understanding these concepts is essential for efficient debugging, performance
optimization, and writing cleaner, more predictable code, especially for senior-level
positions where these details matter in everyday development.
How Do Angular Directives Work Under the Hood in JavaScript?
1.1 What Are Angular Directives?
Angular directives are special markers or functions that extend HTML functionality.
They can change the appearance, behavior, or layout of DOM elements, and they
help manage DOM manipulation in a declarative manner.
There are three main types of directives in Angular:
 Component Directives: Directives that have their own template.
 Structural Directives: Directives that alter the structure of the DOM (e.g.,
*ngIf, *ngFor).
 Attribute Directives: Directives that change the appearance or behavior of
an element (e.g., ngClass, ngStyle).
1.2 How Do Angular Directives Work Under the Hood?
Angular directives are implemented using a class that is decorated with the
@Directive decorator. This class typically has lifecycle hooks, such as ngOnInit,
ngOnChanges, ngAfterViewInit, and so on.
Angular compiles templates that contain directives using its template compiler
during runtime. Here's how they work under the hood:
 Compiler Stage: The template is parsed and converted into a set of
instructions for creating the DOM elements. Directives are identified in the
template, and Angular compiles the template to associate them with their
respective behaviors.
 Renderer: Directives can use the Angular Renderer2 service to manipulate
the DOM. For instance, an attribute directive may use the Renderer2 API to
add, remove, or modify DOM attributes or classes.
 Change Detection: Directives participate in Angular's change detection
mechanism. If a directive modifies data that affects the view, Angular will re-
evaluate the bindings and trigger change detection for that component or
directive.
96

For example, an ngClass directive dynamically changes the class of an element


based on the component's state, and Angular uses its change detection to keep the
DOM in sync with the data.

22. What is RxJS, and How Does it Integrate with JavaScript to Handle
Asynchronous Operations?
2.1 What is RxJS?
RxJS (Reactive Extensions for JavaScript) is a library for composing
asynchronous and event-based programs using observable sequences. It makes it
easier to handle asynchronous operations like HTTP requests, user input events,
timers, and more, in a more functional, declarative, and composable way.
RxJS is based on the observer pattern, where observers subscribe to streams
(observables) and receive notifications when new data is emitted.
2.2 How Does RxJS Integrate with JavaScript?
RxJS works by turning async data (such as user input, HTTP requests, or intervals)
into observable streams. These streams are processed using a series of operators
that can transform, filter, combine, or react to the emitted values.
 Observables: RxJS introduces the concept of an observable, which
represents a stream of values over time.
 Operators: RxJS operators (e.g., map, filter, merge, combineLatest,
switchMap) allow you to transform, combine, and manage observable
streams.
 Subscription: You can subscribe to an observable to listen for values
emitted by that stream. The subscription will execute a callback each time a
new value is emitted.
 Handling Asynchronous Operations: RxJS helps manage async operations
by using operators like mergeMap, concatMap, or switchMap to handle
promises or HTTP responses in a more manageable way.
For example, instead of handling multiple asynchronous HTTP requests with
callbacks, you can use the switchMap operator to chain requests:
typescript
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';

this.http.get('/user')
.pipe(switchMap(user => this.http.get(`/posts?userId=${user.id}`)))
.subscribe(posts => console.log(posts));
97

This code uses RxJS to manage asynchronous HTTP requests in a more readable and
maintainable manner.

23. How Is JavaScript Used for Customizing Angular's Dependency


Injection System?
3.1 What is Dependency Injection (DI) in Angular?
Dependency Injection is a design pattern where objects are passed to other objects,
rather than objects creating their own dependencies. In Angular, DI helps manage
the lifecycle of services and their dependencies by allowing them to be injected into
components, services, or other classes.
Angular provides a powerful DI system that allows developers to manage
dependencies easily and efficiently.
3.2 How JavaScript Customizes Angular's DI System?
In Angular, services, factories, injectors, and providers are the primary tools for
customizing the DI system.
 Providers: The @Injectable decorator marks a service as available for
dependency injection. By providing a service, you tell Angular how to create
and inject it.
Example:
typescript
@Injectable({
providedIn: 'root',
})
export class MyService {
constructor(private http: HttpClient) {}
}
 Custom Providers: You can define custom providers for services using a
factory function or a value, or by specifying a class to use as a provider.
Example of a custom provider:
typescript
{ provide: MyService, useFactory: () => new MyService(customDependency) }
 Multi Providers: Angular allows you to configure multiple providers for the
same service. This can be useful when you want to provide different
implementations depending on the use case.
Example of multi providers:
typescript
98

{ provide: MyService, useClass: MyServiceImplementation, multi: true }


 Optional and Self Injected Services: You can control how services are
injected into your components and services using decorators like
@Optional(), @Self(), and @SkipSelf() to customize DI.
 Tokens for DI: Sometimes, you need to inject non-class objects. You can
define tokens using InjectionToken to inject values or functions.
Example of using an InjectionToken:
typescript
import { InjectionToken } from '@angular/core';

export const API_URL = new InjectionToken<string>('API_URL', {


providedIn: 'root',
factory: () => 'https://api.example.com',
});

24. Explain the Concept of Zones in Angular and Their Impact on JavaScript
Code Execution
4.1 What Are Zones in Angular?
A zone is an execution context that allows Angular to keep track of asynchronous
operations, such as setTimeout, promises, HTTP requests, etc. The concept of zones
is powered by the Zone.js library, which Angular uses internally to detect changes
and trigger updates to the view when data changes asynchronously.
 Zone.js intercepts asynchronous tasks and keeps track of the context in
which they were initiated, ensuring that Angular knows when to trigger
change detection, even if the operation occurs outside of Angular's normal
flow (e.g., within a third-party library or custom async function).
4.2 How Do Zones Work in Angular?
When Angular runs in a zone, it can track all asynchronous tasks that occur within it.
It does this by using Zone.js, a library that wraps asynchronous operations (like
setTimeout, Promise, etc.) and adds additional logic to them.
 NgZone: Angular provides a service called NgZone to manage zones within
Angular applications. The NgZone service allows Angular to control and
optimize the change detection process based on which zone the
asynchronous task is executed in.
 Change Detection: When an asynchronous operation completes (e.g., an
HTTP request, timer, or event), NgZone checks if it needs to trigger change
detection. If the task is executed outside Angular's zone, Angular will not
automatically update the view. However, by running the task within Angular’s
99

zone (using NgZone.run()), Angular will trigger change detection when the
task completes.
4.3 Example:
typescript
import { Component, NgZone } from '@angular/core';

@Component({
selector: 'app-zone-example',
template: `<h1>{{message}}</h1>`
})
export class ZoneExampleComponent {
message = 'Hello';

constructor(private ngZone: NgZone) {}

changeMessage() {
setTimeout(() => {
this.message = 'Updated Message';
this.ngZone.run(() => {
// Triggers Angular's change detection to update the view
});
}, 1000);
}
}
In this example, setTimeout is executed inside Angular's zone. The ngZone.run()
method ensures that change detection is triggered when the asynchronous
operation completes.
4.4 Impact on JavaScript Code Execution
 Zones allow Angular to manage change detection more efficiently by tracking
asynchronous operations and determining when the view needs to be
updated.
 Performance: Managing zones can help improve performance by avoiding
unnecessary change detections, as only the tasks inside Angular's zone will
trigger updates.
100

 Cross-library Integration: Zones make it easier for Angular to integrate


with third-party libraries (e.g., jQuery or non-Angular async code) and still
detect changes reliably.

Conclusion
These concepts play a critical role in Angular applications and JavaScript code
execution:
 Directives enhance the declarative nature of DOM manipulation.
 RxJS provides a powerful way to handle asynchronous tasks.
 Angular's DI system can be customized for more flexible dependency
management.
 Zones ensure that Angular tracks and responds to asynchronous tasks
correctly, triggering change detection when necessary.
1. String Methods
 String.prototype.includes(substring): Checks if a string contains a given
substring.
 "hello world".includes("world"); // true
 String.prototype.indexOf(substring): Returns the index of the first occurrence
of a substring, or -1 if not found.
 "hello world".indexOf("world"); // 6
 String.prototype.slice(start, end): Extracts a section of a string.
 "hello world".slice(0, 5); // "hello"
 String.prototype.split(delimiter): Splits a string into an array of substrings.
 "a,b,c".split(","); // ["a", "b", "c"]
 String.prototype.replace(search, replacement): Replaces occurrences of a
substring with another.
 "hello world".replace("world", "JavaScript"); // "hello JavaScript"
 2. Array Methods
 Array.prototype.push(element): Adds one or more elements to the end of an
array.
 let arr = [1, 2];
 arr.push(3); // [1, 2, 3]
 Array.prototype.pop(): Removes the last element of an array.
 let arr = [1, 2, 3];
 arr.pop(); // [1, 2]
101

 Array.prototype.shift(): Removes the first element of an array.


 let arr = [1, 2, 3];
 arr.shift(); // [2, 3]
 Array.prototype.unshift(element): Adds one or more elements to the
beginning of an array.
 let arr = [2, 3];
 arr.unshift(1); // [1, 2, 3]
 Array.prototype.map(callback): Creates a new array by applying a callback
function to each element.
 [1, 2, 3].map(x => x * 2); // [2, 4, 6]
 Array.prototype.filter(callback): Creates a new array with elements that pass
a test.
 [1, 2, 3].filter(x => x > 1); // [2, 3]
 Array.prototype.reduce(callback, initialValue): Reduces an array to a single
value using a callback.
 [1, 2, 3].reduce((sum, x) => sum + x, 0); // 6
 3. Object Methods
 Object.keys(obj): Returns an array of an object's keys.
 Object.keys({a: 1, b: 2}); // ["a", "b"]
 Object.values(obj): Returns an array of an object's values.
 Object.values({a: 1, b: 2}); // [1, 2]
 Object.entries(obj): Returns an array of key-value pairs.
 Object.entries({a: 1, b: 2}); // [["a", 1], ["b", 2]]
 Object.assign(target, ...sources): Copies properties from source objects to a
target object.
 Object.assign({a: 1}, {b: 2}); // {a: 1, b: 2}
 4. Math Methods
 Math.max(...numbers): Returns the largest of the numbers.
 Math.max(1, 2, 3); // 3
 Math.min(...numbers): Returns the smallest of the numbers.
 Math.min(1, 2, 3); // 1
 Math.round(num): Rounds a number to the nearest integer.
 Math.round(4.5); // 5
102

 Math.random(): Returns a random number between 0 and 1.


 Math.random(); // e.g., 0.345
 5. Date Methods
 Date.now(): Returns the current timestamp.
 Date.now(); // e.g., 1635735796825
 new Date(): Creates a new Date object.
 new Date().toISOString(); // "2024-11-27T10:45:00.000Z"
 6. Utility Functions
 JSON.stringify(obj): Converts an object to a JSON string.
 JSON.stringify({a: 1}); // '{"a":1}'
 JSON.parse(json): Converts a JSON string back to an object.
 JSON.parse('{"a":1}'); // {a: 1}
 typeof variable: Returns the type of a variable.
 typeof 42; // "number"

 Additional Key Functions to Highlight


 Array.prototype.forEach: Iterates over each element of an array.
 [1, 2, 3].forEach(x => console.log(x)); // Logs 1, 2, 3
 Promise.all: Executes multiple promises in parallel.
 Promise.all([Promise.resolve(1), Promise.resolve(2)])
 .then(results => console.log(results)); // [1, 2]

Additional Key JavaScript Features and Functions


1. Spread Operator (...)
 Purpose: Expands elements of an iterable (like an array or object) into
individual elements.
 Usage in Arrays:
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
 Usage in Objects:
const obj1 = { a: 1, b: 2 };
103

const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }


2. Rest Operator (...)
 Purpose: Collects multiple elements into a single array or object.
 Usage in Function Parameters:
function sum(...numbers) {
return numbers.reduce((acc, num) => acc + num, 0);
}
sum(1, 2, 3); // 6
 Usage in Object Destructuring:
const { a, ...rest } = { a: 1, b: 2, c: 3 }; // rest = { b: 2, c: 3 }
3. Destructuring
 Purpose: Extract values from arrays or properties from objects into distinct
variables.
 Array Destructuring:
const [first, second] = [1, 2, 3]; // first = 1, second = 2
 Object Destructuring:
const { name, age } = { name: "Alice", age: 25 }; // name = "Alice", age = 25
4. Optional Chaining (?.)
 Purpose: Access nested object properties safely without throwing errors if a
property is undefined or null.
const user = { profile: { name: "Alice" } };
console.log(user.profile?.name); // "Alice"
console.log(user.address?.city); // undefined
5. Nullish Coalescing Operator (??)
 Purpose: Provides a default value if a variable is null or undefined.
const value = null ?? "default"; // "default"
const value2 = 0 ?? "default"; // 0
6. Default Function Parameters
 Purpose: Assign default values to function parameters if no value is
provided.
function greet(name = "Guest") {
return `Hello, ${name}`;
104

}
greet(); // "Hello, Guest"
7. Array.prototype.some
 Purpose: Checks if at least one element in an array passes a test.
[1, 2, 3].some(num => num > 2); // true
8. Array.prototype.every
 Purpose: Checks if all elements in an array pass a test.
[1, 2, 3].every(num => num > 0); // true
9. Array.prototype.find
 Purpose: Returns the first element that satisfies a condition.
[1, 2, 3].find(num => num > 1); // 2
10. Template Literals
 Purpose: Allow embedding expressions in strings using backticks (`).
const name = "Alice";
console.log(`Hello, ${name}!`); // "Hello, Alice!"
11. Set and Map
 Set:
o A collection of unique values.
const mySet = new Set([1, 2, 2, 3]);
console.log(mySet); // Set { 1, 2, 3 }
 Map:
o A collection of key-value pairs.
const myMap = new Map();
myMap.set("key", "value");
console.log(myMap.get("key")); // "value"
12. typeof vs instanceof
 typeof: Determines the type of a variable (primitive types).
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
 instanceof: Checks if an object is an instance of a specific class or
constructor.
105

console.log([] instanceof Array); // true


13. Dynamic Import (import())
 Purpose: Load modules dynamically at runtime.
import("./module.js").then(module => {
module.doSomething();
});
14. Promise.race
 Purpose: Resolves/rejects as soon as one of the promises in the iterable
resolves/rejects.
Promise.race([
new Promise(resolve => setTimeout(resolve, 500, "First")),
new Promise(resolve => setTimeout(resolve, 100, "Second")),
]).then(result => console.log(result)); // "Second"
15. Function Binding (.bind)
 Purpose: Creates a new function with this bound to a specific value.
const obj = { value: 42 };
function printValue() {
console.log(this.value);
}
const boundPrint = printValue.bind(obj);
boundPrint(); // 42
16. Event Loop and Callback Queue
 While already covered in the "Event Loop" topic, revisiting macrotasks (e.g.,
setTimeout) and microtasks (e.g., Promises) in execution order is vital.
17. Custom Iterators
 Purpose: Create iterable objects.
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
106

};
console.log([...myIterable]); // [1, 2, 3]

1. Tagged Template Literals


 Purpose: Process template literals with a custom tag function for advanced
string manipulation.
function highlight(strings, ...values) {
return strings.map((str, i) => `${str}<strong>${values[i] ||
""}</strong>`).join("");
}
const name = "Alice";
const age = 25;
console.log(highlight`My name is ${name} and I am ${age} years old.`);
// Output: "My name is <strong>Alice</strong> and I am <strong>25</strong>
years old."
2. Immutable Data Structures
 While not built into JavaScript natively, understanding immutability (e.g.,
using Object.freeze, Map, or libraries like Immutable.js) is essential for
scalable applications.
const obj = Object.freeze({ key: "value" });
obj.key = "newValue"; // No effect in strict mode
3. Module Bundlers and Tools
 Webpack, Rollup, and Vite are critical for modern JS development.
 Understand how JavaScript files are bundled and tree-shaken for production.
4. Decorators (Proposed)
 Purpose: Enhance classes or methods. These are heavily used in TypeScript
and frameworks like Angular.
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
107

class Example {
@readonly
name() {
return "Readonly";
}
}
5. Object Property Shorthand
 Purpose: Simplify object declarations when variable names match property
names.
const name = "Alice";
const obj = { name }; // { name: "Alice" }
6. Dynamic Property Names
 Purpose: Use expressions to create object keys dynamically.
const key = "dynamicKey";
const obj = { [key]: "value" }; // { dynamicKey: "value" }
7. Async Iteration with for-await-of
 Purpose: Iterate over asynchronous data sources (e.g., streams).
async function fetchData() {
const data = [Promise.resolve(1), Promise.resolve(2)];
for await (const num of data) {
console.log(num);
}
}
fetchData(); // Logs: 1, 2
8. Intl API
 Purpose: Handle internationalization and localization.
o DateTimeFormat:
const date = new Date();
console.log(new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date));
o NumberFormat:
const num = 1234567.89;
108

console.log(new Intl.NumberFormat("en-US", { style: "currency", currency:


"USD" }).format(num));
9. Deep Copying
 Purpose: Safely duplicate objects to avoid unintended mutations.
o Using structuredClone (Modern):
const obj = { key: "value" };
const clone = structuredClone(obj);
o Using Libraries (e.g., Lodash):
const clone = _.cloneDeep(obj);
10. Function Currying
 Purpose: Transform a function so it can be called with fewer arguments at a
time.
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}

const add = (a, b) => a + b;


const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)); // 3
11. Event Propagation (Bubbling & Capturing)
 Purpose: Understand how events travel through the DOM hierarchy.
document.querySelector("#child").addEventListener(
"click",
event => console.log("Capturing Phase"),
true // Use capturing phase
);
109

document.querySelector("#child").addEventListener("click", () =>
console.log("Bubbling Phase"));
12. WeakMap and WeakSet
 Purpose: Avoid memory leaks by holding "weak" references to objects.
const wm = new WeakMap();
let obj = {};
wm.set(obj, "value");
obj = null; // The reference in WeakMap will be garbage collected.
13. Custom Error Types
 Purpose: Create application-specific error messages.
class CustomError extends Error {
constructor(message) {
super(message);
this.name = "CustomError";
}
}
throw new CustomError("Something went wrong!");
14. Function Composition
 Purpose: Combine multiple functions into one.
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const double = x => x * 2;
const square = x => x * x;
const composed = compose(square, double);
console.log(composed(2)); // 16
15. Module Lazy Loading
 Purpose: Load parts of your application on demand for performance.
const loadModule = async () => {
const { default: module } = await import("./module.js");
module();
};
16. Custom Promises
110

 Purpose: Understanding custom implementations for deeper promise


handling.
class MyPromise {
constructor(executor) {
this.callbacks = [];
const resolve = value => {
this.callbacks.forEach(cb => cb(value));
};
executor(resolve);
}
then(callback) {
this.callbacks.push(callback);
}
}
new MyPromise(resolve => resolve("Done")).then(console.log); // "Done"

You might also like