Skip to content

前端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>
  • 为什么重要
    1. 给机器看:搜索引擎(SEO)更容易读懂你的网页结构,排名更高。
    2. 给特殊人群看:盲人使用的屏幕阅读器能直接跳到 <nav> 导航或 <main> 内容区,体验更好(无障碍访问)。

3. BFC (块级格式化上下文)

  • 通俗解释:BFC 就像是一个完全隔离的独立房间。房间里的家具(元素)怎么乱摆,都不会影响到房间外面;外面的洪水也流不进房间里。
  • 如何触发(建这个房间)
    • 设置 overflow: hidden(最常用)。
    • 设置 display: flexinline-block
    • 设置 position: absolutefixed
  • 解决了什么问题
    1. 清除浮动:子元素浮动了(float),父元素高度塌陷(变成0),把父元素变成 BFC,它就能包裹住浮动的子元素。
    2. 防止外边距合并:上下两个盒子都有 margin,会重叠在一起。把其中一个放进 BFC 房间,margin 就不会重叠了。

4. 盒模型 (Box Model)

  • 通俗解释:网页里的每个元素都是一个盒子。盒子的大小怎么算?有两种流派:
    1. W3C 标准模型 (content-box):你设置 width: 100px,这仅仅是内容的宽度。如果你再加 10px 的 padding,盒子实际总宽度变成了 120px。坑点:布局容易撑爆。
    2. IE/怪异模型 (border-box):你设置 width: 100px,这 100px 包含了 内容 + padding + border。你加 padding,内容区会自动缩小,总宽度死死卡在 100px。
  • 最佳实践:所有项目都在 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:核武器,无视规则,直接生效(尽量少用)。

8. 水平垂直居中方案

  • 方案一 (现代版):父元素 display: flex; justify-content: center; align-items: center;(最推荐)。
  • 方案二 (Grid版):父元素 display: grid; place-items: center;(代码最少)。
  • 方案三 (绝对定位版):子元素 absolutetop: 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 就指向谁(箭头函数除外)。
  • 分情况
    1. obj.fn():this 指向 obj
    2. fn()(普通调用):this 指向全局(window/global),严格模式下是 undefined
    3. new Fn():this 指向新创建出来的实例对象。
    4. 箭头函数:它没有自己的 this,它的 this 是写代码时定义位置的外层上下文(出生时就定死了)。
    5. call/apply/bind:强行改变 this 指向。

第二部分:ES6+ 新特性与异步编程 (16-30)

16. var, let, const 区别

  • var:老古董。
    • 函数作用域:在 iffor 里定义的 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; 简洁优美。
  • 关键区别
    1. 没有自己的 this:它直接继承外层作用域的 this。这解决了以前在回调函数里 this 乱跑的问题。
    2. 不能当构造函数:不能用 new 调用。
    3. 没有 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 是单线程的(只有一个厨师)。那怎么处理耗时任务(炖汤)?
    1. 同步代码 (切菜):直接在主线程(执行栈)做。
    2. 异步代码 (炖汤):丢给浏览器其他线程去看着(计时器、网络请求),主线程继续切别的菜。
    3. 回调 (汤好了):炖汤线程把回调函数放到任务队列里排队。
    4. 循环:主线程切完所有菜(执行栈空了),就会去检查任务队列,把排队的任务拿过来执行。
  • 宏任务与微任务
    • 微任务 (Microtask):VIP 通道。Promise 的 .then, process.nextTick
    • 宏任务 (Macrotask):普通通道。setTimeout, setInterval
    • 顺序:主线程做完 -> 清空所有微任务 -> 渲染 UI -> 取出一个宏任务 -> ...

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):浏览器和现代前端标准。
    • importexport
    • 特点:静态分析(编译时就能确定依赖关系,这是 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 结构。
  • 工作流程
    1. 状态改变。
    2. 生成新的虚拟 DOM 树(新蓝图)。
    3. Diff:对比新旧两张蓝图,找出不一样的地方(比如只是墙纸颜色变了)。
    4. Patch:只把变化的部分应用到真实 DOM 上(只刷墙,不拆房)。
  • 误区:虚拟 DOM 不一定比直接操作 DOM 快。它的价值在于跨平台(蓝图可以生成网页,也可以生成 App 界面)和保证了下限(不管你怎么写,性能都不会太差)。

32. Vue2 vs Vue3 核心差异

  • 响应式原理
    • Vue2Object.defineProperty。只能劫持对象的属性读取/写入。缺点是监听不到数组下标变化,也监听不到对象新增/删除属性(需要用 $set)。
    • Vue3Proxy。直接代理整个对象。上面提到的缺点全没了,性能也更好。
  • API 风格
    • Vue2:Options API (data, methods, mounted 分开放)。代码多了逻辑会很分散,跳来跳去。
    • Vue3:Composition API (setup 函数)。把同一个功能的逻辑(比如“搜索功能”的状态、方法、生命周期)写在一起,更像 React Hooks。

33. Vue 双向绑定原理

  • 核心模式数据劫持 + 发布订阅模式
  • 三个角色
    1. Observer (观察者):给数据装上监控摄像头(defineProperty/Proxy)。一变动就通知 Dep。
    2. Dep (依赖收集器):一个管家。谁用了这个数据,就把它记在本子上(收集依赖)。
    3. 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)。下次再切回来,直接从内存里拿出来渲染,跳过 createdmounted 钩子,触发 activated 钩子。
  • LRU 算法:内存有限,缓存满了怎么办?最近最少使用算法。把最久没用过的那个组件踢出去。

38. React Fiber 架构

  • 背景:React 15 的更新是同步的,一旦开始 Diff,就必须一口气做完。如果组件树很大,这口气太长,主线程一直被占用,页面就会卡顿(掉帧)。
  • Fiber:把更新任务拆分成一个个小任务单元
  • 时间切片 (Time Slicing):React 像个懂事的打工仔。做一会任务,看看浏览器主线程有没有急事(用户点击、动画)。如果有,就暂停手里的活,让给浏览器先处理;处理完回来继续做。实现了异步可中断渲染

39. React Hooks 原理与限制

  • 为什么有 Hooks:类组件太重,逻辑复用难(HOC/Render Props 嵌套地狱)。
  • 核心原理:闭包 + 链表。
    • React 内部维护了一个链表来存每个 Hook 的状态。
  • 两大铁律
    1. 只能在函数组件顶层调用:不能写在 if, for, function 内部。因为 React 是靠调用顺序来对应状态的。如果你套了个 if,下次渲染少调了一个 Hook,后面的链表全错位了,数据就乱了。
    2. 只能在 React 函数中调用

40. React 合成事件 (SyntheticEvent)

  • 机制:你在 React 写的 onClick 并没有直接绑定到那个按钮 DOM 上,而是全都绑定到了根节点(document 或 root)上(事件代理)。
  • 目的
    1. 跨浏览器兼容:React 帮你抹平了不同浏览器事件对象的差异。
    2. 性能:减少内存消耗,不用给成千上万个元素分别绑定事件。
  • 注意:阻止冒泡 e.stopPropagation() 是阻止 React 内部的冒泡,原生事件的冒泡还在。

41. Diff 算法详解

  • 核心策略 (O(n) 复杂度)
    1. 同层比较:只比同一层级,不跨层级比。
    2. Key 的作用
      • 如果没有 key:React 看到列表变了,会傻傻地把旧的删了,建新的。
      • 如果有 key:React 发现“哦,key=A 的这个元素只是从第一位跑到了第三位”,那就直接移动它,而不是删除重建。大大提升性能。
    3. 类型不同直接换:如果是 <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)
    1. 用户请求页面。
    2. Node.js 服务器运行 React/Vue 代码,生成完整的 HTML 字符串。
    3. 服务器把 HTML 发给浏览器。用户立刻看到内容(首屏快)。
    4. 注水 (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 就像一个打包工厂。
  • 流程
    1. 初始化:读取配置文件 (webpack.config.js)。
    2. 编译 (Compile):从入口文件 (entry) 开始,递归寻找所有依赖的模块。
    3. 转换:遇到不认识的文件(scss, vue, ts),调用对应的 Loader 翻译成 JS。
    4. 构建依赖图:搞清楚谁依赖谁。
    5. 输出 (Emit):根据依赖图,把模块组装成一个或多个 Chunk(代码块),写入文件系统 (dist 目录)。
  • 关键概念
    • Loader:翻译官。css-loader, babel-loader
    • Plugin:插件。在打包的特定时机干坏事(压缩代码、拷贝文件、生成 HTML)。

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 是秒开
  • 生产环境 (Build)
    • 使用 Rollup 进行打包。因为原生 ESM 在生产环境(尤其是有几千个模块时)网络请求太多,性能不行,所以还是得打包。

49. Babel 原理

  • 作用:JS 编译器。把 ES6+ 的新语法(const, arrow function)转译成 ES5 老语法,让旧浏览器(IE)也能跑。
  • 三步走
    1. 解析 (Parse):把代码变成 AST (抽象语法树)。就像把句子拆解成“主谓宾”结构。
    2. 转换 (Transform):遍历 AST,增删改查。比如看到“箭头函数”节点,把它改成“普通函数”节点。这是 Babel 插件工作的地方。
    3. 生成 (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 颜色,浏览器不用刷新页面,按钮颜色直接变了,而且输入框里填的内容还在。
  • 流程
    1. Webpack 监听到文件变化,重新编译该模块。
    2. 服务端(WDS)通过 WebSocket 推送更新消息(hash值)给浏览器。
    3. 浏览器运行时代码收到消息,请求新的模块代码。
    4. 替换:尝试用新模块替换旧模块。如果失败(比如没写 accept 逻辑),则回退到整页刷新。

52. 浏览器缓存策略

  • 强缓存(浏览器自作主张):
    • Expires:绝对时间(2025年到期)。受本地时间影响,已淘汰。
    • Cache-Controlmax-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
  • 查找顺序
    1. 浏览器缓存(之前访问过吗?)。
    2. 系统 hosts 文件
    3. 路由器缓存
    4. ISP DNS 服务器(电信/移动的服务器)。
    5. 根域名服务器 -> 顶级域名服务器 (.com) -> 权威域名服务器 (google.com)(递归/迭代查询)。

54. CDN (内容分发网络)

  • 通俗解释:京东的快递仓库。如果仓库只在背景,海南用户买东西要等很久。CDN 就是在全国各地建仓库(节点)。
  • 原理:用户访问图片时,DNS 会根据用户的 IP,把请求解析到离用户最近的那个 CDN 节点 IP。
  • 好处:速度快、减轻源站压力。

55. TCP 三次握手与四次挥手

  • 三次握手 (建立连接)
    1. Client: “我想连你,听到吗?” (SYN)
    2. Server: “听到了,我也想连你,你听到吗?” (SYN + ACK)
    3. Client: “听到了,连接建立。” (ACK)
    • 为什么三次? 为了防止已失效的连接请求突然又传到了服务端,造成资源浪费。
  • 四次挥手 (断开连接)
    1. Client: “我说完了,要挂了。” (FIN)
    2. Server: “知道了,等我把剩下的话说完。” (ACK)
    3. Server: “我说完了,挂吧。” (FIN)
    4. 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 (安全层)。
  • 流程
    1. Client Hello:支持的加密算法。
    2. Server Hello:选定算法,发来数字证书(含公钥)。
    3. 验证证书:Client 找操作系统确认证书是不是真的(防中间人攻击)。
    4. 生成密钥:Client 生成一个随机数(对称密钥),用公钥加密发给 Server。
    5. 握手完成:Server 用私钥解密拿到随机数。之后双方就用这个随机数进行对称加密通信(速度快)。

58. CSRF 攻击与防御

  • CSRF (跨站请求伪造):借刀杀人。
    • 场景:你登录了银行网站 A(Cookie 存着)。然后你手贱点开了钓鱼网站 B。B 网站里有一个隐藏的图片 <img src="http://bank.com/transfer?to=hacker&money=1000">
    • 后果:浏览器会自动带上银行 A 的 Cookie 发送请求,银行以为是你本人操作的,钱没了。
  • 防御
    • SameSite Cookie:设置 Cookie 只能在同站发送。
    • CSRF Token:提交表单时必须带一个随机 Token,钓鱼网站拿不到这个 Token。
    • 验证 Referer:检查请求是从哪来的。

59. XSS 攻击与防御

  • XSS (跨站脚本攻击):代码注入。
    • 场景:你在评论区写了一段 <script>alert(document.cookie)</script>。如果网站没过滤直接显示出来,所有看评论的人的 Cookie 都会被你偷走。
  • 防御
    • 转义:把 < 变成 &lt;> 变成 &gt;。永远不要相信用户的输入。
    • CSP (内容安全策略):告诉浏览器只准加载哪里的脚本,禁止内联脚本。
    • HttpOnly Cookie:禁止 JS 读取 Cookie。

60. 前端性能优化指标 (Core Web Vitals)

  • Google 定义的网页体检报告:
    • LCP (最大内容渲染时间):网页里最大的那张图或那段字多久显示出来?(衡量加载速度,越快越好,<2.5s)。
    • FID (首次输入延迟):用户点了按钮,网页多久才有反应?(衡量交互性,越短越好,<100ms)。注:即将被 INP 替代。
    • CLS (累积布局偏移):看网页的时候,图片有没有突然加载出来把文字顶下去了?(衡量视觉稳定性,越稳越好,<0.1)。

第五部分:浏览器原理、性能优化与 TypeScript (61-75)

61. 浏览器渲染原理 (关键路径)

  • 通俗解释:浏览器拿到 HTML 代码后,怎么把它画到屏幕上?
  • 五步走
    1. 解析 HTML:生成 DOM 树(骨架)。
    2. 解析 CSS:生成 CSSOM 树(皮肤样式)。
    3. 合成 (Render Tree):把 DOM 和 CSSOM 结合。注意:display: none 的节点不会出现在渲染树中,但 visibility: hidden 会。
    4. 回流/重排 (Layout/Reflow):计算每个节点在屏幕上的确切位置和大小(算坐标)。
    5. 重绘 (Repaint):填充像素(画颜色、阴影)。
    6. 合成 (Composite):如果有多个图层(比如用了 transformz-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. 省内存:绑 1 个事件 vs 绑 1000 个事件。
    2. 动态支持:后来通过 JS 新增的 li 元素,不用重新绑定,天然就有事件。
  • 实现ul.addEventListener('click', (e) => { if (e.target.tagName === 'LI') { ... } })

67. TypeScript 核心优势

  • 静态类型:代码还没跑,写的时候就能发现错误(比如拼写错误、参数传错了)。
  • 智能提示:IDE 知道在这个对象里有什么属性,点一下全出来,开发效率极高。
  • 可维护性:对于大型项目,TS 就是文档。看一眼接口定义 (interface) 就知道数据长啥样。

68. TypeScript: Interface vs Type

  • Interface (接口):主要用来定义对象的形状。
    • 特性:可以合并(同名 interface 会自动合并属性)。支持 extends 继承。
  • 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 媒体查询,根据屏幕宽度动态修改 htmlfont-size
  • 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
  • APIIntersectionObserver。比传统的监听 scroll 事件性能好得多,浏览器原生帮你检测元素是否可见。

72. Web Worker

  • 痛点:JS 是单线程的,计算量太大(比如图像处理、加密解密)会卡死主线程,页面就动不了了。
  • Web Worker:允许你在后台开启一个新的线程运行 JS。
  • 限制:Worker 线程不能操作 DOM,不能访问 windowdocument。它只能做纯计算,通过 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)

  • 核心逻辑
    1. 定义一个变量 timer 保存定时器 ID。
    2. 返回一个函数。
    3. 每次调用时,先 clearTimeout(timer) 清除之前的定时器。
    4. 重新 setTimeout,在延迟时间后执行真正的函数。
  • 用途:搜索框输入、窗口调整大小。

77. 手写深拷贝 (Deep Clone)

  • 乞丐版JSON.parse(JSON.stringify(obj))。缺点:会丢失函数、undefinedSymbol,无法处理循环引用。
  • 面试版逻辑
    1. 判断类型,如果是基本类型直接返回。
    2. 如果是对象/数组,创建一个新的 {}[]
    3. 递归遍历属性,赋值给新对象。
    4. 循环引用:使用 WeakMap 做一个缓存字典。每次拷贝前先看字典里有没有这个对象,有就直接返回,没有就存进去。

78. 手写 Promise.all

  • 核心逻辑
    1. 接收一个 Promise 数组。
    2. 返回一个新的 Promise。
    3. 内部维护一个计数器 count 和结果数组 results
    4. 遍历数组,执行每个 Promise。
    5. 每成功一个,把结果存入 results (注意索引要对应),count++
    6. count === 数组长度 时,resolve(results)
    7. 只要有一个失败,立刻 reject(error)

79. 手写发布订阅模式 (Event Emitter)

  • 结构:一个类,里面有一个对象 events = {} 存事件。
  • 方法
    1. on(name, fn): 订阅。this.events[name].push(fn)
    2. emit(name, ...args): 发布。找到 this.events[name] 数组,遍历执行里面的函数。
    3. off(name, fn): 取消。filter 掉那个函数。
    4. 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 实现
    1. 把函数挂到目标对象上:context.fn = this
    2. 执行这个函数:context.fn(...args)
    3. 删除这个属性: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 干了四件事
    1. 创建一个新对象 obj
    2. obj 的原型指向构造函数的原型 (obj.__proto__ = Constructor.prototype)。
    3. 执行构造函数,并把 this 绑定到 obj 上。
    4. 关键:如果构造函数显式返回了一个对象,就返回那个对象;否则返回 obj

85. 函数柯里化 (Currying)

  • 概念:把 add(1, 2, 3) 变成 add(1)(2)(3)
  • 逻辑
    1. 判断当前传入参数的个数 (args.length)。
    2. 如果参数够了 (>= 原函数需要的参数),直接执行原函数。
    3. 如果不够,返回一个新的函数,继续收集参数,直到参数凑齐。

86. 解析 URL 参数

  • 需求?name=jack&age=18 -> {name: 'jack', age: '18'}
  • 方案
    1. window.location.search 拿到字符串。
    2. 去掉 ?,用 & 分割成数组。
    3. 遍历数组,用 = 分割 key 和 value。
    4. 神器Object.fromEntries(new URLSearchParams('?name=jack&age=18')) 一行代码搞定。

87. 快速排序 (Quick Sort)

  • 算法思路 (分治法)
    1. 基准:找一个中间数(Pivot)。
    2. 分区:比它小的放左边数组,比它大的放右边数组。
    3. 递归:对左边和右边重复这个过程。
    4. 合并[...quickSort(left), pivot, ...quickSort(right)]
  • 复杂度:平均 O(n log n)。

88. 列表转树形结构 (List to Tree)

  • 场景:后端给你一个扁平数组(包含 id, parentId),你要转成嵌套的树给菜单组件用。
  • 逻辑
    1. 用一个 Map (或对象) 存储所有节点,id 为 key。
    2. 遍历数组,找到每个节点的 parent。
    3. 如果 parent 存在,就把当前节点 push 到 parent 的 children 数组里。
    4. 如果 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 个知识点

如何复习:

  1. 查漏补缺:看着标题,如果你能自己讲出个大概,就过;如果卡住了,去 Google 搜详细解析。
  2. 深度优先:对于框架原理(Vue/React)和工程化(Webpack),不仅要背结论,最好能看过源码浅析。
  3. 手写练习:第 76-90 题,请务必在电脑上敲一遍,肌肉记忆很重要。

祝你面试顺利,拿到心仪的 Offer!

基于 VitePress 构建