JavaScript Essentials for Technical Interviews
JavaScript Essentials for Technical Interviews
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.
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 car = {
brand: "Toyota",
start() {
console.log("Car started");
},
};
car.start();
4. Imperative Programming:
Explicitly defines the control flow using loops, conditionals, and statements.
5. Declarative Programming:
Focuses on describing the desired outcome rather than the control flow. Often used in functional
programming.
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.
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:
3. Math.pow()
Returns the base raised to the exponent power.
Example:
2. parseInt()
Parses a string and returns an integer, stopping at the first non-numeric character.
Example:
These are essential tools for working with numbers and strings in JavaScript.
// Access values
console.log(map.get("name")); // Output: Steven
console.log(map.get(42)); // Output: The answer
console.log(map.get(true)); // Output: Yes
// Add values
set.add(1);
set.add(2);
set.add(2); // Duplicate, will be ignored
set.add("Hello");
set.add(true);
// Delete a value
set.delete(2);
console.log(set.has(2)); // Output: false
These examples demonstrate how to use Map for key-value management and Set for ensuring
unique values.
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:
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+' │
└─────────┴──────────┴───────┘
*/
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!
*/
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");
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
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.
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.
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.
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:
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:
Variable Declarations
let
Example:
{
let blockVar = "I am block-scoped";
console.log(blockVar); // "I am block-scoped"
}
// console.log(blockVar); // ReferenceError: blockVar is not defined
var
Example:
function example() {
console.log(varVar); // undefined
var varVar = "I am function-scoped";
console.log(varVar); // "I am function-scoped"
}
example();
const
Example:
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.
Hoisting in JavaScript is the process where variable declarations are moved to the top of their scope
during the code's execution phase.
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.
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.
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:
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:
Array Basics
Creating Arrays
You can create arrays using different methods:
console.log(array.includes(2)); // true
console.log(array.indexOf(2)); // 1
console.log(array.lastIndexOf(2)); // -1 (if not found)
array.push(4);
console.log(array); // [1, 2, 3, 4]
console.log(array.pop()); // 4
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.reverse();
console.log(array); // [3, 'new', 1]
2. for-of Loop:
3. forEach() Method:
Array Functions
Transformation
map() : Creates a new array by applying a function to each element.
Filtering
filter() : Creates a new array with elements that pass a condition.
Finding
find() : Returns the first element that meets a condition.
Checking Conditions
every() : Checks if all elements meet a condition.
Reducing
reduce() : Accumulates values from left to right.
Sorting
Default Sorting:
const nums = [5, 7, 3, 0];
nums.sort();
console.log(nums); // [0, 3, 5, 7]
Custom Sorting:
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
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.
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.
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:
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:
Expected Output:
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:
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
Example:
const website = {
name: "stevencodecraft",
domain: "stevencodecraft.com",
};
console.log(website.name); // 'stevencodecraft'
Bracket Notation:
console.log(website["domain"]); // 'stevencodecraft.com'
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:
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'
Example:
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;
},
};
Example:
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
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.
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.
Expected Output:
{
name: "John",
favoriteLanguage: "Python"
}
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:
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:
Example:
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");
}
How it works:
Example:
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
Special Cases
1. NaN:
The value NaN (Not-a-Number) is unique in that it is not equal to any other value, including
itself.
3. Objects:
When comparing objects, strict equality checks if they reference the same object in memory, not
if they have the same content.
Questions
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.
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 .
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:
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
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:
Multi-line functions:
Single-line functions:
Examples:
1. Array Destructuring:
2. Object Destructuring:
3. Renaming Fields:
4. Function Parameters:
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 }
Examples:
1. Combining Arrays:
2. Cloning Objects:
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:
Nullish Coalescing ( ?? )
The nullish coalescing operator provides a default value if the left-hand side is null or undefined .
Example:
Optional Chaining ( ?. )
Optional chaining safely accesses nested properties without throwing an error if an intermediate
property is null or undefined .
Example:
&& : 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:
2. || Example:
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
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 ).
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 obj1 = { a: 1 };
const obj2 = { b: 2 };
const merged = { ...obj1, ...obj2 };
// merged is { a: 1, b: 2 }
Summary:
Benefits:
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:
Example Input:
processArray([1, 2, 3, 4]);
Expected Output:
[1, 4, 6, 8];
{
name: string,
address?: {
city: string,
street?: string
}
}
Example Input:
Expected Output:
Example Input:
getUserDetails({
name: "Bob",
address: { city: "Metropolis", street: "Main St" },
});
Expected Output:
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.
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:
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:
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.
<body>
<button>Click Me</button>
<script src="script.js"></script>
</body>
Scripts won’t start downloading until the browser reaches the <script> tag, delaying their
execution compared to defer .
Example:
window.addEventListener("DOMContentLoaded", function () {
const button = document.querySelector("button");
button.addEventListener("click", function () {
document.body.style.backgroundColor = "#00334C";
});
});
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
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.
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.
Steps:
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:
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
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 .
3. document.querySelectorAll(selector) :
Selects all elements that match a CSS selector and returns them as a NodeList .
4. document.getElementsByTagName(tagName) :
Selects all elements with a specific HTML tag and returns them as an HTMLCollection .
5. document.getElementsByClassName(className) :
Selects all elements with a specific class and returns them as an HTMLCollection .
1. Inline Styles:
2. textContent :
Gets or sets the text content of an element, including its children.
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");
1. document.createElement(tagName) :
Creates a new HTML element.
2. appendChild(childElement) :
Appends a child element to a parent element.
document.body.appendChild(newElement);
3. append() :
Appends multiple elements or text nodes.
4. prepend() :
Prepends multiple elements or text nodes.
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:
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();
element.scrollTo({
top: 100,
behavior: "smooth", // Enables smooth scrolling
});
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
Example:
Example:
element.classList.add("active");
// Adds the class "active" to the selected element.
Example:
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.
Steps:
Hints:
Expected Behavior:
Clicking the button changes the background to a random color and displays the color code
below the button.
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:
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
Example:
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).
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.
3. passive :
Indicates that event.preventDefault() will not be called, allowing the browser to optimize
performance.
Useful for touch events like touchstart or touchmove .
4. signal :
Uses an AbortSignal to remove the event listener when abort() is called.
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 :
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:
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.
button.addEventListener(
"click",
(event) => {
console.log("Button clicked");
},
{
capture: true,
once: true,
passive: true,
signal: abortController.signal,
}
);
Conclusion
Event-Driven Programming is a cornerstone of JavaScript for handling user interactions and dynamic
behaviors. By mastering:
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
This ensures the event listener fires during the capturing phase instead of the default bubbling
phase.
Example:
This means the listener will only run the first time the event occurs and will then be removed from the
element.
Example:
Benefits:
Example:
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:
element.addEventListener("click", handleClick, {
signal: abortController.signal,
});
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.
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:
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:
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:
Handling Promises
Promises provide three primary methods for handling outcomes:
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.
Example:
fetchData();
Example:
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:
Questions
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.
Example:
const promise = new Promise((resolve, reject) => {
const success = true; // Simulating success or failure
if (success) {
resolve("Operation succeeded");
} else {
reject("Operation failed");
}
});
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.
Example:
promise
.then((value) => {
console.log("Success:", value);
})
.catch((error) => {
console.log("Error:", error);
})
.finally(() => {
console.log("Promise is settled");
});
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.
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:
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
promise
.then((result) => nextAsyncOperation(result))
.then((finalResult) => console.log(finalResult))
.catch((error) => console.log("Error:", error));
Benefits:
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:
Steps:
Example Input:
fetchData([
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
]);
Expected Output:
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:
fetch(BASE_API)
.then((response) => response.json())
.then((data) => console.log("Data received:", data))
.catch((error) => console.error("Error fetching data:", error));
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));
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();
fetchJsonData();
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();
<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);
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:
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.
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:
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
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.
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => 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.
Expected Output:
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:
let counter = 0;
const intervalID = setInterval(() => {
console.log(`Counter: ${++counter}`);
}, 1000); // Increments counter every second
clearInterval(intervalID); // Stops the interval
setTimeout
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);
}
}
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.
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
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:
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:
Date Methods
Get specific 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:
These tools are essential for managing time and animations in your JavaScript applications.
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.
Advantages:
Ensures animations run smoothly by executing only when the browser is ready to repaint.
Reduces unnecessary work, conserving system resources.
Cause performance issues if the interval is faster than the screen refresh rate.
Lead to skipped frames or choppy animations.
Importance:
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.
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:
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.
Steps:
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.
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;
The logNum function keeps a reference to num , even though the example function has already
finished running.
Example:
function makeFunctions() {
let privateNum = 0;
function privateIncrement() {
privateNum++;
}
return {
logNum: () => console.log(privateNum),
increment: () => {
privateIncrement();
console.log("Incremented");
},
};
}
Closures in Loops
Using let :
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
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.
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.
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.
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,
};
}
In this example:
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.
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.
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.
The counter value should be private and only accessible via the object's methods.
Example Usage:
Use closures to ensure each timeout function remembers the correct delay and string.
Example Input:
Expected Output:
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.
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.
Global Context
At the global level, this refers to the window object (in a browser):
"use strict";
console.log(this); // Logs: undefined
this in Functions
Standard Function:
function logThis() {
console.log(this);
}
const obj = {
num: 7,
logThis: function () {
console.log(this);
},
};
button.addEventListener("click", function () {
console.log(this); // Logs: button
});
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);
}
function logThis(x, y) {
console.log(this, x, y);
}
[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`);
}
}
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.
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.
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.
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.
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.
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
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:
Example JavaScript:
setupClickLogger(btn1);
setupClickLogger(btn2);
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.`);
};
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.`);
}
}
function Device(name) {
this.name = name;
}
Device.prototype.describe = function () {
console.log(`This device is called ${this.name}`);
};
Example:
class Device {
constructor(name) {
this.name = name;
}
describe() {
console.log(`This device is called ${this.name}`);
}
}
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:
showOS() {
console.log(`${this.name} runs on ${this.operatingSystem}`);
}
}
The super() keyword is used to call the parent class’s constructor or methods.
Example:
class Device {
static category = "Electronics";
constructor(name) {
this.name = name;
}
}
Example:
class SmartDevice {
#operatingSystem;
constructor(name, operatingSystem) {
this.name = name;
this.#operatingSystem = operatingSystem;
}
getOperatingSystem() {
return this.#operatingSystem;
}
}
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.
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 .
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:
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.
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
}
}
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.
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]."
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."
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.
This is achieved by creating functions that return other functions, using closures to maintain access
to the parameters.
function curriedMultiply(a) {
return function (b) {
return a * b;
};
}
Calling curriedMultiply(3) returns a new function that multiplies 3 by the next argument:
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;
};
};
}
console.log(curriedMultiply(2)(3)(4)); // Outputs: 24
function curry(func) {
return function (a) {
return function (b) {
return function (c) {
return func(a, b, c);
};
};
};
}
Example:
You can use the same curry function for other functions:
function divide(a, b, c) {
return a / b / c;
}
function curry(func) {
return (a) => (b) => (c) => func(a, b, c);
}
1. Partial Application: Currying allows you to create a function with some arguments "pre-filled"
for later use.
Example:
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.
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.
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.
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;
}
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:
Example Usage:
They are defined using the function* syntax and use the yield keyword to pause and resume
execution.
Each time you call next() on the generator object, it resumes execution until the next yield
statement or the end of the function.
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";
}
Each next() call returns the next value and indicates whether the generator is done.
function* processInput() {
const input = yield "Enter a value";
yield `You entered: ${input}`;
}
Here, the value "Hello" is passed into the generator and used in the next yield statement.
function* errorGenerator() {
try {
yield 1;
yield 2;
} catch (error) {
console.log("Caught error:", error.message);
}
}
This stops the generator unless the error is caught with a try-catch block inside the generator
function.
function* vegetables() {
yield "carrot";
yield "spinach";
}
function* food() {
yield* fruits();
yield "and";
yield* vegetables();
}
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.
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:
Example Usage:
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
Requirements:
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);
}
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
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.
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:
Importing
1. Named Imports:
Import specific items by their names:
2. Default Imports:
Import the default export with any name:
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
app.js
console.log(pi); // 3.14
console.log(multiply(3, 4)); // 12
Dynamic Imports
Load modules only when necessary to improve performance:
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.
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.
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:
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.
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:
Expected Results:
Addition result: 15
Multiplication result: 50
1. mathHelper.js :
Export a function called calculateArea that calculates the area of a rectangle:
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:
Example Usage:
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.
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.
Output:
Synchronous Execution
Microtask Executed
Deferred Task
Explanation:
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();
}
Summary
The Event Loop explains how JavaScript handles asynchronous tasks in a single-threaded
environment. By understanding its mechanics, you can:
This knowledge ensures your web applications perform smoothly, providing a better user experience.
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.
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.
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.
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.
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:
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:
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:
HTML Setup:
// Create Worker
const worker = new Worker("worker.js");
// Greet Message
greetButton.addEventListener("click", () => {
console.log("Hello, User!");
});
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:
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.
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.
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:
Task:
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:
Task:
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.
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" })
);
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:
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);
// 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.
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.
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:
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.
Requirements:
1. Identify at least two potential security risks in the code.
2. Propose and explain two solutions to mitigate these risks.
Requirements:
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:
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:
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:
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:
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.
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:
Example:
Requirements:
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.
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.
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.
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.
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.
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.
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.
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.
React’s declarative style simplifies code and reduces the risk of bugs by abstracting away DOM
operations.
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?
Basic Example
Let's look at a simple example:
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:
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.
Now, if you try to reassign isActive to a string, you’ll get an error. You can also set the type to any:
Union Types
If you want a variable to accept more than one type, use a union type:
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:
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" :
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
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:
interface Product {
name: string;
price: number;
inStock?: boolean; // Optional property
}
If you add a property that isn’t in the interface, TypeScript will throw an error. You can make
properties optional using ? .
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:
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.
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:
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:
Interfaces: Enforce the structure of objects or classes, ensuring they adhere to a specific format.
Example:
interface User {
username: string;
phoneNumber?: number; // Optional property
}
Example:
enum Direction {
North,
South,
East,
West,
}
Literal Types: Define a variable that can only take specific values.
Example:
Enums are more versatile and can represent multiple values, while literal types provide strict control
over allowed values.
Example:
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.
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:
id : a number .
name : a string .
price : a number .
Then, implement a class ShoppingCart that includes the following methods:
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:
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
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.
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.
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.
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.
Clicking the "Grow" button will pause the code at the event handler, and you can inspect variables
just like before.
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:
Breakpoints help debug without modifying the code, unlike console.log or debugger .
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:
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.
Requirements:
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.
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:
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.
fetch("https://api.example.com/submit", {
method: "POST",
body: JSON.stringify(data),
});
}
document.getElementById("submitButton").addEventListener("click", submitForm);
Task:
Moreover, our goal is to write idiomatic JavaScript code, which means writing code that adheres to
the best practices and standards of the language.
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.
2. Destructuring Syntax
Destructuring makes your code more readable by extracting values from arrays or properties
from objects easily.
// 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.
// Instead of
const message = "The cost of the " + product + " is $" + cost;
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:
console.log(capitalized); // ["BANANA"]
1. Unformatted Code:
2. Prettier-Formatted Code:
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.
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.
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.
3. What are some best practices for using the this keyword in JavaScript?
To avoid confusion and bugs:
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:
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.
function fetchData(callback) {
setTimeout(() => {
callback("Data fetched");
}, 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 .
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: