前端100道问答
第一部分:HTML/CSS 与 JS 基础 (1-15)
1. DOMContentLoaded 与 load 事件的区别
- 通俗解释:
DOMContentLoaded就像是“房子盖好了(HTML解析完),墙刷好了(DOM树构建完),但家具(图片、CSS、样式表)还没搬进去”。这时候 JS 就可以运行了,用户看起来页面结构已经出来了。load是“房子盖好,家具搬完,连窗帘都挂好了”。页面上所有的资源(包括巨大的图片、视频)都下载并渲染完成了。
- 应用场景:大多数 JS 初始化逻辑应该放在
DOMContentLoaded里,因为让用户等所有图片加载完再运行 JS,体验会很卡。
2. HTML5 语义化标签
- 通俗解释:用对的标签做对的事。不要满屏都是
<div>。- 标题用
<header>,底部用<footer>,侧边栏用<aside>,独立文章用<article>。
- 标题用
- 为什么重要:
- 给机器看:搜索引擎(SEO)更容易读懂你的网页结构,排名更高。
- 给特殊人群看:盲人使用的屏幕阅读器能直接跳到
<nav>导航或<main>内容区,体验更好(无障碍访问)。
3. BFC (块级格式化上下文)
- 通俗解释:BFC 就像是一个完全隔离的独立房间。房间里的家具(元素)怎么乱摆,都不会影响到房间外面;外面的洪水也流不进房间里。
- 如何触发(建这个房间):
- 设置
overflow: hidden(最常用)。 - 设置
display: flex或inline-block。 - 设置
position: absolute或fixed。
- 设置
- 解决了什么问题:
- 清除浮动:子元素浮动了(float),父元素高度塌陷(变成0),把父元素变成 BFC,它就能包裹住浮动的子元素。
- 防止外边距合并:上下两个盒子都有 margin,会重叠在一起。把其中一个放进 BFC 房间,margin 就不会重叠了。
4. 盒模型 (Box Model)
- 通俗解释:网页里的每个元素都是一个盒子。盒子的大小怎么算?有两种流派:
- W3C 标准模型 (
content-box):你设置width: 100px,这仅仅是内容的宽度。如果你再加 10px 的 padding,盒子实际总宽度变成了 120px。坑点:布局容易撑爆。 - IE/怪异模型 (
border-box):你设置width: 100px,这 100px 包含了 内容 + padding + border。你加 padding,内容区会自动缩小,总宽度死死卡在 100px。
- W3C 标准模型 (
- 最佳实践:所有项目都在 CSS 开头写上
* { box-sizing: border-box; },这才是符合人类直觉的。
5. Flexbox 布局
- 核心概念:一维布局(主要是一行或一列)。想象成一根绳子串珠子。
- 关键属性:
justify-content:珠子在绳子上怎么摆?(靠左、居中、两端对齐)。align-items:珠子在绳子的垂直方向怎么摆?(靠上、居中、拉伸)。flex: 1:剩下的空间我全包了(常用于自适应宽度)。
6. Grid 布局
- 核心概念:二维布局(有行也有列),像 Excel 表格或棋盘。
- 对比 Flex:Flex 适合局部组件(如导航栏),Grid 适合整个页面的宏观排版(如左侧菜单+右侧内容+底部)。
- 杀手级特性:
grid-template-areas,可以用字符画出布局结构,非常直观。
7. CSS 选择器优先级 (权重)
- 通俗解释:当多条规则通过不同方式选中同一个元素时,谁说了算?看谁的权重大。
- 计算公式 (A, B, C, D):
- A:
style行内样式 (1,0,0,0) - B: ID 选择器 (0,1,0,0)
- C: 类 (.class)、伪类 (:hover)、属性 ([type="text"]) (0,0,1,0)
- D: 标签 (div)、伪元素 (::before) (0,0,0,1)
!important:核武器,无视规则,直接生效(尽量少用)。
- A:
8. 水平垂直居中方案
- 方案一 (现代版):父元素
display: flex; justify-content: center; align-items: center;(最推荐)。 - 方案二 (Grid版):父元素
display: grid; place-items: center;(代码最少)。 - 方案三 (绝对定位版):子元素
absolute,top: 50%; left: 50%; transform: translate(-50%, -50%);(不用知道子元素宽高,兼容性好)。
9. 移动端 1px 边框问题
- 问题来源:现在的手机是高清屏(Retina),你写
1px,在 2倍屏上实际上渲染了 2个物理像素,看起来很粗。 - 解决方案:
- 不要直接写
border: 1px。 - 用伪元素
::after画一个边框。 - 将这个伪元素放大到 200% 或 300%,然后用
transform: scale(0.5)把它缩放回来。这样虽然逻辑占了 1px,但视觉上只有 0.5px 的粗细,在高清屏上刚刚好。
- 不要直接写
10. CSS 预处理器 (Sass/Less)
- 解决了什么:原生 CSS 不能嵌套写,不能定义变量(虽然现在原生有了),很难复用。
- 核心功能:
- 嵌套:
nav { a { color: red } },结构清晰。 - 变量:
$theme-color: blue,改一个地方全站换肤。 - Mixins:像函数一样封装一段 CSS,比如一段清除浮动的代码,哪里需要哪里调。
- 嵌套:
11. PostCSS
- 通俗解释:CSS 界的 Babel。它不是一种语言,而是一个工具平台。
- 最常用插件:
Autoprefixer。你只管写display: flex,它自动帮你加上-webkit-flex,-ms-flex等浏览器前缀,让你不用操心兼容性。
12. 响应式设计与媒体查询
- 原理:
@media screen and (max-width: 768px) { ... }。意思是:如果屏幕宽度小于 768px(手机),就执行这里面的 CSS。 - rem 与 em:
em:相对于父元素字体大小。容易套娃,计算混乱。rem:相对于根元素 (html) 字体大小。现在移动端适配的主流方案(配合 JS 动态设置 html 的 font-size)。- vw/vh:相对于视口宽高的百分比。100vw = 屏幕宽度。
13. 原型与原型链 (Prototype Chain)
- 核心概念:JS 里没有“类”(class 只是语法糖),只有“对象”。对象想继承另一个对象的功能,就通过
__proto__连接起来。 - 链条:当你访问对象
obj.name,JS 引擎先找obj自己有没有;没有就去obj.__proto__(它的原型爸爸)找;还没有就找爸爸的爸爸...直到找到Object.prototype,再往上就是null(尽头)。 - 面试常考:如何判断类型?
typeof:适合基础类型(string, number),但对象、数组、null 都会显示 'object'。instanceof:顺着原型链找,看能不能找到构造函数的 prototype。
14. 作用域与闭包 (Closure)
- 作用域:变量活着的地盘。ES6 之前只有全局和函数作用域,ES6 引入了块级作用域(let/const)。
- 闭包:
- 现象:一个函数内部返回了另一个函数,内部函数引用了外部函数的变量。
- 人话:函数执行完了,本该销毁内部变量,但因为这些变量被返回出去的小函数“揪住”了,所以这些变量被迫留在了内存里,形成了一个专属的“小背包”。
- 用途:私有变量(别人访问不到,只有小函数能访问)、防抖节流。
- 缺点:滥用会导致内存泄漏(变量一直不回收)。
15. this 指向机制
- 一句话口诀:谁调用它,this 就指向谁(箭头函数除外)。
- 分情况:
obj.fn():this 指向obj。fn()(普通调用):this 指向全局(window/global),严格模式下是undefined。new Fn():this 指向新创建出来的实例对象。- 箭头函数:它没有自己的 this,它的 this 是写代码时定义位置的外层上下文(出生时就定死了)。
call/apply/bind:强行改变 this 指向。
第二部分:ES6+ 新特性与异步编程 (16-30)
16. var, let, const 区别
- var:老古董。
- 函数作用域:在
if或for里定义的 var,外面也能访问(坑)。 - 变量提升 (Hoisting):可以在定义之前使用(值为 undefined),非常反直觉。
- 可重复声明:写两次
var a不会报错。
- 函数作用域:在
- let:新时代的变量。
- 块级作用域:
{}也就是花括号里定义的,出了花括号就死。 - 暂时性死区 (TDZ):在定义之前坚决不能用,用了就报错。
- 块级作用域:
- const:常量。
- 规则同 let。
- 不可重新赋值:
const a = 1; a = 2;会报错。 - 注意:如果是对象
const obj = {a:1},你修改obj.a = 2是允许的,因为对象的内存地址没变,变的是房子里的东西。
17. 箭头函数 (Arrow Function)
- 写法:
const add = (a, b) => a + b;简洁优美。 - 关键区别:
- 没有自己的
this:它直接继承外层作用域的this。这解决了以前在回调函数里this乱跑的问题。 - 不能当构造函数:不能用
new调用。 - 没有
arguments对象:要拿所有参数,得用剩余参数...args。
- 没有自己的
18. 解构赋值 (Destructuring)
- 通俗解释:从数组或对象里“提取”数据的快捷方式。
- 数组解构:
const [a, b] = [1, 2];-> a=1, b=2。用来交换变量值特别方便[a, b] = [b, a]。 - 对象解构:
const { name, age } = user;。 - 重命名:
const { name: userName } = user;把提取出来的 name 改名叫 userName。
19. 扩展运算符 (...)
- 展开 (Spread):把数组或对象“炸开”。
const arr2 = [...arr1, 4, 5]:复制数组并添加新元素。const obj2 = { ...obj1, c: 3 }:浅拷贝对象并添加新属性。
- 剩余 (Rest):把剩下的东西“打包”。
function fn(first, ...others) {}:传入 (1, 2, 3),first 是 1,others 是数组[2, 3]。
20. Map 与 Set
- Set (集合):一堆不重复的值。
- 最强用途:数组去重。
[...new Set([1, 2, 2, 3])]瞬间变成[1, 2, 3]。
- 最强用途:数组去重。
- Map (字典):超级键值对。
- 对比 Object:Object 的 key 只能是字符串或 Symbol。Map 的 key 可以是任何东西(对象、函数、DOM节点)。
- WeakMap:弱引用的 Map。如果 key(对象)在外面被销毁了,WeakMap 里的这条数据会自动消失,不占内存。常用于存储 DOM 节点的私有数据。
21. Promise 原理
- 通俗解释:承诺。就像你去买奶茶,店员给你一张小票(Promise)。这时候奶茶还没好(Pending)。
- Resolve (兑现):奶茶做好了,叫号,你拿到奶茶(Data)。状态变为 Fulfilled。
- Reject (拒绝):做奶茶的机器坏了,店员告诉你做不了(Error)。状态变为 Rejected。
- 链式调用:
.then()。拿到奶茶后(第一个 then),我要插吸管(第二个 then),然后喝掉(第三个 then)。这解决了以前“回调地狱”(Callback Hell)一层套一层的问题。 - Promise.all:你去三家店买东西,必须三家都买好了才能回家。只要有一家关门,整个任务就失败。
22. Async / Await
- 本质:Promise 的语法糖。让异步代码看起来像同步代码。
- 写法:javascript
async function getData() { try { const user = await fetchUser(); // 等待fetchUser完成 const posts = await fetchPosts(user.id); // 拿到id再去查帖子 } catch (err) { // 错误处理,替代 .catch() console.error(err); } } - 优势:代码可读性极强,逻辑清晰,特别是处理多个依赖关系的异步请求时。
23. Event Loop (事件循环)
- 核心机制:JS 是单线程的(只有一个厨师)。那怎么处理耗时任务(炖汤)?
- 同步代码 (切菜):直接在主线程(执行栈)做。
- 异步代码 (炖汤):丢给浏览器其他线程去看着(计时器、网络请求),主线程继续切别的菜。
- 回调 (汤好了):炖汤线程把回调函数放到任务队列里排队。
- 循环:主线程切完所有菜(执行栈空了),就会去检查任务队列,把排队的任务拿过来执行。
- 宏任务与微任务:
- 微任务 (Microtask):VIP 通道。Promise 的
.then,process.nextTick。 - 宏任务 (Macrotask):普通通道。
setTimeout,setInterval。 - 顺序:主线程做完 -> 清空所有微任务 -> 渲染 UI -> 取出一个宏任务 -> ...
- 微任务 (Microtask):VIP 通道。Promise 的
24. 垃圾回收机制 (GC)
- 标记清除 (Mark and Sweep):目前主流算法。
- 垃圾回收器定期从“根”(window)出发,顺着引用链往下找。
- 能找到的:打上“活着”的标记。
- 找不到的(孤立的对象):视为垃圾,回收内存。
- 引用计数:老算法。
- 对象每被引用一次 +1,引用断开 -1。变成 0 就回收。
- 致命缺陷:循环引用。A 引用 B,B 引用 A,虽然没人用它们,但计数器永远是 1,内存泄漏。
25. 模块化标准
- CommonJS (CJS):Node.js 用的。
require('fs'),module.exports = ...。- 特点:同步加载(适合服务器,因为文件在硬盘上读得快)。运行时加载。
- ES Module (ESM):浏览器和现代前端标准。
import,export。- 特点:静态分析(编译时就能确定依赖关系,这是 Tree Shaking 也就是“摇树优化”去除无用代码的基础)。
26. Proxy 与 Reflect
- Proxy (代理):像一个拦截器或保镖。
- 你想访问对象
obj.a,代理拦截下来:“等等,我要先记录日志”,或者“如果 a 不存在,我给你返回默认值”。 - Vue3 的核心:Vue3 用 Proxy 替代了 Vue2 的
Object.defineProperty来实现响应式,因为 Proxy 可以直接监听数组变化和对象新增属性。
- 你想访问对象
- Reflect:操作对象的标准 API,配合 Proxy 使用,保持默认行为。
27. 迭代器 (Iterator) 与 生成器 (Generator)
- Iterator:一种接口,只要对象实现了
Symbol.iterator方法,就能被for...of循环遍历。 - Generator:带
*的函数。function* gen() { yield 1; yield 2; }。- 特点:函数可以暂停(yield)和恢复(next)。这是
async/await的底层实现原理。
28. 正则表达式
- 通俗解释:处理字符串的神器。
- 常用:
^开头,$结尾。\d数字,\w字母数字下划线。+至少一个,?0个或1个,*任意个。- 贪婪与非贪婪:默认是贪婪的(尽可能多匹配)。加个
?变成非贪婪。
- 断言:(?=a) 先行断言,找后面跟着 a 的东西。
29. 常用设计模式
- 单例模式:保证全局只有一个实例(比如 Vuex 的 Store,全局唯一的弹窗管理器)。
- 发布订阅模式:
on(订阅),emit(发布)。Vue 的事件总线 (EventBus) 就是这个。 - 观察者模式:Vue 的响应式原理(Dep 和 Watcher)。数据变了,通知所有盯着它的人。
- 工厂模式:根据条件创建不同的对象,隐藏创建逻辑。
30. 函数式编程 (FP)
- 纯函数:输入相同,输出永远相同;没有副作用(不修改外部变量,不发网络请求)。这样的函数非常容易测试和缓存。
- 柯里化 (Currying):把
add(1, 2)变成add(1)(2)。用于参数复用。 - 高阶函数:参数是函数,或者返回一个函数(如
map,filter,reduce)。
第三部分:框架原理与源码分析 (31-45)
31. Virtual DOM (虚拟 DOM)
- 通俗解释:真实的 DOM 操作(操作网页上的元素)非常慢,像是在搬砖。虚拟 DOM 就是用轻量的 JS 对象(蓝图)来描述 DOM 结构。
- 工作流程:
- 状态改变。
- 生成新的虚拟 DOM 树(新蓝图)。
- Diff:对比新旧两张蓝图,找出不一样的地方(比如只是墙纸颜色变了)。
- Patch:只把变化的部分应用到真实 DOM 上(只刷墙,不拆房)。
- 误区:虚拟 DOM 不一定比直接操作 DOM 快。它的价值在于跨平台(蓝图可以生成网页,也可以生成 App 界面)和保证了下限(不管你怎么写,性能都不会太差)。
32. Vue2 vs Vue3 核心差异
- 响应式原理:
- Vue2:
Object.defineProperty。只能劫持对象的属性读取/写入。缺点是监听不到数组下标变化,也监听不到对象新增/删除属性(需要用$set)。 - Vue3:
Proxy。直接代理整个对象。上面提到的缺点全没了,性能也更好。
- Vue2:
- API 风格:
- Vue2:Options API (data, methods, mounted 分开放)。代码多了逻辑会很分散,跳来跳去。
- Vue3:Composition API (
setup函数)。把同一个功能的逻辑(比如“搜索功能”的状态、方法、生命周期)写在一起,更像 React Hooks。
33. Vue 双向绑定原理
- 核心模式:数据劫持 + 发布订阅模式。
- 三个角色:
- Observer (观察者):给数据装上监控摄像头(defineProperty/Proxy)。一变动就通知 Dep。
- Dep (依赖收集器):一个管家。谁用了这个数据,就把它记在本子上(收集依赖)。
- Watcher (订阅者):页面上的组件。当数据变了,Dep 通知 Watcher,Watcher 去更新视图。
- v-model:其实是
value属性 +input事件的语法糖。
34. Vue computed vs watch
- Computed (计算属性):
- 带缓存:只要依赖的数据没变,多次访问直接返回缓存值,不重新计算。
- 同步:里面不能做异步操作(如发请求)。
- 场景:一个值由其他值得出(购物车总价 = 单价 * 数量)。
- Watch (监听器):
- 无缓存。
- 支持异步:数据变了,我可以等 1秒后再去搜索。
- 场景:数据变了要做一些“副作用”(发请求、打印日志、操作 DOM)。
35. Vue 生命周期的理解
- 创建阶段:
beforeCreate:刚起步,data 和 methods 还没好。created:最常用。数据好了,可以发请求拿初始数据,但 DOM 还没渲染。
- 挂载阶段:
mounted:DOM 渲染完了。可以操作 DOM 元素(比如初始化图表)。
- 更新阶段:
beforeUpdate->updated。 - 销毁阶段:
beforeUnmount(Vue3) /beforeDestroy(Vue2)。在这里清除定时器、解绑事件,防止内存泄漏。
36. Vue NextTick 原理
- 问题:你修改了数据
this.msg = 'Hello',然后立刻去获取 DOM 的内容,发现内容还是旧的。为什么? - 原因:Vue 更新 DOM 是异步的。它会把所有数据变更攒起来,放到一个队列里,等当前代码执行完了一次性更新(为了性能)。
- NextTick:就是把你的回调函数放到这个队列的最后面。等到 DOM 更新完了,自然就轮到你的回调执行了。底层用的是 Promise 或 MutationObserver。
37. Keep-Alive 原理
- 作用:缓存组件。比如从列表页点进详情页,再退回来,列表页不用重新刷新,滚动条位置也在。
- 原理:组件切换时,不销毁组件实例,而是把它存到内存的一个对象里(cache)。下次再切回来,直接从内存里拿出来渲染,跳过
created和mounted钩子,触发activated钩子。 - LRU 算法:内存有限,缓存满了怎么办?最近最少使用算法。把最久没用过的那个组件踢出去。
38. React Fiber 架构
- 背景:React 15 的更新是同步的,一旦开始 Diff,就必须一口气做完。如果组件树很大,这口气太长,主线程一直被占用,页面就会卡顿(掉帧)。
- Fiber:把更新任务拆分成一个个小任务单元。
- 时间切片 (Time Slicing):React 像个懂事的打工仔。做一会任务,看看浏览器主线程有没有急事(用户点击、动画)。如果有,就暂停手里的活,让给浏览器先处理;处理完回来继续做。实现了异步可中断渲染。
39. React Hooks 原理与限制
- 为什么有 Hooks:类组件太重,逻辑复用难(HOC/Render Props 嵌套地狱)。
- 核心原理:闭包 + 链表。
- React 内部维护了一个链表来存每个 Hook 的状态。
- 两大铁律:
- 只能在函数组件顶层调用:不能写在
if,for,function内部。因为 React 是靠调用顺序来对应状态的。如果你套了个if,下次渲染少调了一个 Hook,后面的链表全错位了,数据就乱了。 - 只能在 React 函数中调用。
- 只能在函数组件顶层调用:不能写在
40. React 合成事件 (SyntheticEvent)
- 机制:你在 React 写的
onClick并没有直接绑定到那个按钮 DOM 上,而是全都绑定到了根节点(document 或 root)上(事件代理)。 - 目的:
- 跨浏览器兼容:React 帮你抹平了不同浏览器事件对象的差异。
- 性能:减少内存消耗,不用给成千上万个元素分别绑定事件。
- 注意:阻止冒泡
e.stopPropagation()是阻止 React 内部的冒泡,原生事件的冒泡还在。
41. Diff 算法详解
- 核心策略 (O(n) 复杂度):
- 同层比较:只比同一层级,不跨层级比。
- Key 的作用:
- 如果没有 key:React 看到列表变了,会傻傻地把旧的删了,建新的。
- 如果有 key:React 发现“哦,key=A 的这个元素只是从第一位跑到了第三位”,那就直接移动它,而不是删除重建。大大提升性能。
- 类型不同直接换:如果是
<div>变成了<p>,那子节点都不看了,直接整个替换。
42. 状态管理 (Redux / Vuex / Pinia / MobX)
- 核心思想:单一数据源。把所有组件共享的状态抽离出来,放在一个全局的大仓库(Store)里。
- Redux:
- Flux 架构:单向数据流。
- 流程:View -> Dispatch(Action) -> Reducer(纯函数,计算新状态) -> Store -> View。
- 特点:很啰嗦,但非常规范,易于调试(Time Travel)。
- Vuex / Pinia:
- Vuex:Mutation (同步改状态), Action (异步).
- Pinia:Vuex 5 的精神继承者。去掉了 Mutation,只有 Action,支持 TypeScript 更好,更轻量。
43. React Context API
- 作用:避免“属性钻取” (Prop Drilling)。爷爷传给爸爸,爸爸传给孙子...太累。Context 像是搭建了一个“传送门”,爷爷可以直接传数据给孙子,中间组件不用经手。
- 场景:全局主题(黑夜模式)、多语言、用户信息。
- 缺点:Context 里的值一变,所有消费它的组件都会强制重新渲染。如果用来做高频更新的状态管理,性能会炸。
44. SSR (服务端渲染) 原理
- SPA (单页应用) 痛点:首屏慢(要等 JS 下载执行完才显示内容)、SEO 差(爬虫只看到一个空 div)。
- SSR (Server-Side Rendering):
- 用户请求页面。
- Node.js 服务器运行 React/Vue 代码,生成完整的 HTML 字符串。
- 服务器把 HTML 发给浏览器。用户立刻看到内容(首屏快)。
- 注水 (Hydration):浏览器下载 JS,接管页面,把静态的 HTML 变成可交互的应用。
- 框架:Next.js (React), Nuxt.js (Vue)。
45. 微前端 (Micro-Frontends)
- 解决了什么:巨石应用(Monolith)。几百个人维护一个项目,编译一次半小时,上线风险巨大。
- 核心思想:把大应用拆成一个个独立的小应用(子应用)。
- 技术栈无关:子应用可以用 Vue,可以用 React,甚至 jQuery。
- 独立部署:A 团队发版不影响 B 团队。
- 实现方案:
- iframe:最简单,隔离最完美,但体验差(弹窗遮罩问题、通信难、路由状态丢失)。
- qiankun (基于 single-spa):目前主流。通过 JS 沙箱隔离全局变量,通过样式隔离解决 CSS 冲突。加载 HTML Entry。
第四部分:工程化与构建工具 (46-60)
46. Webpack 核心流程
- 通俗解释:Webpack 就像一个打包工厂。
- 流程:
- 初始化:读取配置文件 (
webpack.config.js)。 - 编译 (Compile):从入口文件 (
entry) 开始,递归寻找所有依赖的模块。 - 转换:遇到不认识的文件(scss, vue, ts),调用对应的 Loader 翻译成 JS。
- 构建依赖图:搞清楚谁依赖谁。
- 输出 (Emit):根据依赖图,把模块组装成一个或多个 Chunk(代码块),写入文件系统 (
dist目录)。
- 初始化:读取配置文件 (
- 关键概念:
- Loader:翻译官。
css-loader,babel-loader。 - Plugin:插件。在打包的特定时机干坏事(压缩代码、拷贝文件、生成 HTML)。
- Loader:翻译官。
47. Loader 与 Plugin 的区别
- Loader (加载器):
- 作用:文件转换。Webpack 原生只懂 JS 和 JSON,Loader 让它能看懂 CSS、图片、TS。
- 运行时机:在读取文件内容时。
- 例子:
sass-loader把 Sass 变成 CSS。
- Plugin (插件):
- 作用:功能扩展。介入打包的整个生命周期,做更复杂的事。
- 运行时机:整个构建过程的任何钩子 (Hooks) 处。
- 例子:
HtmlWebpackPlugin生成 index.html;CleanWebpackPlugin打包前清空 dist 目录。
48. Vite 原理
- 核心优势:快。为什么快?
- 开发环境 (Dev):
- 不打包 (No-Bundle)。利用现代浏览器原生支持 ES Modules (
<script type="module">) 的特性。 - 当你请求一个页面,浏览器遇到
import,再向服务器去请求对应的文件。Vite 只需要按需编译这个文件返回给你。相比 Webpack 把整个项目打包好才能启动,Vite 是秒开。
- 不打包 (No-Bundle)。利用现代浏览器原生支持 ES Modules (
- 生产环境 (Build):
- 使用 Rollup 进行打包。因为原生 ESM 在生产环境(尤其是有几千个模块时)网络请求太多,性能不行,所以还是得打包。
49. Babel 原理
- 作用:JS 编译器。把 ES6+ 的新语法(const, arrow function)转译成 ES5 老语法,让旧浏览器(IE)也能跑。
- 三步走:
- 解析 (Parse):把代码变成 AST (抽象语法树)。就像把句子拆解成“主谓宾”结构。
- 转换 (Transform):遍历 AST,增删改查。比如看到“箭头函数”节点,把它改成“普通函数”节点。这是 Babel 插件工作的地方。
- 生成 (Generate):把改好的 AST 重新变回代码字符串。
50. Tree Shaking (摇树优化)
- 通俗解释:拿起这棵树摇一摇,把枯萎的叶子(没用到的代码)摇下来,不打包进最终文件。
- 前提:必须使用 ES Module (
import/export)。因为 ESM 是静态分析的,打包工具在编译时就能确切知道哪个函数没被引用。CommonJS (require) 是动态的,摇不动。 - Side Effects (副作用):如果一个文件里只有
console.log('hello'),虽然没被 import,但它有副作用,不能摇掉。可以在package.json里配置"sideEffects": false告诉工具放心摇。
51. HMR (热更新) 原理
- 体验:你改了 CSS 颜色,浏览器不用刷新页面,按钮颜色直接变了,而且输入框里填的内容还在。
- 流程:
- Webpack 监听到文件变化,重新编译该模块。
- 服务端(WDS)通过 WebSocket 推送更新消息(hash值)给浏览器。
- 浏览器运行时代码收到消息,请求新的模块代码。
- 替换:尝试用新模块替换旧模块。如果失败(比如没写 accept 逻辑),则回退到整页刷新。
52. 浏览器缓存策略
- 强缓存(浏览器自作主张):
- Expires:绝对时间(2025年到期)。受本地时间影响,已淘汰。
- Cache-Control:
max-age=3600(一小时内别烦我,直接读内存/硬盘缓存)。优先级最高。
- 协商缓存(浏览器问服务器):
- 强缓存失效了,浏览器带上标识问服务器:“这图过期没?”
- Last-Modified / If-Modified-Since:按修改时间。精度只到秒。
- Etag / If-None-Match:按文件指纹(Hash)。只要内容没变,Etag 就不变。最准确。
- 结果:如果没变,服务器回 304 Not Modified(没变,继续用旧的吧),省带宽。
53. DNS 解析过程
- 通俗解释:把域名
www.google.com翻译成 IP 地址142.250.1.1。 - 查找顺序:
- 浏览器缓存(之前访问过吗?)。
- 系统 hosts 文件。
- 路由器缓存。
- ISP DNS 服务器(电信/移动的服务器)。
- 根域名服务器 -> 顶级域名服务器 (.com) -> 权威域名服务器 (google.com)(递归/迭代查询)。
54. CDN (内容分发网络)
- 通俗解释:京东的快递仓库。如果仓库只在背景,海南用户买东西要等很久。CDN 就是在全国各地建仓库(节点)。
- 原理:用户访问图片时,DNS 会根据用户的 IP,把请求解析到离用户最近的那个 CDN 节点 IP。
- 好处:速度快、减轻源站压力。
55. TCP 三次握手与四次挥手
- 三次握手 (建立连接):
- Client: “我想连你,听到吗?” (SYN)
- Server: “听到了,我也想连你,你听到吗?” (SYN + ACK)
- Client: “听到了,连接建立。” (ACK)
- 为什么三次? 为了防止已失效的连接请求突然又传到了服务端,造成资源浪费。
- 四次挥手 (断开连接):
- Client: “我说完了,要挂了。” (FIN)
- Server: “知道了,等我把剩下的话说完。” (ACK)
- Server: “我说完了,挂吧。” (FIN)
- Client: “好,挂了。” (ACK) -> 等待 2MSL 时间后真正关闭。
56. HTTP/1.1 vs HTTP/2 vs HTTP/3
- HTTP/1.1:
- 队头阻塞:一个连接一次只能处理一个请求。前面的图没传完,后面的 CSS 就得等。
- 文本传输。
- HTTP/2:
- 多路复用:一个 TCP 连接可以并发处理无数个请求。解决了队头阻塞(应用层)。
- 二进制分帧:传输更高效。
- 头部压缩 (HPACK):减少重复 Header 传输。
- 服务端推送。
- HTTP/3:
- 基于 UDP (QUIC) 协议。
- 解决了 TCP 丢包导致的队头阻塞问题。连接建立更快(0-RTT)。
57. HTTPS 握手流程
- HTTPS = HTTP + SSL/TLS (安全层)。
- 流程:
- Client Hello:支持的加密算法。
- Server Hello:选定算法,发来数字证书(含公钥)。
- 验证证书:Client 找操作系统确认证书是不是真的(防中间人攻击)。
- 生成密钥:Client 生成一个随机数(对称密钥),用公钥加密发给 Server。
- 握手完成:Server 用私钥解密拿到随机数。之后双方就用这个随机数进行对称加密通信(速度快)。
58. CSRF 攻击与防御
- CSRF (跨站请求伪造):借刀杀人。
- 场景:你登录了银行网站 A(Cookie 存着)。然后你手贱点开了钓鱼网站 B。B 网站里有一个隐藏的图片
<img src="http://bank.com/transfer?to=hacker&money=1000">。 - 后果:浏览器会自动带上银行 A 的 Cookie 发送请求,银行以为是你本人操作的,钱没了。
- 场景:你登录了银行网站 A(Cookie 存着)。然后你手贱点开了钓鱼网站 B。B 网站里有一个隐藏的图片
- 防御:
- SameSite Cookie:设置 Cookie 只能在同站发送。
- CSRF Token:提交表单时必须带一个随机 Token,钓鱼网站拿不到这个 Token。
- 验证 Referer:检查请求是从哪来的。
59. XSS 攻击与防御
- XSS (跨站脚本攻击):代码注入。
- 场景:你在评论区写了一段
<script>alert(document.cookie)</script>。如果网站没过滤直接显示出来,所有看评论的人的 Cookie 都会被你偷走。
- 场景:你在评论区写了一段
- 防御:
- 转义:把
<变成<,>变成>。永远不要相信用户的输入。 - CSP (内容安全策略):告诉浏览器只准加载哪里的脚本,禁止内联脚本。
- HttpOnly Cookie:禁止 JS 读取 Cookie。
- 转义:把
60. 前端性能优化指标 (Core Web Vitals)
- Google 定义的网页体检报告:
- LCP (最大内容渲染时间):网页里最大的那张图或那段字多久显示出来?(衡量加载速度,越快越好,<2.5s)。
- FID (首次输入延迟):用户点了按钮,网页多久才有反应?(衡量交互性,越短越好,<100ms)。注:即将被 INP 替代。
- CLS (累积布局偏移):看网页的时候,图片有没有突然加载出来把文字顶下去了?(衡量视觉稳定性,越稳越好,<0.1)。
第五部分:浏览器原理、性能优化与 TypeScript (61-75)
61. 浏览器渲染原理 (关键路径)
- 通俗解释:浏览器拿到 HTML 代码后,怎么把它画到屏幕上?
- 五步走:
- 解析 HTML:生成 DOM 树(骨架)。
- 解析 CSS:生成 CSSOM 树(皮肤样式)。
- 合成 (Render Tree):把 DOM 和 CSSOM 结合。注意:
display: none的节点不会出现在渲染树中,但visibility: hidden会。 - 回流/重排 (Layout/Reflow):计算每个节点在屏幕上的确切位置和大小(算坐标)。
- 重绘 (Repaint):填充像素(画颜色、阴影)。
- 合成 (Composite):如果有多个图层(比如用了
transform或z-index),GPU 会把它们像 PS 图层一样合并成一张图。
62. 重排 (Reflow) 与 重绘 (Repaint)
- 区别:
- 重排:布局变了(改宽、高、位置、字体大小、删元素)。非常消耗性能,因为牵一发而动全身,周围的元素都要重新算位置。
- 重绘:样子变了但位置没变(改颜色、背景)。性能消耗较小。
- 结论:重排一定导致重绘,重绘不一定导致重排。
- 优化:
- 不要一条条改样式,用
class一次性改。 - 操作 DOM 时,先把元素
display: none(这时操作不触发重排),改完再放回去,只触发两次重排。 - 使用
transform代替top/left做动画(transform会开启 GPU 加速,跳过重排和重绘,直接合成)。
- 不要一条条改样式,用
63. 本地存储方案对比
- Cookie:
- 老旧。只有 4KB。每次请求都会自动带给服务器(浪费带宽)。主要用于存 Session ID。
- LocalStorage:
- 持久存储。5MB 左右。除非你手动删,否则一直都在。不参与服务器通信。
- SessionStorage:
- 会话存储。5MB 左右。页面关了就没了。适合存表单临时数据。
- IndexedDB:
- 数据库。容量大(几百MB)。异步操作。适合存大量结构化数据。
64. 跨域 (CORS) 解决方案
- 同源策略:浏览器为了安全,禁止 协议、域名、端口 不同的网页互相访问数据。
- CORS (跨域资源共享):最正统的方案。
- 后端设置:后端在响应头加
Access-Control-Allow-Origin: *。 - 预检请求 (Options):如果是复杂请求(如 PUT, 自定义 Header),浏览器会先发一个 Options 请求问服务器:“我能发这个吗?”服务器点头后,才发真正的请求。
- 后端设置:后端在响应头加
- Proxy (代理):开发环境常用。
- 前端
localhost:8080-> 代理服务器 (同源) -> 目标服务器。服务器之间没有同源策略。
- 前端
- JSONP:古老方案。利用
<script>标签不受同源策略限制。只能发 GET 请求。
65. 防抖 (Debounce) 与 节流 (Throttle)
- 防抖 (Debounce):“最后一个人说了算”。
- 场景:搜索框输入。你一直打字我就不搜,等你停下来 500ms 不动了,我再去搜。
- 代码思路:清除旧定时器,设置新定时器。
- 节流 (Throttle):“按规定频率执行”。
- 场景:滚动条监听、按钮防狂按。不管你滑得多快,我每隔 100ms 只执行一次。
- 代码思路:如果有定时器在跑,就直接 return,直到定时器结束。
66. 事件委托 (Event Delegation)
- 原理:利用 事件冒泡。不给每个子元素(li)绑事件,而是绑在父元素(ul)上。
- 好处:
- 省内存:绑 1 个事件 vs 绑 1000 个事件。
- 动态支持:后来通过 JS 新增的 li 元素,不用重新绑定,天然就有事件。
- 实现:
ul.addEventListener('click', (e) => { if (e.target.tagName === 'LI') { ... } })。
67. TypeScript 核心优势
- 静态类型:代码还没跑,写的时候就能发现错误(比如拼写错误、参数传错了)。
- 智能提示:IDE 知道在这个对象里有什么属性,点一下全出来,开发效率极高。
- 可维护性:对于大型项目,TS 就是文档。看一眼接口定义 (
interface) 就知道数据长啥样。
68. TypeScript: Interface vs Type
- Interface (接口):主要用来定义对象的形状。
- 特性:可以合并(同名 interface 会自动合并属性)。支持
extends继承。
- 特性:可以合并(同名 interface 会自动合并属性)。支持
- Type (类型别名):更强大,可以定义基本类型、联合类型。
type Status = 'success' | 'fail';type Point = { x: number } & { y: number };(交叉类型)
- 选谁:写库或插件用 Interface(为了让别人能扩展),写业务逻辑一般用 Type 更灵活。
69. TypeScript: Any vs Unknown
- any:放弃治疗。告诉编译器:“别管我,我爱咋用咋用”。失去了 TS 的意义。
- unknown:安全的 any。表示“我不知道这是啥”。但是!在你使用它之前(比如调用它的方法),必须先进行类型判断(类型收窄),否则报错。
70. 移动端适配方案
- Viewport:
<meta name="viewport" content="width=device-width, initial-scale=1.0">。必须加,否则手机会把网页当成电脑版缩放。 - rem:相对于根元素 (
html) 的font-size。- 通常配合 JS 或 CSS 媒体查询,根据屏幕宽度动态修改
html的font-size。
- 通常配合 JS 或 CSS 媒体查询,根据屏幕宽度动态修改
- vw/vh:1vw = 屏幕宽度的 1%。目前最主流的方案。
- 1px 边框问题:高清屏(Retina)下,CSS 的 1px 看起来很粗(实际占了 2 或 3 个物理像素)。
- 解决:用伪元素
::after设置 1px 边框,然后transform: scale(0.5)。
- 解决:用伪元素
71. 图片懒加载 (Lazy Load)
- 原理:图片的
src先不填真地址,填一个占位图。把真地址存在data-src里。 - 检测:监听滚动,当图片即将出现在视口(Viewport)时,把
data-src赋值给src。 - API:IntersectionObserver。比传统的监听 scroll 事件性能好得多,浏览器原生帮你检测元素是否可见。
72. Web Worker
- 痛点:JS 是单线程的,计算量太大(比如图像处理、加密解密)会卡死主线程,页面就动不了了。
- Web Worker:允许你在后台开启一个新的线程运行 JS。
- 限制:Worker 线程不能操作 DOM,不能访问
window和document。它只能做纯计算,通过postMessage和主线程通信。
73. 强类型转换 vs 隐式转换
- 隐式转换 (坑):
[] + []=""[] + {}="[object Object]"1 + "2"="12"
- == vs ===:
==会尝试转换类型再比较(1 == '1'为 true)。===严格比较,类型不同直接 false。永远推荐用 ===。
74. 常见的内存泄漏场景
- 未清理的定时器:组件销毁了,
setInterval还在跑。 - 全局变量:不小心把变量挂到了
window上。 - 闭包:虽然好用,但如果持有了不再需要的大对象引用,GC 无法回收。
- DOM 引用:JS 里存了一个 DOM 节点的引用,后来这个节点从页面删了,但 JS 里还指着它,内存就释放不掉。
75. 进程 (Process) 与 线程 (Thread)
- 比喻:
- 进程:一个工厂(拥有独立的资源、内存空间)。比如 Chrome 的每一个 Tab 页通常是一个独立的进程。
- 线程:工厂里的工人(共享工厂的资源)。一个进程可以有多个线程。
- 浏览器是多进程的:主进程、渲染进程、GPU 进程、插件进程。
- JS 是单线程的:渲染进程里,JS 引擎线程和GUI 渲染线程是互斥的。JS 执行时,页面渲染就会停(所以 JS 写了死循环页面就卡死)。
在面试中,能说出原理是“银”,能手写出代码才是“金”。请务必在一个编辑器里亲自敲一遍这些逻辑。
第六部分:手写代码与算法基础 (76-90)
76. 手写防抖 (Debounce)
- 核心逻辑:
- 定义一个变量
timer保存定时器 ID。 - 返回一个函数。
- 每次调用时,先
clearTimeout(timer)清除之前的定时器。 - 重新
setTimeout,在延迟时间后执行真正的函数。
- 定义一个变量
- 用途:搜索框输入、窗口调整大小。
77. 手写深拷贝 (Deep Clone)
- 乞丐版:
JSON.parse(JSON.stringify(obj))。缺点:会丢失函数、undefined、Symbol,无法处理循环引用。 - 面试版逻辑:
- 判断类型,如果是基本类型直接返回。
- 如果是对象/数组,创建一个新的
{}或[]。 - 递归遍历属性,赋值给新对象。
- 循环引用:使用
WeakMap做一个缓存字典。每次拷贝前先看字典里有没有这个对象,有就直接返回,没有就存进去。
78. 手写 Promise.all
- 核心逻辑:
- 接收一个 Promise 数组。
- 返回一个新的 Promise。
- 内部维护一个计数器
count和结果数组results。 - 遍历数组,执行每个 Promise。
- 每成功一个,把结果存入
results(注意索引要对应),count++。 - 当
count === 数组长度时,resolve(results)。 - 只要有一个失败,立刻
reject(error)。
79. 手写发布订阅模式 (Event Emitter)
- 结构:一个类,里面有一个对象
events = {}存事件。 - 方法:
on(name, fn): 订阅。this.events[name].push(fn)。emit(name, ...args): 发布。找到this.events[name]数组,遍历执行里面的函数。off(name, fn): 取消。filter掉那个函数。once(name, fn): 也就是包装一层函数,执行完立刻调用off。
80. 数组扁平化 (Flatten)
- 需求:
[1, [2, [3, 4]]]->[1, 2, 3, 4]。 - 方案一:
arr.flat(Infinity)(ES6)。 - 方案二 (递归):遍历数组,如果是数组就递归调用,不是就 push。
- 方案三 (Reduce):
arr.reduce((prev, cur) => prev.concat(Array.isArray(cur) ? flatten(cur) : cur), [])。
81. 数组去重
- 方案一 (最快):
[...new Set(arr)]。 - 方案二 (indexOf):遍历,如果你在结果数组里找不到 (
indexOf === -1),就 push 进去。 - 方案三 (filter):
arr.filter((item, index) => arr.indexOf(item) === index)。保留第一次出现的,过滤掉后面重复的。
82. 手写 call / apply / bind
- 核心原理:如何改变
this指向? - Call 实现:
- 把函数挂到目标对象上:
context.fn = this。 - 执行这个函数:
context.fn(...args)。 - 删除这个属性:
delete context.fn。
- 把函数挂到目标对象上:
- Bind 实现:返回一个新的函数,这个新函数内部去调用
apply。注意处理new调用的情况。
83. 手写 instanceof 原理
- 作用:判断
A instanceof B,即 A 是否是 B 的实例。 - 逻辑:沿着 A 的原型链 (
__proto__) 往上找。 - 循环:如果
A.__proto__ === B.prototype,返回 true。如果找到了顶端null还没找到,返回 false。
84. 手写 new 操作符
- new 干了四件事:
- 创建一个新对象
obj。 - 把
obj的原型指向构造函数的原型 (obj.__proto__ = Constructor.prototype)。 - 执行构造函数,并把
this绑定到obj上。 - 关键:如果构造函数显式返回了一个对象,就返回那个对象;否则返回
obj。
- 创建一个新对象
85. 函数柯里化 (Currying)
- 概念:把
add(1, 2, 3)变成add(1)(2)(3)。 - 逻辑:
- 判断当前传入参数的个数 (
args.length)。 - 如果参数够了 (>= 原函数需要的参数),直接执行原函数。
- 如果不够,返回一个新的函数,继续收集参数,直到参数凑齐。
- 判断当前传入参数的个数 (
86. 解析 URL 参数
- 需求:
?name=jack&age=18->{name: 'jack', age: '18'}。 - 方案:
window.location.search拿到字符串。- 去掉
?,用&分割成数组。 - 遍历数组,用
=分割 key 和 value。 - 神器:
Object.fromEntries(new URLSearchParams('?name=jack&age=18'))一行代码搞定。
87. 快速排序 (Quick Sort)
- 算法思路 (分治法):
- 基准:找一个中间数(Pivot)。
- 分区:比它小的放左边数组,比它大的放右边数组。
- 递归:对左边和右边重复这个过程。
- 合并:
[...quickSort(left), pivot, ...quickSort(right)]。
- 复杂度:平均 O(n log n)。
88. 列表转树形结构 (List to Tree)
- 场景:后端给你一个扁平数组(包含 id, parentId),你要转成嵌套的树给菜单组件用。
- 逻辑:
- 用一个
Map(或对象) 存储所有节点,id 为 key。 - 遍历数组,找到每个节点的 parent。
- 如果 parent 存在,就把当前节点 push 到 parent 的
children数组里。 - 如果 parent 不存在(是根节点),放入结果数组。
- 用一个
- 优势:利用对象引用,时间复杂度 O(n),比双重循环 O(n^2) 快得多。
89. LRU 缓存算法 (最近最少使用)
- 需求:设计一个缓存,容量有限。满了之后,删除最久没用过的那个。
- 数据结构:JS 的
Map。因为Map的 Key 是有序的(按插入顺序)。 - Get 逻辑:如果有,先删除,再重新 set 进去(这样它就跑到了链表末尾,变成“最近使用”)。
- Put 逻辑:如果满了,删除
map.keys().next().value(头部的就是最久没用的),然后 set 新的。
90. 图片懒加载 (IntersectionObserver 代码)
- 传统做法:监听
scroll,计算scrollTop+windowHeight>imgOffsetTop。太麻烦且卡顿。 - 现代做法:javascript
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // 进入视口 const img = entry.target; img.src = img.dataset.src; // 替换真实地址 observer.unobserve(img); // 停止观察 } }); }); document.querySelectorAll('img').forEach(img => observer.observe(img));
🎉 全系列完结!
你已经浏览完了前端面试最核心的 90 个知识点。
如何复习:
- 查漏补缺:看着标题,如果你能自己讲出个大概,就过;如果卡住了,去 Google 搜详细解析。
- 深度优先:对于框架原理(Vue/React)和工程化(Webpack),不仅要背结论,最好能看过源码浅析。
- 手写练习:第 76-90 题,请务必在电脑上敲一遍,肌肉记忆很重要。
祝你面试顺利,拿到心仪的 Offer!