💥 前端高频考点连环问:一周实战复盘与解析

84 阅读19分钟

过去一周经历了几场面试,收获不少,也遇到了一些意料之外的题目。这篇文章既是我的复盘,也想分享给正在准备的你!

通常都会从自我介绍开始。这里不仅要简要介绍个人背景,还可以重点讲一讲自己的学习经历和学习方法。

接下来往往会进入项目介绍环节。这个部分需要突出项目的核心功能,面试官可能会更对你开发过程中遇到的技术难点和对应的解决思路更感兴趣,展现出你的实践能力和解决问题的能力。

写一个todolist

是不是很诧异,但确实是碰到的某公司一面的内容,十分钟,手写一个 TodoList。最终我交出的效果长这样 👇:

image.png

其实面试官就是想考察手写代码的基本功——别看现在 AI 能一键生成,但对核心逻辑的理解、代码结构和细节处理,仍然能看出一个人的功底。

言归正传,接下来我们一起分析题目

css盒模型

前端盒模型:从文档流到层叠上下文

CSS 盒模型是网页布局的基础,用于描述元素在页面中占据的空间。包含 4 个核心部分:

  • content(内容区) :元素实际内容(文本、图片等),由 width/height 定义
  • padding(内边距) :内容区与边框之间的空间,会影响元素总尺寸
  • border(边框) :围绕内边距的线条,会增加元素总尺寸
  • margin(外边距) :元素与其他元素之间的空白区域,不影响自身尺寸

CSS3 中的盒模型有以下两种:标准盒模型IE(替代)盒模型

两种盒子模型都是由 content + padding + border + margin 构成,其大小都是由 content + padding + border 决定的,但是盒子内容宽/高度(即 width/height)的计算范围根据盒模型的不同会有所不同:

  • 标准盒模型:只包含 content 。
  • IE(替代)盒模型:content + padding + border 。

可以通过 box-sizing 来改变元素的盒模型:

  • box-sizing: content-box :标准盒模型(默认值)。
  • box-sizing: border-box :IE(替代)盒模型。

水平垂直居中怎么实现

作为常考题,我们首先要分清楚题目要求,详细可以看文章

面试官问「CSS 居中」?这样回答让他眼前一亮

固定宽高块级盒子水平垂直居中

适用于知道元素宽高的情况。常用方法有:

  1. absolute + margin 负值
.child {
  width: 200px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -100px; /* 宽度的一半 */
  margin-top: -50px;   /* 高度的一半 */
}

缺点:必须知道宽高,不适合宽高自适应的元素。

  1. absolute + margin: auto
.parent {
  position: relative;
}
.child {
  width: 200px;
  height: 100px;
  position: absolute;
  top: 0; bottom: 0;
  left: 0; right: 0;
  margin: auto;
}

优点:代码简洁,可读性好;水平垂直都居中。
缺点:同样需要固定宽高。

  1. absolute + calc()
.child {
  width: 200px;
  height: 100px;
  position: absolute;
  top: calc(50% - 50px);
  left: calc(50% - 100px);
}

缺点:计算繁琐,性能略低,不常用。


不固定宽高块级盒子水平垂直居中

适用于宽高未知或者自适应的元素:

  1. absolute + transform
.parent {
  position: relative;
}
.child {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

优点:无需知道元素宽高,适用于自适应布局。
缺点:元素位置会脱离文档流。

  1. flex布局(推荐)
.parent {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center;     /* 垂直居中 */
}

优点:简单、高效,适合响应式布局,无需知道宽高。
缺点:父元素必须支持 flex 布局。

  1. grid布局
.parent {
  display: grid;
  place-items: center; /* 水平垂直居中 */
}
  1. table-cell + text-align + vertical-align
.parent {
  display: table;
  width: 100%;
  height: 300px;
}
.child {
  display: table-cell;
  text-align: center;       /* 水平居中 */
  vertical-align: middle;   /* 垂直居中 */
}

优点:兼容老浏览器,缺点:不适合复杂布局。

  1. line-height + vertical-align(单行文本居中)
.parent {
  height: 50px;
  line-height: 50px; /* 与高度相等 */
  text-align: center; /* 水平居中 */
}
.child {
  display: inline-block;
  vertical-align: middle;
  line-height: normal; /* 恢复元素自身行高 */
}

优点:适合单行文字或小元素,缺点:不适合多行或自适应高度。

  1. writing-mode + text-align + line-height(竖直文本居中)
.parent {
  height: 200px;
  writing-mode: vertical-rl;
  text-align: center;
  line-height: 200px;
}

适合竖排文字居中,兼容性较现代浏览器。

优点:语义清晰,兼容性好,尤其适合复杂布局。

html5新特性

  • 语义化标签<header>、<footer>、<nav>、<article>、<section>等,提升可读性和 SEO
  • 媒体标签<video>、<audio>原生支持音视频播放
  • Canvas 绘图:提供 2D 绘图 API,可用于动画、游戏等
  • 本地存储localStorage(持久化存储)和sessionStorage(会话级存储)
  • 表单增强:新控件(date、time、email、number)和新属性(required、placeholder
  • 地理定位navigator.geolocation获取用户位置
  • Web Worker:允许在后台线程运行脚本,避免阻塞主线程
  • WebSocket:提供全双工通信通道,实现实时通信

es6新特性

  • 变量声明let(块级作用域)和const(常量)替代var
  • 箭头函数(a, b) => a + b,简化函数写法,不绑定this
  • 模板字符串:使用${}嵌入变量,支持多行文本
  • 解构赋值const { name, age } = user快速提取对象 / 数组属性
  • 扩展运算符...用于数组 / 对象的复制和合并
  • 类和继承class关键字,extends实现继承
  • 模块化importexport语句
  • Promise:处理异步操作,解决回调地狱
  • 默认参数function fn(a = 1) {}
  • Set/Map 数据结构:提供更高效的数据存储方案

Commonjs和ES module区别

  • CommonJS: 起源于社区(2009年左右),最初是为了给服务器端的 JavaScript(如 Node.js)提供模块化解决方案而设计的。它不是一个 JavaScript 语言的官方标准,而是一个规范。
  • ES Module: 是 ECMAScript 2015 (ES6) 语言标准的官方模块化规范。它是 JavaScript 语言原生支持的模块系统,旨在成为浏览器和服务器端通用的标准。
特性CommonJSES Module
语法require()/module.exportsimport/export
加载时机运行时动态加载编译时静态分析
加载方式同步加载异步加载
作用域模块内的this指向当前模块模块内的thisundefined
循环依赖基于值的拷贝基于引用的动态绑定
使用场景Node.js 环境浏览器环境(需编译)

CommonJS 导出/导入:

// math.js
function add(a, b) { return a + b; }
function sub(a, b) { return a - b; }
module.exports = { add, sub };

// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5

ES Module 导出/导入:

// math.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }

// 或默认导出
export default function(a, b) { return a + b; }

// app.js
import { add, sub } from './math.js';
console.log(add(2, 3)); // 5

// 默认导入
import addFunc from './math.js';
console.log(addFunc(2,3));

Promise和async/await

Promise

ES6 提供的异步编程解决方案,用来表示一个可能当前或将来才会结束的异步操作。

状态

  • pending(等待中)
  • fulfilled(已成功)
  • rejected(已失败)

状态一旦改变不可逆!

语法

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("成功");
    // reject("失败");
  }, 1000);
});

promise
  .then(value => console.log(value))
  .catch(error => console.log(error));

特点

  • 链式调用 .then().catch()
  • 异步操作结果可统一处理
  • 可以通过 Promise.all() / Promise.race() 并发控制

async/await

ES2017 引入,用于让异步代码写得像同步代码,更直观。async/await 是 Promise 的语法糖,它本质还是基于 Promise 实现的,不能脱离 Promise 单独使用 —— 比如 async 函数的返回值默认就是一个 resolved 状态的 Promise

语法

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function run() {
  console.log("开始");
  await delay(1000);  // 等待 Promise 完成
  console.log("结束");
}

run();

特点

  • async 函数返回 Promise
  • await 可以暂停异步操作,等待 Promise 结果
  • 代码风格像同步,更易读

Promise 与 async/await 对比

特性Promiseasync/await
可读性链式调用,嵌套多时可读性差更像同步代码,易读
错误处理.catch() 捕获错误try { await ... } catch(e) {} 捕获错误
并发Promise.all() 并发执行需结合 Promise.all() 才能并发执行
调试链式嵌套调试较难try/catch 调试方便
浏览器兼容ES6+ES2017+,需 Babel 转码支持老浏览器

Promise 通过链式调用和扁平化结构解决了回调嵌套问题,async/await 则进一步让异步代码像同步一样直观,两者共同消灭了回调地狱,让异步编程更清晰、更可控、更易维护。

深浅拷贝

在 JavaScript 中,拷贝是创建数据副本的过程,根据拷贝的深度可分为两种:

1. 浅拷贝(Shallow Copy)

  • 定义:只复制对象的表层属性,对于嵌套的对象 / 数组,只复制其引用地址
  • 特点
    • 基本类型(number/string/boolean 等)会被完整复制
    • 引用类型(object/array 等)仅复制引用,原对象和拷贝对象会共享同一内存空间
    • 修改嵌套的引用类型数据,会影响原对象

常见浅拷贝方法

// 1. Object.assign()
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);

// 2. 扩展运算符
const shallowCopy2 = { ...obj };

// 3. 数组的slice()/concat()
const arr = [1, [2, 3]];
const shallowArr = arr.slice();

2. 深拷贝(Deep Copy)

  • 定义:递归复制对象的所有层级属性,包括嵌套的对象 / 数组,完全独立于原对象
  • 特点
    • 所有层级的数据都会被复制,形成全新的内存空间
    • 原对象和拷贝对象完全隔离,修改任何一方都不会影响另一方
    • 适用于需要完全独立副本的场景(如状态管理、复杂数据处理)

这是最简单的深拷贝方式,利用 JSON.stringify() 将对象转为 JSON 字符串,再用 JSON.parse() 解析回对象。

const obj = { a: 1, b: { c: 2 }, arr: [3, 4] };
const deepCopy = JSON.parse(JSON.stringify(obj));

特点

  • 优点:写法简洁,无需手动实现递归
  • 缺点:
    • 无法拷贝函数、正则表达式、日期对象(会被转为字符串)
    • 无法处理循环引用(会报错)
    • 会忽略 undefinedSymbol 类型的属性
  • 适用场景:仅包含基本类型、普通对象和数组的简单数据结构

手写深拷贝

// 深拷贝的实现
function deepCopy(object) {
  if (!object || typeof object !== "object") return;

  let newObject = Array.isArray(object) ? [] : {};

  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] =
        typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
    }
  }

  return newObject;
}

浏览器事件循环机制

为什么 setTimeout 总是 “不准时”?揭秘 JS 事件循环的执行逻辑

浏览器是 单线程 执行 JavaScript 的,但它可以同时处理异步任务。
事件循环(Event Loop) 就是浏览器管理 同步任务与异步任务执行顺序的机制。

  1. 宏任务(Macro Task / Task)
    • 一次完整的事件循环队列中的任务
    • 典型来源:
      • setTimeout / setInterval
      • setImmediate(Node.js)
      • I/O 操作
      • UI 渲染
  2. 微任务(Micro Task / Job)
    • 在当前宏任务执行完后立即执行
    • 典型来源:
      • Promise.then / catch / finally
      • queueMicrotask
      • MutationObserver

事件循环执行顺序

浏览器事件循环大致流程:

  1. 执行同步代码(主线程)
  2. 执行当前宏任务队列中的任务
  3. 执行微任务队列,直到队列清空
  4. 渲染 UI(重绘、重排)
  5. 取下一个宏任务,重复 2~4 步

核心点:每个宏任务执行完毕后,微任务队列会被清空,再进行渲染

浏览器存储的方式和区别

1. Cookie

  • 本质:最早的浏览器存储方案,用于客户端与服务器端通信(HTTP 无状态特性的补充)。
  • 关键特性
    • 大小限制:约 4KB(仅能存少量数据);
    • 生命周期:分「会话 Cookie」(浏览器关闭失效)和「持久 Cookie」(通过 expires/max-age 设置过期时间);
    • 通信特性:每次 HTTP 请求会自动携带(通过请求头),可能增加网络开销;
    • 访问范围:同源(协议 + 域名 + 端口)共享,可通过 domain 属性跨子域(如 a.example.com 和 b.example.com)。

2. localStorage

  • 本质:HTML5 新增的「本地持久存储」,纯客户端存储,不与服务器交互。
  • 关键特性
    • 大小限制:5-10MB(不同浏览器略有差异);
    • 生命周期:永久存储,除非手动删除(代码 localStorage.removeItem() 或浏览器设置清除);
    • 通信特性:不随 HTTP 请求发送,仅客户端操作;
    • 访问范围:严格同源共享(子域、端口不同均无法访问);
    • 数据类型:仅支持字符串,复杂数据(对象 / 数组)需用 JSON.stringify() 转换。

3. sessionStorage

  • 本质:与 localStorage 同源,但生命周期限于「当前会话」。
  • 关键特性
    • 大小限制:同 localStorage(5-10MB);
    • 生命周期:会话级,页面关闭 / 标签页关闭后自动清除(刷新页面保留,重新开标签页失效);
    • 访问范围:仅当前标签页,同一域名的其他标签页无法共享(即使同源);
    • 数据类型:同 localStorage(仅字符串)。

4. IndexedDB

  • 本质:浏览器内置的「低级 NoSQL 数据库」,用于存储大量结构化数据。
  • 关键特性
    • 大小限制:理论无上限(受硬盘空间限制),远大于前三者;
    • 生命周期:永久存储,手动删除才失效;
    • 操作特性:异步 API(避免阻塞主线程),支持「事务」「索引」「游标」(类似数据库功能);
    • 数据类型:支持复杂类型(对象、数组、二进制数据如图片 /blob);
    • 访问范围:严格同源共享。

类组件和函数组件

类组件 (Class Components)

定义: 类组件是继承自 React.Component 的 ES6 类。它必须包含一个 render() 方法,该方法返回 JSX。

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

特点:

  1. 状态 (State):  通过 this.state 定义组件内部状态,并通过 this.setState() 方法更新状态。
  2. 生命周期方法:  提供了丰富的生命周期钩子,如 componentDidMountcomponentDidUpdatecomponentWillUnmount 等,用于在组件的不同阶段执行代码。
  3. this 上下文:  需要处理 this 的绑定问题(尤其是在事件处理函数中),有时会比较繁琐。
  4. 更复杂:  代码相对冗长,需要定义类、继承、render 方法等。
  5. 历史遗留:  在 Hooks 出现之前是创建有状态组件的主要方式。虽然现在仍然有效,但新项目中已不推荐作为首选。

函数组件 (Function Components)

定义: 函数组件本质上是 JavaScript 函数。它接收一个 props 对象作为参数,并返回 React 元素(JSX)来描述 UI。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 或使用箭头函数
const Welcome = (props) => {
  return <h1>Hello, {props.name}</h1>;
};

特点:

  1. 简洁性:  代码通常更短,更易于阅读和理解。
  2. Hooks:  从 React 16.8 开始,通过 Hooks(如 useStateuseEffectuseContext 等),函数组件可以拥有状态(state)和生命周期行为,使其功能与类组件几乎完全对等。
  3. 性能:  通常比类组件性能稍好,因为它们是普通函数,没有类实例化的开销。React 团队也更倾向于优化函数组件。
  4. 无 this  避免了 this 关键字带来的复杂性和潜在的绑定问题。
  5. 现代趋势:  目前 React 社区的主流和推荐方式,新项目普遍使用函数组件 + Hooks。

React常用hooks

React Hooks 是 React 16.8 引入的特性,用于在函数组件中管理状态和副作用逻辑。

内置 Hooks (核心 API

(1) 状态管理

  • useState:定义组件状态
  • useReducer:更复杂状态管理(类似 Redux 思路)

(2) 副作用管理

  • useEffect:处理副作用,把副作用和渲染过程分离,异步数据请求、订阅、DOM 操作、设置定时器等
  • useLayoutEffect:在 DOM 更新后、浏览器绘制前同步执行,适合读取布局和同步修改,避免闪烁

(3) 性能优化

  • useMemo:缓存计算结果
  • useCallback:缓存函数引用,避免不必要的子组件重新渲染

(4) 引用与 DOM 操作

  • useRef:存储可变值或 DOM 节点引用
  • useImperativeHandle:配合 forwardRef 暴露自定义的实例方法给父组件

(5) 跨组件通信

  • useContext:共享全局数据,替代多层 props 传递

自定义 Hooks

在项目中,可以把可复用的业务逻辑抽到 hooks/ 目录下,这样 UI 组件可以保持干净、无业务代码污染。 常用的有:

  • useTitle:动态设置页面标题
  • useTodos:管理待办事项逻辑(增删改查、存储)
  • useMouse:监听鼠标位置变化

第三方 Hooks

借助成熟的 Hooks 库来提高开发效率,比如:

  • ahooks(阿里出品):
    • useToggle:布尔值切换
    • useRequest:数据请求封装(自动管理 data、loading、error,我在项目中用它替代手写的请求状态管理)

usestate和useref区别

它们的设计目的和使用场景有本质区别,主要体现在状态管理副作用触发两个核心维度上。

特性useStateuseRef
用途管理组件的状态数据(影响 UI 渲染)存储不需要触发重渲染的数据或引用 DOM 元素
状态更新调用 setXxx 会触发组件重渲染修改 .current 属性不会触发重渲染
值的获取每次渲染获取的是当前状态的快照始终获取最新值(直接访问 .current
数据类型通常存储 primitive 类型或简单对象可存储任意类型(包括 DOM 元素、定时器 ID 等)
初始值初始值仅在组件挂载时生效初始值仅在组件挂载时赋值一次

useState 是 React 中用于管理组件状态的基础 Hook,其核心作用是存储影响 UI 展示的数据,当状态更新时会触发组件重新渲染。

  • 每次调用 setState 时,React 会创建新的状态值,并重新执行组件函数
  • 组件重新渲染时,useState 返回的状态值是当前渲染周期的快照
  • 状态更新是异步的,React 可能会合并多次更新以优化性能

useRef 主要用于存储不需要触发重渲染的数据,其返回的 ref 对象有一个 .current 属性,可读写任意类型的值,且修改后不会引起组件重渲染。

  • ref 对象在组件的整个生命周期中保持不变(引用地址不变)
  • 修改 .current 属性时,不会触发组件重渲染
  • 可以通过 .current 随时访问最新值,不受闭包影响

vite和webpack区别

Vite 和 Webpack 都是现代前端开发中非常重要的构建工具,它们的核心目标都是将各种前端资源(如 JavaScript、CSS、图片、TypeScript、Vue/React 组件等)打包、转换、优化,最终生成适合在浏览器中运行的静态文件。

1. 启动速度

  • Webpack:启动时需要先打包所有依赖,再启动开发服务器;项目大时启动很慢。
  • Vite:基于 原生 ES Module (ESM) ,利用浏览器去解析模块,开发环境无需打包,几乎是秒开。

2. 热更新(HMR)

  • Webpack:修改文件 → 重新打包(整个依赖图或部分依赖),速度较慢。
  • Vite:只更新修改过的模块,基于 ESM 的 按需更新,HMR 更快。

3. 构建原理

  • Webpack:基于 JS bundler,把所有资源(JS、CSS、图片等)都打成一个或多个 bundle 文件。
  • Vite:开发时不打包,生产环境下则用 Rollup 打包,体积优化更好。

4. 配置复杂度

  • Webpack:功能强大,但配置复杂,生态庞大(插件、loader 很多)。
  • Vite:内置很多优化,开箱即用,配置比 Webpack 简单。

5. 生态与兼容性

  • Webpack:发展多年,生态成熟,适配各种场景,兼容性好(老项目常用)。
  • Vite:生态正在快速发展,更适合 Vue、React 等现代框架,社区活跃,但在某些复杂场景下生态还不如 Webpack 完善。

git出现冲突怎么解决

这里对git的考察不仅仅是常用的指令,对协作开发,pr流程的掌控也是一大亮点

别再只会 add/commit/push:这份 Git 进阶指南能让面试官直呼“内行”

产生原因:Git 合并(merge/rebase/cherry-pick)时,同一文件的同一位置被多人修改,Git 无法自动判断保留哪一份,于是产生冲突。

先通过 git status 查看冲突文件,然后手动编辑冲突标记 <<<<<<< ======= >>>>>>>,决定保留哪一部分或合并代码,接着 git add 标记已解决,最后 git commit 完成合并。如果不想继续合并,可以用 git merge --abort 回到之前状态。”

发现页面白屏应该怎么做

  1. 初步检查
    • 查看控制台错误(F12):JS 错误、资源加载失败
    • 检查网络请求:是否有 404/500 错误
    • 确认是否为浏览器兼容问题
  2. 深入排查
    • 检查入口文件是否加载:index.html是否正确返回
    • 检查 JS 执行:是否有语法错误、无限循环
    • 检查 React/Vue 等框架错误:是否有组件渲染异常
    • 检查路由配置:是否匹配到正确路由
  3. 解决方向
    • 修复控制台报错
    • 检查资源路径是否正确
    • 增加错误边界(React)或错误捕获
    • 回滚到上一个正常版本
    • 检查服务器是否正常运行

总结

回顾这一周的面试经历,与其说是 “闯关”,不如说是一次对自己技术栈的 “深度体检”—— 每一道题都是一面镜子,照出了自己的优势,也暴露了需要补足的细节漏洞。 对前端面试而言,有几个核心认知值得分享:

  1. 基础永远是重中之重
  2. “会用” 不如 “懂原理”
  3. 项目经验要 “有血有肉”

至于接下来的规划,我会从三个方向推进:

  • 补全 “知识漏洞” :针对这次面试中模糊的知识点(如 IndexedDB 的实战应用、循环引用的深拷贝优化),整理成 “错题本”,结合官方文档和优质文章逐一突破,做到 “知其然更知其所以然”;
  • 强化 “实战能力” :除了日常业务开发,会刻意做一些 “手写练习”(如手写 Promise、手写防抖节流),同时尝试用不同方案实现同一需求(如用原生 JS、React、Vue 分别写 TodoList),在对比中理解不同技术的适用场景;
  • 积累 “工程化思维” :后续会重点学习前端工程化相关内容(如 Webpack 高级配置、Vite 性能优化),同时深入理解 Git 协作中的复杂场景(如多人开发的冲突处理、分支管理策略),让自己从 “能写代码” 向 “能写好代码、能协作写好代码” 进阶。