Front-end interview question bank ( essential for interviews) Recommended: ★★★★★
Address: Front-end interview question bank
My cousin made her own five-star red flag National Day avatar with just one click. It’s super nice.
1. Ternary operator
In actual work, we often need to judge a variable or expression and perform different operations based on different conditions. Usually, we need to use statements if...else
. However, if...else
the statement is too lengthy and cumbersome and can be optimized using the ternary operator.
Taking the code to implement a state machine as an example, the original code may look like this:
function handleEvent(event) {
if (currentState === STATE_INITIAL) {
// do something
} else if (currentState === STATE_SECONDARY) {
// do something
} else if (currentState === STATE_FINAL) {
// do something
}
}
The ternary operator can be used to simplify such code:
function handleEvent(event) {
currentState === STATE_INITIAL ?
// do something :
currentState === STATE_SECONDARY ?
// do something :
// do something
}
Using the ternary operator can make the code more concise and understandable, and reduce the code file size. At the same time, the ternary operator can improve the readability of the code, and the logic can be seen at a glance, so that the problem can be found and debugged more quickly. In some specific scenarios, using the ternary operator can also improve performance, because some engines cannot understand if...else
the meaning of the statement well, which will reduce its performance.
Note: Although using the ternary operator to optimize
if...else
statements can greatly improve the readability, maintainability and performance of the code, it is worthy of our use in actual work. However, it should be noted that if a condition is complex or has multiple branches, using the ternary operator will make the code difficult to read and understand, so do not abuse it.
2. Be clever
Let’s take a look at the “little clever tricks” in two specific scenarios~
2.1 Short-circuit Evaluation
Short-circuit evaluation means that in logical operations, if the first expression can already determine the value of the entire expression, the subsequent expressions will not be evaluated. Taking advantage of this feature, statements can be shortened using the &&
AND ||
operator if...else
. For example:
let x = 10;
let result;
if (x > 5) {
result = "x is greater than 5";
} else {
result = "x is less than or equal to 5";
}
// 利用 && 运算符
let x = 10;
let result = x > 5 && "x is greater than 5" || "x is less than or equal to 5";
2.2 String concatenation
Let’s first look at a more special type of if...else
statement:
function setAccType(accType) {
if (accType == "PLATINUM") {
return "Platinum Customer";
} else if (accType == "GOLD") {
return "Gold Customer";
} else if (accType == "SILVER") {
return "Silver Customer";
}
}
For statements like this that have a very strong correlation between conditions and return values, we can usually handle them in the following very concise way, so that multiple rows can be converted into one row in seconds:
function setAccType(accType){
return accType[0] + accType.slice(1).toLowerCase() + ' Customer';
// or
return `${accType[0] + accType.slice(1).toLowerCase()} Customer`;
}
3、switch/case
switch/case
Statements are also a commonly used technique. Let’s take a look at the following example:
if (status === 200) {
handleSuccess()
} else if (status === 401) {
handleUnauthorized()
} else if (status === 404) {
handleNotFound()
} else {
handleUnknownError()
}
Like this, we can switch/case
process it through:
switch (status) {
case 200:
handleSuccess()
break
case 401:
handleUnauthorized()
break
case 404:
handleNotFound()
break
default:
handleUnknownError()
break
}
Although there are a few more lines of code, it avoids repeated congruence checks again and again, and is overall more streamlined and intuitive.
4. Array includes method
Suppose we want to return an array of ingredients based on the type of food provided. If using if..else
statements, they would usually be written like this:
function getIngredients(foodType) {
let ingredients = [];
if (foodType === 'pizza') {
ingredients = ['cheese', 'sauce', 'pepperoni'];
} else if (foodType === 'burger') {
ingredients = ['bun', 'beef patty', 'lettuce', 'tomato', 'onion'];
} else if (foodType === 'taco') {
ingredients = ['tortilla', 'beef', 'lettuce', 'shredded cheese'];
} else if (foodType === 'sandwich') {
ingredients = ['bread', 'turkey', 'lettuce', 'mayo'];
} else {
console.log('Unknown food type');
}
return ingredients;
}
In such a scenario, you can use includes
the method to optimize this lengthy if...else
statement:
function getIngredients(foodType) {
const menu = [
{food: 'pizza', ingredients: ['cheese', 'sauce', 'pepperoni']},
{food: 'burger', ingredients: ['bun', 'beef patty', 'lettuce', 'tomato', 'onion']},
{food: 'taco', ingredients: ['tortilla', 'beef', 'lettuce', 'shredded cheese']},
{food: 'sandwich', ingredients: ['bread', 'turkey', 'lettuce', 'mayo']}
];
const item = menu.find(menuItem => menuItem.food === foodType);
if (item === undefined) {
console.log('Unknown food type');
return [];
}
return item.ingredients;
}
The benefits of doing this are:
- More readable: Using a combination of arrays and objects instead of
if..else
statements makes the code more concise and easy to understand, and reduces nesting, making it easier to read and understand. - Better scalability: To add a new menu item, you only need to
menu
add a new object to the array without modifying the original functiongetIngredients
, which greatly improves the scalability of the code. - Better maintainability: When you want to delete or update a menu item, you only need to modify
menu
the array, without modifying the original functiongetIngredients
. Therefore, the maintainability of the code is also better.
5. Use objects or Maps
5.1 Objects
For example, we usually encounter this scenario:
function getStatusColor (status) {
if (status === 'success') {
return 'green'
}
if (status === 'warning') {
return 'yellow'
}
if (status === 'info') {
return 'blue'
}
if (status === 'error') {
return 'red'
}
}
const statusColors = {
success: 'green',
warning: 'yellow',
info: 'blue',
error: 'red'
};
function getStatusColor(status) {
return statusColors[status] || '';
}
There are two benefits to doing this:
- Better readability: When you need to add a new state, you only need to add a new key-value pair.
if..else
This is easier to read and maintain than the original statement. - More efficient execution: Because in JavaScript, object property access is constant time, while
if..else
statements need to match conditions one by one. Since we only need to access one property, using an object mapif..else
is more efficient than a statement.
5.2 Map
Let’s look at the following example, where we want to print fruits based on color:
function test(color) {
switch (color) {
case 'red':
return ['apple', 'strawberry'];
case 'yellow':
return ['banana', 'pineapple'];
case 'purple':
return ['grape', 'plum'];
default:
return [];
}
}
test(null); // []
test('yellow'); // ['banana', 'pineapple']
We can use Map
data structures to optimize the above code. The specific implementation is as follows:
const foodMap = new Map([
['red', ['apple', 'strawberry']],
['yellow', ['banana', 'pineapple']],
['purple', ['grape', 'plum']]
]);
function test(color) {
return foodMap.get(color) || [];
}
The benefits of doing this are:
- This code is more concise: using
Map
data structures makes the code shorter and more readable. - Higher maintainability: Store the data in
Map
. If you need to modify or add a color and corresponding food list, you only need toMap
modify or add a record in . - Faster search time: Since
Map
a hash table is used internally to store key-value pairs, the search time may be faster than the conditional statement-based approach. When there is a large amount of data, this optimization may significantly improve the performance of the code.
6. Reduce nesting and return early
6.1 Scenario 1
Suppose we need to return a string representing the life stage of the user based on their age. Using if..else
a statement, you might generally write it like this:
function getLifeStage(age) {
let stage;
if (age < 1) {
stage = 'baby';
} else {
if (age < 4) {
stage = 'toddler';
} else {
if (age < 13) {
stage = 'child';
} else {
if (age < 20) {
stage = 'teenager';
} else {
if (age < 65) {
stage = 'adult';
} else {
stage = 'senior';
}
}
}
}
}
return `Your life stage is ${stage}`;
}
We can optimize this lengthy statement by reducing nesting and returning early if...else
, as shown below:
function getLifeStage(age) {
if (age < 1) {
return 'Your life stage is baby';
}
if (age < 4) {
return 'Your life stage is toddler';
}
if (age < 13) {
return 'Your life stage is child';
}
if (age < 20) {
return 'Your life stage is teenager';
}
if (age < 65) {
return 'Your life stage is adult';
}
return 'Your life stage is senior';
}
The benefits of doing this are:
- More readable: The nesting levels and braces are reduced, making the code more concise and clear, and easy to read and understand.
- Better scalability: When you need to add or delete an age group, you only need to insert or delete an if statement at the corresponding position without affecting other parts of the code, which greatly improves the scalability of the code.
- Better error handling: Each if statement can handle one case, which returns the correct result more promptly. At the same time, it avoids the situation of repeatedly executing the same code block when multiple judgment conditions match.
6.2 Scenario 2
Let’s take a look at the following example:
function test(fruit, quantity) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
// 条件1: fruit 必须非空
if (fruit) {
// 条件2: 必须是红色的水果
if (redFruits.includes(fruit)) {
console.log('red');
// 条件3: 必须大于10个
if (quantity > 10) {
console.log('big quantity');
}
}
} else {
throw new Error('No fruit!');
}
}
test(null); // error: No fruits
test('apple'); // red
test('apple', 20); // red, big quantity
Looking at the code above, we have:
- 1 if/else statement to filter out invalid conditions
- 3 levels of nested if statements (conditions 1, 2 and 3).
The general rule I personally follow is to return things early that are easier to handle, require less calculations, and have a low probability.
function test(fruit, quantity) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
// 先判断小概率情况,提前返回
if (!fruit) throw new Error('No fruit!');
if (redFruits.includes(fruit)) {
console.log('red');
if (quantity > 10) {
console.log('big quantity');
}
}
}
By doing this, we have one less layer of nested statements. This way of coding is good, especially when you have long if statements (imagine you need to scroll to the bottom to know that there is an else statement, which may not be very readable). We can further reduce nested ifs by inverting conditionals and returning early. for example:
function test(fruit, quantity) {
const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
if (!fruit) throw new Error('No fruit!');
if (!redFruits.includes(fruit)) return;
console.log('red');
if (quantity > 10) {
console.log('big quantity');
}
}
By reversing the condition of condition 2, our code now has no nested statements, making the code very concise and intuitive.
7. Hidden high-level skills: find (Boolean)
Students who write react may often encounter this situation in some component reuse scenarios: a small number of components in the component will be displayed according to the incoming props attributes.
function ComponentByType({ type }) {
if (type === "type_1") {
return (
<>
{/* 其他很复杂的组件 */}
<Component1 />
{/* 其他很复杂的组件 */}
</>
);
}
if (type === "type_2") {
return (
<>
{/* 其他很复杂的组件 */}
<Component2 />
{/* 其他很复杂的组件 */}
</>
);
}
if (type === "type_3") {
return (
<>
{/* 其他很复杂的组件 */}
<Component3 />
{/* 其他很复杂的组件 */}
</>
);
}
if (type === "type_4") {
return (
<>
{/* 其他很复杂的组件 */}
<Component4 />
{/* 其他很复杂的组件 */}
</>
);
}
}
In this case, find(Boolean)
this little trick can be used to greatly simplify it:
function ComponentByType({ type }) {
return (
<>
{/* 其他很复杂的组件 */}
<>
{
[
type === "type_1" && <Component1 />,
type === "type_2" && <Component2 />,
type === "type_3" && <Component3 />,
type === "type_4" && <Component4 />
].find(Boolean)
}
</>
{/* 其他很复杂的组件 */}
</>
);
}
find(Boolean)
Conditional judgment can be achieved because in JavaScript, Boolean values of true and false can be converted into numbers 1 and 0. In this example, when a certain condition is not met, the corresponding React component (for example <Component1 />
) will be parsed as a Boolean value false and stored in the array, and conversely when the condition is met, the corresponding React component will be parsed as a true value true. In this way, the user find(Boolean)
can find the first true value (that is, the first React component that meets the condition) and render it to the page.
It should be noted that when none of the conditions are met,
.find()
the method returns undefined, which will cause React rendering errors. But since in this example we ignore the result using short-circuit syntax, no error will be reported.
If you want to handle the situation where the condition is not met more accurately, you can explicitly .find()
add a default return value later and return the default component when no component that meets the condition is found. For example:
[
type === "type_1" && <Component1 />,
type === "type_2" && <Component2 />,
type === "type_3" && <Component3 />,
type === "type_4" && <Component4 />
].find(Boolean) || <DefaultComponent />
In this way, when none of the four types match, <DefaultComponent>
the component will be rendered by default, avoiding React rendering errors. By using it find(Boolean)
, you can simplify your code and avoid the complexity and verbosity of using multiple if/else
statements or statements.switch/case
What is the principle?
find(Boolean)
The function is to find the first true
element in the array with a Boolean value of , and return the value of that element. The array contains multiple conditional statements and corresponding React components. When a certain condition is met, the corresponding component will be returned and rendered on the page; otherwise, it will be returned. Since we use the conditional operator, the return value false
will &&
be Automatically ignored.
For example, a simple example:
[0,false,null,1,2].find(Boolean) // 返回 1
new
Calling the function without Boolean
it will convert the passed argument to a Boolean value. For each element in the array, Boolean
the results after using the function to perform type conversion are as follows:
0
is converted tofalse
false
itself a boolean valuefalse
null
is converted tofalse
1
is converted totrue
2
is converted totrue
Therefore, when using find
the method to find the first true value, true
the index of the first element with a boolean value of is returned. In this array, the first true
element with a boolean value is 1
, and its index is 3, so find
the method returns 1. It should be noted that if there is no true
element with a Boolean value in the array, find
the method returns undefined.
[0,false,null,1,2].map(Boolean); // [false, false, false, true, true]
[0,false,null,1,2].find(Boolean); // 1
Among them, map
the method applies Boolean
the function once to each element in the array to obtain a new Boolean array. As you can see, in this new array, the first true
element with a Boolean value is true
, and its index is 3. Therefore, when find
the method looks for the first true
element in the original array with a boolean value of , it returns 1.
8. Chain of responsibility model
Chain of Responsibility Pattern is a behavioral design pattern that allows multiple objects to have the opportunity to handle requests. In JavaScript, we can use the chain of responsibility pattern to optimize complex and lengthy if...else
statements.
In the chain of responsibility model, each processor forms a chain. When a request occurs, these processors try to process the request in order until one processor successfully handles the request or all processors have completed their attempts. Therefore, the chain of responsibility pattern can dynamically organize and modify the processing flow, thereby improving the flexibility of the code.
In actual work, there are many scenarios where the chain of responsibility pattern can be used to optimize complex and lengthy if...else
statements.
For example, on an e-commerce platform, users need to perform a series of verification and processing after placing an order, such as verifying whether the product inventory is sufficient, verifying whether the delivery address is correct, etc. These checksum processing operations may have multiple steps. If traditional if...else
statements are used to implement them, the code can easily become lengthy and difficult to understand. Here is a simple example code:
function placeOrder(order) {
if (!order.items || !Array.isArray(order.items) || order.items.length === 0) {
console.log("At least one item should be specified to place an order.");
return;
}
// 校验商品库存是否充足
for (let i = 0; i < order.items.length; i++) {
let item = order.items[i];
if (!checkStock(item.productId, item.quantity)) {
console.log(`Product ${item.productId} is out of stock.`);
return;
}
}
if (!order.address || !isValidAddress(order.address)) {
console.log("Invalid address.");
return;
}
// 扣减库存
for (let i = 0; i < order.items.length; i++) {
let item = order.items[i];
decreaseStock(item.productId, item.quantity);
}
// 生成订单
let orderId = generateOrderId();
console.log(`Order ${orderId} placed successfully.`);
}
In the above code, we use multiple if...else
statements to complete the order verification and processing operations. Such code is difficult to maintain and extend, and does not comply with the open-closed principle (open for extension, closed for modification).
Using the chain of responsibility model can effectively solve this problem. Here is a sample code that uses the chain of responsibility pattern to implement order processing:
class OrderValidator {
constructor() {
this.nextHandler = null;
}
setNext(handler) {
this.nextHandler = handler;
return handler;
}
handle(order) {}
}
class ItemValidator extends OrderValidator {
handle(order) {
if (!order.items || !Array.isArray(order.items) || order.items.length === 0) {
console.log("At least one item should be specified to place an order.");
return false;
} else {
return this.nextHandler.handle(order);
}
}
}
class StockValidator extends OrderValidator {
handle(order) {
for (let i = 0; i < order.items.length; i++) {
let item = order.items[i];
if (!checkStock(item.productId, item.quantity)) {
console.log(`Product ${item.productId} is out of stock.`);
return false;
}
}
return this.nextHandler.handle(order);
}
}
class AddressValidator extends OrderValidator {
handle(order) {
if (!order.address || !isValidAddress(order.address)) {
console.log("Invalid address.");
return false;
} else {
return this.nextHandler.handle(order);
}
}
}
class StockDeductor extends OrderValidator {
handle(order) {
for (let i = 0; i < order.items.length; i++) {
let item = order.items[i];
decreaseStock(item.productId, item.quantity);
}
return this.nextHandler.handle(order);
}
}
class OrderGenerator extends OrderValidator {
handle(order) {
let orderId = generateOrderId();
console.log(`Order ${orderId} placed successfully.`);
return true;
}
}
let itemValidator = new ItemValidator();
let stockValidator = new StockValidator();
let addressValidator = new AddressValidator();
let stockDeductor = new StockDeductor();
let orderGenerator = new OrderGenerator();
itemValidator.setNext(stockValidator).setNext(addressValidator).setNext(stockDeductor).setNext(orderGenerator);
function placeOrder(order) {
itemValidator.handle(order);
}
In the above code, we define multiple order processors (such as ItemValidator
, StockValidator
etc.), which inherit from the OrderValidator
class. These processors form a processing chain and setNext()
are connected by methods. When the user places an order, we only need to call placeOrder()
the function and pass in the order object as a parameter.
During processing, each processor checks whether the order meets its requirements. If satisfied, continue to hand it over to the next processor for processing, otherwise return directly. If all processors can successfully process the order, the prompt message "Order x placed successfully." will eventually be output.
Benefits of using the chain of responsibility model:
-
The code is more flexible and easy to expand: when you need to add or modify processing steps, you only need to add or modify the corresponding processor class, and there is no need to modify the original code.
-
Higher code reusability: different processor classes can be reused, resulting in better readability and maintainability.
-
The code structure is clearer and easier to understand: use the chain of responsibility pattern to decompose the system into multiple small and simple parts, each part has its own processing logic and responsibilities. This code structure is clearer and easier to understand.
Front-end interview question bank ( essential for interviews) Recommended: ★★★★★
Address: Front-end interview question bank
My cousin made her own five-star red flag National Day avatar with just one click. It’s super nice.