8 Advanced JavaScript Interview Questions for Senior Roles

Rabi Siddique
Level Up Coding
Published in
14 min readAug 20, 2023

--

JavaScript Interview Questions

JavaScript is a powerful language that is one of the primary building blocks of the web. This powerful language also has some of its quirks. For instance, did you know that 0 === -0 evaluates to true, or that Number("") yields 0?

The thing is sometimes these quirks can leave you scratching your head or even questioning was Brendon Eich high, the day he was inventing JavaScript. Well, the point is not here that JavaScript is a bad programming language or it is evil as its critics say. All programming languages have some sort of weirdness associated with them and JavaScript is not an exception.

In this blog post, we will see an in-depth explanation of some important JavaScript interview questions. My goal will be to explain these interview questions thoroughly so that we can understand the underlying concepts and hopefully solve other similar questions in interviews.

1-A Closer Look at the + and - Operators

console.log(1 + '1' - 1); 

Can you guess the behaviour of JavaScript’s + and - operators in situations like the one above?

When JavaScript encounters 1 + '1', it processes the expression using the + operator. One interesting property of the + operator is that it prefers string concatenation when one of the operands is a string. In our case, '1' is a string, so JavaScript implicitly coerces the numeric value 1 into a string. Consequently, 1 + '1' becomes '1' + '1', resulting in the string '11'.

Now, our equation is '11' - 1. The behaviour of the - operator is quite the opposite. It prioritizes numeric subtraction, regardless of the types of operands. When the operands are not of the number type, JavaScript performs implicit coercion to convert them into numbers. In this case, '11' is converted to the numeric value 11, and the expression simplifies to 11 - 1.

Putting it all together:

'11' - 1 = 11 - 1 = 10

2-Duplicating Array Elements

Consider the following JavaScript code and try to find any issues in this code:

function duplicate(array) {
for (var i = 0; i < array.length; i++) {
array.push(array[i]);
}
return array;
}

const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);

In this code snippet, we are required to create a new array containing the duplicated elements of the input array. Upon initial inspection, the code appears to create a new array newArr, by duplicating each element from the original array arr. However, a critical issue arises within the duplicate function itself.

The duplicate function uses a loop to go through each item in the given array. But inside the loop, it is adding a new element at the end of the array, using the push() method. This makes the array longer each time, creating a problem where the loop never stops. The loop condition (i < array.length) always stays true because the array keeps getting bigger. This makes the loop go on forever, causing the program to get stuck.

To address the problem of the infinite loop caused by the growing array length, you can store the initial length of the array in a variable before entering the loop. Then, you can use this initial length as the limit for the loop iteration. This way, the loop will only run for the original elements in the array and won’t be affected by the array’s growth due to duplicates being added. Here is the modified version of the code:

function duplicate(array) {
var initialLength = array.length; // Store the initial length
for (var i = 0; i < initialLength; i++) {
array.push(array[i]); // Push a duplicate of each element
}
return array;
}

const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);

The output will show the duplicated elements at the end of the array, and the loop won’t result in an infinite loop:

[1, 2, 3, 1, 2, 3]

3-Difference between prototype and __proto__

The prototype property is an attribute associated with constructor functions in JavaScript. Constructor functions are used to create objects in JavaScript. When you define a constructor function, you can also attach properties and methods to its prototype property. These properties and methods then become accessible to all instances of objects created from that constructor. Thus, theprototype property serves as a common repository for methods and properties that are shared among instances.

Consider the following code snippet:

// Constructor function
function Person(name) {
this.name = name;
}

// Adding a method to the prototype
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}.`);
};

// Creating instances
const person1 = new Person("Haider Wain");
const person2 = new Person("Omer Asif");

// Calling the shared method
person1.sayHello(); // Output: Hello, my name is Haider Wain.
person2.sayHello(); // Output: Hello, my name is Omer Asif.

In this example, we have a constructor function named Person. By extending the Person.prototype with a method like sayHello, we're adding this method to the prototype chain of all Person instances. This allows each instance of Person to access and utilize the shared method. Instead of each instance having its own copy of the method.

On the other hand, the __proto__ property, often pronounced as "dunder proto," exists in every JavaScript object. In JavaScript, everything, except primitive types, can be treated as an object. Each of these objects has a prototype, which serves as a reference to another object. The __proto__ property is simply a reference to this prototype object. The prototype object is used as a fallback source for properties and methods when the original object doesn’t possess them. By default, when you create an object, its prototype is set to Object.prototype.

When you attempt to access a property or method on an object, JavaScript follows a lookup process to find it. This process involves two main steps:

  1. Object’s Own Properties: JavaScript first checks if the object itself directly possesses the desired property or method. If the property is found within the object, it’s accessed and used directly.
  2. Prototype Chain Lookup: If the property is not found in the object itself, JavaScript looks at the object’s prototype (referenced by the __proto__ property) and searches for the property there. This process continues recursively up the prototype chain until the property is found or until the lookup reaches the Object.prototype.

If the property is not found even in the Object.prototype, JavaScript returns undefined, indicating that the property does not exist.

4-Scopes

When writing JavaScript code, it’s important to understand the concept of scope. Scope refers to the accessibility or visibility of variables within different parts of your code. Before proceeding with the example, if you’re unfamiliar with hoisting and how JavaScript code is executed, you can learn about it from this link. This will help you understand how JavaScript code works in more detail.

Let’s take a closer look at the code snippet:

function foo() {
console.log(a);
}

function bar() {
var a = 3;
foo();
}

var a = 5;
bar();

The code defines 2 functions foo() and bar() and a variable a with a value of 5. All these declarations happen in the global scope. Inside the bar() function, a variable a is declared and assigned the value 3. So when thebar() function is called, what value of a do you think it will print?

When the JavaScript engine executes this code, the global variable a is declared and assigned the value 5. Then the bar() function is called. Inside the bar() function, a local variable a is declared and assigned the value 3. This local variable a is distinct from the global variable a. After that, the foo() function is called from within the bar() function.

Inside the foo() function, the console.log(a) statement tries to log the value of a. Since there is no local variable a defined within the foo() function’s scope, JavaScript looks up the scope chain to find the nearest variable named a. The scope chain refers to all the different scopes that a function has access to when it’s trying to find and use variables.

Now, let’s address the question of where JavaScript will search for the variable a. Will it look within the scope of the bar function, or will it explore the global scope? As it turns out, JavaScript will search in the global scope, and this behaviour is driven by a concept called lexical scope.

Lexical scope refers to the scope of a function or variable at the time it was written in the code. When we defined the foo function, it was given access to both its own local scope and the global scope. This characteristic remains consistent no matter where we call the foo function—whether inside the bar function or if we export it to another module and run it there. Lexical scope is not determined where we call the function.

The upshot of this is that the output will always be the same: the value of a found in the global scope, which in this case is 5.

However, if we had defined the foo function within the bar function, a different scenario emerges:

function bar() {
var a = 3;

function foo() {
console.log(a);
}

foo();
}

var a = 5;
bar();

In this situation, the lexical scope of foo would encompass three distinct scopes: its own local scope, the scope of the bar function, and the global scope. Lexical scope is determined by where you place your code in the source code during compile time.

When this code runs, foo is situated within the bar function. This arrangement alters the scope dynamics. Now, when foo attempts to access the variable a, it will first search within its own local scope. Since it doesn't find a there, it will broaden its search to the scope of the bar function. Lo and behold, a exists there with the value 3. As a result, the console statement would prints 3.

5-Object Coercion

const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');

One intriguing facet to explore is how JavaScript handles the conversion of objects into primitive values, such as strings, numbers, or booleans. This is an interesting question that tests whether you know how coercion works with Objects.

This conversion is crucial when working with objects in scenarios like string concatenation or arithmetic operations. To achieve this, JavaScript relies on two special methods: valueOf and toString.

The valueOf method is a fundamental part of JavaScript's object conversion mechanism. When an object is used in a context that requires a primitive value, JavaScript first looks for the valueOf method within the object. In cases where the valueOf method is either absent or doesn't return an appropriate primitive value, JavaScript falls back to the toString method. This method is responsible for providing a string representation of the object.

Returning to our original code snippet:

const obj = {
valueOf: () => 42,
toString: () => 27
};

console.log(obj + '');

When we run this code, the object obj is converted to a primitive value. In this case, the valueOf method returns 42, which is then implicitly converted to a string due to the concatenation with an empty string. Consequently, the output of the code will be 42.

However, in cases where the valueOf method is either absent or doesn't return an appropriate primitive value, JavaScript falls back to the toString method. Let’s modify our previous example:

const obj = {
toString: () => 27
};

console.log(obj + '');

Here, we’ve removed the valueOf method, leaving only the toString method, which returns the number 27. In this scenario, JavaScript will resort to the toString method for object conversion.

6-Understanding Object Keys

When working with objects in JavaScript, it’s important to grasp how keys are treated and assigned within the context of other objects. Consider the following code snippet and take some time to guess the output:

let a = {};
let b = { key: 'test' };
let c = { key: 'test' };

a[b] = '123';
a[c] = '456';

console.log(a);

At first glance, it might seem like this code should produce an object a with two distinct key-value pairs. However, the outcome is quite different due to JavaScript's handling of object keys.

JavaScript employs the default toString() method to convert object keys into strings. But why? In JavaScript, object keys are always strings(or symbols), or they are automatically converted to strings via implicit coercion. When you use any value other than a string (e.g., a number, object, or symbol) as a key in an object, JavaScript will internally convert that value to its string representation before using it as a key.

Consequently, when we use objects b and c as keys in object a, both are transformed into the same string representation: [object Object]. Due to this behaviour, the second assignment, a[b] = '123'; will overwrite the first assignment a[c] = '456';. Let’s break down the code step by step:

  1. let a = {};: Initializes an empty object a.
  2. let b = { key: 'test' };: Creates an object b with a property key having the value 'test'.
  3. let c = { key: 'test' };: Defines another object c with the same structure as b.
  4. a[b] = '123';: Sets the value '123' to the property with key [object Object] in object a.
  5. a[c] = '456';: Updates the value to '456' for the same property with key [object Object] in object a, replacing the previous value.

Both assignments utilize the identical key string [object Object]. As a result, the second assignment overwrites the value set by the first assignment.

When we log the object a, we observe the following output:

{ '[object Object]': '456' }

7-The Double Equals Operator

console.log([] == ![]); 

This one is a bit complex. So, what do you think will be the output? Let’s evaluate this step by step. Let’s first start by seeing the types of both operands:

typeof([]) // "object"
typeof(![]) // "boolean"

For [] it is an object, which is understandable. As everything in JavaScript is an object including arrays and functions. But how does the operand![]has a type of boolean? Let’s try to understand this. When you use ! with a primitive value, the following conversions happen:

  1. Falsy Values: If the original value is a falsy value (such as false, 0, null, undefined, NaN, or an empty string ''), applying ! will convert it to true.
  2. Truthy Values: If the original value is a truthy value (any value that is not falsy), applying ! will convert it to false.

In our case [] is an empty array, which is a truthy value in JavaScript. Since [] is truthy, ![] becomes false. So our expression becomes:

[] == ![]
[] == false

Now let’s move ahead and understand the == operator. Whenever 2 values are compared using == operator, JavaScript performs the Abstract Equality Comparison Algorithm. The algorithm has the following steps:

Abstract Equality Comparison Algorithm

As you can see this algorithm takes into account the types of the compared values and performs necessary conversions.

For our case, let’s denote x as [] and y as ![]. We inspected the types of x and y and found x as an object and y as boolean. Since y is a boolean and x is an object, condition 7 from the abstract equality comparison algorithm is applied:

If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).

Meaning if one of the types is a boolean, we need to convert it into a number before comparison. What’s the value of ToNumber(y)? As we saw, []is a truthy value, negating makes it false. As a result,Number(false)is 0.

[] == false
[] == Number(false)
[] == 0

Now we have the comparison [] == 0 and this time condition 8 comes into play:

If Type(x) is either String or Number and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).

Based on this condition, if one of the operands is an object, we must convert it into a primitive value. This is where the ToPrimitive algorithm comes into the picture. We need to convert x which is [] to a primitive value. Arrays are objects in JavaScript. As we saw earlier that when converting objects to primitives, valueOf and toString methods come into play. In this case, valueOf returns the array itself which is not a valid primitive value. As a result, we move to toString for an output. Applying the toStringmethod to an empty array results in obtaining an empty string, which is a valid primitive:

[] == 0
[].toString() == 0
"" == 0

Converting the empty array to a string gives us an empty string, "" and now we face the comparison: "" == 0.

Now that one of the operands is of the type stringand the other one is of the type number, condition 5 holds:

If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.

Hence, we need to convert the empty string, "" to a number, which gives us a 0.

"" == 0
ToNumber("") == 0
0 == 0

Finally, both operands have the same type and condition 1 holds. As both have the same value, the final output is:

0 == 0 // true

So far we made use of coercion in the last few questions we explored, which is an important concept in mastering JavaScript and tackling questions like this in an interview, which tend to be asked a lot. I really encourage you to check out my detailed blog post about coercion. It explains this concept in a clear and thorough way. Here’s the link.

8-Closures

This is one of the most famous interview questions asked related to closures:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}

If you know the output, then well and good. So let’s try to understand this snippet. At the face value, it looks like this snippet would give us the output of:

Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

But this is not the case over here. Due to the concept of closures and how JavaScript handles variable scope, the actual output will be different. When the setTimeout callbacks are executed after the delay of 3000 milliseconds, they will all reference the same variable i, which will have a final value of 4 after the loop has been completed. As a result, the output of the code will be:

Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined

This behaviour occurs because the var keyword does not have a block scope, and the setTimeout callbacks capture the reference to the same i variable. When the callbacks execute, they all see the final value of i, which is 4, and try to access arr[4], which is undefined.

To achieve the desired output, you can use the let keyword to create a new scope for each iteration of the loop, ensuring that each callback captures the correct value of i:

const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}

With this modification, you will get the expected output:

Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

Using let creates a new binding for i in each iteration, ensuring that each callback refers to the correct value.

Often, developers have become familiar with the solution involving the let keyword. However, interviews can sometimes take a step further and challenge you to solve the problem without using let. In such cases, an alternative approach involves creating a closure by immediately invoking a function(IIFE) inside the loop. This way, each function call has its own copy of i. Here's how you can do it:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
(function(index) {
setTimeout(function() {
console.log('Index: ' + index + ', element: ' + arr[index]);
}, 3000);
})(i);
}

In this code, the immediately invoked function (function(index) { ... })(i); creates a new scope for each iteration, capturing the current value of i and passing it as the index parameter. This ensures that each callback function gets its own separate index value, preventing the closure-related issue and giving you the expected output:

Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

Thank you for reading. I hope this post is helpful in your interview preparation journey. If you have any further questions, don’t hesitate to reach out. I’m always happy to help.

Let’s connect:
LinkedIn
Twitter

Level Up Coding

Thanks for being a part of our community! Before you go:

🔔 Follow us: Twitter | LinkedIn | Newsletter

🚀👉 Join the Level Up talent collective and find an amazing job

--

--

A passionate Software Engineer who intends to be the best and nothing less.