新年 10 个面试题,我曾 10 次拷问我的灵魂
大家好,这里是大家的林语冰。坚持阅读,自律打卡,每天一次,进步一点。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 10 Interview Questions Every JavaScript Developer Should Know in 2024。
本期共享的是 —— 新年里前端面试需要掌握的十大面试题,知识面虽小,但思路清晰。
JS 的世界日新月异,多年来面试趋势也与时俱进。本文科普了新年每个 JS 开发者必知必会十大基本问题,涵盖了从闭包到 TDD(测试驱动开发)的一系列主题,为大家提供应对现代 JS 挑战的知识和信心。
1. 闭包到底是什么鬼物?
闭包让我们有权从内部函数访问外部函数的作用域。当函数嵌套时,内部函数可以访问外部函数作用域中声明的变量,即使外部函数返回后也是如此:
const createCat = cat => {
return {
getCat: () => cat,
setCat: newCat => {
cat = newCat
}
}
}
const myCat = createCat('薛定谔')
console.log(myCat.getCat()) // 薛定谔
myCat.setCat('龙猫')
console.log(myCat.getCat()) // 龙猫
闭包变量是对外部作用域变量的实时引用,而不是拷贝。这意味着,如果变更外部作用域变量,那么变更会反映在闭包变量中,反之亦然,这意味着,在同一外部函数中声明的其他函数将可以访问这些变更。
闭包的常见用例包括但不限于:
- 数据隐藏
- 柯里化和偏函数(经常用于改进函数组合,比如形参化 Express 中间件或 React 高阶组件)
- 与事件处理程序和回调共享数据
数据隐藏
封装是面向对象编程的一个重要特征。封装允许我们向外界隐藏类的实现细节。JS 中的闭包允许我们声明对象的私有变量:
// 数据隐藏
const createGirlFans = () => {
let fans = 0
return {
increment: () => ++fans,
decrement: () => --fans,
getFans: () => fans
}
}
柯里化函数和偏函数:
// 一个柯里化函数一次接受多个参数。
const add = a => b => a + b
// 偏函数是已经应用了某些参数的函数,
// 但没有完全应用所有参数。
const increment = add(1) // 偏函数
increment(2) // 3
2. 纯函数是什么鬼物?
纯函数在函数式编程中兹事体大。纯函数是可预测的,这使得它们比非纯函数更易理解、调试和测试。纯函数遵循两个规则:
- 确定性 —— 给定相同的输入,纯函数会始终返回相同的输出。
- 无副作用 —— 副作用是在被调用函数外部可观察到的、不是其返回值的任何 App 状态更改。
非确定性函数依赖于以下各项的函数,包括但不限于:
- 随机数生成器
- 可以改变状态的全局变量
- 可以改变状态的参数
- 当前系统时间
副作用包括但不限于:
- 修改任何外部变量或对象属性(比如全局变量或父函数作用域链中的变量)
- 打印到控制台
- 写入屏幕、文件或网络
- 报错。相反,该函数应该返回表明错误的结果
- 触发任何外部进程
在 Redux 中,所有 reducer
都必须是纯函数。如果不是,App 的状态不可预测,且时间旅行调试等功能无法奏效。reducer
函数中的杂质还可能导致难以追踪的错误,包括过时的 React 组件状态。
3. 函数组合是什么鬼物?
函数组合是组合两个或多个函数,产生新函数或执行某些计算的过程:(f ∘ g)(x) = f(g(x))
。
const compose = (f, g) => x => f(g(x))
const g = num => num + 1
const f = num => num * 2
const h = compose(f, g)
h(20) // 42
React 开发者可通过函数组合来清理大型组件树。我们可以将它们组合,创建一个新的高阶组件,而不是嵌套组件,该组件可以通过附加功能强化传递给它的任何组件。
4. 函数式编程是什么鬼物?
函数式编程是一种使用纯函数作为主要组合单元的编程范式。组合在软件开发中兹事体大,几乎所有编程范式都是根据它们使用的组合单元来命名的:
- 面向对象编程使用对象作为组合单元
- 过程式编程使用过程作为组合单元
- 函数式编程使用函数作为组合单元
函数式编程是一种声明式编程范式,这意味着,程序是根据它们做什么,而不是如何做来编写的。这使得函数式程序比命令式程序更容理解、调试和测试。它们往往更加简洁,这降低了代码复杂性,并使其更易维护。
函数式编程的其他关键方面包括但不限于:
- 不变性 —— 不可变数据结构比可变数据结构更易推理
- 高阶函数 —— 将其他函数作为参数或返回函数作为结果的函数
- 避免共享可变状态 —— 共享可变状态使程序难以理解、调试和测试。这也使得推断程序的正确性更加头大
5. Promise
是什么鬼物?
JS 中的 Promise
是一个表示异步操作最终完成或失败的对象,它充当最初未知值的占位符,通常是因为该值的计算尚未完成。
Promise
的主要特征包括但不限于:
- 有状态:
Promise
处于以下三种状态之一:- 待定:初始状态,既未成功也未失败
- 已完成:操作成功完成
- 拒绝:操作失败
- 不可变:一旦
Promise
被完成或拒绝,其状态就无法改变。它变得不可变,永久保留其结果。这使得Promise
在异步流控制中变得可靠。 - 链接:
Promise
可以链接起来,这意味着,一个Promise
的输出可以用作另一个Promise
的输入。这通过使用.then()
表示成功或使用.catch()
处理失败来链接,从而允许优雅且可读的顺序异步操作。链接是函数组合的异步等价物。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功!')
// 我们也可以在失败时 reject 新错误。
}, 1000)
})
promise
.then(value => {
console.log(value) // 成功
})
.catch(error => {
console.log(error)
})
6. TS 是什么鬼物?
TS 是 JS 的超集,由微软开发和维护。近年来,TS 的人气与日俱增,如果您是一名 JS 工程师,您最终很可能需要使用 TS。它为 JS 添加了静态类型,JS 是一种动态类型语言。静态类型可以辅助开发者在开发过程的早期发现错误,提高代码质量和可维护性。
TS 的主要特点包括但不限于:
- 静态类型:定义变量和函数参数的类型,确保整个代码一致性。
- 给力的 IDE 支持:IDE(集成开发环境)可以提供更好的自动补全、导航和重构,使开发过程更加高效。
- 编译:TS 代码被转译为 JS,使其与任何浏览器或 JS 环境兼容。在此过程中,类型错误会被捕获,使代码更鲁棒。
- 接口:接口允许我们指定对象和函数必须满足的抽象契约。
- 与 JS 的兼容性:Ts 与现有 JS 代码高度兼容。JS 代码可以逐步迁移到 JS,使现有项目能够顺利过渡。
interface User {
id: number
name: string
}
type GetUser = (userId: number) => User
const getUser: GetUser = userId => {
// 从数据库或 API 请求用户数据
return {
id: userId,
name: '人猫神话'
}
}
防范 bug 的最佳方案是代码审查、TDD 和 lint 工具(比如 ESLint)。TS 并不能替代这些做法,因为类型正确性并无法保证程序的正确性。即使应用了所有其他质量措施后,TS 偶尔也会发现错误。但它的主要好处是通过 IDE 支持,提供改进的开发体验。
7. Web Components 是什么鬼物?
WC(Web 组件)是一组 Web 平台 API,允许我们创建新的自定义、可重用、封装的 HTML 标签,在网页和 Web App 中使用。WC 是使用 HTML、CSS 和 JS 等开放 Web 技术构建的。它们是浏览器的一部分,不需要外部库或框架。
WC 对于拥有一大坨可能使用不同框架的工程师的大型团队特别有用。WC 允许我们创建可在任何框架或根本没有框架中使用的可重用组件。举个栗子,Adobe(PS 那个公司)的某个设计系统是使用 WC 构建的,并与 React 等流行框架顺利集成。
WC 由来已久,但最近人气爆棚,尤其是在大型组织中。它们被所有主要浏览器支持,并且是 W3C 标准。
8. React Hook 是什么鬼物?
Hook 是让我们无需编写类即可使用状态和其他 React 功能的函数。Hook 允许我们通过调用函数而不是编写类方法,来使用状态、上下文、引用和组件生命周期事件。函数的额外灵活性使我们更好地组织代码,将相关功能分组到单个钩子调用中,并通过在单独的函数调用中实现不相关功能,分离不相关的功能。Hook 提供了一种给力且富有表现力的方式来在组件内编写逻辑。
重要的 React Hook 包括但不限于:
useState
—— 允许我们向函数式组件添加状态。状态变量在重新渲染之间保留。useEffect
—— 允许我们在函数式组件中执行副作用。它将componentDidMount/componentDidUpdate/componentWillUnmount
的功能组合到单个函数调用中,减少了代码,并创建了比类组件更好的代码组织。useContext
—— 允许我们使用函数式组件中的上下文。useRef
—— 允许我们创建在组件的生命周期内持续存在的可变引用。- 自定义 Hook —— 封装可重用逻辑。这使得在不同组件之间共享逻辑变得容易。
Hook 的规则:Hook 必须在 React 函数的顶层使用(不能在循环、条件或嵌套函数内),且能且只能在 React 函数式组件或自定义 Hook 中使用。
Hook 解决了类组件的若干常见痛点,比如需要在构造函数中绑定方法,以及需要将功能拆分为多个生命周期方法。它们还使得在组件之间共享逻辑以及重用有状态逻辑,而无需更改组件层次结构更容易。
9. 如何在 React 中创建点击计数器?
我们可以使用 useState
钩子在 React 中创建点击计数器,如下所示:
import React, { useState } from 'react'
const ClickCounter = () => {
const [count, setCount] = useState(0) // 初始化为 0
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count => count + 1)}>Click me</button>
</div>
)
}
export default ClickCounter
粉丝请注意,当我们从现有状态派生新值时,将函数传递给 setCount
是最佳实践,确保我们始终使用最新状态。
10. TDD 是什么鬼物?
TDD(测试驱动开发)是一种软件开发方法,其中测试是在实际代码之前编写的。它围绕一个简短的重复开发周期,旨在确保代码满足指定的要求且没有错误。TDD 在提高代码质量、减少错误和提高开发者生产力方面,可以发挥至关重要的作用。
开发团队生产力最重要的衡量标准之一是部署频率。持续交付的主要障碍之一是对变化的恐惧。TDD 通过确保代码始终处于可部署状态,辅助减少这种恐惧。这使得部署新功能和错误修复更容易,提高了部署频率。
测试先行多了一大坨福利,包括但不限于:
- 更好的代码覆盖率:测试先行更有可能覆盖所有极端情况。
- 改进的 API 设计:测试迫使我们在编写代码之前考虑 API 设计,这有助于避免将实现细节泄漏到 API 中。
- 更少的 bug:测试先行可以辅助在开发过程中尽早发现错误,这样更容易修复。
- 更好的代码质量:测试先行迫使我们编写模块化、松耦合的代码,这样更容易维护和重用。
TDD 的关键步骤包括但不限于:
- 编写测试:此测试最初会失败,因为相应的功能尚不存在。
- 编写实现:足以通过测试。
- 自信重构:一旦测试通过,就可以自信重构代码。重构是在不改变其外部行为的情况下,重构现有代码的过程。其目的是清理代码、提高可读性并降低复杂性。测试到位后,如果我们犯错了,我们会立即因测试失败而收到警报。
重复:针对每个功能需求重复该循环,逐步构建软件,同时确保所有测试继续通过。
学习曲线:TDD 是一项需要相当长的时间才能培养的技能和纪律。经过大半年的 TDD 体验后,我们可能仍觉得 TDD 难如脱单,且妨碍了生产力。虽然但是,使用 TDD 两年后,我们可能会发现它已经成为第二天性,并且比以前更有效率。
耗时:为每个小功能编写测试一开始可能会感觉很耗时,但长远来看,这通常会带来回报,减少错误并简化维护。我常常告诫大家,“如果你认为自己没有时间进行 TDD,那么你真的没有时间跳过 TDD。”
本期话题是 —— 你遭遇灵魂拷问的回头率最高的面试题是哪一道?
欢迎在本文下方群聊自由言论,文明共享。谢谢大家的点赞,掰掰~
《前端 9 点半》每日更新,坚持阅读,自律打卡,每天一次,进步一点。
来源:juejin.cn/post/7334653735359348777