Stumped! Eight Level JavaScript Interview Questions for Advanced Front-End

JavaScript is a powerful language and one of the foundations upon which the modern Web is built. This powerful language also has some quirks of its own. For example, did you know that 0 === -0 evaluates to true, or that Number("") returns 0?

Sometimes, these quirks leave you baffled and make you wonder if Brendan Eich was in bad shape the day he invented JavaScript. But the point here is not that JavaScript is a bad programming language or, as its critics say, an "evil" language. All programming languages ​​have some degree of quirks, and JavaScript is no exception.

In this blog post, we will explain some important JavaScript interview questions in depth. My goal is to explain these interview questions thoroughly so that we can understand the basic concepts behind them and hopefully address other similar questions during interviews.

1. Observe the + and - operators carefully

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

Can you guess how JavaScript's + and - operators will behave in this case?

When JavaScript encounters 1 + '1', it uses the + operator to process this expression. An interesting property of the + operator is that it prefers to perform string concatenation when one of its operands is a string. In our example, '1' is a string, so JavaScript implicitly converts the number 1 to a string. Therefore, 1 + '1' becomes '1' + '1', and the result is the string '11'.

Now, our equation is '11' - 1. The - operator behaves exactly the opposite. It prefers to perform numeric subtraction regardless of the type of operands. When operands are not of numeric type, JavaScript performs an implicit conversion to convert them to numbers. In this case, '11' is converted to the numeric value 11 and the expression simplifies to 11 - 1.

Considering:

'11' - 1 = 11 - 1 = 10

2. Copying array elements

Consider the following JavaScript code and try to identify the problem:

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 need to create a new array containing the repeated elements of the input array. Upon initial inspection, the code appears to create a new array newArr by copying each element in the original array arr. However, a serious problem arises inside the duplicate function.

The duplicate function uses a loop to iterate through each item in the given array. But inside the loop, it uses push() method to add new elements at the end of the array. This causes the array to get longer each time, creating a problem: the loop never stops. Because the array length keeps increasing, the loop condition (i < array.length) is always true. This causes the loop to continue indefinitely, causing the program to reach a deadlock.

To solve the problem of infinite loops due to the length of the array growing, you can store the initial length of the array in a variable before entering the loop. This initial length can then be used as a limit for loop iterations. This way, the loop will only loop over the original elements in the array and will not be affected by the array's growth due to the addition of duplicates. Here is the modified code:

function duplicate(array) {
  var initialLength = array.length; // 存储初始长度
  for (var i = 0; i < initialLength; i++) {
    array.push(array[i]); // 推入每个元素的副本
  }
  return array;
}

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

The output will show duplicate elements at the end of the array, and the loop will not cause an infinite loop:

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

3. The difference between prototype and prototype

The prototype property is a property associated with constructors in JavaScript. Constructors are used to create objects in JavaScript. When you define a constructor, you can also attach properties and methods to its prototype property. These properties and methods then become accessible to all object instances created by this constructor. Therefore, the prototype attribute acts as a common repository for shared methods and properties.

Consider the following code snippet:

// 构造函数
function Person(name) {
  this.name = name;
}

// 添加一个方法到 prototype
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

// 创建实例
const person1 = new Person("Haider Wain");
const person2 = new Person("Omer Asif");

// 调用共享的方法
person1.sayHello();  // 输出:Hello, my name is Haider Wain.
person2.sayHello();  // 输出:Hello, my name is Omer Asif.

On the other hand, the __proto__ attribute, often pronounced "dunder proto", exists in every JavaScript object. In JavaScript, everything can be treated as an object except primitive types. Each such object has a prototype, which serves as a reference to another object. The __proto__ attribute is simply a reference to this prototype object.

When you try to access a property or method on an object, JavaScript performs a lookup process to find it. This process mainly involves two steps:

Object's own properties: JavaScript first checks whether the object itself directly possesses the required property or method. If the property is found within the object, it is accessed and used directly. Prototype chain lookup: If the property is not found on the object itself, JavaScript will look at the object's prototype (referenced by the __proto__ attribute) and search for the property there. This process proceeds recursively along the prototype chain until the property is found or until the search reaches Object.prototype. If the property is not even found in Object.prototype, JavaScript will return undefined, indicating that the property does not exist.

4. Scope

When writing JavaScript code, it's important to understand the concept of scope. Scope refers to the accessibility or visibility of a variable in different parts of the code. Let's take a closer look at this concept through a code snippet:

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

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

var a = 5;
bar();

The code defines two functions foo() and bar(), and a variable a with a value of 5. All these declarations occur in the global scope. Inside the bar() function, a variable a is declared and assigned a value of 3. So when the bar() function is called, which value of a do you think will be output?

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

Inside the foo() function, the console.log(a) statement attempts to output the value of variable a. Since the local variable a is not defined within the scope of the foo() function, JavaScript looks up the scope chain to find the nearest variable named a.

Now, let's answer the question of where JavaScript will search for variable a. Will it look in the bar function's scope, or will it explore the global scope? It turns out that JavaScript searches in the global scope, and this behavior is driven by a concept called lexical scoping.

Lexical scope refers to the scope of a function or variable when it is written in the code. When we define foo function, it is given access to its own local scope and global scope. This property is consistent no matter where we call the foo function, whether it is running inside the bar function or in another module. Lexical scope is not determined by where we call the function.

The end result is that the output is always the value of a found in the global scope, which is 5 in this example.

However, if we define the foo function inside the bar function, the situation is different:

function bar() {
  var a = 3;

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

  foo();
}

var a = 5;
bar();

In this case, the lexical scope of foo will include three different scopes: its own local scope, the scope of the bar function, and the global scope. Lexical scope is determined at compile time by where in the source code you place the code.

When this code runs, foo is inside the bar function. This arrangement changes the dynamics of the scope. Now, when foo tries to access variable a, it first searches within its own local scope. Since a is not found, it expands the search scope to the scope of the bar function. Sure enough, there is an a with a value of 3. Therefore, the console statement will output 3.

5. Object cast type conversion

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

One fascinating aspect is exploring how JavaScript handles converting objects into primitive values ​​such as strings, numbers, or booleans. This is a fun question that tests your understanding of object casting.

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 basic value, JavaScript first looks for the valueOf method inside the object. In cases where the valueOf method does not exist or does not return an appropriate base value, JavaScript falls back to the toString method. This method is responsible for providing a string representation of the object.

Back 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 the empty string. Therefore, the output of the code will be 42.

However, in the case where the valueOf method does not exist or does not return an appropriate base value, JavaScript falls back to the toString method. Let's modify our previous example:

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

console.log(obj + '');

Here, we have removed the valueOf method, leaving only the toString method that returns the number 27. In this case, JavaScript will rely on the toString method for object conversion.

6. Understand Object Keys

When working with objects in JavaScript, it's important to understand how keys are handled and assigned in the context of other objects. Consider the following code snippet and take a moment 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 seems like this code should produce an object a with two different key-value pairs. However, due to the way JavaScript handles object keys, the results are completely different.

JavaScript uses the default toString() method to convert object keys to strings. why? In JavaScript, object keys are always strings (or symbols), or are automatically converted to strings via implicit casts. When you use any value other than a string (for example, a number, object, or symbol) as a key in an object, JavaScript will internally convert the value to its string representation before using it as a key.

So when we use objects b and c as keys in object a, both are converted to the same string representation: [object Object]. Because of this behavior, the second assignment a[c] = '456'; overwrites the first assignment a[b] = '123';.

Finally, when we log object a, we observe the following output:

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

7. Double equal sign operator

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

This one is a bit complicated. So, what do you think the output will be?

This issue is quite complex. So, what do you think the output will be? Let's evaluate it step by step. First, let's look at the types of the two operands:

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

For [], it is an object, which is understandable because in JavaScript, everything including arrays and functions are objects. But how does the operand ![] have type Boolean? Let's try to understand. When you use ! with a primitive value, the following conversion occurs:

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

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

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

Now, let’s move on to the == operator. When you use the == operator to compare two values, JavaScript performs the "Abstract Equality Comparison Algorithm." This algorithm takes into account the type of the compared values ​​and performs the necessary conversions.

In our case, let us denote x as [] and y as ![]. We checked the types of x and y and found that x is an object and y is a boolean. Since y is a boolean and x is an object, the 7th condition of the algorithm is applied:

如果 Type(y) 是 Boolean,则返回 x == ToNumber(y) 的比较结果。

This means that if one of the types is a boolean, we need to convert it to a number before comparing. What is the value of ToNumber(y)? As we can see, [] is a true value and negation makes it false. Therefore, Number(false) is 0.

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

Now we have a comparison of [] == 0, and this time the 8th condition of the algorithm comes into play:

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

Based on this condition, if one of the operands is an object, we must convert it to a primitive value. This is where the "ToPrimitive algorithm" comes in. We need to convert x (i.e. []) into a primitive value. Arrays are objects in JavaScript. The valueOf and toString methods work when converting an object to a primitive value. In this case, valueOf returns the array itself, which is not a valid primitive value. So we turn to toString to get the output. Applying the toString method to an empty array results in an empty string, which is a valid primitive value:

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

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

Now that one of the operands is of type string and the other is a number, the fifth condition of the algorithm holds:

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

Therefore, 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. Since both have the same value, the final output is:

0 == 0 // true

At this point, we've used coercion to solve the last few problems we've discussed, which is an important concept for mastering JavaScript and solving common problems like these in interviews. I highly recommend you check out my detailed blog post on casting. It explains the concept in a clear and thorough way. Here is the link.

Guess you like

Origin blog.csdn.net/wangonik_l/article/details/133080717
Recommended