0% found this document useful (0 votes)
2 views

JavaScript Essentials for Technical Interviews

This document provides an overview of JavaScript basics, including its characteristics, programming paradigms, data types, and key features. It also covers JavaScript's evolution, error handling, and debugging methods, along with coding problems to reinforce understanding. Additionally, it encourages creating an account on StevenCodeCraft.com for further learning resources.

Uploaded by

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

JavaScript Essentials for Technical Interviews

This document provides an overview of JavaScript basics, including its characteristics, programming paradigms, data types, and key features. It also covers JavaScript's evolution, error handling, and debugging methods, along with coding problems to reinforce understanding. Additionally, it encourages creating an account on StevenCodeCraft.com for further learning resources.

Uploaded by

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

StevenCodeCraft.

com
This PDF contains the video lesson transcripts from Section 10 of the
JavaScript Pro course, active recall study questions to prepare for phone
screen interviews, and JavaScript coding problems to reinforce your
knowledge.

Consider creating an account on StevenCodeCraft.com for both FREE and


premium courses to help you learn to code and build apps.
JavaScript Basics
In this video, we’ll cover the fundamental concepts and terminology of JavaScript. Understanding
these basics is key to writing effective code and working with the language’s unique features.

What is JavaScript?
JavaScript is the primary programming language for the web. It’s a general-purpose, multi-
paradigm language with dynamic typing, primarily used to add functionality to websites.

Key Characteristics:
Multi-Paradigm:
JavaScript supports multiple programming styles, giving developers flexibility in how they
structure their code.
Dynamic Typing:
Variables can hold values of any type, and their types can change during execution.
Interpreted Language:
JavaScript runs directly in the browser through a JavaScript engine. Modern engines use Just-
in-Time (JIT) compilation for better performance.

Programming Paradigms
JavaScript’s support for multiple paradigms makes it a versatile language. Here are the main
paradigms:

1. Event-Driven Programming:
Functions respond to user interactions, such as clicks or scroll events.

button.addEventListener("click", () => {
console.log("Button clicked");
});

2. Functional Programming:
Focuses on writing pure functions—functions that return the same output for given inputs
without side effects.
First-Class Functions: Functions can be treated as values. For example you can assign a
function to a variable.
Higher-Order Functions: Functions can take other functions as arguments or return
functions.

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


const applyFunction = (func, x, y) => func(x, y);
console.log(applyFunction(add, 2, 3)); // 5

3. Object-Oriented Programming (OOP):


Objects can store data and methods and inherit from other objects.

const car = {
brand: "Toyota",
start() {
console.log("Car started");
},
};
car.start();

4. Imperative Programming:
Explicitly defines the control flow using loops, conditionals, and statements.

for (let i = 0; i < 5; i++) {


console.log(i);
}

5. Declarative Programming:
Focuses on describing the desired outcome rather than the control flow. Often used in functional
programming.

[1, 2, 3].forEach((num) => console.log(num));

Data Types and Primitives


JavaScript has seven primitive data types:

Number: Numeric values, including integers and decimals.


BigInt: For integers too large to store as numbers ( 100n ).
Boolean: true or false .
String: Sequences of characters.
Symbol: Unique identifiers created with Symbol() .
Null: An explicit assignment of no value.
Undefined: A value that has not been assigned.

You can check the type of a value using the typeof operator. However, note that typeof can have
some unintuitive behavior at times, such as returning "function" for functions even though
functions are objects in JavaScript. This is done for backwards compatibility with older versions of
JavaScript.

JavaScript’s Evolution
JavaScript is constantly evolving, with updates standardized under ECMAScript (ES).

ES6 (2015): A major update introducing modern features like let , const , arrow functions, and
template literals.
Annual Updates: New features are added yearly, ensuring the language remains up-to-date.
However, it’s important to note that it may take time for all browsers to fully support the latest
features, so developers often need to use tools like polyfills or transpilers (e.g., Babel) to ensure
compatibility across different environments.

Key JavaScript Features


1. Automatic Semicolon Insertion:
JavaScript adds semicolons where it thinks they’re missing, but it’s best practice to include them
explicitly.
2. Math Functions:
Commonly used functions include Math.floor() , Math.random() , and Math.pow() .
3. String and Number Conversion:
JavaScript provides methods like Number() , parseInt() , and parseFloat() for converting
strings to numbers.

Math Functions in JavaScript


1. Math.floor()
Rounds a number down to the nearest integer.
Example:

console.log(Math.floor(4.7)); // Output: 4
console.log(Math.floor(-4.7)); // Output: -5

2. Math.random()
Generates a pseudo-random number between 0 (inclusive) and 1 (exclusive).
Example:

console.log(Math.random()); // Output: e.g., 0.123456


// Generate a random integer between 0 and 9
console.log(Math.floor(Math.random() * 10));

3. Math.pow()
Returns the base raised to the exponent power.
Example:

console.log(Math.pow(2, 3)); // Output: 8


// Equivalent to using the exponentiation operator (**)
console.log(2 ** 3); // Output: 8

String and Number Conversion in JavaScript


1. Number()
Converts a value to a number. If the value cannot be converted, it returns NaN .
Example:

console.log(Number("123")); // Output: 123


console.log(Number("123abc")); // Output: NaN
console.log(Number(true)); // Output: 1

2. parseInt()
Parses a string and returns an integer, stopping at the first non-numeric character.
Example:

console.log(parseInt("123")); // Output: 123


console.log(parseInt("123abc")); // Output: 123
console.log(parseInt("abc123")); // Output: NaN
// Specify a radix (base) for better control
console.log(parseInt("11", 2)); // Output: 3 (binary 11 = decimal 3)
3. parseFloat()
Parses a string and returns a floating-point number.
Example:

console.log(parseFloat("123.45")); // Output: 123.45


console.log(parseFloat("123.45abc")); // Output: 123.45
console.log(parseFloat("abc123.45")); // Output: NaN

These are essential tools for working with numbers and strings in JavaScript.

4. Working with Objects:


Use JSON.stringify() to convert objects to strings and JSON.parse() to parse strings back
into objects.

const person = { name: "Steven", favoriteLanguage: "JavaScript" };


const personStr = JSON.stringify(person);
console.log(JSON.parse(personStr));

5. Maps and Sets:


Map: Allows any type of key with built-in methods for key-value management.
Set: Ensures unique values and provides methods for adding, deleting, and checking values.
Map: Key-Value Management

// Create a new Map


const map = new Map();

// Add key-value pairs


map.set("name", "Steven"); // String key
map.set(42, "The answer"); // Number key
map.set(true, "Yes"); // Boolean key

// Access values
console.log(map.get("name")); // Output: Steven
console.log(map.get(42)); // Output: The answer
console.log(map.get(true)); // Output: Yes

// Check if a key exists


console.log(map.has("name")); // Output: true

// Delete a key-value pair


map.delete(42);
console.log(map.has(42)); // Output: false

// Iterate through Map


for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// Output:
// name: Steven
// true: Yes

// Get the size of the Map


console.log(map.size); // Output: 2
Set: Unique Value Management

// Create a new Set


const set = new Set();

// Add values
set.add(1);
set.add(2);
set.add(2); // Duplicate, will be ignored
set.add("Hello");
set.add(true);

// Check if a value exists


console.log(set.has(1)); // Output: true
console.log(set.has(3)); // Output: false

// Delete a value
set.delete(2);
console.log(set.has(2)); // Output: false

// Iterate through Set


for (const value of set) {
console.log(value);
}
// Output:
// 1
// Hello
// true

// Get the size of the Set


console.log(set.size); // Output: 3

Key Differences Between Map and Set:


Feature Map Set

Purpose Stores key-value pairs Stores unique values

Keys Any type of key No keys, only values

Methods set() , get() , has() , delete() add() , has() , delete()


Feature Map Set

Iteration Iterates over key-value pairs Iterates over values

These examples demonstrate how to use Map for key-value management and Set for ensuring
unique values.

Functions as First-Class Citizens


Functions in JavaScript are treated as objects. They can:

Be assigned to variables.
Be passed as arguments to other functions.
Return other functions.

Error Handling
JavaScript uses try...catch for handling errors:

try {
throw new Error("An error occurred");
} catch (error) {
console.error(error.message);
}

Console Methods
The console object provides several methods for debugging:

console.log() : General output.


console.table() : Displays tabular data.
console.count() : Counts the number of calls.
console.time() / console.timeEnd() : Measures execution time.
1. console.log() - General Output
The most commonly used method for debugging.

console.log("Hello, world!"); // Output: Hello, world!


console.log("The value of x is:", 42); // Output: The value of x is: 42

let user = { name: "Steven", favoriteLanguage: "JavaScript" };


console.log("User details:", user); // Output: User details: { name: "Steven", favorite

2. console.table() - Displays Tabular Data


Useful for viewing arrays or objects in a tabular format.

let students = [
{ name: "John", grade: "A" },
{ name: "Jane", grade: "B" },
{ name: "Sam", grade: "A+" },
];

console.table(students);
/* Output:
┌─────────┬──────────┬───────┐
│ (index) │ name │ grade │
├─────────┼──────────┼───────┤
│ 0 │ 'John' │ 'A' │
│ 1 │ 'Jane' │ 'B' │
│ 2 │ 'Sam' │ 'A+' │
└─────────┴──────────┴───────┘
*/

3. console.count() - Counts the Number of Calls


Great for tracking how often a function or piece of code runs.
function greet(name) {
console.count("Greet function called");
console.log(`Hello, ${name}!`);
}

greet("Steven");
greet("Bob");
greet("Steven");

/* Output:
Greet function called: 1
Hello, Steven!
Greet function called: 2
Hello, Bob!
Greet function called: 3
Hello, Steven!
*/

4. console.time() and console.timeEnd() - Measures


Execution Time
Useful for benchmarking code performance.

console.time("Loop Timer");

let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}

console.timeEnd("Loop Timer");
// Output: Loop Timer: [execution time in milliseconds]

5. Combination of Methods
Mixing multiple methods for a comprehensive debugging example:
console.log("Starting program...");
console.time("Processing");

let data = Array.from({ length: 5 }, (_, i) => ({


id: i,
value: Math.random(),
}));
console.log("Generated data:");
console.table(data);

data.forEach((item) => console.count("Processing item"));

console.timeEnd("Processing");
console.log("Program finished!");

Strict Mode
Adding 'use strict'; at the top of a file enables stricter parsing and error handling, helping catch
potential issues early.

That concludes this overview of JavaScript basics. In the next lesson, we’ll dive deeper into
specific JavaScript features and their applications.

Questions

160) What is JavaScript?


JavaScript is the primary programming language of the web, mainly used to add dynamic
functionality to websites.
It is a general-purpose, multi-paradigm programming language with dynamic typing.
161) What is a programming paradigm?
A programming paradigm refers to a style or approach to programming, defining how solutions to
problems are structured and implemented.

162) Can you identify and explain the five primary programming
paradigms supported in JavaScript?
Event-Driven:
JavaScript allows functions to respond to events, such as a user clicking on an element or
scrolling down a page.
Functional:
Functions in JavaScript can be written as "pure functions," which always return the same output
for a given set of inputs without causing side effects.
JavaScript supports first-class functions (functions treated as values) and higher-order functions
(functions that can accept or return other functions).
Object-Oriented:
JavaScript enables the creation of objects as custom data stores, with the ability for objects to
inherit properties and methods from other objects.
Imperative:
Programs are written by explicitly defining the control flow using constructs like loops,
conditionals, and statements.
Declarative:
Programs focus on describing the desired outcome without detailing the control flow. This
approach is often linked to functional programming, such as using the forEach method instead
of a traditional for loop.

163) What are the 7 primitive data types in JavaScript?


Primitive types are the most basic data types in JavaScript:

1. Number: Numeric values, including integers and decimals.


2. BigInt: Integers too large to be stored as the Number data type.
3. Boolean: A binary value of true or false .
4. String: A sequence of characters.
5. Symbol: A unique and dynamically generated value.
6. null: Represents a nonexistent or empty value.
7. undefined: Represents a value that has not been set.

164) What is the purpose of Just-in-Time (JIT) compilation in


modern JavaScript engines, and how does it differ from
traditional interpretation?
Just-in-Time (JIT) compilation improves performance by converting JavaScript code into machine
code while the program is running.

Unlike traditional interpretation, which reads and executes code line by line, JIT compilation
optimizes and compiles the code on the fly.
This results in faster execution and better performance, as the browser applies optimizations during
runtime.

165) How does the Map object in JavaScript differ from a


standard object, and what advantage does it offer when
managing key-value pairs?
The Map object differs from a standard object in the following ways:

It allows keys of any type, including objects and functions, not just strings or symbols.
It preserves the insertion order of key-value pairs.

Advantages of using a Map include built-in methods like set , get , and has , which make
managing keys and values more efficient.
This flexibility is particularly useful when working with non-string keys or when maintaining key order
is important.
Coding Questions
Here are two coding problems designed to test your understanding of the JavaScript basics
covered in your lesson.

What These Problems Test:


1. Problem 1: Tests understanding of:
Data types ( typeof operator).
Working with arrays ( for loops or forEach ).
Objects and dynamic key-value management.
2. Problem 2: Tests understanding of:
Object-oriented programming (creating an object with methods).
Event-driven programming (simulating actions with setTimeout ).
Console debugging ( console.log ).

Problem 1: JavaScript Data Types and Functions


Write a JavaScript function called processData that accepts an array of mixed data types (e.g.,
numbers, strings, booleans). The function should:

1. Count how many items belong to each data type (e.g., number , string , boolean , etc.).
2. Return an object where the keys are the data types and the values are the counts.

Example Input:

const inputArray = [42, "hello", true, 99, "world", false];

Expected Output:

{
number: 2,
string: 2,
boolean: 2
}
Problem 2: Event-Driven and Functional Programming
Create a simple counter application using JavaScript. Your task is to:

1. Write an object called counter that has the following:


A property count initialized to 0.
A method increment that increases count by 1.
A method decrement that decreases count by 1.
A method reset that sets count back to 0.
2. Simulate user interaction by using setTimeout to call the methods in this sequence:
Increment the counter twice.
Decrement it once.
Reset the counter.

Expected Output in the Console:

Count after increment: 1


Count after increment: 2
Count after decrement: 1
Count after reset: 0
Variables and Scoping
In this lesson, we’ll discuss the different ways to declare variables in JavaScript, focusing on the use
of let , var , and const . We’ll also explore the concepts of block scope, function scope, and
hoisting.

Variable Declarations
let

let is used to declare a variable with block scope.


A block is typically defined by a pair of curly braces ( {} ), and the variable cannot be accessed
outside this block.
Variables declared with let cannot be accessed before their initialization.

Example:

{
let blockVar = "I am block-scoped";
console.log(blockVar); // "I am block-scoped"
}
// console.log(blockVar); // ReferenceError: blockVar is not defined

var

var is used to declare a variable with function scope.


Unlike let , a var variable is automatically initialized to undefined when it is hoisted.
It can be accessed before its declaration, but its value will be undefined until it is assigned.

Example:

function example() {
console.log(varVar); // undefined
var varVar = "I am function-scoped";
console.log(varVar); // "I am function-scoped"
}
example();
const

const is used to declare a constant value.


It has block scope, like let , but with the added restriction that it cannot be reassigned after
initialization.
Note: const does not make the value immutable if the value is an object or array. It only ensures
the reference cannot be changed.

Example:

const constantValue = 42;


// constantValue = 50; // TypeError: Assignment to constant variable

const arr = [1, 2];


arr.push(3); // This is allowed
console.log(arr); // [1, 2, 3]

Scopes
Block Scope
Variables declared with let or const are confined to the block they are declared in.
They are not accessible outside the block, ensuring better control over variable usage.

Example:

{
let x = 10;
const y = 20;
}
// console.log(x); // ReferenceError: x is not defined
Function Scope
Variables declared with var are accessible throughout the function where they are defined,
regardless of the block they are in.

Example:

function example() {
if (true) {
var scopedVar = "I am function-scoped";
}
console.log(scopedVar); // "I am function-scoped"
}
example();

Hoisting
Hoisting is a process in JavaScript where variable declarations are moved to the top of their scope
during the compilation phase.
However, initialization is not hoisted, which can lead to different behaviors for var , let , and
const :

1. var Variables:
Hoisted and initialized to undefined . They can be accessed before their declaration line, but
their value will be undefined .
2. let and const Variables:
Hoisted but not initialized. They cannot be accessed before their declaration and will throw a
ReferenceError if accessed early.

Example:

console.log(varNum); // undefined
console.log(letNum); // ReferenceError: letNum is not defined

var varNum = 5;
let letNum = 5;

console.log(varNum); // 5
console.log(letNum); // 5
Summary
Use let for variables that need to be reassigned within a block.
Use const for variables that should not be reassigned after initialization.
Avoid var in modern JavaScript due to its function-scoped behavior and potential for
unintended issues.
Understand hoisting to avoid errors when accessing variables before their declaration.

This overview covers the essentials of variable declarations and scoping in JavaScript. In the next
lesson, we’ll dive deeper into JavaScript’s data types and their unique characteristics.

Questions

166) How does the choice between let , var , and const
impact the scope and behavior of variables in JavaScript, and
why is it important to understand these differences?
The choice between let , var , and const in JavaScript affects how and where variables can be
accessed in your code.

var :
Function-scoped, meaning it can be accessed anywhere within the function where it is
declared.
It is hoisted and initialized to undefined , which can lead to unexpected behavior if not
carefully managed.
let :
Block-scoped, limiting its accessibility to the block (usually within curly braces) where it is
declared.
It is hoisted but not initialized, so accessing it before declaration causes an error.
const :
Block-scoped like let , but it creates a constant value that cannot be reassigned after its
initial assignment.
Understanding these differences is crucial for avoiding bugs, ensuring variables are only accessible
where they should be, and maintaining predictable and secure code.

167) What role does hoisting play in the execution of JavaScript


code, particularly in relation to variable declarations with var ,
let , and const ?

Hoisting in JavaScript is the process where variable declarations are moved to the top of their scope
during the code's execution phase.

With var , the variable is hoisted and initialized to undefined .


It can be accessed before its declaration but will hold undefined until it is assigned a value.
With let and const , the variables are hoisted but not initialized.
This means they cannot be accessed before their declaration, leading to a reference error if
you try to use them too early.

Understanding hoisting helps prevent errors and ensures that variables are used in the correct order
in your code.

Coding Questions
What These Problems Test:
1. Problem 3: Tests your understanding of scoping rules and how var , let , and const behave
differently in block and global scopes.
2. Problem 4: Tests your knowledge of hoisting and their ability to anticipate and handle errors
related to variable initialization.

Problem 3: Scoping and Hoisting


Write a function called testScopes that demonstrates the behavior of var , let , and const in
different scopes. The function should:

1. Declare variables using var , let , and const inside a block.


2. Attempt to access those variables both inside and outside the block to demonstrate their scope.
3. Log the results to the console to show which variables are accessible and which cause errors.
Expected Output:

Inside the block: var is function scope, let is block scope, const is block scope;
Outside block: var is function scope, let and const cause ReferenceError.

Problem 4: Hoisting in Practice


Create a function called hoistingExample that demonstrates the effect of hoisting on var , let ,
and const . Specifically:

1. Declare a variable with var and log it to the console before and after assigning it a value.
2. Declare variables with let and const and attempt to log them to the console before their
declaration.
3. Handle any ReferenceError exceptions that occur using a try...catch block.

Expected Output:

myVar before assignment: undefined


myVar after assignment: 1
Error: ReferenceError: Cannot access \'myLet\' before initialization
myLet after assignment: 2
Error: ReferenceError: Cannot access \'myConst\' before initialization
myConst after assignment: 3
Video 4: Arrays

Key Terms

Array
A data structure for storing lists of information.
Arrays in JavaScript are mutable, meaning their contents can be changed, and they can hold
data of different types.
Although arrays are essentially objects, they have a special syntax for creation and manipulation.

Example:

const array = [1, 2, 3];


console.log(array[1]); // 2

Array Basics

Creating Arrays
You can create arrays using different methods:

1. Bracket Notation (preferred):

const array = [1, 2, 3];


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

2. Array Constructor (less common):

const arr = new Array(1, 2, 3);


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

3. Creating an Empty Array:


const arr2 = new Array(5).fill(0); // Creates an array with 5 zeros
console.log(arr2); // [0, 0, 0, 0, 0]

Common Array Methods

Checking Array Contents


includes() : Checks if the array contains a specific value.

console.log(array.includes(2)); // true

indexOf() and lastIndexOf() : Finds the first or last occurrence of a value.

console.log(array.indexOf(2)); // 1
console.log(array.lastIndexOf(2)); // -1 (if not found)

Adding and Removing Elements


push() : Adds elements to the end of the array.
pop() : Removes and returns the last element.

array.push(4);
console.log(array); // [1, 2, 3, 4]
console.log(array.pop()); // 4

unshift() : Adds elements to the beginning of the array.


shift() : Removes the first element.

array.unshift(0);
console.log(array); // [0, 1, 2, 3]
console.log(array.shift()); // 0
Identifying Arrays
Use Array.isArray() or the instanceof operator to check if a variable is an array.

console.log(Array.isArray(array)); // true
console.log(array instanceof Array); // true

Modifying Arrays
splice() : Modifies the array by adding or removing elements.

array.splice(1, 2, "new"); // Removes 2 elements at index 1, adds 'new'


console.log(array); // [1, 'new', 3]

slice() : Returns a portion of the array without modifying the original.

const sliced = array.slice(1, 3);


console.log(sliced); // ['new', 3]

concat() : Combines arrays.

const combined = array.concat(["hello", "world"]);


console.log(combined); // [1, 'new', 3, 'hello', 'world']

reverse() : Reverses the array in place.

array.reverse();
console.log(array); // [3, 'new', 1]

join() : Combines array elements into a string with a specified delimiter.

console.log(array.join(", ")); // "3, new, 1"

Looping Through Arrays


1. Traditional for Loop:
for (let i = 0; i < array.length; i++) {
console.log(array[i]);
}

2. for-of Loop:

for (const value of array) {


console.log(value);
}

3. forEach() Method:

array.forEach((value, index) => {


console.log(value, index);
});

Array Functions

Transformation
map() : Creates a new array by applying a function to each element.

const mapped = array.map((value) => value * 2);


console.log(mapped); // [6, 'newnew', 2]

Filtering
filter() : Creates a new array with elements that pass a condition.

const filtered = array.filter((value) => typeof value === "number");


console.log(filtered); // [3, 1]

Finding
find() : Returns the first element that meets a condition.

const found = array.find((value) => typeof value === "string");


console.log(found); // 'new'
findIndex() : Returns the index of the first element that meets a condition.

const index = array.findIndex((value) => typeof value === "string");


console.log(index); // 1

Checking Conditions
every() : Checks if all elements meet a condition.

const allPass = array.every((value) => typeof value === "number");


console.log(allPass); // false

some() : Checks if any element meets a condition.

const anyPass = array.some((value) => typeof value === "string");


console.log(anyPass); // true

Reducing
reduce() : Accumulates values from left to right.

const sum = array.reduce((acc, value) => acc + value, 0);


console.log(sum); // Depends on array contents

reduceRight() : Works like reduce() but from right to left.

const diff = array.reduceRight((acc, value) => acc - value, 0);


console.log(diff); // Depends on array contents

Sorting
Default Sorting:
const nums = [5, 7, 3, 0];
nums.sort();
console.log(nums); // [0, 3, 5, 7]

Custom Sorting:

nums.sort((a, b) => a - b); // Ascending order


nums.sort((a, b) => b - a); // Descending order

Conclusion
In this lesson, we explored JavaScript arrays, their creation, manipulation, and the wide range of
methods they offer. Arrays are a fundamental data structure in JavaScript, and understanding how to
work with them effectively is crucial for solving problems and writing efficient code.

Questions

168) What are the differences between the splice() and


slice() methods in JavaScript when modifying arrays?
The splice() and slice() methods in JavaScript both deal with parts of an array, but they work
differently:

splice() :
Modifies the original array by adding, removing, or replacing elements. It changes the array in
place and returns the removed elements.
slice() :
Does not modify the original array. Instead, it returns a new array containing a portion of the
original array, based on the specified start and end indices.

Summary:
splice() changes the original array, while slice() creates a new array without altering the
original.
169) How can you check if a variable is an array in JavaScript,
and why might you choose one method over another?
You can check if a variable is an array in JavaScript using two main methods:

1. Array.isArray(variable) :
This method is specifically designed to check if a variable is an array. It is the most reliable and
preferred way because it directly checks for array types.
2. variable instanceof Array :
This checks if the variable is an instance of the Array class. While it works, it might be less
reliable in certain scenarios, like when dealing with arrays across different frames or windows.

Why choose one over the other?


Array.isArray() is generally preferred because it is more reliable and clearly indicates the intent to
check for an array. It handles edge cases better, such as arrays created in different execution
contexts.

170) What is the purpose of the reduce() method in


JavaScript, and how does it differ from other array methods
like map() or filter() ?
The reduce() method in JavaScript is used to iterate over an array and accumulate its elements into
a single value, such as a sum, product, or a more complex result. It applies a function to each
element, passing the result of the previous iteration as an accumulator, and returns the final
accumulated value.

How it differs from map() and filter() :

map() : Creates a new array by applying a function to each element of the original array.
filter() : Creates a new array containing only the elements that meet a specific condition.
reduce() : Does not return an array; it returns a single value based on the entire array's
contents.
Coding Questions
What These Problems Test:
1. Problem #5:
Tests basic array manipulations, including adding, removing, and inserting elements, as well as
checking for a value using includes() .
2. Problem #6:
Tests intermediate array operations, focusing on filter() and map() for transforming data, as
well as chaining methods for a clean solution.

Problem #5: Array Manipulation


Write a function called arrayOperations that performs the following tasks:

1. Create an array called fruits with the values ["apple", "banana", "cherry"] .
2. Add "orange" to the end of the array.
3. Remove the first element from the array.
4. Insert "grape" at the second position.
5. Check if "banana" is still in the array using the includes() method.
6. Return the updated array.

Expected Output:

// After performing all operations, the returned array should be:


["banana", "grape", "cherry", "orange"];
// And the check for "banana" should return true.

Problem #6: Filtering and Transforming Arrays


Create a function called processNumbers that takes an array of numbers as input and performs the
following tasks:

1. Use the filter() method to remove all numbers less than 10.
2. Use the map() method to multiply the remaining numbers by 2.
3. Return the new transformed array.
Example Input:

const inputArray = [5, 10, 15, 20, 25];

Expected Output:

// After filtering and transforming, the result should be:


[20, 30, 40, 50];
Video 5: Objects

Key Terms

Object
Objects are the base non-primitive data structure in JavaScript, used to store key-value pairs.
Keys are typically strings or symbols, while values can be of any type.
Objects are usually declared using the object literal syntax:

Example:

const website = {
name: "StevenCodeCraft",
domain: "StevenCodeCraft.com",
};

Symbol
Symbol is a primitive data type used to create unique values in JavaScript.
Symbols are created using the Symbol(description) function, ensuring each symbol is unique
even if descriptions are identical.

Example:

const uniqueId = Symbol("id");


console.log(uniqueId); // Symbol(id)

Global Symbols: Symbols can also be created using Symbol.for(key) , which reuses a symbol
if the same key is used:

Example:
const globalSymbol1 = Symbol.for("key");
const globalSymbol2 = Symbol.for("key");
console.log(globalSymbol1 === globalSymbol2); // true

Video Notes

What Are Objects?


Objects in JavaScript are versatile structures used to store key-value pairs.

Keys: Usually strings but can also be symbols.


Values: Can be any data type.

Example:

const website = {
name: "stevencodecraft",
domain: "stevencodecraft.com",
};

Accessing and Modifying Objects


Dot Notation:

console.log(website.name); // 'stevencodecraft'

Bracket Notation:

console.log(website["domain"]); // 'stevencodecraft.com'

Objects are mutable, meaning you can:

Add new properties:

website.rating = 5;
Modify existing properties:

website.name = "StevenCodeCraft";

Delete properties:

delete website.rating;

Comparing Objects
Objects are compared by reference, not value.
Two objects with identical properties are not considered equal.

Example:

const obj1 = { name: "example" };


const obj2 = { name: "example" };
console.log(obj1 === obj2); // false

Symbols in Objects
Symbols provide unique keys for objects, ensuring no property conflicts.

Example:

const id = Symbol("id");
const obj = { [id]: 1234 };
console.log(obj[id]); // 1234

Inheritance
Objects can inherit properties from other objects using the __proto__ property or
Object.create() .

Example:
const parent = { greeting: "hello" };
const child = Object.create(parent);
console.log(child.greeting); // 'hello'

Iterating Over Objects


JavaScript provides several methods to iterate over object properties:

Object.keys() : Returns an array of keys.


Object.values() : Returns an array of values.
Object.entries() : Returns an array of [key, value] pairs.

Example:

Object.entries(website).forEach(([key, value]) => {


console.log(`${key}: ${value}`);
});

Object Methods
You can define methods directly within objects:

Example:
const website = {
name: "stevencodecraft",
sayHello() {
console.log("Hello world");
},
get rating() {
return this._rating;
},
set rating(value) {
this._rating = value;
},
};

website.sayHello(); // 'Hello world'


website.rating = 5;
console.log(website.rating); // 5

Object Modification and Protection


JavaScript provides ways to modify or protect objects:

Object.assign() : Copies properties from one object to another.


Object.freeze() : Makes an object immutable (cannot be modified or extended).
Object.seal() : Allows modification of existing properties but prevents adding new ones.

Example:

const obj = { name: "example" };


Object.freeze(obj);
obj.name = "test"; // Error in strict mode or no effect otherwise

Special Object Methods


1. toString() and valueOf() :
These methods convert an object into a string or primitive value.
Example:
const obj = { name: "example" };
obj.toString = function () {
return "Custom toString";
};
console.log(obj.toString()); // 'Custom toString'

2. Symbol.toPrimitive :
Customizes how an object is converted to a primitive value.
Example:

const obj = {
[Symbol.toPrimitive](hint) {
return hint === "string" ? "string version" : 123;
},
};
console.log(`${obj}`); // 'string version'
console.log(+obj); // 123

Conclusion
In this lesson, we explored the fundamentals of objects in JavaScript, including their creation,
manipulation, and unique features like symbols. Objects are a cornerstone of JavaScript, providing
the flexibility needed to structure complex applications. Mastering these concepts will significantly
enhance your ability to work effectively with the language.

Questions

171) How do symbols in JavaScript ensure uniqueness, and


what is a practical use case for using symbols as object keys?
Symbols in JavaScript ensure uniqueness by generating a completely unique identifier each time
Symbol(description) is called, even if the description is the same.

This makes symbols ideal for use as object keys when you need to avoid property name conflicts,
especially when adding properties to objects from third-party libraries or APIs.
Since symbols are hidden from most iteration methods, they prevent accidental property overrides
and keep your keys distinct from any existing keys on the object.

172) What is the difference between Object.freeze() and


Object.seal() when modifying objects in JavaScript?
The difference between Object.freeze() and Object.seal() in JavaScript lies in how they restrict
modifications to objects:

Object.freeze() :
Makes an object completely immutable. You cannot add, remove, or modify any properties of the
object.
Object.seal() :
Allows you to modify existing properties but prevents adding new properties or deleting existing
ones.

Summary:
Object.freeze() fully locks the object, while Object.seal() allows changes to existing properties
but prevents structural changes.

Coding Questions
What These Problems Test:
1. Problem #7: Tests understanding of basic object manipulation—adding, updating, and deleting
properties.
2. Problem #8: Tests comprehension of Symbol usage, its non-enumerable nature, and iterating
over objects effectively.

Problem #7: Object Property Management


Write a function called manageObject that performs the following tasks:

1. Create an object called user with the properties:


name : "John"
favoriteLanguage : "JavaScript"
2. Add a new property email with the value "[email protected]" .
3. Update the favoriteLanguage property to Python .
4. Delete the email property.
5. Return the final state of the object.

Expected Output:

{
name: "John",
favoriteLanguage: "Python"
}

Problem #8: Using Symbols and Iterating Over Objects


Write a function called useSymbols that does the following:

1. Create a Symbol called uniqueId and use it as a key in an object called product with the
value 12345 .
2. Add additional properties to the product object:
name : "Laptop"
price : 1500
3. Use Object.entries() to log all enumerable properties of the object (the Symbol key-value
pair should not appear).
4. Access the Symbol property directly to log its value.

Expected Output:

// Log from Object.entries():


"name: Laptop";
"price: 1500";
// Log of the Symbol property:
"Symbol(uniqueId): 12345";
Video 6: Equality and Type Coercion

Equality Operators in JavaScript


In this lesson, we’ll explore the two primary equality operators in JavaScript—loose equality ( == )
and strict equality ( === )—and understand how type coercion plays a role in comparisons.

Loose Equality ( == )
The loose equality operator compares values without considering their types. It performs type
coercion, converting values to a common type before making the comparison.

How it works:

1. If both values are null or undefined , it returns true .


2. Booleans are converted to numbers:
true → 1
false → 0
3. When comparing a number to a string, the string is converted to a number.
4. When comparing an object to a string, the object is converted using its toString() or
valueOf() method.
5. If the values are of the same type, it performs a strict comparison (similar to === ).

Example:

console.log(5 == "5"); // true


console.log(true == 1); // true
console.log(null == undefined); // true

Use Case:
Loose equality can be helpful when checking if a value is either null or undefined , as both are
treated as equal:
if (value == null) {
console.log("Value is null or undefined");
}

However, strict equality is generally preferred due to its predictability.

Strict Equality ( === )


The strict equality operator compares both the value and the type without performing any type
coercion.

How it works:

1. If either value is NaN , it returns false .


2. If the values have different types, it returns false .
3. If both values are null or undefined , it returns true .
4. If both values are objects, it returns true only if they reference the same object. Otherwise, it
returns false .
5. If both values are of the same primitive type, it returns true if their values are identical.
Otherwise, it returns false .

Example:

console.log(5 === "5"); // false


console.log(null === undefined); // false
console.log({} === {}); // false (different object references)

Type Coercion
Type coercion occurs when JavaScript automatically converts one or both values to a common type
during loose equality comparisons. This often involves converting values to numbers.

Example:
console.log(true == 1); // true
console.log("10" == 10); // true

Explicit Type Coercion:


You can manually perform type coercion using methods like Number() , String() , or Boolean() :

const num = Number(false); // num becomes 0


console.log(num); // 0

Special Cases
1. NaN:
The value NaN (Not-a-Number) is unique in that it is not equal to any other value, including
itself.

console.log(NaN == NaN); // false


console.log(NaN === NaN); // false

2. null and undefined :


These values are equal when using loose equality but not when using strict equality.

console.log(null == undefined); // true


console.log(null === undefined); // false

3. Objects:
When comparing objects, strict equality checks if they reference the same object in memory, not
if they have the same content.

const obj1 = {};


const obj2 = {};
console.log(obj1 === obj2); // false
console.log(obj1 == obj2); // false
Conclusion
Strict Equality ( === ):
Preferred for comparisons because it avoids the unpredictable behavior caused by type
coercion. It ensures both the value and type are the same.
Loose Equality ( == ):
Useful in specific cases, such as checking if a value is either null or undefined . However, it
requires a solid understanding of how type coercion works.

Best Practice: Use === whenever possible to avoid unexpected results.


Understand and use == only when necessary and with caution.

Questions

173) What is the main difference between loose equality ( == )


and strict equality ( === ) when comparing values in JavaScript?
The main difference between loose equality ( == ) and strict equality ( === ) in JavaScript is that:

== : Compares values after performing type conversion.


=== : Compares both the value and type without any conversion.

This means == may consider different types as equal if they can be coerced to the same value, while
=== requires both the value and type to be exactly the same.

174) Why is strict equality ( === ) generally preferred over loose


equality ( == ) in JavaScript, and in what situations might loose
equality be useful?
Strict equality ( === ) is generally preferred in JavaScript because it avoids unexpected behavior by
ensuring that both the value and type must match exactly. This makes the code more predictable and
easier to debug.
Loose equality ( == ), which performs type conversion, can lead to confusing results if you're not
careful.

When might loose equality be useful?


Loose equality can be helpful in specific situations, such as when you want to check if a value is
either null or undefined in one step:

if (value == null) {
console.log("Value is null or undefined");
}

Coding Questions
What These Problems Test:
1. Problem #9:
Tests understanding of the differences between == and === and how type coercion affects
loose equality.
2. Problem #10:
Tests handling of special cases like object comparison, NaN behavior, and null vs.
undefined .

Problem #9: Comparing Values


Write a function called compareValues that takes two arguments and returns:

1. "Loose Equality: true" or "Loose Equality: false" depending on whether the values are
equal using == .
2. "Strict Equality: true" or "Strict Equality: false" depending on whether the values
are equal using === .

Example Input:

compareValues(5, "5");
compareValues(null, undefined);
compareValues(0, false);
Expected Output:

Loose Equality: true, Strict Equality: false


Loose Equality: true, Strict Equality: false
Loose Equality: true, Strict Equality: false

Problem #10: Handling Special Cases


Create a function called specialCases that performs the following:

1. Compare two objects with identical properties using == and === and log the results.
2. Check if NaN is equal to itself using both == and === .
3. Compare null and undefined using both == and === .

Expected Output:

// Comparing objects
Objects are loosely equal: false
Objects are strictly equal: false

// Comparing NaN
NaN loose equality: false
NaN strict equality: false

// Comparing null and undefined


null == undefined: true
null === undefined: false
Video 7: Syntactic Sugar and Modern
JavaScript

Introduction
In this lesson, we’ll explore modern JavaScript features that simplify code, making it more concise
and readable. These features are often referred to as "syntactic sugar."

Arrow Functions
Arrow functions provide a shorter syntax for writing functions, particularly anonymous ones. They are
ideal for one-line functions and do not have their own this binding. They also cannot be used as
constructors or generators.

Syntax:

const doubledValues = [1, 2, 3].map((num) => num * 2);

Multi-line functions:

const add = (a, b) => {


const sum = a + b;
return sum;
};

Single-line functions:

const multiply = (a, b) => a * b;

Single parameter: Parentheses can be omitted:

const square = (num) => num * num;


Destructuring Assignment
Destructuring allows you to extract values from arrays or objects into variables in a concise manner.

Examples:
1. Array Destructuring:

const [first, second] = [1, 2, 3];


console.log(first); // 1
console.log(second); // 2

2. Object Destructuring:

const { name } = { name: "Steven" };


console.log(name); // 'Steven'

3. Renaming Fields:

const { name: firstName } = { name: "Steven" };


console.log(firstName); // 'Steven'

4. Function Parameters:

function printName({ name }) {


console.log(name);
}
const person = { name: "Steven" };
printName(person); // 'Steven'

Rest Operator ( ... )


The rest operator condenses multiple elements into an array or object. It’s useful in destructuring and
function parameters.
Examples:
1. Array Destructuring:

const arr = [1, 2, 3];


const [first, ...rest] = arr;
console.log(rest); // [2, 3]

2. Function Parameters:

function myFunc(...args) {
console.log(args);
}
myFunc(1, 2, 3, 4); // [1, 2, 3, 4]

3. Object Destructuring:

const obj = { a: 1, b: 2, c: 3 };
const { a, ...rest } = obj;
console.log(rest); // { b: 2, c: 3 }

Spread Operator ( ... )


The spread operator expands an array or object into individual elements. It is useful for combining
arrays or shallow cloning objects.

Examples:
1. Combining Arrays:

const arr1 = [1, 2, 3];


const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

2. Cloning Objects:

const obj = { key: "value" };


const clone = { ...obj };
console.log(clone); // { key: 'value' }
3. Function Arguments:

function sum(a, b, c) {
return a + b + c;
}
const nums = [1, 2, 3];
console.log(sum(...nums)); // 6

Template Literals
Template literals use backticks ( ) to create strings with embedded expressions using ${} .

Example:

const name = "Steven";


console.log(`Hello, ${name}`); // 'Hello, Steven'

Nullish Coalescing ( ?? )
The nullish coalescing operator provides a default value if the left-hand side is null or undefined .

Example:

const name = null;


const defaultName = name ?? "Default name";
console.log(defaultName); // 'Default name'

Optional Chaining ( ?. )
Optional chaining safely accesses nested properties without throwing an error if an intermediate
property is null or undefined .
Example:

const person = { company: { website: "example.com" } };


console.log(person?.company?.website); // 'example.com'
console.log(person?.address?.street); // undefined

Short Circuit Evaluation


Short circuit evaluation uses logical operators ( && and || ) to conditionally execute code.

&& : If the left-hand side is false , the right-hand side is not evaluated.
|| : If the left-hand side is true , the right-hand side is not evaluated.

Examples:
1. && Example:

const shouldRun = true;


shouldRun && console.log("This will run"); // 'This will run'

2. || Example:

const shouldNotRun = false;


shouldNotRun || console.log("Fallback logic"); // 'Fallback logic'

Conclusion
Modern JavaScript features like arrow functions, destructuring, and the rest/spread operators
improve code readability and flexibility. Understanding and using these features effectively will make
your code more concise, maintainable, and expressive.
Questions

175) What is the primary difference between arrow functions


and traditional functions in JavaScript, particularly regarding
this binding?
The primary difference between arrow functions and traditional functions in JavaScript is how they
handle this binding:

Arrow Functions:
Arrow functions do not have their own this context. Instead, they inherit this from the
surrounding (lexical) scope.
Traditional Functions:
Traditional functions have their own this binding, which can change depending on how the
function is called (e.g., as a method, in a constructor, or with call / apply ).

Why This Matters:


Arrow functions are particularly useful when you want to preserve the this value from the
surrounding context.

176) How do the rest and spread operators differ in their usage,
and can you give an example of each?
The rest ( ... ) and spread ( ... ) operators in JavaScript both use the same syntax but serve
different purposes:

Rest Operator:
Purpose: Gathers multiple elements into a single array or object.
Common Use Case: Function parameters or capturing remaining elements.

Example:
const [first, ...rest] = [1, 2, 3, 4];
// rest is [2, 3, 4]

function sum(...numbers) {
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 6

Spread Operator:
Purpose: Expands an array or object into individual elements.
Common Use Case: Combining arrays, copying objects, or passing elements as arguments.

Example:

const arr1 = [1, 2, 3];


const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
// combined is [1, 2, 3, 4, 5, 6]

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = { ...obj1, ...obj2 };
// merged is { a: 1, b: 2 }

Summary:

The rest operator gathers elements into a single array.


The spread operator spreads elements out into individual items.

177) What problem does the optional chaining operator ( ?. )


solve in JavaScript, and how does it improve code safety when
accessing nested properties?
The optional chaining operator ( ?. ) in JavaScript solves the problem of safely accessing nested
object properties that might be null or undefined .
Problem Without Optional Chaining:
Trying to access a deeply nested property on a null or undefined object throws an error,
potentially crashing your code:

const person = null;


console.log(person.address.street);
// TypeError: Cannot read property 'street' of null

Solution With Optional Chaining:


The ?. operator safely returns undefined if any part of the chain is null or undefined :

const person = null;


console.log(person?.address?.street);
// undefined

Benefits:

Prevents runtime errors.


Simplifies code for accessing nested properties.
Makes code safer and more predictable when dealing with uncertain object structures.

const person = { company: { website: "example.com" } };


console.log(person?.company?.website); // 'example.com'
console.log(person?.address?.street); // undefined

Coding Questions
What These Problems Test:
1. Problem #11:
Tests understanding of destructuring, arrow functions, and the spread operator in practical array
manipulation.
2. Problem #12:
Tests ability to use optional chaining and nullish coalescing to handle potentially undefined
properties safely and effectively.
Problem #11: Working with Arrow Functions, Destructuring,
and Spread
Write a function called processArray that performs the following tasks:

1. Accepts an array of numbers as input.


2. Uses destructuring to extract the first number into a variable called first and the rest of the
numbers into an array called remaining .
3. Uses an arrow function to multiply each number in the remaining array by 2.
4. Combines first (unchanged) and the doubled values from remaining into a new array using
the spread operator.
5. Returns the new array.

Example Input:

processArray([1, 2, 3, 4]);

Expected Output:

[1, 4, 6, 8];

Problem #12: Optional Chaining and Nullish Coalescing


Write a function called getUserDetails that accepts a user object with the following structure:

{
name: string,
address?: {
city: string,
street?: string
}
}

The function should:


1. Use optional chaining to safely access the city and street properties of the address
object.
2. Use nullish coalescing ( ?? ) to provide a default value of "Unknown Street" if the street
property is null or undefined .
3. Return a string in the following format:
"User lives in [city], [street]" .

Example Input:

getUserDetails({ name: "Tony", address: { city: "New York" } });

Expected Output:

"User lives in New York, Unknown Street";

Example Input:

getUserDetails({
name: "Bob",
address: { city: "Metropolis", street: "Main St" },
});

Expected Output:

"User lives in Metropolis, Main St";


Video 8: Connecting JavaScript to HTML

Introduction
In this lesson, we’ll cover how to connect JavaScript to an HTML document using the <script> tag.
We’ll also discuss the different ways to manage how scripts are loaded and executed to optimize
performance and ensure predictable behavior.

The <script> Tag


The <script> tag is used to add JavaScript to an HTML document. Typically, it is placed in the
<head> section or the end of the <body> .

Example:

<head>
<title>Connecting JS to HTML</title>
<script src="script.js" defer></script>
</head>
<body>
<button>Click Me</button>
</body>

Default Behavior:
By default, when the browser encounters a <script> tag, it pauses rendering the rest of the
page until the script is fully downloaded and executed.
This can delay the page load, especially for large scripts.
Attributes for Script Loading

Defer Attribute
The defer attribute downloads the script asynchronously, without blocking the page, and
executes it only after the DOM is fully parsed.

Example:

<script src="script.js" defer></script>

Recommended Use:
The defer attribute is the preferred approach for most cases, as it ensures the script runs after
the HTML is fully loaded, without delaying the page load.

Async Attribute
The async attribute downloads the script asynchronously but executes it as soon as it’s ready,
even if the DOM hasn’t finished loading.

Example:

<script src="script.js" async></script>

Use Case:
Useful for scripts that don’t interact with the DOM, such as analytics or advertising scripts.
Caution:
Use async carefully, as it can lead to unpredictable behavior if the script needs access to the
DOM.

Traditional Placement in <body>


Before defer and async attributes became common, scripts were often placed at the end of the
<body> to ensure the DOM was fully loaded before the script ran.
Example:

<body>
<button>Click Me</button>
<script src="script.js"></script>
</body>

Why It’s Suboptimal:

Scripts won’t start downloading until the browser reaches the <script> tag, delaying their
execution compared to defer .

Waiting for DOMContentLoaded


If you’re not using defer or async , you can manually ensure the DOM is ready before running
JavaScript by listening for the DOMContentLoaded event.

Example:

window.addEventListener("DOMContentLoaded", function () {
const button = document.querySelector("button");
button.addEventListener("click", function () {
document.body.style.backgroundColor = "#00334C";
});
});

Why This Matters:

Ensures your script interacts with the DOM only after it has fully loaded.

Summary
Use defer :
For scripts that need to interact with the DOM but don’t need to block the page load.
Use async :
For scripts that can run independently of the DOM, like analytics or ads.
Avoid placing scripts at the bottom of the <body> :
When using defer provides a more efficient and modern solution.

Questions

178) What is the primary difference between the defer and


async attributes when loading JavaScript with the <script>
tag?
The primary difference between the defer and async attributes when loading JavaScript is how
and when the script is executed:

defer :
The script is downloaded asynchronously.
It is executed only after the entire HTML document has been parsed, ensuring that the
DOM is fully loaded before the script runs.
async :
The script is downloaded asynchronously.
It is executed as soon as it’s ready, even if the HTML document is still being parsed.
This can lead to unpredictable behavior if the script depends on the DOM.

179) Why is it generally recommended to use the defer


attribute instead of placing scripts at the end of the <body>
tag?
It’s generally recommended to use the defer attribute instead of placing scripts at the end of the
<body> tag because:

defer :
Allows the script to be downloaded earlier, while the HTML is still being parsed.
Ensures the script runs only after the DOM is fully loaded.
Results in faster page loads and better performance compared to waiting until the
<body> tag is reached to start downloading the script.

This approach optimizes both download timing and execution, making it a more efficient solution.

Coding Questions
What These Problems Test:
1. Problem #13:
Understanding of DOMContentLoaded and attaching event listeners only after the DOM is
fully parsed.
Basic DOM manipulation using JavaScript.
2. Problem #14:
Correct use of defer and async attributes to optimize script loading.
Ability to differentiate between scripts that interact with the DOM and those that run
independently.

Problem #13: Using DOMContentLoaded for DOM Manipulation


Write a script that waits for the DOM to load fully before attaching an event listener to a button. When
the button is clicked, the text of a paragraph element with the ID "output" should change to
"Button Clicked!" .

Steps:

1. Add an event listener for the DOMContentLoaded event.


2. Inside the listener, find the button using querySelector .
3. Attach a click event listener to the button that updates the paragraph text.

HTML Example:
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
<script src="script.js"></script>
</head>
<body>
<button id="myButton">Click Me</button>
<p id="output">Initial Text</p>
</body>
</html>

Expected Behavior:

Initially, the paragraph displays "Initial Text" .


After clicking the button, the paragraph displays "Button Clicked!" .

Problem #14: Experimenting with defer and async


You are tasked with optimizing the script loading for a web page that includes the following:

A JavaScript file ( script.js ) that manipulates the DOM (e.g., updates a header text).
Another JavaScript file ( analytics.js ) that logs analytics data and does not depend on the
DOM.

Steps:

1. Write the appropriate <script> tags in the correct order to optimize page load performance:
Use defer for script.js .
Use async for analytics.js .
2. Ensure script.js changes the text of an <h1> element with the ID "title" to
"Welcome to My Page!" .

HTML Template:
<!DOCTYPE html>
<html>
<head>
<title>Optimized Scripts</title>
<!-- Add the correct <script> tags here -->
</head>
<body>
<h1 id="title">Loading...</h1>
</body>
</html>

Expected Behavior:

The <h1> element should display "Welcome to My Page!" after the DOM has loaded.
analytics.js should load independently without affecting the page’s rendering.
Video 9: DOM Manipulation

Introduction
In this lesson, we’ll explore the fundamentals of DOM manipulation in JavaScript. This includes:

Selecting elements.
Setting attributes and styles.
Adding and removing elements.
Working with sizes and scrolling.

Understanding these concepts is essential for creating dynamic and interactive web pages.

Key Concepts

What is DOM Manipulation?


DOM Manipulation refers to using JavaScript to change the content, structure, or styles of a web
page. JavaScript provides numerous methods and properties for interacting with the DOM.

Selecting Elements
To interact with elements on a web page, you first need to select them. JavaScript offers several
methods for this:

1. document.getElementById(id) :
Selects a single element by its id .

const element = document.getElementById("element-id");


2. document.querySelector(selector) :
Selects the first element that matches a CSS selector.

const element = document.querySelector(".class-name");

3. document.querySelectorAll(selector) :
Selects all elements that match a CSS selector and returns them as a NodeList .

const elements = document.querySelectorAll("li");

4. document.getElementsByTagName(tagName) :
Selects all elements with a specific HTML tag and returns them as an HTMLCollection .

const elements = document.getElementsByTagName("p");

5. document.getElementsByClassName(className) :
Selects all elements with a specific class and returns them as an HTMLCollection .

const elements = document.getElementsByClassName("class-name");

Setting Attributes and Styles


Once you’ve selected an element, you can modify its attributes or styles:

1. Inline Styles:

element.style.color = "red"; // Sets a CSS property

2. textContent :
Gets or sets the text content of an element, including its children.

element.textContent = "Hello World";

3. classList :
Manages CSS classes with methods like add() , remove() , and toggle() .

element.classList.add("active");
element.classList.remove("hidden");
element.classList.toggle("highlight");
4. setAttribute :
Sets an HTML attribute directly.

element.setAttribute("data-info", "value");

5. Direct Attribute Access:


You can access attributes like value or src directly as properties.

element.value = "New Value";

Adding and Removing Elements


JavaScript allows you to create, append, and remove elements from the DOM:

1. document.createElement(tagName) :
Creates a new HTML element.

const newElement = document.createElement("p");


newElement.textContent = "New paragraph";

2. appendChild(childElement) :
Appends a child element to a parent element.

document.body.appendChild(newElement);

3. append() :
Appends multiple elements or text nodes.

parentElement.append(newElement, "Some text");

4. prepend() :
Prepends multiple elements or text nodes.

parentElement.prepend(newElement, "Start text");

5. remove() :
Removes an element from the DOM.

newElement.remove();
Working with Sizes and Scrolling
JavaScript provides methods to get and set element sizes and manage scrolling:

1. Window Dimensions:

console.log(window.innerWidth); // Width of the browser window


console.log(window.innerHeight); // Height of the browser window

2. Element Dimensions:
element.clientHeight : Height of visible content + padding.
element.offsetHeight : Height of visible content, padding, borders, and scrollbars.
element.scrollHeight : Height of all content, including scrolled-out content.

console.log(element.scrollHeight);

3. Scroll Methods:
scrollIntoView() : Scrolls an element into view.

element.scrollIntoView();

scrollTo() : Scrolls an element to a specific position.

element.scrollTo({
top: 100,
behavior: "smooth", // Enables smooth scrolling
});

Example: Changing Background Color


Here’s a simple example of DOM manipulation:
window.addEventListener("DOMContentLoaded", function () {
const button = document.querySelector("button");
button.addEventListener("click", function () {
document.body.style.backgroundColor = "#00334C";
});
});

Conclusion
DOM manipulation is a fundamental skill for creating interactive web pages. By mastering techniques
like:

Selecting elements.
Setting attributes and styles.
Adding or removing elements.
Working with sizes and scrolling.

You’ll be equipped to build dynamic, user-friendly interfaces. Experiment with these methods to see
how they can bring your web pages to life!

Questions

180) What method would you use to select an element by its


id in JavaScript?
You would use the document.getElementById(id) method to select an element by its id in
JavaScript.

Example:

const element = document.getElementById("myId");


// Selects the element with the id of "myId".
181) How can you add a CSS class to an element using
JavaScript?
You can add a CSS class to an element using the element.classList.add('className') method.

Example:

element.classList.add("active");
// Adds the class "active" to the selected element.

182) Which method allows you to create a new HTML element


in JavaScript?
You can create a new HTML element in JavaScript using the document.createElement('tagName')
method.

Example:

const newElement = document.createElement("p");


// Creates a new <p> (paragraph) element.

183) What property would you use to get the total height of the
content inside an element, including content not visible due to
overflow?
You would use the element.scrollHeight property to get the total height of the content inside an
element, including content not visible due to overflow.

Example:

console.log(element.scrollHeight);
// Logs the total height of the content inside the element.
184) How can you scroll an element into view using JavaScript?
You can scroll an element into view using the element.scrollIntoView() method in JavaScript.

Example:

element.scrollIntoView();
// Automatically scrolls the page so that the selected element is visible.

Coding Questions
What These Problems Test:
1. Problem #15:
Understanding of document.createElement() and appendChild() for adding new
elements dynamically.
Ability to manipulate styles and content dynamically.
Generating random values and updating the DOM in response to user interactions.
2. Problem #16:
Proficiency in selecting multiple elements ( querySelectorAll ).
Using loops to manipulate a collection of elements.
Adding CSS classes dynamically to update styles.

Problem #15: Dynamic Button Actions


Write a script that dynamically adds a button to the page. When the button is clicked:

1. Change the background color of the page to a random color.


2. Display the current background color in a new paragraph added below the button.

Steps:

1. Use document.createElement() to create a button with the text "Change Background" .


2. Append the button to the body using appendChild() .
3. Add a click event listener to the button that:
Changes the background color of the page to a random color.
Creates a new paragraph element and displays the color in the format:
"Current color: #RRGGBB" .

Hints:

Use Math.random() to generate random colors.


Use document.body.style.backgroundColor to change the background.

Expected Behavior:

Clicking the button changes the background to a random color and displays the color code
below the button.

Problem #16: Highlight List Items


Write a function called highlightItems that highlights all list items in a <ul> element when a
button is clicked. Specifically:

1. Create a button dynamically with the text "Highlight Items" .


2. Attach the button to the page using document.querySelector() to select an existing container
element.
3. Add a click event listener to the button that:
Iterates over all <li> elements inside the <ul> .
Adds the CSS class "highlight" to each <li> .

HTML Example:
<!DOCTYPE html>
<html>
<head>
<title>Highlight List</title>
<style>
.highlight {
background-color: yellow;
}
</style>
</head>
<body>
<div id="container">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<script src="script.js"></script>
</body>
</html>

Expected Behavior:

When the button is clicked, all <li> elements in the <ul> are highlighted with a yellow
background.
Video 10: Event-Driven Programming

Introduction
In this lesson, we’ll explore Event-Driven Programming in JavaScript. This includes:

Creating and managing event listeners.


Understanding event propagation.
Leveraging event delegation.

Event-Driven Programming is a paradigm where code runs in response to events, such as user
interactions or system-generated events. JavaScript’s event model is essential for building dynamic,
interactive web applications.

Key Concepts

Creating an Event Listener


To create an event listener, use the addEventListener() method. This method attaches a function
(callback) to run when the specified event is triggered.

Example:

const button = document.querySelector("button");


button.addEventListener("click", onClick);

function onClick(event) {
console.log("Button clicked");
}

Parameters:
i. Event name (e.g., 'click' ).
ii. Callback function to execute when the event occurs.
iii. Options or boolean to control listener behavior (optional).

Managing Event Listeners


The behavior of an event listener can be customized using an options object:

1. capture :
Specifies whether the listener should be triggered during the capturing phase ( true ) or bubbling
phase ( false ).
2. once :
Automatically removes the event listener after it is triggered once.

button.addEventListener("click", onClick, { once: true });

3. passive :
Indicates that event.preventDefault() will not be called, allowing the browser to optimize
performance.
Useful for touch events like touchstart or touchmove .

button.addEventListener("touchstart", onTouch, { passive: true });

4. signal :
Uses an AbortSignal to remove the event listener when abort() is called.

const abortController = new AbortController();


button.addEventListener("click", onClick, {
signal: abortController.signal,
});
abortController.abort(); // Removes the event listener

Removing an Event Listener:


To remove an event listener, use removeEventListener() with the same parameters used in
addEventListener() .

button.removeEventListener("click", onClick);
Event Propagation
Event propagation describes how an event travels through the DOM in three phases:

1. Capturing Phase:
The event travels from the root of the DOM down to the target element.
2. Target Phase:
The event reaches the target element and triggers the event listener attached to it.
3. Bubbling Phase:
The event bubbles back up the DOM, triggering event listeners on parent elements.

By default, event listeners are triggered during the bubbling phase. To change this to the capturing
phase, set the capture option to true :

button.addEventListener("click", onClick, { capture: true });

Stopping Propagation:
To stop an event from propagating further:

event.stopPropagation();

Event Delegation
Event delegation is a technique where a single event listener is attached to a parent element to
handle events for its child elements. This reduces the number of active event listeners and improves
performance.

Example:

const container = document.querySelector("#container");


container.addEventListener("click", (event) => {
if (event.target !== container) {
event.target.textContent = "Clicked";
}
});

The event listener on the parent element ( container ) handles clicks on its child elements.
Use event.target to determine which child element triggered the event.

Benefits of Event Delegation:

Fewer event listeners in the DOM.


Improved performance for large, dynamic lists of elements.

Example: Customizing Event Behavior


Using Multiple Options:

const button = document.querySelector("button");


const abortController = new AbortController();

button.addEventListener(
"click",
(event) => {
console.log("Button clicked");
},
{
capture: true,
once: true,
passive: true,
signal: abortController.signal,
}
);

// Remove the event listener programmatically


abortController.abort();

Conclusion
Event-Driven Programming is a cornerstone of JavaScript for handling user interactions and dynamic
behaviors. By mastering:

Event Listeners: Adding and customizing event handlers.


Event Propagation: Understanding the flow of events through the DOM.
Event Delegation: Efficiently managing events for multiple child elements.

You can write more efficient and maintainable JavaScript code for modern web applications.
Experiment with these techniques to fully leverage the power of JavaScript’s event model!

Questions

185) What is Event-Driven Programming, and how does it relate


to user interactions in JavaScript?
Event-Driven Programming is a programming paradigm where code runs in response to events,
such as user actions like clicks or key presses.

In JavaScript, it relates to user interactions by allowing developers to add event listeners to


elements on a webpage. These listeners detect specific events and trigger corresponding functions,
enabling dynamic and interactive web applications.

186) How can you specify that an event listener should be


triggered during the capturing phase instead of the default
bubbling phase?
You can specify that an event listener should be triggered during the capturing phase by:

Passing true as the third argument to addEventListener .


Using an options object with { capture: true } .

This ensures the event listener fires during the capturing phase instead of the default bubbling
phase.

Example:

element.addEventListener("click", callback, true);


// OR
element.addEventListener("click", callback, { capture: true });
187) What does the once option do when used in an event
listener's options object?
The once option in an event listener's options object ensures that the event listener is automatically
removed after it is triggered once.

This means the listener will only run the first time the event occurs and will then be removed from the
element.

Example:

element.addEventListener("click", callback, { once: true });

188) What is event delegation, and how does it improve


performance in JavaScript applications?
Event delegation is a technique where a single event listener is added to a parent element to
handle events for all of its child elements. Instead of adding individual listeners to each child, the
parent handles events as they bubble up from the children.

Benefits:

Reduces the number of event listeners in the application.


Improves performance, especially when dealing with a large number of child elements.

Example:

const parent = document.querySelector("#parent");


parent.addEventListener("click", (event) => {
if (event.target.tagName === "BUTTON") {
console.log("Button clicked:", event.target.textContent);
}
});
189) How can you remove an event listener using an
AbortController in JavaScript?

You can remove an event listener using an AbortController by passing the controller's signal to
the event listener's options object. When you call abortController.abort() , it automatically
removes the event listener.

Example:

const abortController = new AbortController();

element.addEventListener("click", handleClick, {
signal: abortController.signal,
});

// To remove the event listener


abortController.abort();

Coding Questions
What These Problems Test:
1. Problem #17:
Understanding of the once option in event listeners.
Dynamically managing element behavior (e.g., disabling a button after interaction).
Writing clean and concise event listener logic.
2. Problem #18:
Application of event delegation for efficient event management.
Handling dynamically created elements using parent listeners.
Modifying styles and logging information based on event targets.

Problem #17: Customizing Event Listeners


Write a script that does the following:

1. Adds a click event listener to a button with the ID "actionButton" .


2. The event listener should:
Log "Button clicked" to the console.
Disable the button after it is clicked for the first time.
Ensure the event listener is triggered only once.
3. Use the once option in the event listener's options object to automatically remove the listener
after it is triggered.

HTML Example:

<!DOCTYPE html>
<html>
<head>
<title>Event Listener Example</title>
</head>
<body>
<button id="actionButton">Click Me</button>
<script src="script.js"></script>
</body>
</html>

Expected Behavior:

Clicking the button logs "Button clicked" to the console.


The button is disabled after the first click and cannot be clicked again.

Problem #18: Event Delegation for Dynamic Elements


Write a script that uses event delegation to handle click events on dynamically created buttons
inside a <div> container with the ID "buttonContainer" . Specifically:

1. Add a click event listener to the container.


2. When a button inside the container is clicked:
Log the button's text content to the console.
Change its background color to green.
3. Dynamically add three buttons to the container with the text "Button 1" , "Button 2" , and
"Button 3" .

HTML Example:
<!DOCTYPE html>
<html>
<head>
<title>Event Delegation Example</title>
</head>
<body>
<div id="buttonContainer"></div>
<script src="script.js"></script>
</body>
</html>

Expected Behavior:

Clicking any dynamically created button logs its text content to the console and changes its
background color to green.
The container's event listener handles all button clicks, even for buttons added after the event
listener was created.
Video 11: Promises

Introduction
In this lesson, we’ll explore Promises in JavaScript, a key concept for handling asynchronous
operations. Promises provide a clean and structured way to manage asynchronous tasks, such as
fetching data or waiting for a timeout.

What is a Promise?
A Promise is an object representing the eventual completion (or failure) of an asynchronous
operation. It has three possible states:

1. Pending: The operation has not completed yet.


2. Fulfilled: The operation completed successfully, and the promise holds a resulting value.
3. Rejected: The operation failed, and the promise holds an error.

Acronym to remember: PFR (Pending, Fulfilled, Rejected).

Creating a Promise
You can create a Promise using the Promise constructor, which takes a function (often called the
executor) with two parameters: resolve and reject .

Example:

const promise = new Promise((resolve, reject) => {


setTimeout(() => resolve(2), 1000);
});

After 1 second, the promise will be fulfilled with the value 2 .


You can use resolve(value) to fulfill the promise or reject(error) to reject it.

Handling Promises
Promises provide three primary methods for handling outcomes:

1. then(fulfilledFn, rejectedFn) : Handles the fulfilled value or the rejected error.


2. catch(rejectedFn) : Handles only the rejected error.
3. finally(callback) : Runs a callback when the promise is settled (fulfilled or rejected).

Example:

promise
.then((value) => console.log("Fulfilled:", value))
.catch((error) => console.log("Rejected:", error))
.finally(() => console.log("Done"));

Chaining Promises
Promises can be chained to handle multiple asynchronous operations in sequence. Each then()
returns a new promise, enabling you to chain further.

Example:

promise
.then((value) => value * 2)
.then((value) => value + 1)
.then(console.log) // Logs the final result
.catch(console.error) // Catches any error in the chain
.finally(() => console.log("Chain complete"));
Promise Utility Functions
JavaScript provides several utility functions to work with multiple promises:

1. Promise.all([]) :
Waits for all promises in the array to settle. Returns an array of results if all are fulfilled, or the first
rejection if any are rejected.

Promise.all([
Promise.resolve(3),
Promise.resolve(5),
Promise.reject("Error"),
])
.then(console.log)
.catch(console.error);

2. Promise.race([]) :
Returns the result of the first promise to settle, regardless of whether it’s fulfilled or rejected.

Promise.race([
new Promise((res) => setTimeout(() => res(3), 1000)),
new Promise((res) => setTimeout(() => res(5), 2000)),
]).then(console.log); // Logs 3

3. Promise.any([]) :
Returns the first fulfilled promise, ignoring rejected ones. Throws an error if all promises are
rejected.

Promise.any([
Promise.reject("Error"),
Promise.resolve(4),
Promise.resolve(6),
])
.then(console.log) // Logs 4
.catch(() => console.error("All promises rejected"));
Async/Await
Async/Await is a modern, readable way to handle promises. It allows you to write asynchronous
code that looks synchronous.

async : Marks a function as asynchronous, implicitly returning a promise.


await : Pauses execution until the promise settles.

Example:

async function fetchData() {


try {
const result = await new Promise((resolve) =>
setTimeout(() => resolve(3), 1000)
);
console.log(result); // Logs 3
} catch (error) {
console.error("Error:", error);
}
}

fetchData();

Combining Promises and Async/Await


You can combine the traditional promise syntax with async/await for flexibility.

Example:

async function example() {


return await new Promise((resolve) => setTimeout(() => resolve(3), 1000));
}

example()
.then(console.log) // Logs 3
.catch(console.error);
Conclusion
Promises and async/await are essential tools for handling asynchronous operations in JavaScript.
By understanding how to:

Create and handle promises.


Chain promises for sequential operations.
Use utility functions like Promise.all , Promise.race , and Promise.any .
Leverage async/await for clean and readable code.

You’ll be well-equipped to manage complex asynchronous tasks in modern web applications.


Practice these concepts to build dynamic, responsive applications with ease!

Questions

190) What are the three possible states of a Promise in


JavaScript?
The three possible states of a Promise in JavaScript are:

1. Pending: The initial state, where the operation is still in progress and hasn't completed yet.
2. Fulfilled: The operation completed successfully, and the promise has a resulting value.
3. Rejected: The operation failed, and the promise has an error.

191) How do you create a new Promise in JavaScript, and what


are the roles of resolve and reject ?
You create a new Promise in JavaScript using the Promise constructor, which takes a function
(executor) with two parameters: resolve and reject .

resolve(value) : Fulfills the promise and sets its resulting value.


reject(error) : Rejects the promise and sets an error.

Example:
const promise = new Promise((resolve, reject) => {
const success = true; // Simulating success or failure
if (success) {
resolve("Operation succeeded");
} else {
reject("Operation failed");
}
});

192) What does the then method do when working with a


Promise?
The then method in a Promise is used to handle the result of a fulfilled promise. It takes two
optional callback functions:

1. The first callback is executed if the promise is fulfilled, receiving the resolved value.
2. The second callback (optional) is executed if the promise is rejected, receiving the error.

The then method also returns a new promise, allowing further chaining.

Example:

promise
.then((value) => {
console.log(value); // Handles the fulfilled value
})
.catch((error) => {
console.log(error); // Handles any errors
});

193) How can you handle errors that occur in a Promise chain?
You can handle errors in a Promise chain using the catch method. The catch method takes a
callback function that is executed if any promise in the chain is rejected. It prevents errors from
propagating further in the chain.

Example:
promise
.then((value) => {
// Process the fulfilled value
})
.catch((error) => {
console.log("Error:", error); // Handle the error
});

By placing catch at the end of the chain, you ensure any errors in the preceding then calls are
caught.

194) What is the purpose of the finally method in a Promise?


The finally method in a Promise is used to execute a callback function when the promise is
settled, regardless of whether it was fulfilled or rejected. It is useful for running cleanup tasks or
actions that should occur no matter the outcome.

Example:

promise
.then((value) => {
console.log("Success:", value);
})
.catch((error) => {
console.log("Error:", error);
})
.finally(() => {
console.log("Promise is settled");
});

195) How does chaining Promises help in managing multiple


asynchronous operations?
Chaining Promises helps manage multiple asynchronous operations by executing them in sequence,
where each operation depends on the result of the previous one. Each then method returns a new
promise, allowing for clean, readable code.
Example:

promise
.then((result) => {
// Use result in the next async operation
return nextAsyncOperation(result);
})
.then((finalResult) => {
console.log(finalResult); // Handle the final result
})
.catch((error) => {
console.log("Error:", error); // Handle any errors
});

Chaining ensures that each step waits for the previous one to complete, maintaining order and clarity.

196) What is the difference between Promise.all ,


Promise.race , and Promise.any ?

1. Promise.all([]) :
Waits for all promises to settle (fulfilled or rejected).
Resolves with an array of results if all promises are fulfilled.
Rejects with the first error if any promise is rejected.
Example:

Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(console.log); // [1, 2]

2. Promise.race([]) :
Resolves or rejects as soon as any one promise settles (fulfilled or rejected).
Example:

Promise.race([Promise.resolve(1), Promise.reject("Error")]).then(
console.log
); // 1

3. Promise.any([]) :
Resolves with the first fulfilled promise.
If all promises are rejected, it rejects with an error.
Example:

Promise.any([Promise.reject("Error"), Promise.resolve(2)]).then(
console.log
); // 2

197) How does async/await improve the readability of


asynchronous code compared to using then and catch ?
async/await improves readability by allowing asynchronous operations to be written in a linear,
synchronous-like style. This avoids deeply nested chains of then and catch calls, making the code
easier to read and debug.

Example with then :

promise
.then((result) => nextAsyncOperation(result))
.then((finalResult) => console.log(finalResult))
.catch((error) => console.log("Error:", error));

Example with async/await :

async function example() {


try {
const result = await promise;
const finalResult = await nextAsyncOperation(result);
console.log(finalResult);
} catch (error) {
console.log("Error:", error);
}
}

Benefits:

Reads more like standard, synchronous code.


Easier to follow the flow of asynchronous operations.
Coding Questions
What These Problems Test:
1. Problem #19:
Understanding of creating and chaining promises.
Handling asynchronous operations sequentially with .then() .
2. Problem #20:
Using Promise.all to manage multiple asynchronous operations.
Handling the results of multiple promises together.
Using the fetch API and error handling in promises.

Problem #19: Creating and Chaining Promises


Write a function called processNumber that:

1. Accepts a number as input.


2. Returns a promise that doubles the number after 1 second.
3. Chains another promise that adds 10 to the doubled number after another 1 second.
4. Logs the final result to the console.

Steps:

Use new Promise to create the promises for doubling and adding.
Chain the promises using .then() to handle the results.

Example Input:

processNumber(5);

Expected Output:

20

Explanation:

First promise doubles 5 to 10 after 1 second.


Second promise adds 10 to 10 , resulting in 20 .

Problem #20: Using Promise.all to Fetch Multiple Results


Write a function called fetchData that:

1. Accepts an array of URLs.


2. Creates a promise for each URL using fetch .
3. Uses Promise.all to wait for all the promises to resolve.
4. Logs the response statuses to the console.

Steps:

Use Promise.all to handle the array of promises created by fetch .


Handle errors if any promise is rejected.

Example Input:

fetchData([
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
]);

Expected Output:

Response statuses: [200, 200]

Hint:

Use the status property of the response objects to log the statuses.
Use catch to handle network errors.
Video 12: Working with the Server

Introduction
In this lesson, we’ll explore how to work with servers in JavaScript. The primary tool we’ll use is the
fetch() function, which allows us to make network requests. Additionally, we’ll cover handling form
submissions, using async/await , and aborting requests with AbortController .

Key Terms

fetch
The fetch() function is used to make network requests in JavaScript and returns a Promise.
Common options include:
method : HTTP method like 'GET' or 'POST' .
body : Request body, often FormData or JSON.
headers : Additional metadata for the request.
signal : Used to abort a request with an AbortController .

Response Handling
When a request completes, the promise resolves to a Response object. Key properties and methods
include:

response.text() : Resolves with the text content of the response.


response.json() : Resolves with the parsed JSON object.
response.status : Numeric status code (e.g., 200 for success).
response.ok : Boolean indicating success (true for 200–299).
Making a Basic Request
Here’s an example of a simple GET request:

const BASE_API = "http://localhost:3000/data";

fetch(BASE_API)
.then((response) => response.json())
.then((data) => console.log("Data received:", data))
.catch((error) => console.error("Error fetching data:", error));

Working with Parameters

Using String Interpolation:

fetch(`${BASE_API}?category=books&sort=popular`)
.then((response) => response.json())
.then((data) => console.log("Filtered data:", data))
.catch((error) => console.error("Error fetching filtered data:", error));

Using a URL Object:

const url = new URL(BASE_API);


url.searchParams.set("category", "books");
url.searchParams.set("sort", "popular");

fetch(url)
.then((response) => response.json())
.then((data) => console.log("Filtered data:", data))
.catch((error) => console.error("Error fetching filtered data:", error));

Using async/await
Instead of chaining .then() , we can use async/await for cleaner code:
async function fetchData() {
try {
const response = await fetch(BASE_API);
const data = await response.json();
console.log("Data received:", data);
} catch (error) {
console.error("Error fetching data:", error);
}
}

fetchData();

Working with JSON APIs


Most APIs return JSON data. Use response.json() to parse the response:

async function fetchJsonData() {


try {
const response = await fetch("http://localhost:3000/products");
const products = await response.json();
console.log("Products:", products);
} catch (error) {
console.error("Error fetching products:", error);
}
}

fetchJsonData();

Making POST Requests


For POST requests, include the data and headers in the options object:
async function submitData() {
const data = { username: "JaneDoe", age: 30 };

try {
const response = await fetch("http://localhost:3000/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
console.log("Response from server:", result);
} catch (error) {
console.error("Error submitting data:", error);
}
}

submitData();

Handling Form Submissions


You can handle form data dynamically with FormData :

<form>
<label for="email">Email</label>
<input id="email" type="email" name="email" required />
<button>Submit</button>
</form>
const form = document.querySelector("form");
form.addEventListener("submit", handleSubmit);

async function handleSubmit(event) {


event.preventDefault();

const options = {
method: "POST",
body: new FormData(form),
};

try {
const response = await fetch("http://localhost:3000/submit", options);
const result = await response.json();
console.log("Form submission result:", result);
} catch (error) {
console.error("Error submitting form:", error);
}
}

Aborting Requests
Use AbortController to cancel requests:
async function fetchWithTimeout() {
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // Cancel after 5 seconds

try {
const response = await fetch("http://localhost:3000/slow-response", {
signal: controller.signal,
});
const data = await response.text();
console.log("Received data:", data);
} catch (error) {
if (error.name === "AbortError") {
console.error("Request was aborted:", error);
} else {
console.error("Error during fetch:", error);
}
}
}

fetchWithTimeout();

Conclusion
In this lesson, we covered:

Using the fetch() function to interact with servers.


Handling asynchronous operations with async/await .
Sending data with POST requests.
Managing form submissions.
Aborting requests for better control.

By mastering these techniques, you can build dynamic and interactive web applications that
communicate effectively with servers.

Questions
198) What is the primary purpose of the fetch function in
JavaScript, and how does it handle network requests?
The primary purpose of the fetch function in JavaScript is to make network requests, such as
retrieving data from a server. It works asynchronously and returns a Promise, allowing you to handle
the response once the request is complete.

You provide a URL and optionally include options like the request method ( GET , POST ) and
headers.
When the request completes, the Promise resolves to a Response object, which can be used to
access the response data, status, and other information.

199) How can the response object be used to determine


whether a network request was successful?
You can determine if a network request was successful by checking the following properties of the
response object:

response.ok : A boolean that is true if the request was successful (status code in the 200-299
range).
response.status : The numeric status code of the response. A code between 200 and 299
indicates success (e.g., 200 for "OK").

Example:

if (response.ok) {
console.log("Request succeeded with status:", response.status);
} else {
console.error("Request failed with status:", response.status);
}
200) What is the role of the Headers object in making network
requests, and how does it differ from using a plain object for
headers?
The Headers object in JavaScript is used to manage HTTP request and response headers in a
structured way.

Role: It allows you to easily set, get, and delete headers when making network requests.
Key Differences from a Plain Object:
The Headers object includes built-in methods like .append() , .get() , and .set() for
managing headers.
It handles header case sensitivity and duplicates more efficiently compared to plain objects.
A plain object only stores key-value pairs without these advanced methods.

Example:

const headers = new Headers();


headers.append("Content-Type", "application/json");
headers.set("Authorization", "Bearer token");

201) Explain the purpose of an AbortController in JavaScript.


How does it help manage network requests?
The AbortController in JavaScript is used to cancel or abort network requests that take too long or
are no longer needed.

It works by generating an AbortSignal that can be passed to a fetch request.


When the controller’s abort() method is called, the associated network request is canceled.

Benefits:

Prevents unnecessary processing and saves resources when a request is no longer required.
Useful for managing long-running or outdated requests.

Example:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // Cancel after 5 seconds

fetch("https://api.example.com/data", { signal: controller.signal })


.then((response) => response.json())
.catch((error) => {
if (error.name === "AbortError") {
console.error("Request was aborted");
} else {
console.error("Another error occurred:", error);
}
});

202) Why is using async and await generally preferred over


chaining .then() for handling asynchronous operations in
modern JavaScript?
Using async and await is generally preferred over chaining .then() because it makes
asynchronous code easier to read and write.

Advantages:

1. Improved Readability: Code using async/await looks more like regular, synchronous code,
making it easier to follow.
2. Reduced Nesting: Avoids deeply nested .then() chains, resulting in cleaner code.
3. Better Error Handling: Errors can be managed using try/catch blocks, instead of needing
separate .catch() methods.

Example with .then() :

fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error));

Example with async/await :


async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}

fetchData();

The async/await version is more intuitive and reduces complexity, especially for larger workflows.

Coding Questions
What These Problems Test:
1. Problem #21:
Basic understanding of the fetch API and its usage for GET requests.
Parsing JSON responses and extracting relevant data.
Using async/await for cleaner asynchronous code.
2. Problem #22:
Working with HTML forms and preventing default submission behavior.
Using FormData to collect form inputs dynamically.
Making a POST request and handling server responses.

Problem #21: Fetch and Handle JSON Data


Write a function called getUserData that:

1. Fetches user data from the following API endpoint:


https://jsonplaceholder.typicode.com/users/1
2. Parses the response as JSON.
3. Logs the user's name and email to the console.
Steps:

Use fetch() to make a GET request.


Use async/await for handling the asynchronous operation.
Handle any potential errors using a try/catch block.

Expected Output:

Name: Leanne Graham


Email: [email protected]

Problem #22: Form Submission with Fetch


Write a script that dynamically handles a form submission. The script should:

1. Select a form element with the ID "userForm" .


2. On form submission:
Prevent the default browser behavior.
Collect the form data using the FormData object.
Send the form data to this mock endpoint:
https://jsonplaceholder.typicode.com/posts
using the POST method.
Log the response from the server to the console.

HTML Example:

<form id="userForm">
<label for="username">Username</label>
<input id="username" name="username" type="text" required />
<button type="submit">Submit</button>
</form>

Expected Behavior:

When the form is submitted, the data is sent to the server.


The server response is logged to the console.
Video Lesson #13: Timers and Intervals
Key Terms
setInterval

A JavaScript function for repeatedly calling a callback function at a specified interval.


Example:

let counter = 0;
const intervalID = setInterval(() => {
console.log(`Counter: ${++counter}`);
}, 1000); // Increments counter every second
clearInterval(intervalID); // Stops the interval

setTimeout

A JavaScript function for delaying the execution of a callback function.


Example:

const timeoutID = setTimeout(() => {


console.log("This runs after 1 second");
}, 1000); // Executes after 1 second
clearTimeout(timeoutID); // Cancels the timeout before it runs

requestAnimationFrame

A JavaScript function for scheduling a callback to execute before the next browser repaint.
Often used for animations.
Example:
let position = 0;

function animate() {
position += 1;
console.log(`Position: ${position}`);
if (position < 100) {
requestAnimationFrame(animate);
}
}

const animationFrameID = requestAnimationFrame(animate); // Starts animation


cancelAnimationFrame(animationFrameID); // Cancels the animation

Video Notes
In this lesson, we’ll explore how to manage time and animations in JavaScript using intervals,
timeouts, and animation frames. We'll also work with dates and performance timing.

Intervals, Timeouts, and Animation Frames

1. Intervals
Used to repeatedly execute a function at fixed time intervals.
Code Example:

function logMessage() {
const timerID = setInterval(() => {
console.log("Interval running...");
}, 500); // Logs message every half-second

setTimeout(() => clearInterval(timerID), 5000); // Stops after 5 seconds


}

logMessage();
2. Timeouts
Executes a function once after a specified delay.
Code Example:

setTimeout(() => {
console.log("Delayed message after 1 second");
}, 1000);

3. Animation Frames
Tied to the screen’s refresh rate (approximately 60fps).
Useful for animations and smoother execution tied to rendering cycles.
Code Example:

let x = 0;

function moveBox() {
console.log(`Box position: ${x}px`);
x += 5;
if (x <= 100) {
requestAnimationFrame(moveBox);
}
}

requestAnimationFrame(moveBox);

4. Performance Timing
performance.now() provides a high-resolution timestamp for precise timing.

Code Example:

const start = performance.now();

setTimeout(() => {
const end = performance.now();
console.log(`Elapsed time: ${end - start}ms`);
}, 1000);
Working with Dates

Creating Dates
Use the Date object to manage and manipulate dates and times.
Examples:

const now = new Date();


console.log(`Current Date: ${now.toString()}`);

const specificDate = new Date("2025-01-01T05:25:10Z");


console.log(`Specific Date: ${specificDate.toISOString()}`);

Date Methods
Get specific parts of a date:

console.log(now.getFullYear()); // Current year


console.log(now.getMinutes()); // Current minute
console.log(now.getUTCDay()); // Day of the week (UTC)

Set parts of a date:

specificDate.setFullYear(2030);
console.log(`Updated Year: ${specificDate.getFullYear()}`);
Full Example: Timer with Buttons

<div id="count">0</div>
<button id="start">Start</button>
<button id="stop">Stop</button>

<script>
const startButton = document.getElementById("start");
const stopButton = document.getElementById("stop");
const countDisplay = document.getElementById("count");

let count = 0;
let intervalID;

startButton.addEventListener("click", () => {
intervalID = setInterval(() => {
count++;
countDisplay.textContent = count;
}, 500); // Updates count every half-second
});

stopButton.addEventListener("click", () => {
clearInterval(intervalID);
});
</script>

Conclusion
In this lesson, we’ve covered:

1. Using setInterval and setTimeout for timing operations.


2. Leveraging requestAnimationFrame for animations.
3. Using performance timing and date objects for precise control.

These tools are essential for managing time and animations in your JavaScript applications.

Questions and Answers: Timers and Intervals in JavaScript


198) What is the main difference between using intervals and timeouts for
scheduling functions in JavaScript?
The main difference between intervals and timeouts is how often they run a function:

Intervals: Repeatedly run a function at a set time interval (e.g., every second) until explicitly
stopped.
Timeouts: Run a function only once after a specified delay.

Key Difference: Intervals keep going until you stop them, while timeouts happen just once.

199) How does requestAnimationFrame() optimize performance compared


to using setInterval() for animations?
requestAnimationFrame() optimizes performance by synchronizing animations with the browser's
refresh rate (typically 60fps).

Advantages:
Ensures animations run smoothly by executing only when the browser is ready to repaint.
Reduces unnecessary work, conserving system resources.

In contrast, setInterval() runs at fixed intervals, which can:

Cause performance issues if the interval is faster than the screen refresh rate.
Lead to skipped frames or choppy animations.

200) In what situations would you use clearTimeout() or clearInterval() ,


and why is it important to cancel timers?
You use clearTimeout() or clearInterval() when you need to stop a scheduled function from
running. For example:

Stopping a countdown timer or animation when it’s no longer needed.


Preventing redundant operations when a user navigates away from a page.

Importance:

Avoids unnecessary memory usage and performance issues.


Prevents unexpected behavior from functions running when they’re not needed.
201) Why is performance.now() preferred over Date.now() for measuring
precise time intervals in performance-critical applications?
performance.now() is preferred because it provides:

Sub-millisecond precision, making it ideal for performance-critical tasks.


Time measured relative to when the page started loading, ensuring accuracy.

In contrast, Date.now() :

Measures the current time in milliseconds since 1970, which is less precise.
Can be affected by system clock changes, leading to inaccuracies.

Use Case: performance.now() is better for animations, benchmarking, or calculating precise


intervals in complex applications.

202) What are some practical use cases for using the Date object in web
development, and how do developers typically work with dates and times?
The Date object is used in web development for handling dates and times. Practical use cases
include:

1. Displaying current dates and times:


Example: Showing the current date on a webpage or adding timestamps to posts.
2. Scheduling tasks:
Example: Setting reminders, countdowns, or deadlines.
3. Calculating time differences:
Example: Determining how many days are left until an event or how long ago something
occurred.

Typical Developer Methods:

Retrieving Date Parts:


getDate() , getMonth() , getFullYear() to access parts of a date.
Modifying Dates:
setDate() , setMonth() to change date values.
Getting Timestamps:
Date.now() for the current timestamp in milliseconds.
Coding Questions
What These Problems Test:
1. Problem #23:
Understanding of setInterval and clearInterval .
Using the Date object for real-time updates.
DOM manipulation to display dynamic data.
2. Problem #24:
Using requestAnimationFrame for smooth animations.
Calculating and managing incremental updates over time.

Problem #23: Auto-Updating Clock


Write a function called startClock that:

1. Uses setInterval to update the current time every second.


2. Displays the time in a div with the ID "clock" in the format HH:MM:SS .
3. Stops updating when a button with the ID "stopClock" is clicked.

HTML Example:

<div id="clock">00:00:00</div>
<button id="stopClock">Stop Clock</button>

Steps:

Use the Date object to get the current hours, minutes, and seconds.
Format the time as a string using toString().padStart(2, "0") for double digits.
Update the text content of the clock.
Use clearInterval to stop the updates when the stop button is clicked.

Example Output:

Initially displays 00:00:00 and updates every second (e.g., 12:34:56 , 12:34:57 ...).
Stops updating when the stop button is clicked.

Problem #24: Smooth Scroll to Top


Write a function called smoothScrollToTop that:

1. Uses requestAnimationFrame to create a smooth scrolling animation.


2. Scrolls the page to the top of the document over 1 second.

Steps:

Get the current scroll position using window.scrollY .


Calculate the distance to scroll in each frame based on the time elapsed.
Use requestAnimationFrame to update the scroll position in small increments.
Stop the animation once the page is scrolled to the top.

Example Input:

smoothScrollToTop();

Expected Behavior:

Smoothly scrolls the page from the current scroll position to the top within 1 second.
Video Lesson: Closures and Lexical Scoping in JavaScript

Key Terms
1. Closure:
A function along with a saved reference to the lexical environment it was defined in.
Allows functions to access variables from their parent scope, even after the parent function
has returned.
2. Lexical Environment:
An internal structure for tracking identifiers (variable and function names) and their values.
Stores locally available identifiers and a reference to the parent environment.
3. Lexical Scoping:
JavaScript's scoping system, ensuring code blocks have access to identifiers in their parent
environment.
When a variable isn’t found locally, JavaScript looks up the parent scopes until the global
scope.

Understanding Lexical Scoping

let globalNum = 7;

function logNum() {
const localNum = 1;
console.log(globalNum + localNum);
}

logNum(); // Output: 8

Lexical Scoping: Functions have access to variables from their parent scope. Here, logNum
accesses both globalNum (from the global scope) and localNum (from its local scope).
Closures
Definition:
A closure is created when a function is declared. It captures its parent scope, allowing access to
variables in that scope even after the parent function has finished executing.

Example:

function example() {
const num = 5;

return function logNum() {


console.log(num);
};
}

const innerFunction = example();


innerFunction(); // Output: 5

The logNum function keeps a reference to num , even though the example function has already
finished running.

Practical Use of Closures: Private Methods


Closures are commonly used to create private variables and methods, offering encapsulation.

Example:
function makeFunctions() {
let privateNum = 0;

function privateIncrement() {
privateNum++;
}

return {
logNum: () => console.log(privateNum),
increment: () => {
privateIncrement();
console.log("Incremented");
},
};
}

const { logNum, increment } = makeFunctions();


logNum(); // Output: 0
increment(); // Output: "Incremented"
logNum(); // Output: 1

privateIncrement and privateNum are inaccessible outside of makeFunctions , but logNum


and increment provide controlled access.

Closures in Loops
Using let :

for (let i = 0; i < 3; i++) {


setTimeout(() => {
console.log(i);
}, 1000);
}
// Output: 0, 1, 2

Why?: Each iteration creates a new block-scoped variable i , so each timeout captures a
different i .

Using var :
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
// Output: 3, 3, 3

Why?: var is function-scoped, so all iterations share the same i . By the time the timeouts run,
i has reached its final value (3).

Conclusion
Closures and lexical scoping are essential concepts in JavaScript. They explain how functions access
variables and enable powerful patterns like private methods and callbacks. Mastering these concepts
helps you write more efficient and flexible code.

Questions

203) What is lexical scoping in JavaScript, and how does it


affect how functions access variables?
Lexical scoping in JavaScript means that a function can access variables from its own scope and
from the scopes of its parent functions. This is because functions are able to "remember" the
environment where they were created.

It affects how functions access variables by allowing them to use variables defined in outer scopes,
even if those variables aren't inside the function itself.
204) How is a closure created in JavaScript, and why is it useful
for functions?
A closure is created in JavaScript when a function is defined inside another function and has access
to the outer function's variables. This happens because the inner function "remembers" the
environment in which it was created, even after the outer function has finished running.

Why It's Useful:

Closures are useful because they allow functions to keep access to variables from their outer scope,
which can be used later. This is helpful for tasks like maintaining private variables or creating
functions that remember specific data.

205) What does it mean when we say that a function retains


access to its outer scope, even after the outer function has
finished executing?
When we say a function retains access to its outer scope even after the outer function has finished
executing, it means that the inner function "remembers" the variables from the outer function.

Even though the outer function is no longer running, the inner function can still use its variables. This
is possible because of closures in JavaScript, where the inner function keeps a reference to the
environment where it was created.

206) How can closures be used to create private methods in


JavaScript?
Closures can be used to create private methods in JavaScript by keeping certain variables and
functions hidden within a function's scope. You define variables or helper functions inside a function
and only expose the parts you want to be publicly accessible by returning an object with specific
methods.

The variables stay private because they can only be accessed by the inner functions, not from
outside the closure. This helps create "private" data that can't be directly modified from outside the
function.

Example:
function createCounter() {
let count = 0; // Private variable

function increment() {
count++;
}

function getCount() {
return count;
}

return {
increment,
getCount,
};
}

const counter = createCounter();


counter.increment();
console.log(counter.getCount()); // Outputs: 1
console.log(counter.count); // Undefined (cannot access private variable)

In this example:

count is a private variable.


increment and getCount have access to count through the closure.
The returned object exposes increment and getCount , but not count itself.

207) What is the difference between block-scoped variables


(like let ) and function-scoped variables (like var ), and how
does this affect closures in loops?
The difference between block-scoped variables ( let ) and function-scoped variables ( var ) is where
they are accessible in your code.

Block-scoped ( let ):
Variables declared with let are limited to the block (e.g., inside a loop or an if statement)
where they are defined.
Each iteration of a loop with let creates a new variable.
Function-scoped ( var ):
Variables declared with var are limited to the function they are defined in, not individual
blocks.
All iterations of a loop share the same var variable.

Effect on Closures in Loops:

In loops, this affects closures because:

With let , each iteration has its own separate variable. Closures capture the value of i for
each iteration, so they "remember" the correct value.
With var , since the variable is shared across all iterations, closures will remember the final value
of the loop variable after the loop ends.

Example with let :

for (let i = 0; i < 3; i++) {


setTimeout(() => {
console.log(i);
}, 1000);
}
// Outputs after 1 second: 0, 1, 2

Each timeout function captures its own i value.

Example with var :

for (var i = 0; i < 3; i++) {


setTimeout(() => {
console.log(i);
}, 1000);
}
// Outputs after 1 second: 3, 3, 3

All timeout functions capture the same i variable, which has the final value 3 after the loop
completes.
Coding Questions
What These Problems Test:
1. Problem #25:
Understanding closures for encapsulating private variables.
Ability to create and manage methods within a closure.
2. Problem #26:
Correct handling of closures in loops with asynchronous operations.
Ability to ensure each timeout remembers its corresponding value and delay.

Problem #25: Counter Factory


Create a function called createCounter that generates a counter object. The counter object should
have the following methods:

1. increment() : Increases the counter by 1.


2. decrement() : Decreases the counter by 1.
3. reset() : Resets the counter to 0.
4. getValue() : Returns the current value of the counter.

The counter value should be private and only accessible via the object's methods.

Example Usage:

const counter = createCounter();


counter.increment();
counter.increment();
console.log(counter.getValue()); // 2
counter.decrement();
console.log(counter.getValue()); // 1
counter.reset();
console.log(counter.getValue()); // 0
Problem #26: Delayed Logger
Write a function called delayedLogger that takes an array of strings as input and logs each string to
the console with a delay. The first string should be logged after 1 second, the second after 2
seconds, and so on.

Use closures to ensure each timeout function remembers the correct delay and string.

Example Input:

delayedLogger(["Hello", "World", "JavaScript"]);

Expected Output:

After 1 second: Logs "Hello" .


After 2 seconds: Logs "World" .
After 3 seconds: Logs "JavaScript" .
Video Lesson: Understanding this in
JavaScript

Key Terms

this
A JavaScript keyword that refers to the context in which the current code is running. The value of
this is determined at runtime and can vary depending on where and how the code is executed.

General Rules for this in the Browser


1. Global Context (Top-Level):
this refers to the global object ( window in the browser).

2. In a Standard Function (Non-Strict Mode):


this refers to the global object ( window ).
3. In a Standard Function (Strict Mode):
this is undefined .
4. In an Object Method:
this refers to the object the method belongs to.
5. In a Constructor Function:
this refers to the new object being constructed.
6. In Event Listeners:
this refers to the element the event listener is attached to (if a standard function is used).

Arrow Functions and this


Arrow functions do not create their own this context. Instead, they inherit this from their
enclosing scope.

Binding this
JavaScript provides three methods for explicitly binding this :

1. func.bind(thisArg) :
Returns a new function with thisArg bound as this .
2. func.call(thisArg, arg1, arg2, ...) :
Calls func immediately, with thisArg as this and arguments passed individually.
3. func.apply(thisArg, [arg1, arg2, ...]) :
Calls func immediately, with thisArg as this and arguments passed as an array.

Video Notes: Examples and Use Cases

Global Context
At the global level, this refers to the window object (in a browser):

console.log(this); // Logs: Window

In strict mode, this becomes undefined :

"use strict";
console.log(this); // Logs: undefined

this in Functions

Standard Function:

function logThis() {
console.log(this);
}

logThis(); // Logs: Window (or undefined in strict mode)


Object Method:

const obj = {
num: 7,
logThis: function () {
console.log(this);
},
};

obj.logThis(); // Logs: obj

this in Event Listeners

const button = document.querySelector("button");

button.addEventListener("click", function () {
console.log(this); // Logs: button
});

Using an arrow function:

button.addEventListener("click", () => {
console.log(this); // Logs: Window
});

Arrow functions do not create their own this , so it refers to the outer scope ( Window in this case).
Binding this Explicitly
Using bind :

function logThis() {
console.log(this);
}

const boundLogThis = logThis.bind({ num: 7 });


boundLogThis(); // Logs: { num: 7 }

Using call and apply :

function logThis(x, y) {
console.log(this, x, y);
}

logThis.call({ num: 7 }, 10, 20); // Logs: { num: 7 } 10 20


logThis.apply({ num: 7 }, [10, 20]); // Logs: { num: 7 } 10 20

this in Iteration Methods

[1, 2, 3].forEach(
function (num) {
console.log(this, num); // Logs: Window (or undefined in strict mode)
},
{ num: 7 }
); // Logs: { num: 7 } for each iteration

this in Classes
In a class constructor or method, this refers to the instance being created:
class Programmer {
constructor(name) {
this.name = name;
}

code() {
console.log(`${this.name} writes code`);
}
}

const dev = new Programmer("Steven");


dev.code(); // Logs: Steven writes code

Conclusion
The behavior of this in JavaScript depends on the context in which it is used. While it can be
complex, understanding how this works in different scenarios is crucial for writing effective
JavaScript code. Key tools like bind , call , and apply provide powerful ways to control this ,
and recognizing the nuances of arrow functions helps avoid common pitfalls.

Questions and Answers


208) How is the value of the this keyword determined at
runtime in JavaScript, and why does it vary depending on the
context?
The value of the this keyword in JavaScript is determined at runtime based on how and where a
function is called. It can vary depending on the context:

In the global context, this refers to the global object (the window in a browser).
Inside a regular function, this also refers to the global object, unless the function is in strict
mode, where this is undefined .
Inside an object method, this refers to the object that owns the method.
In a constructor function or class, this refers to the new object being created.
In an event listener, this refers to the element that triggered the event.
Because this is determined by the function's call context, it changes depending on how the
function is used.

209) What are the main differences between how this


behaves in regular functions and arrow functions in
JavaScript?
The main difference between how this behaves in regular functions and arrow functions is:

In regular functions, this is set based on how the function is called. It can refer to the global
object, an object method, or something else, depending on the context.
In arrow functions, this doesn’t change based on how or where the function is called.
Instead, it inherits this from the surrounding (or enclosing) scope where the arrow function was
defined. Arrow functions don’t have their own this context.

This makes arrow functions useful when you want to maintain the this value from the outer scope.

210) In what scenarios would you use bind() , call() , or


apply() to explicitly set the value of this in JavaScript?
You would use bind() , call() , or apply() to explicitly set the value of this in JavaScript when
you want to control the context in which a function is executed. Here’s when to use each:

bind() : Use bind() when you want to create a new function with this set to a specific
value, but you don’t want to call the function immediately. It returns a new function that you can
call later.
call() : Use call() when you want to invoke a function immediately and set this to a
specific value. You pass arguments to the function individually.
apply() : Similar to call() , but use apply() when you want to invoke a function immediately
and pass arguments as an array.

These methods are helpful when you want to use a function in different contexts, but need to control
what this refers to during the function's execution.
211) How does the behavior of this change when using strict
mode, and what are the implications for function calls?
In strict mode, the behavior of this changes in that it does not default to the global object. Instead:

In regular functions, if this is not explicitly set, it will be undefined in strict mode. In non-
strict mode, it would default to the global object (like window in browsers).
In methods and objects, strict mode doesn’t change how this works—it will still refer to the
object the method belongs to.

The implication is that in strict mode, you need to ensure this is properly set when calling
functions, or else it will be undefined , which can prevent unintentional references to the global
object. This makes your code safer and less prone to errors.

212) How does the this keyword function differently in event


listeners, and why is understanding this behavior important
when working with DOM events?
In event listeners, the this keyword refers to the element that the event is attached to. For example,
if you add a click event to a button, this inside the event handler will refer to that button.

Understanding this is important because it lets you interact with the specific element that triggered
the event. For instance, you can change its style or properties based on user actions.

However, if you use an arrow function inside the event listener, this will behave differently. It won’t
refer to the element but instead inherit this from the surrounding context, which could be the global
object or something else.

This difference is crucial when working with DOM events, as it affects how you interact with elements
in response to user actions.

Coding Questions
What These Problems Test:
1. Problem #27:
Understanding how this works in object methods.
Ensuring proper use of this within different contexts.
2. Problem #28:
Demonstrating the difference in this behavior between regular and arrow functions,
especially in the context of DOM events.
Applying knowledge of event listeners and function scoping.

Problem #27: Object Method Context


Create an object called calculator that has the following properties and methods:

1. A property called value initialized to 0 .


2. A method add(number) that adds the given number to value and returns the new value.
3. A method subtract(number) that subtracts the given number from value and returns the
new value.
4. A method reset() that sets value back to 0 .
5. A method logThis() that logs the value of this to demonstrate the context when the method
is called.

Requirements:

Ensure this correctly refers to the calculator object when using its methods.

Example Usage:

calculator.add(10); // Returns 10
calculator.subtract(5); // Returns 5
calculator.logThis(); // Logs the calculator object
calculator.reset(); // Sets value back to 0

Problem #28: Arrow Functions and Event Listeners


Write a function called setupClickLogger that takes a DOM element as an argument and adds a
click event listener to it. The event listener should log the id of the clicked element using the
this keyword. Use the following rules:

1. If a regular function is used as the event listener, this should correctly refer to the clicked
element.
2. If an arrow function is used, log a message explaining why this does not refer to the clicked
element.

Requirements:

Demonstrate the behavior of both regular functions and arrow functions in event listeners.

Example HTML:

<button id="btn1">Button 1</button> <button id="btn2">Button 2</button>

Example JavaScript:

const btn1 = document.getElementById("btn1");


const btn2 = document.getElementById("btn2");

setupClickLogger(btn1);
setupClickLogger(btn2);

// When Button 1 is clicked, logs: "Clicked element ID: btn1"


// When Button 2 is clicked, logs: "Clicked element ID: btn2"
// If an arrow function is used, logs: "Arrow function used, 'this' does not refer to t
Understanding Classes and Prototypal Inheritance in
JavaScript
In this lesson, we’ll explore the concept of classes in JavaScript and how they relate to prototypal
inheritance. While JavaScript now provides a class syntax, it still operates on the principle of
prototypal inheritance under the hood.

Key Terms
Prototypal Inheritance
The inheritance model used in JavaScript, where objects inherit directly from other objects rather
than from class blueprints.
Objects share properties and methods through a prototype chain.

Prototype Chain
The mechanism by which JavaScript resolves property lookups.
If a property is not found on an object, JavaScript looks up the chain to its prototype, continuing
until it reaches null .
Methods for interacting with prototypes:
Object.getPrototypeOf(obj) : Retrieves the prototype.
Object.setPrototypeOf(obj, proto) : Sets the prototype.
Object.create(proto) : Creates a new object with a specified prototype.

Function Constructor
A function used to create objects with the new keyword.
Sets the prototype of the created object to the prototype property of the constructor
function.
Example:
function Animal(species) {
this.species = species;
}

Animal.prototype.speak = function () {
console.log(`The ${this.species} makes a sound.`);
};

const lion = new Animal("Lion");


lion.speak(); // Logs: "The Lion makes a sound."

Class
A cleaner, modern syntax introduced in ES6 for creating objects and inheritance.
Classes are syntactic sugar over the prototype system.
Example:

class Animal {
constructor(species) {
this.species = species;
}

speak() {
console.log(`The ${this.species} makes a sound.`);
}
}

const lion = new Animal("Lion");


lion.speak(); // Logs: "The Lion makes a sound."

Prototype Chain and Object Inheritance


JavaScript objects inherit properties and methods through their prototype. Consider the following:

const animal = { canMove: true };


const bird = Object.create(animal);
console.log(bird.canMove); // true

In this example, bird inherits the canMove property from animal .


Using Constructor Functions
Before ES6, objects were created using constructor functions:

function Device(name) {
this.name = name;
}

Device.prototype.describe = function () {
console.log(`This device is called ${this.name}`);
};

const phone = new Device("Smartphone");


phone.describe(); // Logs: "This device is called Smartphone"

The prototype object of Device contains shared properties and methods.

Modern Class Syntax


With ES6, the class keyword provides a more intuitive way to define objects and inheritance.

Example:

class Device {
constructor(name) {
this.name = name;
}

describe() {
console.log(`This device is called ${this.name}`);
}
}

const phone = new Device("Smartphone");


phone.describe(); // Logs: "This device is called Smartphone"

Classes internally still use prototypes but offer a cleaner and more readable syntax.
Inheritance with Classes
Classes can extend other classes, allowing for hierarchical inheritance.

Example:

class SmartDevice extends Device {


constructor(name, operatingSystem) {
super(name); // Calls the parent constructor
this.operatingSystem = operatingSystem;
}

showOS() {
console.log(`${this.name} runs on ${this.operatingSystem}`);
}
}

const laptop = new SmartDevice("Laptop", "Windows");


laptop.describe(); // Logs: "This device is called Laptop"
laptop.showOS(); // Logs: "Laptop runs on Windows"

The super() keyword is used to call the parent class’s constructor or methods.

Static Properties and Methods


Static properties and methods belong to the class itself, not its instances.

Example:

class Device {
static category = "Electronics";

constructor(name) {
this.name = name;
}
}

console.log(Device.category); // Logs: "Electronics"


Private Fields
Private fields in classes, denoted by # , allow encapsulation of data.

Example:

class SmartDevice {
#operatingSystem;

constructor(name, operatingSystem) {
this.name = name;
this.#operatingSystem = operatingSystem;
}

getOperatingSystem() {
return this.#operatingSystem;
}
}

const tablet = new SmartDevice("Tablet", "Android");


console.log(tablet.getOperatingSystem()); // Logs: "Android"

Summary
Prototypal Inheritance: Objects inherit directly from other objects through a prototype chain.
Constructor Functions: Used before ES6 to create objects with the new keyword.
Classes: Simplified syntax for working with inheritance but still based on prototypes.
Static Properties/Methods: Belong to the class rather than instances.
Private Fields: Encapsulate data and prevent external access.

JavaScript’s inheritance system is versatile, allowing for both classical-style and prototype-based
object creation. Understanding these concepts ensures cleaner and more maintainable code.

Questions and Answers


213) What is the key difference between prototypal inheritance in JavaScript
and classical inheritance in languages like Java or C#?
The key difference is that prototypal inheritance in JavaScript allows objects to inherit directly from
other objects.

In contrast, classical inheritance (found in languages like Java or C#) uses blueprint-like classes that
define how objects are created.

In JavaScript, you don't need classes for inheritance—objects can inherit properties and methods
from other existing objects through prototypes.

In classical inheritance, you create new objects based on predefined class structures.

214) How does the prototype chain work in JavaScript, and what happens
when a property is not found on an object?
The prototype chain in JavaScript is a system where objects can inherit properties and methods from
other objects.

When you try to access a property on an object and it’s not found, JavaScript looks at the object’s
prototype.

If the property isn’t found there, it keeps going up the chain of prototypes until it either finds the
property or reaches null , which ends the chain.

If the property is not found anywhere in the prototype chain, JavaScript returns undefined .

215) What are some of the modern alternatives to the deprecated


__proto__ property for getting and setting an object's prototype? {#215-
what-are-some-of-the-modern-alternatives-to-the-deprecated-proto-
property-for-getting-and-setting-an-objects-prototype }
Modern alternatives to the deprecated __proto__ property for getting and setting an object's
prototype in JavaScript are:

1. Object.getPrototypeOf(obj) : Used to get the prototype of an object. It safely returns the


object's prototype without using the old __proto__ syntax.
2. Object.setPrototypeOf(obj, proto) : Used to set the prototype of an object to another object
( proto ). It allows you to change the prototype in a modern, safer way.

These methods are preferred over __proto__ because they are more reliable and future-proof.

216) How does the new keyword function when creating an object with a
constructor function in JavaScript?
The new keyword in JavaScript is used to create a new object from a constructor function. Here's
how it works:

1. Creates a new object: A new empty object is automatically created.


2. Sets the prototype: The new object's prototype is set to the constructor function's prototype
property.
3. Binds this : Inside the constructor function, this refers to the new object, allowing properties
and methods to be added to it.
4. Returns the object: The new keyword automatically returns the new object, unless the
constructor explicitly returns something else.

In short, new helps create an object and sets it up with the correct prototype and properties.

217) What is the role of the super keyword in class inheritance, and when
would you use it?
The super keyword in JavaScript is used in class inheritance to call the constructor or methods of a
parent class.

In a constructor: You use super() to call the parent class's constructor and pass any required
arguments. This ensures the parent class is properly initialized before adding new properties to
the child class.
In methods: You use super.method() to call a method from the parent class when the child
class needs to extend or modify the behavior of that method.

You would use super whenever you need to access or reuse functionality from a parent class in a
child class.
218) How do static properties and methods in JavaScript classes differ from
instance properties and methods?
In JavaScript classes, static properties and methods belong to the class itself, not to individual
instances of the class.

This means you can access them directly from the class without creating an object (instance) of the
class.

Static properties/methods: Access these directly on the class, like


ClassName.staticMethod() .

Instance properties/methods: These are specific to each object created from the class, and
you access them through the individual instances (objects), like
instanceName.instanceMethod() .

Static methods are used when functionality is related to the class as a whole, while instance methods
are for actions specific to an individual object.

219) What are private fields in JavaScript classes, and why are they useful
for encapsulating data?
Private fields in JavaScript classes are variables that are only accessible inside the class where they
are defined.

They are marked with a # symbol and cannot be accessed or modified from outside the class.
class Car {
#mileage; // private field

constructor(mileage) {
this.#mileage = mileage;
}

getMileage() {
return this.#mileage; // accessible inside the class
}
}

const car = new Car(12000);


console.log(car.getMileage()); // Logs: 12000
console.log(car.#mileage); // Error: Private field '#mileage' must be declared in an en

Private fields are useful for encapsulating data, meaning you can keep certain information hidden and
control how it's accessed or changed.

This helps prevent accidental or unwanted changes to important data from outside the class.

Coding Questions
What These Problems Test:
1. Problem #29:
Understanding of ES6 classes, inheritance, and the super keyword.
Ability to structure code for reusable and maintainable design.
2. Problem #30:
Familiarity with prototypal inheritance using constructor functions and Object.create .
Ability to set up and use prototype chains effectively.

Problem #29: Class and Inheritance


Create a Person class and a Student class that extends it.

1. Person Class:
Properties:
name (string)
occupation (string)
Method:
introduce() : Logs "Hello, my name is [name] and I work as a [occupation]."
2. Student Class (inherits from Person ):
Additional Property:
major (string, e.g., "Computer Science", "Mathematics", etc.)
Additional Method:
study() : Logs "[name] is studying [major]."

Problem #30: Prototypal Inheritance


Using the constructor function approach, implement a simple inheritance system for a Vehicle and
Car .

1. Vehicle Constructor:
Property:
type (string, e.g., "Truck", "Bike", etc.)
Method:
describe() : Logs "This is a [type]."
2. Car Constructor (inherits from Vehicle ):
Additional Property:
brand (string, e.g., "Toyota", "Ford")
Additional Method:
getBrand() : Logs "This car is a [brand]."
3. Requirements:
Use Object.create to set up the prototype chain.
Ensure methods are properly inherited.

Example Usage:
const vehicle = new Vehicle("Truck");
vehicle.describe(); // Logs: "This is a Truck."

const car = new Car("Car", "Toyota");


car.describe(); // Logs: "This is a Car."
car.getBrand(); // Logs: "This car is a Toyota."
Video Notes: Currying in JavaScript

Understanding Currying
Currying is the process of transforming a function so that it takes its parameters one at a time, in a
sequence of individual function calls.

For example, a function multiply(a, b, c) can be transformed into multiply(a)(b)(c) .

This is achieved by creating functions that return other functions, using closures to maintain access
to the parameters.

Basic Curried Function


A simple curried function can look like this:

function curriedMultiply(a) {
return function (b) {
return a * b;
};
}

Calling curriedMultiply(3) returns a new function that multiplies 3 by the next argument:

const multiplyByThree = curriedMultiply(3);


console.log(multiplyByThree(4)); // Outputs: 12

Currying a Multi-Parameter Function


Let's say we have a regular function:

function multiply(a, b, c) {
return a * b * c;
}
To curry this function, we can rewrite it as nested functions:

function curriedMultiply(a) {
return function (b) {
return function (c) {
return a * b * c;
};
};
}

Now, you can call it as:

console.log(curriedMultiply(2)(3)(4)); // Outputs: 24

General Currying Function


To make currying more flexible, we can create a generic curry function that works for any function
with three parameters:

function curry(func) {
return function (a) {
return function (b) {
return function (c) {
return func(a, b, c);
};
};
};
}

Example:

const curriedMultiply = curry(multiply);


console.log(curriedMultiply(2)(3)(4)); // Outputs: 24

You can use the same curry function for other functions:
function divide(a, b, c) {
return a / b / c;
}

const curriedDivide = curry(divide);


console.log(curriedDivide(12)(2)(3)); // Outputs: 2

Simplifying the Curried Function


We can simplify the currying logic using arrow functions with implicit returns:

function curry(func) {
return (a) => (b) => (c) => func(a, b, c);
}

Why Use Currying?


While currying is not always necessary, it offers some key benefits:

1. Partial Application: Currying allows you to create a function with some arguments "pre-filled"
for later use.

Example:

const curriedMultiply = curry(multiply);


const double = curriedMultiply(2);

console.log(double(5)(2)); // Outputs: 20

Here, double is a partially applied version of curriedMultiply with the first argument fixed as 2 .

2. Reusability: Currying helps in creating flexible and reusable functions, especially when dealing
with a sequence of operations.
Conclusion
Currying transforms a function so that it takes arguments one by one.
It leverages closures to maintain access to earlier arguments.
Currying is particularly useful for creating partial functions and simplifying complex operations.

Though not needed in every scenario, currying provides flexibility in how functions are used and
structured, making your code more modular and reusable.

Questions and Answers: Currying in JavaScript

220) What is currying in JavaScript, and how does it transform the way
functions handle their parameters?
Currying in JavaScript is a technique that transforms a function so that it takes its parameters one at
a time, instead of all at once.

For example, a regular function might take three arguments like func(a, b, c) .

After currying, this function becomes func(a)(b)(c) , where each call returns a new function that
takes the next argument.

This transformation allows you to "save" some arguments and pass the rest later, making functions
more flexible and reusable.

221) How does currying make use of closures in JavaScript to handle


multiple function calls?
Currying uses closures to handle multiple function calls by keeping track of the arguments passed in
earlier calls.

When you call a curried function, it returns a new function for the next argument, and each of these
returned functions "remembers" the previous arguments through closures.

This means the function can "hold onto" values until all the arguments are provided, and then it
processes them together.

Closures allow each function in the chain to access the variables from its surrounding context, which
is how currying works in JavaScript.
222) In what scenarios might currying be useful when designing functions in
JavaScript?
Currying can be useful in scenarios where you want to create more flexible and reusable functions.
Some examples include:

1. Partial application: You can "preset" some arguments in a curried function and reuse it later
with different values for the remaining arguments.
For example, you could create a function that always adds a fixed number to another number.
2. Configuration: When you need to configure a function with some values upfront and apply the
rest of the values later.
This is helpful in settings like event handling or API calls where certain parameters stay constant.
3. Function composition: Currying makes it easier to break complex functions into smaller, simpler
pieces that can be combined or reused in different ways.

By splitting function calls, currying provides more control and flexibility over how and when
arguments are applied.

223) What is the benefit of using a generic curry function, and how does it
improve function flexibility?
A generic curry function allows you to take any function and transform it into a curried version.

This means the function can accept its arguments one at a time, making it more flexible.

Benefits:

Partial application: You can pass some arguments now and save the rest for later.
Reusability: The curried function can be reused in different contexts with different sets of
arguments without rewriting the original function.
Modularity: Breaking down complex function calls into simpler steps makes your code easier to
understand and maintain.

A generic curry function enhances flexibility by enabling more dynamic and customizable function
calls.
Coding Questions
What These Problems Test:
1. Problem #30:
Understanding of currying and closures.
Ability to write a generic curry function that works for any multi-parameter function.
2. Problem #31:
Practical application of currying to achieve partial application.
Ability to use curried functions to simplify repetitive tasks.

Problem #30: Implement a Generic Curry Function


Create a generic curry function that transforms any given multi-parameter function into its curried
form. Test your implementation with two different functions:

1. sum(a, b, c): Returns the sum of three numbers.


2. multiply(a, b, c): Returns the product of three numbers.

Requirements:

Implement the curry function such that it accepts a function as an argument and returns its
curried version.
Ensure that the curried function takes one argument at a time.

Example Usage:

function sum(a, b, c) {
return a + b + c;
}

function multiply(a, b, c) {
return a * b * c;
}

const curriedSum = curry(sum);


console.log(curriedSum(1)(2)(3)); // Outputs: 6

const curriedMultiply = curry(multiply);


console.log(curriedMultiply(2)(3)(4)); // Outputs: 24
Problem #31: Create a Partially Applied Function Using
Currying
Using a curried version of the calculatePrice function, create a partially applied function for
applying discounts to products.

1. Original Function:
The calculatePrice(basePrice, taxRate, discount) function calculates the final price of a
product.
basePrice : Initial price of the product.
taxRate : Tax rate as a percentage (e.g., 0.1 for 10%).
discount : Discount amount to be subtracted.
2. Curried Function:
Transform the function into a curried version so that you can partially apply a fixed tax rate and
reuse the resulting function to calculate prices with different discounts.

Requirements:

Implement the curried version of calculatePrice .


Create a partially applied function for a tax rate of 10% (0.1).

Example Usage:

function calculatePrice(basePrice, taxRate, discount) {


return basePrice + basePrice * taxRate - discount;
}

const curriedCalculatePrice = curry(calculatePrice);

// Create a partially applied function with a tax rate of 10%


const calculateWithTax = curriedCalculatePrice(0.1);

console.log(calculateWithTax(100)(5)); // Outputs: 105 (100 + 10 - 5)


console.log(calculateWithTax(200)(20)); // Outputs: 200 (200 + 20 - 20)
Video Lesson Script: Generators in JavaScript

Understanding Generators in JavaScript


Generators are a special type of function in JavaScript that allow you to create iterable objects.

They are defined using the function* syntax and use the yield keyword to pause and resume
execution.

How Generators Work


A generator function allows you to yield values one by one.

Each time you call next() on the generator object, it resumes execution until the next yield
statement or the end of the function.

The generator object provides three methods:

1. next(value)
Resumes the generator function and returns an object with two properties:
value : The value yielded by the generator.
done : A boolean indicating whether the generator has finished.
2. return(value)
Stops the generator, returning the passed value and marking done as true .
3. throw(error)
Throws an error inside the generator, halting its execution unless the error is caught within the
generator function.
Basic Generator Example

function* generateColors() {
yield "red";
yield "green";
yield "blue";
}

const colorGenerator = generateColors();

console.log(colorGenerator.next()); // { value: "red", done: false }


console.log(colorGenerator.next()); // { value: "green", done: false }
console.log(colorGenerator.next()); // { value: "blue", done: false }
console.log(colorGenerator.next()); // { value: undefined, done: true }

Each next() call returns the next value and indicates whether the generator is done.

Passing Values into Generators


You can pass values back into a generator through the next() method:

function* processInput() {
const input = yield "Enter a value";
yield `You entered: ${input}`;
}

const inputGenerator = processInput();

console.log(inputGenerator.next()); // { value: "Enter a value", done: false }


console.log(inputGenerator.next("Hello")); // { value: "You entered: Hello", done: fals

Here, the value "Hello" is passed into the generator and used in the next yield statement.

Using return and throw in Generators


return() : Ends the generator early and provides a final value.
console.log(inputGenerator.return("Goodbye")); // { value: "Goodbye", done: true }

throw() : Throws an error inside the generator.

function* errorGenerator() {
try {
yield 1;
yield 2;
} catch (error) {
console.log("Caught error:", error.message);
}
}

const gen = errorGenerator();


gen.next();
gen.throw(new Error("Something went wrong")); // Logs: "Caught error: Something went wr

This stops the generator unless the error is caught with a try-catch block inside the generator
function.

Delegating Generators with yield*


Generators can delegate control to other generators using the yield* syntax:
function* fruits() {
yield "apple";
yield "banana";
}

function* vegetables() {
yield "carrot";
yield "spinach";
}

function* food() {
yield* fruits();
yield "and";
yield* vegetables();
}

const foodGenerator = food();


console.log(foodGenerator.next()); // { value: "apple", done: false }
console.log(foodGenerator.next()); // { value: "banana", done: false }
console.log(foodGenerator.next()); // { value: "and", done: false }
console.log(foodGenerator.next()); // { value: "carrot", done: false }
console.log(foodGenerator.next()); // { value: "spinach", done: false }

This allows you to combine multiple generators into one.

Iterating Over Generators


You can use a for...of loop to iterate over all the values yielded by a generator:

for (let item of food()) {


console.log(item);
}

Conclusion
Generators are a useful feature in JavaScript for creating iterable objects.
Although not used often in everyday programming, they are valuable for situations where you need to
control the flow of execution or manage complex iterations.

It's important to understand how generators work, especially since they may come up in technical
interviews.

Questions and Answers: Generators in JavaScript

224) What is a generator in JavaScript, and how is it different from a regular


function?
A generator in JavaScript is a special type of function that can pause its execution and resume later.

It's different from a regular function because, instead of running from start to finish in one go, a
generator can yield values one at a time and pick up where it left off when you call its next()
method.

Regular functions run completely once called, while generators can stop, return intermediate results,
and continue running later.

225) How does the yield keyword work in a generator, and what role does
it play in pausing and resuming function execution?
The yield keyword in a generator pauses the function's execution and returns a value.

When the generator is called again with next() , it resumes from where it left off after the last
yield .

This allows the generator to pause and resume multiple times, returning different values each time.

226) What is the purpose of the next() , return() , and throw() methods
in generator objects, and how do they control the generator's behavior?
The methods next() , return() , and throw() in generator objects control the generator's
behavior:
next() : Resumes the generator's execution from where it last paused and returns the next
value. It also indicates if the generator is done.
return() : Ends the generator early and returns a specified value, marking the generator as
done.
throw() : Throws an error inside the generator, halting its execution unless the error is caught
within the generator.

These methods let you manage how the generator runs and when it stops.

227) In what scenarios might you use the yield* syntax in a generator, and
how does it allow delegation to other generators?
You use the yield* syntax in a generator when you want to delegate control to another generator or
iterable.

This means the generator temporarily hands over execution to another generator, yielding all of its
values before continuing with its own code.

This is useful when you want to combine multiple generators or iterables into one seamless sequence
without manually managing each one.

Coding Questions
What These Problems Test:
1. Problem #32:
Understanding of basic generator syntax.
Ability to use yield to produce values dynamically in a sequence.
2. Problem #33:
Mastery of generator delegation with yield* .
Combining multiple generators into a single iterable sequence.
Problem #32: Create a Generator for a Fibonacci Sequence
Write a generator function called fibonacciGenerator that generates the Fibonacci sequence
indefinitely. The Fibonacci sequence starts with 0 and 1, and each subsequent number is the sum of
the two preceding ones.

Requirements:

Use a generator function with the yield keyword.


Allow the generator to produce as many Fibonacci numbers as needed by calling next()
repeatedly.

Example Usage:

const fibonacci = fibonacciGenerator();

console.log(fibonacci.next().value); // 0
console.log(fibonacci.next().value); // 1
console.log(fibonacci.next().value); // 1
console.log(fibonacci.next().value); // 2
console.log(fibonacci.next().value); // 3
console.log(fibonacci.next().value); // 5

Problem #33: Delegating Generators with yield*


Create two generator functions, evenNumbers and oddNumbers , which yield even and odd numbers
respectively up to a given limit. Then, create a third generator function called mixedNumbers that
uses yield* to combine the values from both evenNumbers and oddNumbers .

Requirements:

1. evenNumbers(limit) : Yields even numbers starting from 0 up to the given limit.


2. oddNumbers(limit) : Yields odd numbers starting from 1 up to the given limit.
3. mixedNumbers(limit) : Combines the output of evenNumbers and oddNumbers using yield* .

Example Usage:
function* evenNumbers(limit) {
for (let i = 0; i <= limit; i += 2) {
yield i;
}
}

function* oddNumbers(limit) {
for (let i = 1; i <= limit; i += 2) {
yield i;
}
}

function* mixedNumbers(limit) {
yield* evenNumbers(limit);
yield* oddNumbers(limit);
}

const mixed = mixedNumbers(5);

console.log(mixed.next().value); // 0 (even)
console.log(mixed.next().value); // 2 (even)
console.log(mixed.next().value); // 4 (even)
console.log(mixed.next().value); // 1 (odd)
console.log(mixed.next().value); // 3 (odd)
console.log(mixed.next().value); // 5 (odd)
Updated Script: Modules in JavaScript

Understanding Modules in JavaScript

Modules in JavaScript are isolated code files that help you organize and manage code without
polluting the global namespace. They allow you to export and import specific parts of your code
between files, providing better modularity and reusability.

Features of Modern Modules (type="module")


When you use type="module" in a <script> tag, the following effects occur:

1. Scoped to the File:


Variables and functions declared at the top level are scoped to the file, not globally.
2. Strict Mode by Default:
Modules run in strict mode automatically, so you don’t need to add "use strict" .
3. Top-Level await :
You can use the await keyword at the top level without wrapping it in an async function.
4. Deferred Execution:
Modules are automatically deferred, meaning they wait for the HTML to fully parse before
running.

Exporting and Importing


Modules use the export and import keywords to share and access code between files.

Exporting
1. Named Exports:
Export multiple items from a file using their names:
export const pi = 3.14;
export function multiply(a, b) {
return a * b;
}

2. Default Exports:
Export a single item as the default:

export default class MathUtils {


square(num) {
return num * num;
}
}

Importing
1. Named Imports:
Import specific items by their names:

import { pi, multiply } from "./utils.js";

2. Default Imports:
Import the default export with any name:

import MathUtils from "./utils.js";

3. Dynamic Imports:
Load modules conditionally or on-demand:

if (isMathRequired) {
const module = await import("./utils.js");
console.log(module.multiply(5, 6));
}
Example: Importing and Exporting
utils.js

export const pi = 3.14;

export function multiply(a, b) {


return a * b;
}

export default class MathUtils {


square(num) {
return num * num;
}
}

app.js

import MathUtils, { pi, multiply } from "./utils.js";

console.log(pi); // 3.14
console.log(multiply(3, 4)); // 12

const math = new MathUtils();


console.log(math.square(5)); // 25

Dynamic Imports
Load modules only when necessary to improve performance:

async function loadMathUtils() {


const { multiply } = await import("./utils.js");
console.log(multiply(7, 8));
}

loadMathUtils();
Immediately Invoked Function Expressions (IIFE)
Before modules, Immediately Invoked Function Expressions (IIFE) were used to isolate code:

(function () {
console.log("IIFE Example: Code is isolated");
})();

This creates a local scope, but modules provide a cleaner, modern alternative.

Fallback for Older Browsers


To support browsers that don’t recognize modules, use the nomodule attribute:

<script src="legacy.js" nomodule></script>

Browsers that don’t support modules will load the nomodule script, while modern browsers will
ignore it.

Summary
Modules help organize code and prevent global namespace pollution.
Use export and import to share and access code between files.
Features like automatic strict mode, top-level await , and deferred execution improve efficiency.
Modules are the modern replacement for IIFE, offering cleaner and more maintainable code.
Use nomodule for compatibility with older browsers.

Modules bring modularity, reusability, and better structure to JavaScript development.

Questions and Answers: Modules in JavaScript


228) What are the main benefits of using JavaScript modules compared to
traditional scripts in managing code?
The main benefits of using JavaScript modules are:

Prevents global namespace pollution: Modules keep variables and functions scoped to their
own file, avoiding conflicts with other code.
Better organization: Modules allow you to split code into smaller, reusable pieces, making it
easier to manage.
Explicit imports and exports: You can control what functions, variables, or classes are shared
between files, reducing accidental access to unnecessary code.
Automatic strict mode: Modules automatically run in strict mode, making your code safer by
catching common mistakes.

229) How does using type="module" in a script tag help prevent global
namespace pollution in JavaScript?
Using type="module" in a script tag helps prevent global namespace pollution by:

Automatically scoping variables and functions to the file they are defined in.
Ensuring that they are not accessible globally, reducing the chances of naming conflicts and
keeping your code isolated and organized.
Only allowing explicitly exported functions or variables to be available for import in other files.

230) What is the difference between named exports and default exports in
JavaScript modules, and when would you use each?
The difference between named exports and default exports in JavaScript is:

Named Exports:
Allow you to export multiple items from a file.
Items must be imported using their exact names.
Example:
export const maxLimit = 100;
export function calculate(a, b) {
return a + b;
}
import { maxLimit, calculate } from "./helpers.js";

Default Exports:
Allow you to export a single main item.
The item can be imported with any name.
Example:

export default class Calculator {


constructor() {
this.total = 0;
}
}
import Calculator from "./helpers.js";

Use Cases:

Use named exports when you need to export multiple things from a file.
Use a default export when you want to export one primary item.

231) What are dynamic imports in JavaScript, and why might they be useful
for optimizing application performance?
Dynamic imports in JavaScript allow you to load modules only when they are needed, rather than at
the start of the program.

Benefits:
Reduce initial load time by loading code only when required.
Optimize application performance, especially in large applications.
Enable conditional or on-demand loading for specific features.

Example:
if (userWantsFeature) {
const { featureFunction } = await import("./featureModule.js");
featureFunction();
}

Dynamic imports are particularly useful for improving efficiency in cases like:

Loading code for a feature only when the user interacts with it.
Reducing the size of the initial JavaScript bundle.

Coding Questions
What These Problems Test:
1. Problem #34:
Understanding the difference between named and default exports.
Ability to correctly import and use multiple types of exports.
2. Problem #35:
Ability to implement and use dynamic imports.
Understanding of conditional module loading to optimize performance and reduce
unnecessary code execution.

Problem #34: Implement and Use Named and Default Exports


Create two JavaScript files:

1. mathOperations.js :
Export a default function called add that takes two numbers and returns their sum.
Export a named function called multiply that takes two numbers and returns their
product.
2. main.js :
Import both the default and named exports from mathOperations.js .
Use both functions to calculate and log the results of:
Adding 10 and 5.
Multiplying 10 and 5.
Requirements:

Ensure proper use of named and default exports.


Show how to handle multiple imports in main.js .

Expected Results:

Running main.js should log:

Addition result: 15
Multiplication result: 50

Problem #35: Use Dynamic Imports for Conditional Module


Loading
Write a program that conditionally imports a module based on user input.

1. mathHelper.js :
Export a function called calculateArea that calculates the area of a rectangle:

export function calculateArea(length, width) {


return length * width;
}

2. main.js :
Prompt the user to decide if they want to calculate the area of a rectangle.
If the user confirms, dynamically import mathHelper.js and call calculateArea with
hardcoded values (e.g., length = 5 , width = 10 ).
Log the result to the console.

Requirements:

Use await import() for dynamic module loading.


Show the result only if the user agrees to perform the calculation.

Example Usage:

When the user confirms, the program logs:


The area of the rectangle is: 50

If the user declines, no import or calculation occurs.


Script for Video #20: The Event Loop

JavaScript Engine
A JavaScript Engine is a program used to execute JavaScript code. Each engine has two primary
components:

Heap:
Used for memory allocation to store objects.
Acts as a largely unstructured data store.
Call Stack:
A stack data structure that tracks the currently executing function.
Functions are pushed onto the stack when called and popped off when they finish.
If the stack is empty, no code is currently running.

JavaScript Runtime Environment


JavaScript runs within the Runtime Environment, which provides access to Web APIs like:

setTimeout()
fetch()
DOM manipulation methods

These APIs extend JavaScript's functionality, enabling interaction with the web and asynchronous
tasks.

Event Loop
The Event Loop is JavaScript's concurrency model, responsible for managing asynchronous tasks
and ensuring the single-threaded environment remains non-blocking. It operates as follows:

1. Task Queue:
A queue (FIFO) for asynchronous callbacks like timers or HTTP requests.
Also called the Message Queue or Callback Queue.
2. Microtask Queue:
A high-priority queue for callbacks from:
promise.then() , catch() , finally()
async/await
queueMicrotask()
The Event Loop processes all microtasks before moving to tasks in the Task Queue.
3. Event Loop Algorithm:
Dequeue one task from the Task Queue.
Execute the task until the Call Stack is empty.
Execute all microtasks from the Microtask Queue.
Render DOM changes.
Repeat the process.

Example: Asynchronous Behavior

setTimeout(() => console.log("Deferred Task"), 0);


queueMicrotask(() => console.log("Microtask Executed"));
console.log("Synchronous Execution");

Output:

Synchronous Execution
Microtask Executed
Deferred Task

Explanation:

1. Synchronous Execution runs first.


2. The microtask (from queueMicrotask ) runs before the task (from setTimeout ).
3. The task runs last.

Chunking
Chunking prevents long-running tasks from blocking the Event Loop. By splitting tasks into smaller
chunks using setTimeout() , you allow other tasks and microtasks to execute in between.
Example:

function processDataInChunks(data) {
let index = 0;
function handleChunk() {
const chunkSize = 50;
while (index < data.length && chunkSize--) {
console.log(data[index++]);
}
if (index < data.length) {
setTimeout(handleChunk, 0);
}
}
handleChunk();
}

This ensures the UI remains responsive while processing large datasets.

Why the Event Loop Matters


1. Timers are not exact:
setTimeout() waits for the call stack to clear and the task queue to process, leading to
slight delays.
Use Date.now() or performance.now() for precise timing.
2. Promise callbacks:
Can be delayed if the Microtask Queue or Call Stack is busy.
3. Avoiding slow tasks:
Slow tasks block the Event Loop, delaying rendering, timers, and other tasks.
Use chunking or Web Workers for better performance.

Summary
The Event Loop explains how JavaScript handles asynchronous tasks in a single-threaded
environment. By understanding its mechanics, you can:

Write efficient, responsive JavaScript code.


Avoid blocking the Event Loop with slow tasks.
Use techniques like chunking to manage large computations.

This knowledge ensures your web applications perform smoothly, providing a better user experience.

Questions and Answers: The Event Loop

232) What are the two primary components of the JavaScript engine, and
what are their functions?
The two primary components of the JavaScript engine are:

Heap:
A memory allocation area used to store objects and data.
It’s an unstructured data store where memory is allocated as needed by your code.
Call Stack:
A stack data structure that keeps track of function calls.
Each time a function is called, a stack frame is pushed onto the stack. When the function
finishes, the frame is popped off.
The call stack helps the engine know which function is currently being executed.

233) What is the event loop?


The Event Loop is a mechanism in JavaScript that manages the execution of asynchronous tasks.

It allows JavaScript, which runs on a single thread, to perform non-blocking operations.


The Event Loop continuously checks the Task Queue and executes tasks when the Call Stack is
empty.
It processes tasks, handles microtasks, and ensures the browser updates the user interface
efficiently, enabling JavaScript to handle multiple operations seemingly at the same time.

234) How does the event loop handle tasks and microtasks differently in
JavaScript?
The Event Loop handles tasks and microtasks differently:

Tasks:
Placed in the Task Queue.
Include events like setTimeout() callbacks or user interactions.
Processed one at a time, only after the Call Stack is empty.
Microtasks:
Placed in the Microtask Queue.
Include promise callbacks ( then() , catch() , finally() ) and queueMicrotask()
functions.
Have higher priority than tasks.
The Event Loop processes all microtasks before moving to the next task in the Task Queue.

This prioritization ensures microtasks are handled immediately after the current code execution,
making them more responsive.

235) What is chunking, and why is it important for maintaining a responsive


user interface in JavaScript?
Chunking is a technique used in JavaScript to break up large, time-consuming tasks into smaller
pieces.

How it works:
By using setTimeout() or similar methods, each chunk of the task is processed separately.
This allows the Event Loop to handle other tasks and update the user interface in between
chunks.
Why it matters:
Prevents the main thread from being blocked by long-running tasks.
Ensures that the UI remains responsive, allowing users to interact with the page smoothly.

Chunking helps maintain a seamless user experience, especially in scenarios involving


computationally heavy operations.

Coding Questions
What These Problems Test:
1. Problem #36:
Understanding of the Event Loop, Task Queue, and Microtask Queue.
Ability to analyze and explain the order of execution in asynchronous JavaScript.
2. Problem #37:
Practical application of chunking to manage long-running tasks.
Ensuring responsiveness while handling large datasets.

Problem #36: Analyze the Output of Asynchronous Code


Given the following JavaScript code, determine the order of the console output and explain why it
occurs:

console.log("Start");

setTimeout(() => {
console.log("Timeout 1");
}, 0);

Promise.resolve()
.then(() => console.log("Microtask 1"))
.then(() => console.log("Microtask 2"));

console.log("End");

Requirements:

1. Write the correct output order.


2. Explain why the Event Loop handles the tasks in this order, focusing on the differences between
microtasks and tasks.

Expected Output:

Start
End
Microtask 1
Microtask 2
Timeout 1
Problem #37: Implement Chunking for Large Dataset
Processing
Write a function called processLargeDataset that takes an array of numbers and processes them in
chunks of 100 items at a time. The function should log each number to the console. Use
setTimeout() to ensure that the UI remains responsive during processing.

function processLargeDataset(data) {
// Implement the chunking logic here
}

// Example usage:
const largeArray = Array.from({ length: 1000 }, (_, i) => i + 1);
processLargeDataset(largeArray);

Requirements:

1. Process the dataset in chunks of 100 numbers.


2. Allow the Event Loop to process other tasks between chunks.
3. Ensure that all numbers in the dataset are logged in the correct order.

Expected Behavior:

The numbers 1 to 1000 are logged in chunks of 100, with a small delay between each chunk.
The UI should remain responsive during processing.
Example: Using a WebWorker for a Slow Calculation
Purpose: WebWorkers are essential for handling heavy computations without slowing down your
webpage. In JavaScript, long-running tasks block the main thread, causing the user interface to
become unresponsive. For example, performing large calculations or processing data in real-time can
freeze your web app, frustrating users. WebWorkers solve this by running code in a separate thread,
ensuring that your UI remains smooth and interactive.

Benefits:

The main thread stays responsive.


Heavy operations run seamlessly in the background.

Key WebWorker Concepts


No DOM Manipulation: Workers can’t interact with the DOM.
Communication: Use postMessage to send data between the main thread and workers.
Listening for Messages:
Main thread: worker.addEventListener('message', callback) .
Worker: self.addEventListener('message', callback) .

HTML Setup:

<button id="compute">Compute Factorial</button>


<button id="greet">Greet</button>

Main JavaScript (main.js):


// Select buttons
const computeButton = document.getElementById("compute");
const greetButton = document.getElementById("greet");

// Create Worker
const worker = new Worker("worker.js");

// Factorial computation using WebWorker


computeButton.addEventListener("click", () => {
worker.postMessage({ type: "compute", number: 10 });
});

// Greet Message
greetButton.addEventListener("click", () => {
console.log("Hello, User!");
});

// Receive message from Worker


worker.addEventListener("message", (event) => {
console.log(`Result from Worker: ${event.data}`);
});

WebWorker File (worker.js):

self.addEventListener("message", (event) => {


if (event.data.type === "compute") {
const num = event.data.number;
const result = factorial(num);
postMessage(`Factorial of ${num} is ${result}`);
}
});

function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

Explanation
main.js: Sets up the buttons and a WebWorker. The factorial calculation runs in worker.js ,
keeping the main thread responsive.
worker.js: Computes the factorial of a number and sends the result back to the main script using
postMessage .

This keeps the main page interactive even during intensive tasks!

Purpose: WebWorkers run code in a separate thread, preventing the main thread from being blocked
by long-running operations.

Benefits:

The main thread stays responsive.


Heavy operations run seamlessly in the background.

General Questions about Web Workers

1) What is the primary purpose of Web Workers in web development, and


how do they improve performance?
Web Workers allow JavaScript code to run in a separate thread from the main execution thread.

This helps prevent the main thread from becoming blocked by long-running operations, ensuring that
the user interface remains responsive while the worker performs computationally intensive tasks in
the background.

2) How do Web Workers communicate with the main thread, and what
methods or events are used for this communication?
Web Workers communicate with the main thread using the postMessage(message) method to send
messages and the onmessage event to receive them.

This enables bidirectional communication between the worker and the main script without interfering
with the main thread's execution.
3) What are the limitations of Web Workers, and why can’t they perform
DOM manipulation directly?
Web Workers cannot access or manipulate the DOM directly because they run in a separate thread
with their own global context (the WorkerGlobalScope ).

This isolation ensures that changes to the DOM remain thread-safe and prevents potential conflicts or
race conditions that could occur if multiple threads tried to modify the DOM simultaneously.

4) What is the difference between a Dedicated Worker and a SharedWorker,


and when might you choose one over the other?
Dedicated Workers: These are tied to a single script and communicate exclusively with the
script that created them. They are best for tasks specific to a single instance, like handling user
interactions in a single tab.
SharedWorkers: These can be shared across multiple scripts, tabs, or iframes, allowing
communication between different contexts. They are useful for tasks that need to coordinate
data across multiple browser contexts, such as maintaining a shared state between tabs.

However, SharedWorkers have limited browser support, so their use may depend on project
requirements and compatibility considerations.

Coding Questions
What These Problems Test:
Problem #39: Tests understanding of creating and using a WebWorker for heavy computations,
handling message passing, and ensuring a responsive UI.
Problem #40: Expands on the use of WebWorkers by applying them to real-world data
processing tasks, focusing on efficient handling of large datasets.

Problem #39: Optimize a Long-Running Calculation with a


WebWorker
Your task is to modify the following JavaScript code to use a WebWorker for performing a slow
calculation. The goal is to ensure that the UI remains responsive while the calculation is performed in
the background.
Current Code (Blocking the Main Thread):

document.getElementById("start").addEventListener("click", () => {
let sum = 0;
for (let i = 0; i < 1e9; i++) {
sum += i;
}
console.log(`Sum: ${sum}`);
});

document.getElementById("hello").addEventListener("click", () => {
console.log("Hello, World!");
});

HTML Setup:

<button id="start">Start Calculation</button>


<button id="hello">Say Hello</button>

Task:

1. Move the calculation logic to a WebWorker.


2. Use postMessage to send the result back to the main thread.
3. Update the main script to receive and log the result from the worker.
4. Ensure the "Say Hello" button remains responsive during the calculation.

Problem #40: Use a WebWorker to Filter Large Data Efficiently


You are given a large array of numbers, and you need to filter out all the even numbers using a
WebWorker. The filtering process should not block the main thread.
Current Code (Blocking the Main Thread):

const data = Array.from({ length: 1e7 }, () => Math.floor(Math.random() * 100));

document.getElementById("filter").addEventListener("click", () => {
const evenNumbers = data.filter((num) => num % 2 === 0);
console.log(`Filtered ${evenNumbers.length} even numbers`);
});

document.getElementById("hello").addEventListener("click", () => {
console.log("Hello, World!");
});

HTML Setup:

<button id="filter">Filter Even Numbers</button>


<button id="hello">Say Hello</button>

Task:

1. Move the filtering logic to a WebWorker.


2. Use postMessage to send the filtered result back to the main thread.
3. Update the main script to receive and log the result from the worker.
4. Ensure the "Say Hello" button remains responsive during the filtering process.
Introduction to Browser Storage
In this video, we’ll explore browser storage—the various methods we can use to store data in a
user’s browser. Understanding these options is crucial for creating better user experiences, like
keeping users logged in or maintaining a shopping cart even when they return the next day.

We’ll cover different storage types, explain their use cases, and see how they operate in practice. By
the end, you’ll know when and how to use each method, as well as their limitations.

Browser Storage Definitions and Scenarios


Cookies: Cookies are the simplest form of browser storage, consisting of key-value string pairs. They
are often set by the server for tasks like storing a logged-in user’s data. For example, a website might
use cookies to keep you signed in. Cookies can also be created in JavaScript using
document.cookie , but their API is somewhat outdated and more complex to work with.

Local Storage: Part of the Web Storage API, local storage is a modern, straightforward way to store
data in the browser with no expiration date. It’s useful for saving settings or preferences, like a
theme choice or a saved game state, that should persist even after the browser is closed. You can
add data to local storage with localStorage.setItem('key', 'value') and retrieve it using
localStorage.getItem('key') .

Session Storage: Also part of the Web Storage API, session storage works similarly to local storage
but expires at the end of a session. This is useful for temporary data, like form inputs or session-
specific settings. Data can be added using sessionStorage.setItem('key', 'value') and
retrieved with sessionStorage.getItem('key') .

IndexedDB: IndexedDB is a more advanced browser API for storing large amounts of structured
data. It’s similar to a database, using object stores (like tables) where each object must have a unique
key. This is ideal for offline applications or situations where complex data needs to be stored, such as
entire files or JSON objects.
Examples

1. Cookies
Definition: Cookies store small pieces of data as string key-value pairs. They're commonly used for
session management, such as keeping users logged in.

Code Example:

// Setting a cookie
document.cookie =
"userToken=abc123; expires=Fri, 31 Dec 2025 23:59:59 GMT; path=/";

// Reading cookies
console.log(document.cookie);

// Deleting a cookie
document.cookie =
"userToken=abc123; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";

Explanation: The document.cookie API sets and retrieves cookies. The expires attribute defines
when the cookie will be deleted. Cookies are automatically sent with every HTTP request to the
server, making them useful for session persistence.

2. Local Storage
Definition: Local storage is part of the Web Storage API, providing a way to store data in the browser
with no expiration date.

Code Example:
// Adding data to local storage
localStorage.setItem(
"userPreferences",
JSON.stringify({ theme: "dark", language: "en" })
);

// Retrieving data from local storage


const preferences = JSON.parse(localStorage.getItem("userPreferences"));
console.log(preferences); // Outputs: { theme: "dark", language: "en" }

// Removing an item from local storage


localStorage.removeItem("userPreferences");

// Clearing all data from local storage


localStorage.clear();

Explanation: Local storage is simple and synchronous. Data persists even if the browser is closed
and reopened. It’s useful for settings or information you want to store indefinitely.

3. Session Storage
Definition: Session storage, also part of the Web Storage API, stores data for the duration of the
page session (until the browser/tab is closed).

Code Example:

// Adding data to session storage


sessionStorage.setItem("cart", JSON.stringify(["item1", "item2"]));

// Retrieving data from session storage


const cart = JSON.parse(sessionStorage.getItem("cart"));
console.log(cart); // Outputs: ["item1", "item2"]

// Removing an item from session storage


sessionStorage.removeItem("cart");

// Clearing all data from session storage


sessionStorage.clear();
Explanation: Session storage is used for data that should only be available as long as the browser
tab is open, such as temporary state or user input.

4. IndexedDB
Definition: IndexedDB is a low-level API for storing significant amounts of structured data, such as
entire files or complex objects.

Code Example:

// Opening a database
const dbRequest = indexedDB.open("AppDatabase", 1);

dbRequest.onupgradeneeded = (event) => {


const db = event.target.result;
db.createObjectStore("products", { keyPath: "id" });
};

dbRequest.onsuccess = (event) => {


const db = event.target.result;
const transaction = db.transaction("products", "readwrite");
const store = transaction.objectStore("products");

// Adding data
store.add({ id: 1, name: "Laptop", price: 1500 });

// Retrieving data
const getRequest = store.get(1);
getRequest.onsuccess = () => {
console.log(getRequest.result); // Outputs: { id: 1, name: "Laptop", price: 150
};
};

Explanation: IndexedDB is asynchronous and provides more robust data storage, making it suitable
for complex applications, such as offline data storage in web apps. It supports transactions, making
data handling reliable.

General Questions About Browser Storage


1. What are the main differences between cookies, local storage, and
session storage in terms of use cases and persistence?
Cookies, local storage, and session storage differ in their use cases and how long data persists:

Cookies: Designed for small data storage, often used for session management. They persist
based on the expiration date set or until manually deleted.
Local Storage: Stores data indefinitely (unless explicitly cleared) and is ideal for long-term
storage of user preferences or settings.
Session Storage: Stores data only for the duration of the browser session (tab open) and is best
for temporary, session-specific data.

2. Why is IndexedDB better suited for storing large amounts of structured


data compared to other browser storage options?
IndexedDB is better suited for large, structured data because:

It functions like a database, allowing for complex data storage in object stores with unique keys.
It supports transactions, ensuring reliable and consistent data manipulation.
Unlike cookies or local/session storage, IndexedDB can handle entire files or complex JSON
objects, making it ideal for offline web applications or large datasets.

3. In what scenarios would you use session storage instead of local storage
or cookies for storing user data?
Session storage is ideal for scenarios where data is only needed temporarily, such as:

Keeping form inputs while navigating within the same session.


Storing session-specific settings or preferences that do not need to persist after the tab is
closed.
Temporary caching of user interactions or state within a single session.

4. What are some potential security considerations when using browser


storage options like cookies, local storage, and IndexedDB?
Security considerations for browser storage include:
Cookies: Vulnerable to cross-site scripting (XSS) attacks if not properly secured. Use the
HttpOnly and Secure flags to mitigate risks.
Local/Session Storage: Accessible via JavaScript, making them susceptible to XSS attacks.
Avoid storing sensitive information like user passwords.
IndexedDB: Generally secure but could be accessed by malicious scripts if your site has
vulnerabilities. Use HTTPS and proper input validation to protect against data breaches.

Coding Questions
What These Problems Test:
1. Problem #40:
Understanding of security risks associated with browser storage.
Ability to analyze code and propose practical, secure solutions.
2. Problem #41:
Practical skills in using local storage to create a functional application.
Handling dynamic updates in a web application with JavaScript.

Problem #38: Identify and Resolve Security Risks in Browser


Storage
You are tasked with improving the security of a web application that uses browser storage. Review
the following code snippet and identify the potential security risks associated with its usage. Propose
at least two solutions to mitigate the risks.

// Storing sensitive user data in local storage


localStorage.setItem("userToken", "abc12345");

// Reading the token later for an API request


const token = localStorage.getItem("userToken");
fetch("https://api.example.com/user", {
headers: {
Authorization: `Bearer ${token}`,
},
});

Requirements:
1. Identify at least two potential security risks in the code.
2. Propose and explain two solutions to mitigate these risks.

Problem #39: Implement a Simple Note-Taking Application with


Browser Storage
Create a basic note-taking application that allows users to add, view, and delete notes. Use local
storage to persist the notes so that they remain available even after the browser is closed.

Requirements:

1. Implement the following features:


Add a new note.
View all saved notes.
Delete a specific note by its index.
2. Use local storage to store and retrieve notes.
3. Ensure the application is responsive and updates the displayed notes whenever a note is added
or deleted.

Starter Code:

// HTML Structure
/*
<div id="app">
<textarea id="noteInput"></textarea>
<button id="addNote">Add Note</button>
<ul id="notesList"></ul>
</div>
*/

document.getElementById("addNote").addEventListener("click", () => {
const note = document.getElementById("noteInput").value;
// Add logic to save the note to local storage
});

// Add logic to load notes from local storage and display them in the <ul id="notesList

Expected Behavior:
Notes persist between browser sessions using local storage.
Adding or deleting a note updates the displayed notes list in real time.
Built-in Data Structures in JavaScript
Map: A built-in JavaScript class for holding key-value pairs. Maps differ from standard objects in
several ways:

Map keys can be of any type, while object keys must be strings or symbols.
Maps maintain the insertion order, which is useful for iteration.
Maps are difficult to convert to JSON, unlike objects.
Objects use the prototype chain for inheritance, which Maps do not support.

Set: A built-in JavaScript class for holding unique values of any type. Values in Sets are considered
unique if they are different primitives or refer to different objects, even if the objects have the same
contents. Sets maintain insertion order for iteration.

WeakMap: Similar to the Map class but with two key differences:

WeakMaps can only have objects as keys, not primitives.


WeakMaps hold "weak" references, meaning objects can be garbage collected if no other
references exist. Because of this, WeakMaps cannot be iterated over.

WeakSet: Similar to the Set class but behaves like a WeakMap:

Only objects can be added to a WeakSet.


WeakSets hold "weak" references, allowing objects to be garbage collected if there are no other
references.

Stacks
JavaScript doesn’t have a built-in stack class, but you can use arrays. Stacks are Last-In, First-Out
(LIFO) structures. Here's an example:

const stack = [];


stack.push(1); // Add 1 to stack
stack.push(2); // Add 2 to stack
console.log(stack.pop()); // Removes and logs 2 (last added)
Queues
Queues are First-In, First-Out (FIFO) structures. Arrays can also be used for queues:

const queue = [];


queue.push(1); // Add 1 to queue
queue.push(2); // Add 2 to queue
console.log(queue.shift()); // Removes and logs 1 (first added)

Note: shift() can be less efficient compared to pop() . If performance is a concern, consider
using a linked list.

Maps
Maps are used for storing key-value pairs, and the Map class is more versatile than objects,
especially with non-string keys:

const map = new Map();


map.set("test", 123); // Add key-value pair
map.set(10, "ten"); // Keys can be any type
console.log(map.get(10)); // Logs "ten"
console.log(map.size); // Logs 2

Objects are simpler but have limitations, like requiring string or symbol keys. Use maps when non-
string keys or reliable key order is needed.

Sets
Sets store unique values. Here’s how to use them:

const set = new Set();


set.add(123); // Add a value
set.add(456);
console.log(set.has(123)); // Logs true
set.delete(123); // Removes the value
To remove duplicates from an array, you can use:

const array = [1, 2, 2, 3];


const uniqueArray = Array.from(new Set(array));
console.log(uniqueArray); // Logs [1, 2, 3]

That covers JavaScript’s built-in data structures. For more advanced structures like trees or graphs,
you’ll need to implement them using classes or explore external JavaScript libraries.

General Questions and Answers on JavaScript Data Structures

1. What is the primary difference between a Map and an Object in


JavaScript?
Maps allow keys of any data type, including objects, and maintain the insertion order of entries.
Objects require keys to be strings or symbols and rely on the prototype chain for inheritance,
which can sometimes lead to unexpected behavior.
Maps are ideal for cases where non-string keys or ordered iteration is needed, while objects are
better for simple key-value storage.

2. In what situations would you use a Set instead of an Array in JavaScript?


A Set is preferred over an Array when:

You need to store only unique values.


You want faster lookups for existing values ( has() in Sets is more efficient than includes() in
Arrays).
You need to remove duplicates from an Array, as Sets automatically filter out duplicate values.

3. What are the key differences between WeakMap and Map in JavaScript?
WeakMap keys must be objects, while Map keys can be any type (e.g., primitives, objects).
WeakMap holds "weak" references, allowing key objects to be garbage collected if there are no
other references. Map holds strong references, preventing such garbage collection.
WeakMap is not iterable, meaning you cannot loop through its keys or values, whereas Map
supports iteration.

4. Why are stacks and queues important, and how can you implement them
in JavaScript?
Stacks (LIFO - Last-In, First-Out) are used in scenarios like function call stacks, undo operations,
or navigation history. You can implement a stack using an array with push() and pop() .
Queues (FIFO - First-In, First-Out) are used for tasks like message handling, scheduling, or order
processing. You can implement a queue using an array with push() and shift() , although
using a linked list is more efficient for large-scale operations.

5. What are some practical use cases for using WeakSet in JavaScript?
WeakSet is useful when:

You need to store objects temporarily without preventing them from being garbage collected.
You want to track objects without exposing them directly or iterating over the collection.
Examples include caching DOM elements or managing event listeners where the objects may be
dynamically removed.

Coding Questions
What These Problems Test:
1. Problem #40:
Understanding of Sets and their ability to handle unique values.
Ability to work with basic array transformations.
2. Problem #41:
Familiarity with implementing a data structure from scratch.
Understanding and application of the LIFO principle.
Use of array operations to simulate stack behavior.
Problem #40: Unique Value Filter with a Set
Write a function called filterUnique that takes an array of numbers and returns a new array
containing only the unique values from the input array. Use a Set to achieve this functionality.

Requirements:

1. Use the Set class to filter out duplicate values.


2. Ensure the function maintains the original order of the unique values as they appear in the input
array.

Example:

const input = [1, 2, 2, 3, 4, 4, 5];


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

Problem #41: Implement a Stack Data Structure


Create a Stack class in JavaScript with the following methods:

1. push(value) : Adds a value to the top of the stack.


2. pop() : Removes and returns the value from the top of the stack.
3. peek() : Returns the value at the top of the stack without removing it.
4. isEmpty() : Returns true if the stack is empty, otherwise false .
5. size() : Returns the number of elements in the stack.

Requirements:

Internally use an array to manage the stack data.


Ensure the stack follows the Last-In, First-Out (LIFO) principle.

Example:
const stack = new Stack();
stack.push(10);
stack.push(20);
console.log(stack.peek()); // Output: 20
console.log(stack.pop()); // Output: 20
console.log(stack.size()); // Output: 1
console.log(stack.isEmpty()); // Output: false
stack.pop();
console.log(stack.isEmpty()); // Output: true
In this lesson, we're going to discuss JavaScript frameworks and libraries. We’ll explain the
differences between these two terms and walk through some examples.

Definitions
JavaScript Library: A library is a collection of reusable code, often provided in the form of functions,
that you can call throughout your project to simplify development. Libraries are generally
unopinionated, meaning they don’t enforce a specific structure on your project. Instead, you decide
where and how to use the library’s functions.

JavaScript Framework: A framework is also a collection of reusable code, but it provides a


structured way to build your application. Frameworks are more opinionated, meaning they enforce a
specific way of organizing and structuring your code. An important concept here is inversion of
control: with a library, you call the code when needed, but with a framework, the framework calls
your code to fill in specific parts of the structure.

Utility Libraries
Let’s start by talking about some utility libraries. These libraries offer generic functions to manipulate
objects, arrays, and other data structures.

1. Underscore: An older utility library with over 100 functions for manipulating arrays, objects,
strings, and more. It was one of the first libraries to make JavaScript easier to work with.
2. Lodash: A fork of Underscore that provides similar functionality but with a more consistent API
and improved performance. People often choose between these based on personal preference
or the needs of their application.

Both libraries can be used together, but most projects stick to one based on which has the functions
they need.

DOM Manipulation Library: jQuery


jQuery is a library that simplifies DOM manipulation, animations, event handling, and AJAX requests.
It was once the most popular JavaScript library and made working with JavaScript far more
accessible.

However, many of its features have since been incorporated into JavaScript itself or modern
frameworks, reducing jQuery’s popularity in new projects. Still, it remains in use on a significant
portion of the web, and there's nothing inherently wrong with using it today.

Declarative UI Library: React


React is a widely used library developed by Facebook. It focuses on building user interfaces using a
declarative approach, meaning you specify what the UI should look like, and React updates the
DOM as needed. React uses JSX (JavaScript XML), a syntax that allows you to write HTML-like code
within JavaScript.

Technically, React describes itself as a library, even though it has features similar to frameworks. This
is because React mainly focuses on building components and handling the virtual DOM. For a
complete framework-like experience, you usually combine React with additional tools, like React
DOM, to manage rendering in the browser.

Frameworks: Angular and Vue


Now, let’s discuss some popular frameworks that provide more structure than libraries like React.

1. Angular: A comprehensive framework developed by Google. It uses HTML templates for its
components and comes with a suite of developer tools. Angular uses TypeScript by default and
enforces a more structured way of building applications. It has a steep learning curve compared
to other frameworks but is highly powerful once mastered.
2. Vue: A progressive framework that can be adopted incrementally. It uses HTML templates by
default but also supports JSX. Vue has a virtual DOM similar to React's and is considered easier
to learn. It provides the flexibility to be used as a library for small features or as a full framework
for building complex applications.

Choosing a Framework or Library


Choosing between a library or framework depends on your needs and preferences:
1. Learning Curve: Some tools are easier to learn than others. Check the quality of the
documentation, as good documentation can make learning significantly easier.
2. Community Support: Look for frameworks or libraries with active communities and lots of
available packages. A robust ecosystem can save you development time and make finding
solutions to problems easier.
3. Project Size: For substantial projects, a framework can help manage complexity. For smaller
projects, like a personal portfolio, plain JavaScript, HTML, and CSS might be enough.

Additional Tools
Here are a few more tools that can help with JavaScript development:

1. Babel: A JavaScript compiler that allows you to use modern JavaScript features in older
browsers. It acts like a polyfill, translating new syntax into code that’s widely supported.
2. Webpack: A module bundler that organizes your JavaScript into a dependency graph, optimizing
it for production. Other bundlers like Rollup are also available.
3. TypeScript: A superset of JavaScript that adds strong typing to help prevent bugs. Many
frameworks and libraries support TypeScript, making it a valuable tool for large projects.
4. Node.js: A JavaScript runtime for building back-end applications. Although we focus on front-
end development here, it's worth mentioning that Node.js allows JavaScript to be used server-
side.

That concludes our discussion on JavaScript frameworks and libraries. Remember, you don’t always
need a framework, but they can be invaluable for large-scale projects.

General Questions and Answers on JavaScript Frameworks


and Libraries

1. What is the key difference between a JavaScript library and a framework?


A library is a collection of reusable code that you call as needed to perform specific tasks. It is
unopinionated, meaning it doesn’t enforce a particular structure on your application.
A framework provides a structured way to build an application. It is opinionated, meaning it
enforces a specific architecture and calls your code at predefined points (inversion of control).

2. When would you choose a utility library like Lodash over writing your own
JavaScript functions?
Utility libraries like Lodash are useful when:

You need consistent and reliable utilities for manipulating objects, arrays, or strings.
You want to save time by avoiding the need to write common utility functions from scratch.
You need optimized and tested solutions that handle edge cases effectively.

For small projects, you might write your own functions, but for larger or more complex applications, a
library can reduce development effort.

3. Why has jQuery's popularity declined in modern web development?


jQuery's popularity has declined because:

Many of its features, such as simplified DOM manipulation and AJAX requests, are now available
natively in JavaScript.
Modern frameworks like React, Angular, and Vue provide more robust solutions for building
dynamic user interfaces and managing application state.
Developers prioritize performance, and jQuery adds extra overhead that may not be necessary in
newer projects.

However, jQuery is still widely used on legacy websites and remains relevant for projects with specific
requirements.

4. How does React’s declarative approach differ from imperative


programming in JavaScript?
React’s declarative approach focuses on describing what the UI should look like in a given state,
and React automatically updates the DOM to match that state.
In contrast, imperative programming requires explicitly coding every step to manipulate the DOM,
such as finding elements and updating them manually.

React’s declarative style simplifies code and reduces the risk of bugs by abstracting away DOM
operations.

5. What are some factors to consider when choosing a JavaScript


framework or library for a project?
When choosing a framework or library, consider:

Learning Curve: How easy is it to learn and use? Does it have good documentation and
tutorials?
Community Support: Does it have an active community and ecosystem of packages?
Scalability: Is it suitable for the size and complexity of your project?
Flexibility vs. Structure: Do you need the freedom of a library or the structure of a framework?
Performance: How well does it perform for your use case?

6. What roles do Babel and Webpack play in modern JavaScript


development?
Babel: A JavaScript compiler that allows developers to use the latest JavaScript syntax while
ensuring compatibility with older browsers by translating modern code into backward-
compatible versions.
Webpack: A module bundler that organizes JavaScript and other assets into optimized bundles
for production. It creates a dependency graph and ensures that all assets are included efficiently
in the final build.
What is TypeScript?
TypeScript is a superset of JavaScript. This means that all JavaScript syntax is valid in TypeScript,
but TypeScript introduces additional features. The main feature TypeScript adds is static typing. In
other words, you can declare types for your variables and functions, and TypeScript will throw an
error if you try to assign a value of a different type.

Basic Example
Let's look at a simple example:

let username = "StevenGarcia";


console.log(username); // Logs "StevenGarcia"

This code works just like standard JavaScript. However, TypeScript has implicitly assigned the type
string to username . If I try to change username to a number:

username = 42; // Error: Type 'number' is not assignable to type 'string'

TypeScript will throw an error. It also provides real-time feedback in editors like VS Code, highlighting
the error before you even run the code.

Declaring Types Explicitly


You can explicitly declare a type like this:

let isActive: boolean = true;

Now, if you try to reassign isActive to a string, you’ll get an error. You can also set the type to any:

let data: any = "hello";


data = 42; // No error
Using any is like opting out of TypeScript's type-checking, reverting to standard JavaScript
behavior. It's useful when gradually converting a JavaScript codebase to TypeScript.

Union Types
If you want a variable to accept more than one type, use a union type:

let response: string | number = "success";


response = 200; // No error

However, assigning an unsupported type like a boolean would still result in an error:

response = false; // Error: Type 'boolean' is not assignable to type 'string | number'

Custom Types
You can simplify complex type declarations using custom types:

type StringOrNumber = string | number;


let userId: StringOrNumber = "ABC123";
userId = 12345; // No error

This makes your code cleaner, especially when reusing the same type across multiple variables.

Literal Types
You can also use literal types for stricter type definitions. For example, if you want a variable to only
be "enable" or "disable" :

type Toggle = "enable" | "disable";


let status: Toggle = "enable";
status = "disable"; // No error
Attempting to assign anything other than "enable" or "disable" will result in an error.

Enums
Another way to handle a fixed set of values is with enums:

enum UserRole {
Admin = "admin",
Editor = "editor",
Viewer = "viewer",
}
let currentUserRole: UserRole = UserRole.Admin;
currentUserRole = UserRole.Editor; // No error

Enums can also represent numbers:

enum Priority {
High, // 0
Medium, // 1
Low, // 2
}

If you leave out the assigned values, TypeScript will automatically use numbers starting from 0.

Functions
TypeScript lets you specify the types of function parameters and return values:

function greet(name: string): string {


return `Hello, ${name}!`;
}
console.log(greet("Steven")); // Logs "Hello, Steven!"

If your function doesn’t return anything, use void :


function logError(error: string): void {
console.error(error);
}

Objects and Interfaces


You can enforce the structure of objects with interfaces:

interface Product {
name: string;
price: number;
inStock?: boolean; // Optional property
}

const item1: Product = {


name: "Laptop",
price: 1200,
};

const item2: Product = {


name: "Phone",
price: 699,
inStock: true,
};

If you add a property that isn’t in the interface, TypeScript will throw an error. You can make
properties optional using ? .

Using Interfaces with Classes


You can also use interfaces to define the structure of classes:
class Order implements Product {
name: string;
price: number;

constructor(name: string, price: number) {


this.name = name;
this.price = price;
}
}

const newOrder = new Order("Tablet", 300);

The class must adhere to the structure defined in the Product interface.

Generics
Generics allow you to create reusable code components. Here’s how to create a function that works
with any type:

function wrap<T>(value: T): { value: T } {


return { value };
}
console.log(wrap<number>(42)); // Logs { value: 42 }
console.log(wrap<string>("TypeScript")); // Logs { value: "TypeScript" }

Generics are also useful in interfaces:

interface Pair<K, V> {


key: K;
value: V;
}

const setting: Pair<string, boolean> = {


key: "darkMode",
value: true,
};

This makes your code flexible and type-safe.


Conclusion
TypeScript can be a valuable tool for preventing bugs, especially in medium to large projects. For
smaller projects, it might add unnecessary complexity, so consider your needs before adopting it. If
you want to dive deeper into TypeScript, check out the official documentation.

High-Level Questions and Answers on TypeScript

1. What is TypeScript, and how does it differ from JavaScript?


TypeScript is a superset of JavaScript that adds features like static typing and additional syntax for
better error-checking.

Unlike JavaScript, TypeScript enforces type safety, catching errors during development instead of
runtime. However, TypeScript code must be compiled into JavaScript to run in a browser or Node.js
environment.

2. How does TypeScript help prevent errors in your code?


TypeScript helps prevent errors by enforcing static typing and providing real-time feedback.

Developers can declare variable types, function parameter types, and return types. This ensures that
variables are used as intended, reducing runtime bugs caused by unexpected type mismatches.

3. What are union types in TypeScript, and when would you use them?
Union types allow a variable to hold multiple types of values.

For example:

let input: number | string = 42;


input = "hello"; // No error
Union types are useful when a value can logically belong to more than one type, such as an ID that
could be a number or a string.

4. What are some use cases for custom types and interfaces in TypeScript?
Custom Types: Simplify type declarations and ensure consistency when reusing the same
structure across multiple variables or functions.

Example:

type UserID = number | string;

Interfaces: Enforce the structure of objects or classes, ensuring they adhere to a specific format.

Example:

interface User {
username: string;
phoneNumber?: number; // Optional property
}

5. How do enums and literal types differ in TypeScript?


Enums: Used for defining a fixed set of named values, which can represent either strings or
numbers.

Example:

enum Direction {
North,
South,
East,
West,
}

Literal Types: Define a variable that can only take specific values.
Example:

type Action = "start" | "stop";

Enums are more versatile and can represent multiple values, while literal types provide strict control
over allowed values.

6. What are generics in TypeScript, and why are they useful?


Generics enable the creation of reusable and type-safe components that work with a variety of data
types.

Example:

function getFirstElement<T>(array: T[]): T | undefined {


return array[0];
}

const numbers = [10, 20, 30];


console.log(getFirstElement(numbers)); // Logs 10

const words = ["apple", "banana", "cherry"];


console.log(getFirstElement(words)); // Logs "apple"

Generics are useful for writing flexible, strongly-typed code, such as functions or interfaces that can
handle multiple data types without sacrificing type safety.

7. When might you choose to use TypeScript for a project, and what are its
potential drawbacks?
When to Use:
Large or medium projects where type safety can help catch bugs early.
Teams with multiple developers to ensure consistent code quality.
Projects requiring robust tooling, such as IDE autocomplete and better refactoring support.
Potential Drawbacks:
Adds complexity and requires a compilation step to convert TypeScript into JavaScript.
Might be unnecessary for small projects where the overhead outweighs the benefits.
Coding Questions
What These Problems Test:
1. Problem #42:
Understanding of generics and how they make functions reusable for various data types.
Ability to enforce type safety with TypeScript.
2. Problem #43:
Understanding of interfaces to define object structures.
Usage of classes and methods with strict type enforcement.

Problem #42: Create a Function Using Generics


Write a generic function mergeObjects that takes two objects as arguments and returns a new
object containing all properties from both. Ensure the function is type-safe and works with any object
types.

Requirements:

1. Use TypeScript's generics to make the function reusable for any object types.
2. The function should enforce type safety, ensuring both arguments are objects.

Example:

const obj1 = { name: "Steven" };


const obj2 = { favoriteLanguage: "JavaScript" };

const result = mergeObjects(obj1, obj2);


console.log(result); // Output: { name: "Steven", favoriteLanguage: "JavaScript" }

Problem #43: Define and Use an Interface with a Class


Create an interface Product with the following properties:

id : a number .
name : a string .
price : a number .
Then, implement a class ShoppingCart that includes the following methods:

1. addProduct(product: Product): void - Adds a product to the cart.


2. getTotal(): number - Returns the total price of all products in the cart.

Requirements:

1. Use the Product interface to enforce type safety for the products.
2. Use TypeScript's strong typing to ensure proper usage of the methods.

Example:

const cart = new ShoppingCart();


cart.addProduct({ id: 1, name: "Laptop", price: 1000 });
cart.addProduct({ id: 2, name: "Mouse", price: 50 });

console.log(cart.getTotal()); // Output: 1050


In this lesson, we’ll be discussing some essential debugging strategies specific to JavaScript.

In our example here, we have a simple HTML file. The only elements are a div with an id of
square , which you can see is creating the blue square you see in the output, and a button labeled
"Grow."

HTML Setup

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="square"></div>
<button id="growButton">Grow</button>
<script src="debugging-strategies.js"></script>
</body>
</html>

When we switch over to the CSS, there’s some simple styling for the square to make it visible and
size-adjustable:

CSS Setup

#square {
width: 50px;
height: 50px;
background-color: blue;
position: relative;
}

The goal here is to increase the size of the square each time we click the "Grow" button by
incrementing its width and height properties.

Now, over in our debugging-strategies.js file, we’ve tried to implement this functionality:
Initial JavaScript Code

const button = document.getElementById("growButton");

button.addEventListener("click", () => {
const square = document.getElementById("square");
const { width } = getComputedStyle(square);
square.style.width = width + 10; // This line has a bug
square.style.height = width + 10; // This line has a bug
});

When we run this code and click the "Grow" button, nothing happens. So let's dive into debugging
this and figure out what’s going wrong.

Step 1: Check for Console Errors


The first thing you should always do when debugging JavaScript is to look at the browser console. If
there’s an error in your code, it will usually show up here. In our case, there’s no error message, so
the console doesn’t immediately help us.

Step 2: Use Console Logging


A simple but effective strategy for debugging is to use console.log to check what your code is
doing at various points. Let’s walk through our code using console.log .

1. Check if the Button is Selected Correctly:

const button = document.getElementById("growButton");


console.log(button); // Check if we have the correct button

When we save and refresh, we see that it correctly logs the "Grow" button.
2. Check if the Click Event Handler is Working:

button.addEventListener("click", () => {
console.log("Button clicked"); // Verify the event handler is triggered
});
When we click the button, we see "Button clicked" in the console, confirming that the event
handler is being executed.
3. Check the width Value:

button.addEventListener("click", () => {
const square = document.getElementById("square");
const { width } = getComputedStyle(square);
console.log(width); // Log the `width` value
});

Clicking the button logs "50px". Here’s the issue: "50px" is a string, and when we try to add 10
to it, it results in "50px10", which is not a valid CSS value.

Fixing the Issue


We need to convert "50px" to a number before adding 10 . Here’s how we fix it:

button.addEventListener("click", () => {
const square = document.getElementById("square");
const { width } = getComputedStyle(square);
const newSize = parseInt(width) + 10 + "px"; // Convert to number and add 'px'
square.style.width = newSize;
square.style.height = newSize;
});

Now, when we click the "Grow" button, the square grows as expected. Remember to remove any
console.log statements once you’re done debugging.

Step 3: Use the Debugger Statement


Another helpful method is using the debugger statement. This pauses code execution and lets you
inspect variables.

1. Add a Debugger Statement:


button.addEventListener("click", () => {
debugger; // Pauses code execution and opens the debugger
const square = document.getElementById("square");
const { width } = getComputedStyle(square);
const newSize = parseInt(width) + 10 + "px";
square.style.width = newSize;
square.style.height = newSize;
});

2. When you click the button, the browser pauses and opens the Sources tab, where you can
inspect variables. Use the "Step Over" or "Step Into" options to walk through the code line by
line and observe variable values.

Step 4: Using Breakpoints for Longer Files


If your JavaScript file is large, manually adding breakpoints is more efficient.

1. Set a Breakpoint in the Sources Tab:


Open the Sources tab in Chrome DevTools.
Click on the line number where you want to pause execution, adding a breakpoint.
2. Use Event Listener Breakpoints:
In the Sources tab, go to the Event Listener Breakpoints section.
Expand "Mouse" and check "click" to pause whenever a click event is triggered.

Clicking the "Grow" button will pause the code at the event handler, and you can inspect variables
just like before.

Step 5: The Network Tab


The Network tab is useful for debugging network requests.

1. Check Network Requests:


Open the Network tab.
It shows all network requests, including the HTML, CSS, and JavaScript files.
You can view request details, such as status codes and response data.

This is especially helpful when debugging fetch or AJAX requests.


Wrap-Up
That’s a look at some JavaScript debugging strategies. We covered using console.log , the
debugger statement, breakpoints, and the Network tab. These tools are crucial for effective
JavaScript debugging.

High-Level Questions and Answers on Debugging Strategies in


JavaScript

1. Why is the console a critical tool for debugging JavaScript?


The console provides real-time feedback on errors in your JavaScript code. It shows syntax errors,
runtime exceptions, and warnings. Using the console, developers can identify issues quickly, check
the flow of execution, and validate variable values with console.log .

2. How can console.log be effectively used to debug JavaScript?


console.log allows you to inspect the state of your code at various points:

Check if variables are correctly assigned.


Verify if event listeners are triggered.
Debug unexpected behavior by logging intermediate values.

It’s a simple and quick way to trace the execution flow without using more complex debugging tools.

3. What is the purpose of the debugger statement, and how does it differ
from console.log ?
The debugger statement pauses the execution of your code at a specific point, opening the
browser's debugging tools.

Unlike console.log , which provides output, debugger allows you to inspect the program’s state
interactively, step through code line by line, and evaluate variable values dynamically.
4. When and why should you use breakpoints in debugging?
Breakpoints are used in the browser's DevTools to pause code execution at specific lines. They are
especially useful in larger JavaScript files to:

Inspect variable values at a specific point.


Trace the flow of logic.
Identify bugs in complex or asynchronous code.

Breakpoints help debug without modifying the code, unlike console.log or debugger .

5. How does using the Network tab in DevTools aid in debugging?


The Network tab shows details of all network requests made by the browser, including JavaScript
files, API calls, and other resources. It’s useful for:

Checking the status of requests (e.g., 200 OK, 404 Not Found).
Viewing request and response headers and data.
Debugging issues with fetch or AJAX calls, such as timeouts or incorrect payloads.

6. Why is type conversion important when working with CSS properties like
left in JavaScript?

CSS properties like left are returned as strings (e.g., "0px" ) when using getComputedStyle . To
perform mathematical operations on these values, you must convert them to numbers using
functions like parseInt . Without conversion, operations like left + 10 would result in invalid
values ( "0px10" ).

7. What is the advantage of using tools like breakpoints and the debugger
statement over solely relying on console.log ?
While console.log provides static output, breakpoints and the debugger statement allow for
dynamic inspection of code:

Pause execution at any point to inspect variable values.


Step through code line by line to understand the flow.
Evaluate expressions and modify variables in real-time.

These tools provide deeper insights into the code, making them more effective for complex
debugging scenarios.

Coding Questions
What These Problems Test:
1. Problem #44: Type conversion and fixing common CSS manipulation bugs.
2. Problem #45: Understanding the use of debugger for stepping through code.
3. Problem #46: Debugging asynchronous code and using breakpoints to trace API calls.
4. Problem #47: Debugging logic errors in event handlers using console.log .
5. Problem #48: Using the Network tab and proper error handling for HTTP requests.

Problem #44: Debugging getComputedStyle Conversion Issue


You are tasked with creating a JavaScript function that moves an element to the right when a button
is clicked. However, the following code does not work as intended:

const button = document.getElementById("moveRightButton");


button.addEventListener("click", () => {
const circle = document.getElementById("circle");
const { left } = getComputedStyle(circle);
circle.style.left = left + 10;
});

1. Identify and fix the issue in the code.


2. Write a working solution that converts the left value to a number before performing any
operations.
3. Log the new value of left after each button click.

Requirements:

Use parseInt to convert the left value to a number.


Ensure the new value is updated as a valid CSS string.
Problem #45: Use Debugger to Fix a Logical Error
The following code is supposed to increment a counter every time the button is clicked, but it doesn’t
work correctly:

const button = document.getElementById("incrementButton");


let counter = 0;

button.addEventListener("click", () => {
debugger; // Add a debugger statement here
counter++;
console.log("Counter value:", counter);
});

Task:

1. Debug this script using the debugger statement to inspect how the counter variable behaves.
2. Identify and describe what happens when the debugger statement is reached.
3. Make sure the counter increments correctly every time the button is clicked.

Problem #46: Add Breakpoints to Debug Asynchronous Code


The following code makes an API call and logs the result to the console. However, the data variable
is undefined , leading to a runtime error:

async function fetchData() {


const response = await fetch("https://api.example.com/data");
const data = response.json();
console.log(data.name);
}

fetchData();

Task:

1. Add breakpoints in the DevTools to debug this issue and inspect the values of response and
data .
2. Fix the issue by ensuring the JSON data is properly awaited.
3. Write the corrected code and verify the result.
Problem #47: Debugging and Logging with Multiple Event
Listeners
The following script toggles the background color of a div between red and blue when clicked.
However, clicking the div multiple times results in unexpected behavior:

const box = document.getElementById("box");

box.addEventListener("click", () => {
if (box.style.backgroundColor === "red") {
box.style.backgroundColor = "blue";
} else {
box.style.backgroundColor = "red";
}
});

Task:

1. Debug this script using console.log to inspect the backgroundColor value before toggling.
2. Fix any issues that prevent the toggling from working consistently.
3. Provide the corrected code with proper logging.

Problem #48: Inspect and Debug Network Requests


The following script makes a POST request to an API, but it fails silently without showing any errors in
the console:

async function submitForm() {


const data = { name: "Alice" };

fetch("https://api.example.com/submit", {
method: "POST",
body: JSON.stringify(data),
});
}

document.getElementById("submitButton").addEventListener("click", submitForm);
Task:

1. Use the Network tab in DevTools to inspect the outgoing request.


2. Identify and fix any missing headers or other issues that could cause the request to fail.
3. Update the code to handle the response and log success or error messages.
Today, we’re going to talk about how to write clean and maintainable JavaScript code. Clean code is
code that is easy to understand, easy to update, and easy to maintain.

Moreover, our goal is to write idiomatic JavaScript code, which means writing code that adheres to
the best practices and standards of the language.

Use Modern JavaScript Syntax


One of the best ways to ensure clean code is by using modern JavaScript syntax. This is not only
about writing code that works, but writing code that is clear and easy to follow. We covered modern
syntax in detail earlier in this crash course, but here are some highlights to focus on:

1. Arrow Functions
Arrow functions are great for small inline functions. They provide a more concise syntax, but
keep in mind they don’t work well for every situation, especially when you need this to
refer to the current object.

// Instead of writing a traditional function


function calculateArea(width, height) {
return width * height;
}

// Use an arrow function


const calculateArea = (width, height) => width * height;

2. Destructuring Syntax
Destructuring makes your code more readable by extracting values from arrays or properties
from objects easily.

const car = { make: "Audi", model: "RS5", year: 2025 };

// Instead of
const make = car.make;
const model = car.model;

// Use destructuring
const { make, model } = car;

3. Template Literals
Template literals make string concatenation easier to read and manage.

const product = "smartphone";


const cost = 699;

// Instead of
const message = "The cost of the " + product + " is $" + cost;

// Use a template literal


const message = `The cost of the ${product} is $${cost}`;

Avoid Over-Nesting Callback Functions


Deeply nested callbacks can make your code hard to read and understand. Instead, use Promises
and async / await to simplify asynchronous code:

// Instead of nested callbacks


fetchData((data) => {
processData(data, (processedData) => {
saveData(processedData, (response) => {
console.log(response);
});
});
});

// Use Promises or async/await


const handleData = async () => {
try {
const data = await fetchData();
const processedData = await processData(data);
const response = await saveData(processedData);
console.log(response);
} catch (error) {
console.error(error);
}
};

Using async / await makes the code look synchronous and is generally easier to follow.
Use this Wisely
Overusing the this keyword can lead to confusing and buggy code. Avoid using this as an extra
parameter or assigning values to it that don’t make sense contextually. When possible, prefer using
explicit parameters for clarity:

// Less clear use of `this`


class Counter {
increment() {
this.value = (this.value || 0) + 1;
}
}

// Prefer explicit parameters


function increment(counter) {
counter.value = (counter.value || 0) + 1;
}

Embrace Functional Programming


JavaScript is a multi-paradigm language, but when possible, lean towards functional programming.
Write pure functions, avoid side effects, and use function chaining for a more declarative style:

// Example of a pure function


const multiply = (a, b) => a * b;

// Using function chaining


const words = ["apple", "banana", "cherry"];
const capitalized = words
.map((word) => word.toUpperCase())
.filter((word) => word.startsWith("B"));

console.log(capitalized); // ["BANANA"]

Functional programming makes your code easier to test and understand.


Use a Code Auto-Formatter
To maintain consistency across your codebase, use a tool like Prettier. Prettier automatically formats
your code, helping with indentation, spaces, and overall readability. Let’s see how Prettier works:

1. Unformatted Code:

const multiply = (a, b) => {


return a * b;
};
console.log(multiply(3, 4));

2. Prettier-Formatted Code:

const multiply = (a, b) => {


return a * b;
};

console.log(multiply(3, 4));

You can customize Prettier’s settings for single quotes, tab width, max line length, and more,
depending on your project’s needs.

Follow a Style Guide


Finally, use a style guide to enforce consistent coding standards. Style guides like Google’s
JavaScript Style Guide or Airbnb’s Style Guide can be great resources. They provide rules on variable
naming, spacing, and best practices.

Example Rule from Airbnb’s Style Guide:

Use const by default, and only use let if a variable needs to be reassigned.
Avoid using var entirely.

// Recommended
const MAX_ITEMS = 100;
let currentCount = 0;

// Not Recommended
var total = 50; // Avoid using `var`
Consistency in your codebase makes it easier for teams to collaborate and maintain the code over
time.

Wrap-Up
That’s it for writing clean JavaScript code. Focus on using modern syntax, avoid deeply nested
callbacks, use this carefully, and follow a functional programming style where appropriate. Don’t
forget to use tools like Prettier and adhere to a style guide to ensure your code is both clean and
consistent.

Questions and Answers on Writing Clean and Maintainable


JavaScript Code

1. Why is using modern JavaScript syntax important for writing clean code?
Modern JavaScript syntax, such as arrow functions, destructuring, and template literals, improves
readability and conciseness. It allows developers to write code that is easier to understand, maintain,
and adhere to current standards, ensuring consistency across the codebase.

2. How can deeply nested callbacks be avoided in asynchronous JavaScript


code?
Deeply nested callbacks can be avoided by using Promises or async / await . These approaches
flatten the code structure, making it more readable and easier to follow. async / await is especially
useful as it makes asynchronous code look more like synchronous code.

3. What are some best practices for using the this keyword in JavaScript?
To avoid confusion and bugs:

Use this only when necessary, such as in object-oriented code.


Prefer explicit parameters over using this to pass values.
Use arrow functions in callbacks to maintain the correct this context, as they don’t rebind
this .

4. How does functional programming contribute to clean JavaScript code?


Functional programming promotes writing pure functions that have no side effects and produce
consistent outputs for the same inputs. Techniques like function chaining (e.g., using map , filter ,
and reduce ) make the code more declarative and easier to test and understand.

5. What role do code auto-formatters like Prettier play in maintaining a


clean codebase?
Code auto-formatters like Prettier ensure consistent formatting across a codebase by automatically
handling indentation, spacing, and line breaks. This improves readability, eliminates style debates,
and allows developers to focus on functionality rather than formatting.

6. Why should JavaScript developers follow a style guide, and what are
some common rules?
A style guide enforces consistent coding practices across a project, making it easier for teams to
collaborate and maintain the code. Common rules include:

Use const by default and let only when reassignment is needed.


Avoid using var entirely.
Use camelCase for variable names and PascalCase for class names.

7. How do tools like Prettier and style guides work together to improve
JavaScript code quality?
Prettier handles the automated formatting of code, ensuring consistency in indentation, spacing, and
alignment. Style guides provide broader best practices and rules for writing clean, idiomatic
JavaScript. Together, they help maintain a consistent and readable codebase that aligns with industry
standards.
Coding Questions
What These Problems Test:
Problem #49: Tests understanding of async / await , refactoring for clean code, and handling
asynchronous workflows.
Problem #50: Tests familiarity with modern JavaScript features like destructuring and template
literals, and the ability to refactor verbose code into cleaner and more maintainable code.

Problem #49: Refactor Nested Callbacks Using async / await


The following code uses deeply nested callbacks to simulate an asynchronous workflow. Refactor the
code to use async / await for improved readability and maintainability.

function fetchData(callback) {
setTimeout(() => {
callback("Data fetched");
}, 1000);
}

function processData(data, callback) {


setTimeout(() => {
callback(`${data} processed`);
}, 1000);
}

function saveData(processedData, callback) {


setTimeout(() => {
callback(`${processedData} saved`);
}, 1000);
}

fetchData((data) => {
processData(data, (processedData) => {
saveData(processedData, (result) => {
console.log(result);
});
});
});
Task:

1. Refactor the code using async / await to remove the nested callbacks.
2. Ensure the output is the same: "Data fetched processed saved" .
3. Handle potential errors using try-catch .

Problem #50: Clean Code with Destructuring and Template


Literals
The following code performs calculations and logs the results. However, the code is verbose and can
be improved using modern JavaScript features such as destructuring and template literals.
const rectangle = {
width: 10,
height: 5,
};

function calculateArea(rect) {
const width = rect.width;
const height = rect.height;
const area = width * height;
console.log(
"The area of the rectangle with width " +
width +
" and height " +
height +
" is " +
area
);
}

function calculatePerimeter(rect) {
const width = rect.width;
const height = rect.height;
const perimeter = 2 * (width + height);
console.log(
"The perimeter of the rectangle with width " +
width +
" and height " +
height +
" is " +
perimeter
);
}

calculateArea(rectangle);
calculatePerimeter(rectangle);

Task:

1. Refactor the code to use destructuring to extract width and height .


2. Use template literals to simplify the console.log statements.
3. Ensure the output remains the same.

You might also like