前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
1、三元运算符
在实际工作中,我们常常需要对一个变量或表达式进行判断,根据不同的条件来执行不同的操作,通常情况下需要使用 if...else
语句。但是 if...else
语句过于冗长和繁琐,可以使用三元运算符来优化。
以实现一个状态机的代码为例,原始的代码可能长这样:
function handleEvent(event) { if (currentState === STATE_INITIAL) { // do something } else if (currentState === STATE_SECONDARY) { // do something } else if (currentState === STATE_FINAL) { // do something } }
三元运算符可以用于精简此类代码:
function handleEvent(event) { currentState === STATE_INITIAL ? // do something : currentState === STATE_SECONDARY ? // do something : // do something }
使用三元运算符可以使得代码更加简洁易懂,并且减少代码文件大小。同时,三元运算符可以提高代码可读性,逻辑也可以一眼看出来,从而更快速地找到问题所在和进行调试。在某些特定场景下,使用三元运算符还可以提高性能,因为有些引擎不能很好地理解 if...else
语句的含义,从而会降低其性能表现。
注意:虽然使用三元运算符来优化
if...else
语句能够大大提高代码的可读性、可维护性和性能表现,值得我们在实际工作中加以运用。不过需要注意的是,如果一个条件比较复杂或者拥有多个分支时,使用三元运算符会让代码难以阅读和理解,切忌不要滥用。
2、耍点小聪明
来看两个特定场景下的”小聪明技巧“~
2.1 短路求值(Short-circuit Evaluation)
短路求值是指在逻辑运算中,如果第一个表达式已经能够确定整个表达式的值,就不再计算后面的表达式。利用这个特性,可以通过 &&
和 ||
运算符来缩短 if...else
语句。例如:
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 字符串拼接
先来看一类比较特殊的 if...else
语句:
function setAccType(accType) { if (accType == "PLATINUM") { return "Platinum Customer"; } else if (accType == "GOLD") { return "Gold Customer"; } else if (accType == "SILVER") { return "Silver Customer"; } }
像这种条件和返回值有非常强的相关性的语句,通常我们就可以通过下面这种非常简洁的方式来处理,让多行秒变一行:
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
语句也是比较常用的技巧,来看下面这个例子:
if (status === 200) { handleSuccess() } else if (status === 401) { handleUnauthorized() } else if (status === 404) { handleNotFound() } else { handleUnknownError() }
像这样的,我们就可以通过 switch/case
来进行处理:
switch (status) { case 200: handleSuccess() break case 401: handleUnauthorized() break case 404: handleNotFound() break default: handleUnknownError() break }
虽然多了几行代码,但避免了一次又一次的重复的全等检查,而且整体上更精简、直观。
4、数组的 includes 方法
假设我们要根据所提供的食品类型返回相应的配料数组。如果使用if..else
语句,通常会这样写:
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; }
这样的场景下,就可以使用includes
方法来优化这个冗长的if...else
语句:
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; }
这样做的好处在于:
- 可读性更强:使用数组和对象结合来代替
if..else
语句,代码更加简洁易懂,并且减少了嵌套,更容易阅读和理解。 - 扩展性更好:添加新的菜单项只需要向
menu
数组中添加一个新的对象即可,而不需要修改原函数getIngredients
,大大提高了代码的扩展性。 - 可维护性更好:当想要删除或更新某个菜单项时,只需要修改
menu
数组即可,而不需要修改原函数getIngredients
。因此,代码的可维护性也更好。
5、使用对象或者Map
5.1 对象
比如我们通常会遇到这种场景:
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] || ''; }
这样做的好处有两个:
- 可读性更好:当需要增加新的状态时,只需要添加一个新的键值对即可。这比原来的
if..else
语句更易于阅读和维护。 - 执行效率更高:因为在JavaScript中,对象的属性访问是常数时间,而
if..else
语句则需要逐个匹配条件。由于我们只需要访问一个属性,所以使用对象映射比if..else
语句更有效率。
5.2 Map
来看下面的例子,我们要根据颜色打印水果:
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']
我们可以使用Map
数据结构来优化上面的代码,具体实现如下:
const foodMap = new Map([ ['red', ['apple', 'strawberry']], ['yellow', ['banana', 'pineapple']], ['purple', ['grape', 'plum']] ]); function test(color) { return foodMap.get(color) || []; }
这样做的好处在于:
- 这段代码更加简洁:使用
Map
数据结构可以使代码更短,可读性也更强。 - 可维护性更高:将数据存储在
Map
中,如果需要修改或添加一个颜色和对应的食物列表,只需在Map
中修改或添加一条记录即可。 - 搜索时间更短:由于
Map
内部使用了哈希表来存储键值对,所以搜索时间可能比基于条件语句的方法更快。当有大量数据时,这种优化可能会显著提高代码的性能。
6、减少嵌套,提前返回
6.1 场景1
假设我们需要根据用户的年龄,返回一个字符串表示他们的人生阶段。使用if..else
语句,一般可能会这样写:
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}`; }
我们可以使用减少嵌套和提前返回的方法来优化这个冗长的if...else
语句,代码如下所示:
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'; }
这样做的好处在于:
- 可读性更强:减少了嵌套层次以及大括号,使代码更加简洁明了,并且易于阅读和理解。
- 扩展性更好:当需要添加或删除一个年龄段时,只需要在相应的位置插入或删除一个if语句即可,而不需要影响其他部分的代码,大大提高了代码的扩展性。
- 错误处理更好:每个if语句可以处理一种情况,这样可以更及时地返回正确的结果。同时,避免了多个判断条件匹配的情况下重复执行同一个代码块的情况。
6.2 场景2
来看看下面这个例子:
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
看看上面的代码,我们有:
- 1个if/else语句,过滤掉无效的条件
- 3层嵌套的if语句(条件1、2和3)。
我个人遵循的一般规则是在将一些比较容易处理、计算量小、小概率的提前返回。
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'); } } }
通过这样做,我们就少了一层嵌套语句。这种编码方式很好,特别是当你有很长的if语句时(想象一下,你需要滚动到最下面才知道有一个else语句,这可读性可能不会太好)。我们可以进一步减少嵌套的if,通过倒置条件和提前返回。比如:
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'); } }
通过颠倒条件2的条件,我们的代码现在已经没有了嵌套的语句,代码变得很简洁直观。
7、隐藏高阶技能:find(Boolean)
写 react 的同学可能在一些组件复用的场景会经常遇到这种情况:组件中有少部分组件会根据传入的props属性不同进行展示。
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 /> {/* 其他很复杂的组件 */} </> ); } }
而这种情况,可以通过find(Boolean)
这个小技巧来获得极大的简化:
function ComponentByType({ type }) { return ( <> {/* 其他很复杂的组件 */} <> { [ type === "type_1" && <Component1 />, type === "type_2" && <Component2 />, type === "type_3" && <Component3 />, type === "type_4" && <Component4 /> ].find(Boolean) } </> {/* 其他很复杂的组件 */} </> ); }
find(Boolean)
可以实现条件判断,因为在JavaScript中,布尔值的true和false可以被转换为数字1和0。在这个例子里,当某个条件不满足时,对应的React组件(例如<Component1 />
)会被解析为布尔值false并存储在数组中,相反当条件满足时,对应的React组件会被解析为一个真值true。这样,使用find(Boolean)
就可以找到第一个真值(即第一个满足条件的React组件),并把它渲染到页面上。
需要注意的是,当没有任何一个条件满足时,
.find()
方法返回的是undefined,会导致React渲染错误。但由于在这个例子中我们把结果用短路语法忽略了,因此不会有报错。
如果希望更准确地处理没有满足条件的情况,可以显式地在.find()
后面添加一个默认返回值,在找不到满足条件的组件时返回默认组件。例如:
[ type === "type_1" && <Component1 />, type === "type_2" && <Component2 />, type === "type_3" && <Component3 />, type === "type_4" && <Component4 /> ].find(Boolean) || <DefaultComponent />
这样,当四种类型都不匹配时,就会默认渲染 <DefaultComponent>
组件,避免了React渲染错误。通过使用find(Boolean)
,可以简化代码,避免了使用多个if/else
语句或switch/case
语句的复杂性和冗长性。
原理是什么呢?
find(Boolean)
的作用是查找数组中第一个布尔值为true
的元素,并返回该元素的值。数组中包含多个条件语句以及相应的React组件,当某个条件满足时,对应的组件会被返回并渲染到页面上;否则会返回false
,由于我们使用了条件运算符&&
,所以这个返回值会被自动忽略。
比如一个简单的例子:
[0,false,null,1,2].find(Boolean) // 返回 1
在不加 new
的情况下调用 Boolean
函数会将传入的参数转换为布尔值。对于数组中的各个元素,利用 Boolean
函数进行类型转换后的结果如下:
0
被转换为false
false
本身就是布尔值false
null
被转换为false
1
被转换为true
2
被转换为true
因此,使用 find
方法查找第一个真值时,返回了第一个布尔值为 true
的元素的索引。在这个数组中,第一个布尔值为 true
的元素是 1
,它的索引是 3,因此 find
方法返回了 1。需要注意的是,如果该数组中不存在任何布尔值为 true
的元素,则 find
方法返回 undefined。
[0,false,null,1,2].map(Boolean); // [false, false, false, true, true] [0,false,null,1,2].find(Boolean); // 1
其中,map
方法将数组中的每个元素都应用一次 Boolean
函数,得到一个新的布尔类型的数组。可以看到,在这个新数组中,第一个布尔值为 true
的元素是 true
,它的索引是 3。因此,find
方法在原始数组中查找第一个布尔值为 true
的元素时,返回了 1。
8、责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它可以使多个对象都有机会处理请求。在 JavaScript 中,我们可以利用责任链模式来优化复杂、冗长的 if...else
语句。
在责任链模式中,各个处理者构成了一个链条,当有请求发生时,这些处理者按照顺序依次尝试处理请求,直到有处理者成功处理请求或所有处理者尝试完毕为止。因此,责任链模式可以动态地组织和修改处理流程,从而提高代码的灵活性。
在实际工作中,有很多场景可以使用责任链模式来优化复杂、冗长的 if...else
语句。
比如,在一个电商平台上,用户下单后需要进行一系列的校验和处理,比如校验商品库存是否充足、校验收货地址是否正确等。而这些校验和处理操作可能会有多个步骤,如果采用传统的 if...else
语句来实现,代码很容易变得冗长难懂。下面是一个简单的示例代码:
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.`); }
在上述代码中,我们使用了多个 if...else
语句来完成订单校验和处理操作。这样的代码难以维护和扩展,而且不符合开闭原则(对扩展开放,对修改关闭)。
使用责任链模式可以有效解决这个问题。下面是一个使用责任链模式来实现订单处理的示例代码:
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); }
在上述代码中,我们定义了多个订单处理器(比如 ItemValidator
、StockValidator
等),它们继承自 OrderValidator
类。这些处理器构成了一个处理链,并通过 setNext()
方法连接起来。当用户下单后,我们只需要调用 placeOrder()
函数并将订单对象作为参数传入即可。
在处理过程中,每个处理器都会检查订单是否满足其要求。如果满足就继续交给下一个处理器去处理,否则直接返回。如果所有处理器都能够成功处理订单,最终会输出 “Order x placed successfully.” 的提示信息。
使用责任链模式的好处:
- 代码更加灵活和易于扩展:在需要添加或修改处理步骤时,只需要新增或修改相应的处理器类即可,不需要修改原有的代码。
- 代码复用性更高:不同的处理器类可以被复用,具有更好的可读性和可维护性。
- 代码结构更加清晰和易于理解:使用责任链模式将系统分解成多个小而简单的部分,每一个部分都有自己的处理逻辑和职责。这样的代码结构更加清晰和易于理解。
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库