[JavaScript] 10 tips to get rid of those ugly and lengthy if...else statements in your code~

 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..elsestatements, 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 includesthe method to optimize this lengthy if...elsestatement:

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:

  1. More readable: Using a combination of arrays and objects instead of if..elsestatements makes the code more concise and easy to understand, and reduces nesting, making it easier to read and understand.
  2. Better scalability: To add a new menu item, you only need to menuadd a new object to the array without modifying the original function getIngredients, which greatly improves the scalability of the code.
  3. Better maintainability: When you want to delete or update a menu item, you only need to modify menuthe array, without modifying the original function getIngredients. 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:

  1. Better readability: When you need to add a new state, you only need to add a new key-value pair. if..elseThis is easier to read and maintain than the original statement.
  2. More efficient execution: Because in JavaScript, object property access is constant time, while if..elsestatements need to match conditions one by one. Since we only need to access one property, using an object map if..elseis 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 Mapdata 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:

  1. This code is more concise: using Mapdata structures makes the code shorter and more readable.
  2. Higher maintainability: Store the data in Map. If you need to modify or add a color and corresponding food list, you only need to Mapmodify or add a record in .
  3. Faster search time: Since Mapa 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..elsea 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:

  1. More readable: The nesting levels and braces are reduced, making the code more concise and clear, and easy to read and understand.
  2. 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.
  3. 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/elsestatements or statements.switch/case

What is the principle?

find(Boolean)The function is to find the first trueelement 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 falsewill &&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 to false
  • false itself a boolean value false
  • null is converted to false
  • 1 is converted to true
  • 2 is converted to true

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:

  1. 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.

  2. Higher code reusability: different processor classes can be reused, resulting in better readability and maintainability.

  3. 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.

 

Guess you like

Origin blog.csdn.net/weixin_42981560/article/details/133038639