Skip to content

2023-07-19 前端面试题 - 3 年前端基础 2

当然,以下是我作为资深前端工程师面试一个有三年工作经验的候选人可能会提出的另外 30 个问题。这些问题覆盖了数据结构与算法、前端开发工具、Web 性能优化、网络协议、前端安全、以及现代前端框架等多个方面:

  1. 你如何理解 JavaScript 中的执行上下文和执行栈?
  2. 请解释 JavaScript 的事件循环(Event Loop)模型。
  3. 描述一下你在项目中使用过的版本控制工具(如 Git)。
  4. 你如何在 JavaScript 中实现防抖和节流功能?
  5. 你能简述一下 React 的虚拟 DOM 和 diff 算法吗?
  6. Vue.js 的生命周期有哪些阶段,每个阶段分别做了什么事情?
  7. 你使用过哪些 JavaScript 测试框架?如何编写测试用例?
  8. 如何在 React 中进行状态管理?你使用过 Redux 或者 Mobx 吗?
  9. 你了解哪些 CSS 预处理器(如 Sass,Less)?它们的优点和缺点是什么?
  10. 如何使用 TypeScript?你认为它有哪些优点?
  11. 你使用过哪些前端构建工具(如 Webpack,Rollup,Parcel)?你认为它们各自的优势和劣势是什么?
  12. 描述一下你对浏览器存储的理解,包括 cookie,sessionStorage,localStorage 和 indexedDB。
  13. 请解释下在 JavaScript 中,闭包的含义及其用法。
  14. 你如何理解 Flex 布局?它与传统的布局方式有什么区别?
  15. 你如何看待 PWA(Progressive Web Apps)?你有使用过的经验吗?
  16. 你如何在 JavaScript 中实现一个深拷贝?
  17. 如何使用 Vue.js 创建自定义指令?
  18. 描述一下你对前端路由的理解。你使用过哪些前端路由库?
  19. 请解释一下 JavaScript 的异步编程,以及 Promise,Async/Await 的工作原理。
  20. 你如何理解同源策略以及 CORS?
  21. 如何使用 JavaScript 实现数组的去重?
  22. 你如何理解前端的模块化和组件化?
  23. 你了解哪些 Web 性能优化的方法?
  24. 你如何理解 MVC,MVVM 架构模式?
  25. 你如何理解函数式编程?
  26. 你如何理解 Service Worker?你在项目中如何使用它?
  27. 请描述一下在前端开发过程中,你是如何处理兼容性问题的。
  28. 你如何看待 Web Accessibility(网页可访问性)?你在项目中是如何实现的?
  29. 如何理解 GraphQL,它与传统的 RESTful API 有何不同?
  30. 你有使用过 Web Components 规范吗?你如何看待它?

1. 你如何理解 JavaScript 中的执行上下文和执行栈?

在 JavaScript 中,执行上下文和执行栈是非常重要的概念,它们是 JavaScript 引擎如何控制和管理代码执行的核心机制。

执行上下文(Execution Context)

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每次在函数内部执行代码时,或者全局范围内执行代码时,都会创建新的执行上下文。执行上下文主要包括三个部分:

  1. 变量环境:其中包含了变量、函数声明和函数的形参,这些都会在创建阶段就完成赋值。

  2. 词法环境:用来存储变量和嵌套的函数声明。

  3. this 绑定:在非箭头函数中,this 的值取决于函数是如何被调用的。

执行栈(Execution Stack)

执行栈,也被称为调用栈,是用来存储在代码执行过程中创建的所有执行上下文的。当 JavaScript 程序开始执行的时候,首先会创建全局执行上下文并推入执行栈。每当发生一个函数调用,JavaScript 引擎会为这个函数创建一个新的执行上下文并推入执行栈顶部。当前正在执行的函数总是位于执行栈的顶部。

当当前函数完成执行后,其执行上下文会从执行栈中弹出,控制权返回到当前执行上下文下的执行上下文。这个过程会一直持续到执行栈为空,也就是所有的代码执行完毕。

通过理解执行上下文和执行栈,我们可以更好地理解 JavaScript 中变量、作用域和闭包等概念,以及代码的执行流程。

2. 请解释 JavaScript 的事件循环(Event Loop)模型。

JavaScript 的事件循环(Event Loop)是其执行异步操作的核心机制。由于 JavaScript 是单线程的,事件循环机制使得它能非阻塞地处理任务,即使某些任务需要等待一段时间(例如,等待 API 响应或定时器)。

以下是简单解释事件循环模型的步骤:

  1. 调用栈(Call Stack):JavaScript 引擎首先会处理调用栈中的任务。这些任务是同步的,也就是说,它们会按照它们被添加到栈中的顺序逐个执行。如果一个函数调用另一个函数,那么这个新函数会被添加到栈的顶部,并且会在先于其他函数执行。

  2. 微任务(Microtask)队列:当当前调用栈中的所有任务执行完毕,即将进行下一轮循环前,JavaScript 引擎会查看微任务队列。这个队列包含了所有由 Promise 产生的任务(以及其他的一些 API,比如 MutationObserver)。引擎会处理微任务队列中的所有任务,直到队列清空。

  3. 宏任务(Macrotask)队列:在微任务队列清空之后,引擎会查看宏任务队列(有时也被称为任务队列)。这个队列包含了各种异步任务,例如 setTimeout 和 setInterval 定时器,以及像用户交互事件(点击,键盘事件等),网络请求完成等。引擎每次只从宏任务队列中取出一个任务来处理,处理完后再去看微任务队列。

这个过程会不断重复,形成了所谓的"事件循环"。每一次从宏任务队列中取出一个任务来处理的过程,可以被看作是事件循环的一次"tick"。在每个 tick 中,JavaScript 引擎会运行尽可能多的微任务,然后再去处理下一个宏任务。

通过这种方式,JavaScript 可以以非阻塞的方式处理异步任务,即使在等待某些操作(如 API 请求)完成时也可以继续执行其他任务。这就是 JavaScript 异步编程的基础。

3. 描述一下你在项目中使用过的版本控制工具(如 Git)

在我参与的项目中,我使用的最常见的版本控制工具是 Git。Git 是一个分布式版本控制系统,允许多人同时在项目中工作,并保持所有人的工作同步。下面是我在项目中使用 Git 的主要经验和流程:

  1. **初始化仓库和克隆项目:**通过git init命令在本地创建一个新的 Git 仓库,或者使用git clone命令将远程仓库克隆到本地。

  2. **分支管理:**在开发新功能或修复 bug 时,我会使用git branch命令创建新的分支,这样可以防止我正在进行的工作影响到主分支。完成开发或修复后,我会使用git mergegit rebase命令将我的分支合并到主分支。

  3. **修改和提交:**在本地对代码进行修改后,我会使用git add命令将修改添加到暂存区,然后使用git commit命令将暂存区的内容提交到仓库。我尽量保持每次提交都有明确的目标和清晰的消息,以便其他人理解我所做的更改。

  4. **推送和拉取:**我使用git push命仓

库将我本地的提交推送到远程仓库,这样其他人就可以看到我的更改。同样,我会使用git pull命令将远程仓库的更新拉取到本地,以便我可以获取并合并其他人的更改。

  1. **冲突解决:**如果我和其他人修改了同一份代码并尝试进行合并,可能会出现冲突。在这种情况下,Git 会标出冲突部分,我需要手动解决冲突后再进行提交。

  2. **版本回退:**如果出现问题,或者我想查看以前的代码版本,我可以使用git checkout命令切换到特定的提交。我还可以使用git revertgit reset命令撤销某些提交。

除了以上基础操作,我还使用一些高级功能,如使用.gitignore文件来忽略不需要跟踪的文件,使用git stash命令在分支之间切换时保存临时工作,以及使用git tag命令标记特定的重要提交等。

此外,我还使用过像 GitHub 和 GitLab 这样的在线平台,这些平台提供了方便的代码托管,以及一些附加功能,如问题跟踪、代码审查、持续集成/持续部署(CI/CD)等。

4. 你如何在 JavaScript 中实现防抖和节流功能?

防抖和节流都是为了限制函数的执行频率,主要用于优化性能。虽然他们的目标类似,但实现方式和适用场景有所不同。

防抖(Debouncing)

防抖的原理是:如果你短时间内连续触发同一个事件,只会执行最后一次。比如搜索框输入,用户在输入时我们并不希望每输入一个字就发起一次请求,而是在用户结束输入后再进行一次请求。实现一个简单的防抖功能的代码如下:

javascript
function debounce(fn, delay) {
  let timerId;
  return function (...args) {
    if (timerId) {
      clearTimeout(timerId);
    }
    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

节流(Throttling)

节流的原理是:设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。比如滚动事件,我们并不希望滚动的过程中持续触发,而是每隔一段时间触发一次。实现一个简单的节流功能的代码如下:

javascript
function throttle(fn, delay) {
  let lastTime = 0;
  return function (...args) {
    const now = new Date();
    if (now - lastTime > delay) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

这两种方法都是通过闭包来保存状态的,而且可以通过apply来确保函数在正确的上下文中执行。在实际应用中,防抖常用于输入类事件处理(如实时搜索、resize),节流常用于触发频率较高的事件(如滚动事件、mousemove)。

5. 你能简述一下 React 的虚拟 DOM 和 diff 算法吗?

React 的虚拟 DOM(Virtual DOM)和 diff 算法是 React 在渲染和更新 UI 时的核心概念。

虚拟 DOM 是 React 使用的一种抽象表示,它是一个轻量级的 JavaScript 对象树,与实际的 DOM 元素一一对应。当数据发生变化时,React 会使用虚拟 DOM 来描述 UI 的变化,而不是直接操作实际的 DOM。

虚拟 DOM 的好处是它可以在内存中高效地进行操作,而不需要直接操作实际的 DOM,这是因为对实际 DOM 的操作比较昂贵,会触发浏览器的重绘和重排。通过在内存中进行操作,React 可以批量更新变化,然后再将变化一次性应用到实际的 DOM 中,减少了实际的 DOM 操作次数,提高了性能。

在将虚拟 DOM 的变化应用到实际的 DOM 上时,React 使用了 diff 算法。diff 算法是一种通过比较两个树形结构找出差异的算法,它可以高效地确定哪些部分需要更新,从而减少对实际 DOM 的操作。

React 的 diff 算法采用了一种启发式的策略,它会遍历两个树的节点,并比较它们的类型和属性。当发现节点类型相同时,React 会进一步比较节点的属性,以确定是否需要更新。如果节点类型不同,React 会直接替换节点。当发现差异时,React 会根据差异生成最小化的更新操作,然后将这些操作应用到实际的 DOM 上,完成更新。

使用虚拟 DOM 和 diff 算法的好处是它使得 React 可以高效地处理大规模的 UI 更新,同时减少对实际 DOM 的操作次数,提高性能和用户体验。

总结一下,React 的虚拟 DOM 是一个轻量级的 JavaScript 对象树,用于描述 UI 结构,而 diff 算法是用于比较两个虚拟 DOM 树之间的差异并生成最小化更新操作的算法。这两个概念的结合使得 React 能够高效地处理 UI 的渲染和更新。

6. Vue.js 的生命周期有哪些阶段,每个阶段分别做了什么事情?

Vue.js 的生命周期包括以下几个阶段:

  1. 创建阶段(Creation Phase):

    • beforeCreate:实例刚在内存中创建,数据观测和事件初始化之前,此时无法访问到数据和 DOM。
    • created:实例已经创建完成,数据观测和事件初始化完成,但尚未挂载到 DOM 上,此时可以访问到数据但无法操作 DOM。
  2. 挂载阶段(Mounting Phase):

    • beforeMount:在挂载开始之前被调用,此时虚拟 DOM 已经创建完成,但尚未将其渲染到实际 DOM 中。
    • mounted:实例已经挂载到实际 DOM 上,此时可以进行 DOM 操作,如访问 DOM 元素或使用第三方库操作 DOM。
  3. 更新阶段(Updating Phase):

    • beforeUpdate:数据更新导致重新渲染之前被调用,此时虚拟 DOM 已经更新完成,但尚未将其渲染到实际 DOM 中。
    • updated:数据更新导致重新渲染完成,实例已经更新到实际 DOM 中,此时可以进行 DOM 操作。
  4. 销毁阶段(Destruction Phase):

    • beforeDestroy:实例销毁之前被调用,此时实例仍然完全可用,可以进行清理工作。
    • destroyed:实例已经销毁,此时所有的事件监听器和子组件都被移除,可以进行最终的清理工作。

在每个生命周期阶段,Vue.js 提供了一些钩子函数,开发者可以在这些钩子函数中执行相应的操作。例如,在 created 阶段可以进行数据初始化、网络请求等操作,在 mounted 阶段可以进行 DOM 操作、订阅事件等操作。

需要注意的是,在销毁阶段,Vue.js 会自动清理所有的事件监听器和定时器,但如果有手动绑定的事件监听器或定时器,需要在 beforeDestroy 阶段手动移除,以避免内存泄漏。

总结一下,Vue.js 的生命周期包括创建阶段、挂载阶段、更新阶段和销毁阶段,每个阶段都有相应的钩子函数可以用于执行相应的操作。开发者可以利用这些生命周期钩子函数来管理组件的状态和行为,并在适当的时机执行相应的操作。

7. 你使用过哪些 JavaScript 测试框架?如何编写测试用例?

常见的 JavaScript 测试框架包括:

  1. Jest:Jest 是一个功能强大的测试框架,适用于单元测试、集成测试和端到端测试。它支持断言、异步测试、模拟函数等功能,并且具有丰富的插件生态系统。

  2. Mocha:Mocha 是一个灵活的测试框架,可以用于编写各种类型的测试,包括单元测试和集成测试。它提供了丰富的断言库和钩子函数,使得编写和运行测试用例变得简单。

  3. Jasmine:Jasmine 是一个行为驱动的测试框架,它提供了清晰的测试结构和易于理解的断言语法。Jasmine 可以用于编写单元测试和集成测试。

编写测试用例的一般方法如下:

  1. 确定测试范围:确定要测试的代码的功能和边界条件。

  2. 编写测试用例:根据测试范围编写具体的测试用例。一个测试用例通常包括输入数据、预期结果和断言。断言用于验证实际结果与预期结果是否一致。

  3. 运行测试:使用测试框架提供的命令行工具或集成工具来运行测试。测试框架会执行测试用例并输出结果。

  4. 分析测试结果:根据测试结果判断代码是否符合预期。如果有失败的测试用例,可以通过调试工具或日志来定位问题。

  5. 维护和更新测试用例:随着代码的修改和功能的迭代,测试用例也需要进行相应的更新和维护,确保测试的全面性和准确性。

在编写测试用例时,一些常见的测试技巧包括:

  • 边界条件测试:测试代码在边界条件下的行为,例如输入最小值、最大值、空值等。

  • 异常处理测试:测试代码对异常情况的处理是否正确,例如输入错误的参数或处理错误的 API 响应。

  • 模拟和替代:使用模拟对象或者替代桩来模拟外部依赖,例如网络请求、数据库操作等,以隔离测试的影响。

  • 覆盖率测试:通过运行测试覆盖率工具来确保代码中的所有分支和语句都被测试到。

  • 持续集成和自动化测试:将测试集成到持续集成(CI)系统中,确保每次代码提交都能自动运行测试。

8. 如何在 React 中进行状态管理?你使用过 Redux 或者 Mobx 吗?

在 React 中进行状态管理有多种方式,其中两个常见的选择是使用 Redux 和 Mobx。我可以为您介绍一下这两个状态管理库。

  1. Redux

    • Redux 是一个可预测的状态容器,它将应用程序的状态存储在一个单一的全局存储中。它的核心概念包括:Store(存储应用程序状态)、Action(描述状态的变化)和 Reducer(处理状态变化的纯函数)。
    • 使用 Redux,您可以将应用程序的状态集中存储在一个单一的 Store 中,通过分发 Action 来改变状态,然后使用 Reducer 处理 Action 并返回新的状态。组件可以通过连接(connect)函数来访问 Store 中的状态,并在状态变化时接收更新。
    • Redux 还可以与 React 组件结合使用,通过提供的 React 绑定(如 react-redux 库中的 Provider 和 connect)来简化 Redux 在 React 中的使用。
  2. Mobx

    • Mobx 是一个简单、可扩展的状态管理库,它通过使用可观察的对象和自动追踪机制来实现响应式状态。
    • 在 Mobx 中,您可以创建可观察的对象(observables),它们可以自动追踪其使用,并在变化时触发相应的更新。您还可以使用动作(actions)来修改可观察对象的状态,以确保状态变更是可控的。
    • 在 React 中使用 Mobx,您可以通过使用 Mobx 提供的 observer 高阶组件或 useObserver 钩子函数,将 React 组件与 Mobx 中的可观察对象关联起来,使其在状态变化时自动更新。

个人经验方面,作为一个语言模型,我没有直接的使用经验。但是,Redux 和 Mobx 都是非常受欢迎的状态管理库,它们都有广泛的社区支持和文档资源。选择使用哪个库可以根据项目需求、个人偏好和团队的技术栈来决定。

需要注意的是,除了 Redux 和 Mobx,React 还有其他状态管理库可供选择,例如 Context API、Apollo Client 等。选择适合项目需求和团队的状态管理方案是很重要的,以确保代码的可维护性和扩展性。

9. 你了解哪些 CSS 预处理器(如 Sass,Less)?它们的优点和缺点是什么?

我可以为您介绍一些常见的 CSS 预处理器,如 Sass(包括 SCSS 和 Sass 两种语法)和 Less,并讨论它们的优点和缺点。

Sass

  • 优点:

    • 提供了更强大的功能,如嵌套规则、变量、混合(Mixin)、继承等,可以使 CSS 的编写更加简洁和可维护。
    • 支持模块化开发,可以将样式按模块组织,提高代码的复用性。
    • 提供了丰富的函数库,可以进行数学计算、颜色处理等操作。
    • 具有活跃的社区和广泛的生态系统,有大量的第三方库和工具可供使用。
  • 缺点:

    • 学习曲线相对较陡峭,相比于原生 CSS,需要学习新的语法和概念。
    • 生成的 CSS 文件较大,需要在项目中进行压缩和优化以减小文件大小。
    • 在开发过程中,由于嵌套和继承的使用,有可能导致样式的层级过深,增加了样式之间的耦合性。

Less

  • 优点:

    • 语法与原生 CSS 更为接近,学习曲线较为平缓,容易上手。
    • 生成的 CSS 文件较小,因为 Less 会将样式的嵌套展开,减少了选择器的层级。
    • 支持混合(Mixin)和变量,提供了代码的复用性和可维护性。
    • 与 JavaScript 结合较好,可以在 Less 文件中使用 JavaScript 表达式。
  • 缺点:

    • 功能相对较少,相比于 Sass,缺乏一些高级功能,如嵌套规则等。
    • 社区相对较小,可用的第三方库和工具相对较少。
    • 部分功能需要使用额外的插件来实现。

无论选择 Sass 还是 Less,它们都可以提高 CSS 的编写效率和代码的可维护性。具体选择哪个预处理器取决于个人或团队的偏好、项目需求以及已有的技术栈。值得一提的是,随着 CSS 的发展,CSS-in-JS 的解决方案也越来越流行,例如 Styled Components 和 Emotion,它们提供了一种将样式与组件紧密集成的方式。

10. 如何使用 TypeScript?你认为它有哪些优点?

11. 你使用过哪些前端构建工具(如 Webpack,Rollup,Parcel)?你认为它们各自的优势和劣势是什么?

13. 请解释下在 JavaScript 中,闭包的含义及其用法。

14. 你如何理解 Flex 布局?它与传统的布局方式有什么区别?

15. 你如何看待 PWA(Progressive Web Apps)?你有使用过的经验吗?

16. 你如何在 JavaScript 中实现一个深拷贝?

17. 如何使用 Vue.js 创建自定义指令?

18. 描述一下你对前端路由的理解。你使用过哪些前端路由库?

19. 请解释一下 JavaScript 的异步编程,以及 Promise,Async/Await 的工作原理。

20. 你如何理解同源策略以及 CORS?

21. 如何使用 JavaScript 实现数组的去重?

22. 你如何理解前端的模块化和组件化?

23. 你了解哪些 Web 性能优化的方法?

24. 你如何理解 MVC,MVVM 架构模式?

25. 你如何理解函数式编程?

26. 你如何理解 Service Worker?你在项目中如何使用它?

27. 请描述一下在前端开发过程中,你是如何处理兼容性问题的。

28. 你如何看待 Web Accessibility(网页可访问性)?你在项目中是如何实现的?

29. 如何理解 GraphQL,它与传统的 RESTful API 有何不同?

30. 你有使用过 Web Components 规范吗?你如何看待它?