Some JavaScript Hoisting & Currying interview questions

Mayank C
Tech Tonic

--

1 What is the fundamental difference between hoisting and scope in JavaScript?

Hoisting and scope are two fundamental concepts in JavaScript that are often misunderstood or confused. Understanding the difference between hoisting and scope is crucial for writing efficient and error-free JavaScript code.

Hoisting

Hoisting refers to the behavior of JavaScript where declarations (variables and functions) are moved to the top of their scope, regardless of where they are actually defined. This means that:

  • Variable declarations (e.g., let x;) are hoisted to the top of their scope, but their assignments (e.g., x = 10;) are not.
  • Function declarations (e.g., function foo() { ... };) are fully hoisted, including their definitions.

Here is an example of hoisting:

console.log(x); // outputs: undefined
var x = 10;

foo(); // outputs: Hello!

function foo() {
console.log("Hello!");
}

In this example, the variable x is declared and assigned a value after it is logged to the console. However, due to hoisting, the declaration of x is moved to the top of the scope, so it is defined when it is logged. Similarly, the function foo is declared after it is called, but due to hoisting, it is defined when it is called.

Scope

Scope, on the other hand, refers to the region of the code where a variable or function is defined and can be accessed. In JavaScript, scope is determined by the location of the declaration and the surrounding blocks (e.g., if, for, function).

Here is an example of scope:

function outer() {
let x = 10;

function inner() {
console.log(x); // outputs: 10
}

inner();
}

outer();

In this example, the variable x is declared in the outer function and is only accessible within that scope. The inner function has access to the scope of the outer function, so it can log the value of x to the console.

In conclusion, hoisting refers to the behavior of moving declarations to the top of their scope, while scope refers to the region of code where a variable or function is defined and can be accessed.

2 Can you explain why JavaScript’s hoisting behavior is often referred to as “virtual hoisting” or “illusionary hoisting”?

JavaScript’s hoisting behavior is often referred to as “virtual hoisting” or “illusionary hoisting” because it doesn’t actually move the code around, but rather creates an illusion that the declarations are moved to the top of their scope.

In other words, the code is not physically rearranged, but the JavaScript engine treats it as if it were. This is why it’s called “virtual” or “illusionary” hoisting.

Here’s an example to illustrate this:

console.log(x); // outputs: undefined
var x = 10;

In this example, the console.log statement is executed before the var x = 10 declaration. However, due to hoisting, the JavaScript engine treats it as if the declaration were moved to the top of the scope, so x is defined when it's logged.

But, if we look at the actual code, we can see that the declaration is still in its original position, and the code is not physically rearranged. This is why it’s called “virtual” or “illusionary” hoisting.

It’s a clever trick that JavaScript uses to make our lives easier, but it’s important to understand that it’s not actually changing the code, just how it’s interpreted.

Here’s a quote from MDN that sums it up nicely:

“Hoisting is a JavaScript mechanism that moves the declaration of a variable to the top of its scope, regardless of where the actual declaration is made. However, this is a bit of a misnomer, as the declaration is not actually moved, but rather, it’s as if it were moved.”

3 How does the concept of hoisting relate to the idea of “declarative” vs “imperative” programming in JavaScript?

The concept of hoisting in JavaScript relates to the idea of “declarative” vs “imperative” programming in that it allows for a more declarative programming style.

Declarative programming

Declarative programming focuses on declaring what the program should accomplish, without explicitly specifying how it should be done. It’s a high-level, abstract approach that emphasizes the desired outcome over the implementation details.

In JavaScript, declarative programming is achieved through the use of declarations (e.g., let, const, function) that define what variables and functions should be created, without explicitly specifying when or how they should be created.

Imperative programming

Imperative programming, on the other hand, focuses on explicitly specifying the steps that the program should take to achieve a goal. It’s a more low-level, detailed approach that emphasizes the implementation details over the desired outcome.

In JavaScript, imperative programming is achieved through the use of statements (e.g., if, for, while) that explicitly control the flow of the program.

Hoisting and declarative programming

Hoisting allows JavaScript developers to write code in a more declarative style, by declaring variables and functions without worrying about when or how they will be created. The JavaScript engine takes care of the details, “hoisting” the declarations to the top of their scope, making them available when needed.

This declarative approach enables developers to focus on the desired outcome (what the program should accomplish) rather than the implementation details (how it should be done). It makes the code more concise, expressive, and easier to maintain.

Here’s an example:

console.log(x); // outputs: undefined
var x = 10;

In this example, we declare the variable x and assign it a value after it's been logged to the console. Due to hoisting, the declaration is moved to the top of the scope, making x available when it's logged. This is a declarative programming style, where we focus on declaring what we want (x to be logged) without explicitly specifying how it should be done.

In contrast, an imperative programming style would require us to explicitly create the variable and assign it a value before logging it:

let x = 10;
console.log(x); // outputs: 10

This approach focuses on the implementation details (creating and assigning x) rather than the desired outcome (logging x).

4 Can you write a code snippet that uses hoisting to create a “self-invoking” function that logs its own name?

Here is a code snippet that uses hoisting to create a “self-invoking” function that logs its own name:

(function logMyName() {
console.log(logMyName.name);
})();

// Outputs: "logMyName"

In this code snippet, we define a function expression (not a function declaration) and immediately invoke it. The function logs its own name using the name property.

Here’s how hoisting comes into play:

  • The function expression is defined and immediately invoked, but the logMyName variable is hoisted to the top of the scope.
  • When the function is invoked, it logs its own name, which is available due to hoisting.

Note that this only works with function expressions, not function declarations. If we tried to do this with a function declaration, it would not work:

function logMyName() {
console.log(logMyName.name);
}();

// Error: Cannot call a function declaration

This is because function declarations are not hoisted in the same way as function expressions.

5 Write a code snippet that demonstrates how hoisting can lead to unexpected behavior when using this in a function.

Here is a code snippet that demonstrates how hoisting can lead to unexpected behavior when using this in a function:

console.log(this); // outputs: Window (or global object)

function outer() {
console.log(this); // outputs: Window (or global object)

var inner = function() {
console.log(this); // outputs: Window (or global object), not the outer function!
};

inner();
}

outer();

In this code snippet, we define an outer function that logs the value of this to the console. Inside the outer function, we define an inner function that also logs the value of this to the console.

However, due to hoisting, the inner function is hoisted to the top of the scope, and its this context is set to the global object (Window or global object), rather than the outer function. This leads to unexpected behavior, as we might expect this to refer to the outer function.

This behavior can be surprising, especially when working with object methods or constructor functions, where this is expected to refer to the object or instance being created.

To avoid this issue, it’s important to understand how hoisting affects the this context in functions, and to use techniques like binding or arrow functions to control the this context explicitly.

6 What is the main advantage of using currying in JavaScript?

The main advantage of using currying in JavaScript is that it allows for:

  • Partial application: Currying enables you to apply a function to only some of its arguments, creating a new function that requires the remaining arguments. This is useful for creating reusable functions and reducing code duplication.
  • Improved code readability: Currying can make your code more readable by breaking down complex functions into simpler, more focused functions.
  • Easier function composition: Currying makes it easier to compose functions together, creating a more modular and flexible codebase.
  • Better support for functional programming: Currying is a fundamental concept in functional programming, and using it in JavaScript helps to create more functional, composable, and predictable code.

Here’s an example of currying in JavaScript:

function add(x, y) {
return x + y;
}

const addFive = add.bind(null, 5);
console.log(addFive(10)); // outputs: 15

In this example, we have created a curried version of the add function by using the bind method to fix the first argument to 5. The resulting addFive function requires only one argument, making it more flexible and reusable.

Overall, currying is a powerful technique in JavaScript that can help you write more modular, composable, and readable code.

7 Write a curry function that takes a function with multiple arguments and returns a curried version of that function.

Here is a curry function that takes a function with multiple arguments and returns a curried version of that function:

function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function(...moreArgs) {
return curried(...args.concat(moreArgs));
};
}
};
}

This curry function takes a function fn as an argument and returns a new curried function curried. The curried function takes a variable number of arguments args and checks if the number of arguments is equal to or greater than the number of arguments expected by the original function fn.

If the number of arguments is sufficient, the curried function calls the original function fn with the provided arguments and returns the result.

If the number of arguments is insufficient, the curried function returns a new function that expects the remaining arguments. This new function, when called, concatenates the previously provided arguments with the new arguments and calls the curried function again, repeating the process until all arguments are provided.

Here’s an example usage:

function add(x, y, z) {
return x + y + z;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // outputs: 6
console.log(curriedAdd(1, 2)(3)); // outputs: 6
console.log(curriedAdd(1, 2, 3)); // outputs: 6

In this example, we have defined a function add that takes three arguments and returns their sum. We have then created a curried version of add using the curry function. The curried function curriedAdd can be called with one, two, or three arguments, and it will return the result of the original add function when all arguments are provided.

8 How would you curry a function that takes an object as an argument?

Currying a function that takes an object as an argument requires a slightly different approach than currying a function with separate arguments. Here’s an example of how you can curry a function that takes an object as an argument:

function curryObject(fn) {
return function curried(obj = {}) {
return function(...args) {
return fn({...obj, ...args[0]});
};
};
}

This curryObject function takes a function fn as an argument and returns a new curried function curried. The curried function takes an object obj as an argument, which defaults to an empty object if not provided.

The curried function returns a new function that takes a variable number of arguments. If arguments are provided, the first argument is expected to be an object, which is merged with the original obj using the spread operator (…). The resulting object is then passed to the original function fn.

Here’s an example usage:

function greet(options) {
return `Hello, ${options.name}!`;
}

const curriedGreet = curryObject(greet);

console.log(curriedGreet({ name: 'Alice' })); // outputs: Hello, Alice!
console.log(curriedGreet()( { name: 'Bob' } )); // outputs: Hello, Bob!

In this example, we have defined a function greet that takes an object with a name property and returns a greeting message. We have then created a curried version of greet using the curryObject function.

The curried function curriedGreet can be called with an object as an argument, or it can be called without arguments and then called again with an object as an argument. In both cases, the resulting greeting message is returned.

Note that this implementation assumes that the object passed to the curried function will have the same shape as the object expected by the original function. If the object shape is different, you may need to modify the implementation accordingly.

9 Can you explain how currying relates to the concept of “function composition”?

Currying and function composition are closely related concepts in functional programming.

Function Composition

Function composition is the process of combining two or more functions to create a new function. This is achieved by passing the output of one function as the input to another function. In mathematical notation, this is represented as:

(f ∘ g)(x) = f(g(x))

In JavaScript, this can be achieved using the following syntax:

const composedFunction = (x) => f(g(x));

Currying

Currying, on the other hand, is a technique that transforms a function with multiple arguments into a sequence of functions, each taking a single argument. This allows for partial application and enables us to create new functions by fixing some of the arguments.

Relationship between currying and function composition

Currying and function composition are related in that currying enables us to compose functions more easily. When we curry a function, we create a new function that takes a single argument. This new function can then be composed with other functions, creating a new function that takes a single argument.

In other words, currying allows us to break down complex functions into smaller, more modular functions that can be composed together to create new functions. This makes our code more flexible, reusable, and easier to maintain.

Here’s an example of how currying and function composition can be used together:

const add = (x, y) => x + y;
const double = (x) => x * 2;

const addAndDouble = curry(add)(double);

console.log(addAndDouble(5)); // outputs: 10

In this example, we have defined a function add that takes two arguments and returns their sum. We have also defined a function double that takes a single argument and returns its double. We have curried the add function, fixing the first argument to the result of the double function, creating a new function addAndDouble that takes a single argument. Finally, we have called addAndDouble with the argument 5, which returns 10.

By combining currying and function composition, we can create complex functions from smaller, more modular functions, making our code more flexible and reusable.

10 Write a curried function that takes a string and a separator, and returns a function that splits the string using the separator.

Here is a curried function that takes a string and a separator, and returns a function that splits the string using the separator:

const curriedSplit = (separator) => (string) => string.split(separator);

// Usage:
const splitByComma = curriedSplit(',');
console.log(splitByComma('hello,world,currying')); // Output: ['hello', 'world', 'currying']

const splitBySpace = curriedSplit(' ');
console.log(splitBySpace('hello world currying')); // Output: ['hello', 'world', 'currying']

In this example, the curriedSplit function takes a separator as its first argument and returns a new function that takes a string as its argument. This new function splits the string using the separator provided earlier.

By currying the split function, we can create new functions that split strings using different separators, making our code more flexible and reusable.

11 Write a curried function that takes a function and its arguments, and returns a new function that calls the original function with the given arguments.

Here is a curried function that takes a function and its arguments, and returns a new function that calls the original function with the given arguments:

const curriedCall = (fn) => (...args) => (...moreArgs) => fn(...args, ...moreArgs);

// Usage:
const add = (x, y, z) => x + y + z;
const addFiveAndTen = curriedCall(add)(5, 10);

console.log(addFiveAndTen(2)); // Output: 17

In this example, the curriedCall function takes a function fn as its first argument and returns a new function that takes additional arguments args. This new function returns another function that takes even more arguments moreArgs and calls the original function fn with the combined arguments args and moreArgs.

By currying the call function, we can create new functions that call the original function with some arguments already applied, making our code more flexible and reusable.

12 Write a curried function that takes a function and returns a new function that calls the original function with its arguments reversed.

Here is a curried function that takes a function and returns a new function that calls the original function with its arguments reversed:

const curriedReverseArgs = (fn) => (...args) => fn(...args.reverse());

// Usage:
const subtract = (x, y) => x - y;
const reverseSubtract = curriedReverseArgs(subtract);

console.log(reverseSubtract(10, 5)); // Output: -5 (which is 5 - 10)

In this example, the curriedReverseArgs function takes a function fn as its argument and returns a new function that takes additional arguments args. This new function reverses the args array using the reverse() method and then calls the original function fn with the reversed arguments.

By currying the reverseArgs function, we can create new functions that call the original function with their arguments reversed, making our code more flexible and reusable.

Thanks for reading!

--

--