前端知识补全
前端知识
- 接下来的内容会是我 面试 / 遇到 不会的点,或者是掌握得不清楚的点,我会尽可能将整个流程写明白,让我自己能跑通整个流程。
浏览器的同源策略?
What
- 当我们写好一个后端程序和前端程序的时候,前端向后端发送请求,提示CORS未通过,这通常是浏览器的同源策略导致的。
- 同源策略是浏览器对跨源读写的限制,例子:js不能读另一个源的响应内容。
- 链路:浏览器先执行同源策略 – 如果跨源,则需要带上CORS响应头显式访问 – 否则浏览器拦截
同源策略是拦截策略,而CORS是服务器给浏览器的许可证明
Why
- 这是为了防止Web里的跨源攻击,运行在某个 origin 下的脚本,不能读取其他 origin 的存储(localStorage/cookie/DOM)和响应内容;哪怕这些请求是用户本人在同一个浏览器里触发的。
How
- 服务端通过 CORS 配置决定向哪些 Origin 返回允许头;若未允许,浏览器会拦截前端 JS 读取响应(即使请求到达并拿到 200)。
- 校验三大部分:协议、主机名、端口
1 | public CorsFilter corsFilter() { |
XSS攻击?
What
- XSS攻击是用户将一段脚本(js)代码伪装成评论插入到系统里,当别人展开评论的时候,这段脚本会在网站里执行(因为当你展开评论的时候,浏览器把用户输入当 HTML 解析,前端可能会将评论使用innerHTML插入进到页面里,脚本被执行)
- 用户可能会看到奇奇怪怪的弹窗或者跳转到别的页面
- 账号被莫名改资料
- 如果你是权限比较高的用户,执行了恶意脚本,很可能就会让系统崩溃。
How to prevent
- 核心原则:永远不要把用户的输入当成HTML渲染
- React默认渲染字符串是转义的,{content}
- 坑:如果为了支持富文本(图片,代码,换行)使用了dangerouslySetInnerHTML,这样就会插入脚本。
- 解决方法:
- 输出编码要默认转义,用纯文本来渲染,不允许用html
- 白名单清洗:如果必须要富文本,过滤掉事件脚本,事件属性,危险的url结构
- token尽量不要放在localstorage里,可以放在HttpOnly cookie,这样js读不到cookie
- 设置SameSite,有三种模式,strict,lax以及none,分别控制跨站是否自动带上cookie
- 解决markdown富文本的XSS攻击策略
- 设置白名单,只允许我们可以的标签,比如p,a,strong,code等,对属性也要白名单,禁止所有的事件属性,比如on*
- 禁止inline script
- 代码块永远escape(转义)后放进code和pre里展示
JWT更安全的登录?双Token
What
- 我发现了原来的逻辑是改Localstorage就可以让前端渲染出管理员的界面,这样不行
- 于是引入了JWT的accessToken以及refreshToken
- accessToken一般时间短,15-30分钟,存放在React的内存里,一刷新就没有
- refreshToken时间在7-30天,存放在httponly的cookie里
- 此外这个refreshToken还存放在了redis里,使用版本号控制,让非法用户强制下线
How
登录功能
- 用户首次登陆的时候后端校验账户和密码是否合法,如果合法就签发两个token
- access token 过期 → 前端请求业务接口返回 401
- 前端调用 /api/v1/auth/refresh(不带 refresh token,浏览器会自动附带 HttpOnly Cookie)
- 后端从 Cookie 里拿 refresh token,去 Redis 校验是否存在/是否有效
- 校验通过 → 生成新 access token(和新的 refresh token),并在 Redis 里更新
- 返回新的 access token 给前端,继续请求
来回时间就是一个rtt
踢人功能
- 在redis里添加tokenVersion,因为我们登陆的时候用户还是有当前的accessToken,所以他还能存活这么久
- 加一个
user:tokenVersion:<uid>在redis里,签发的时候把ver写进JWT, - 踢人就增加版本号,这样让他的所有旧accessToken失效。原因是我们的在redis里的逻辑是
user:tokenVersion:<uid> = N,拦截器校验时比较Redis ver,如果不一致就401
- 加一个
既然有了踢人功能,那能不能把accessToken设置为24h?
- 不建议
- jwt最大的优点就是无状态,后端只需要使用cpu进行简单的数学运算就可以得到结果
- 如果是24h,那用户每一次调用接口的时候都会查询redis,那本质上和session没有区别,失去了 JWT 无状态校验的性能优势。
- 缩短风险窗口: 短时效(15-30min)的 AT 配合长时效的 RT,可以在‘不需要频繁查库’和‘用户被盗号后的损失’之间取得平衡。即便不实时查 Redis,黑客的攻击窗口也仅限于这几分钟。
项目登录流转
How
- 用户点击页面触发接口请求前端,拦截器从
localstorage取hnu_token,放到请求头Authorization: Bearer xxx - 后端从
Authorization里取Bearer token解析JWT,再校验token是否一致 - token过期/无效时,接口会返回401,前端响应拦截器会调用refresh的api,这个请求会自动带上httpOnly的
hnu_refresh_token的cookie - refresh成功后,后端返回新的accessToken,前端setToken()写回localStorage,然后重放刚刚失效的请求。
- 为什么刷新页面登录态不掉,因为不是只放内存:accessToken 持久化在 localStorage。页面刷新后,AuthProvider 初始化会重新读 localStorage,并调用 /api/v1/users/me 做一次有效性校验。
refresh token 为什么放 Cookie,不放前端 localStorage?
答:refresh token 通过 HttpOnly Cookie 下发,前端 JS 拿不到,减少 XSS 窃取风险;access token 才放 localStorage 走请求头。
“每次请求都带 Bearer accessToken,后端拦截器验 JWT + Redis 版本;401 时用 cookie 里的 refreshToken 换新 token;刷新页面靠 localStorage 恢复,所以不会直接掉线。”
项目路由管理
- 公开路由
- /login、/register、/、/posts/:id 直接可访问。
- 登录保护路由
- 我封装了 RequireAuth,基于 useAuth() 的 token + loading 判断。
- 没登录就 Navigate 到 /login。
- 管理员路由
- 我再封装 RequireAdmin,在登录基础上再判断 user.role === ‘ADMIN’,否则跳回首页。
浏览器的事件循环机制是什么?
What
- 事件循环是浏览器在单线程上运行 JS 的调度机制:把“同步 JS 执行”与“外部事件/异步结果(定时器、网络、交互等)”通过任务队列串起来,循环取任务执行。
- 由三部分组成:调用栈、Web APIS、任务队列
1 | setTimeout(()=>{ |
会打印2,1。是因为这一段script是一个宏任务,然后setTimeout先是注册了一个定时器任务,把这个回调函数log1放到了宏任务队列里,就到微任务,微任务将这一段script的宏任务收尾,所以先log,然后清空了微任务之后。script 这个宏任务已经执行完了;之后从宏任务队列里取的是 定时器回调这个新的宏任务。
- 宏任务:整段Script,
setTimeout\setInterval回调,DOM事件回调(Click,Input) - 微任务:Promise
Why
- js是单线程的,同一时间只能做一件事,避免多线程共享DOM带来的竞态
- 让页面响应可控,长任务不会永远占住
How
- “执行一个宏任务 → 然后清空微任务队列 →(必要时渲染)→ 下一轮宏任务”。
- 执行的顺序
- 宏任务:定时器(setTimeout,setInterval)回调、点击回调、整段Script
- 微任务:
Promise.then、catch/finally用于“本轮宏任务的收尾/延续”
- 如果浏览器一直执行宏任务?
- 问题:理论上来说因为是单线程,宏任务如果一直不返回(同步死循环/超长计算),浏览器确实会一直卡在这个宏任务里。
- 解决方法
- 把大任务分成多个短任务
- 时间分片
- 给Worker
- 在浏览器的事件循环标准中,UI 渲染通常发生在所有微任务(Microtasks)执行完毕之后,以及下一个宏任务(Macrotask)开始之前。
节流和防抖有什么区别?
What
- 防抖(debounce):在连续触发中不断重置计时器,停止触发一段时间后才执行一次(“取最后一次”)。
- 节流(throttle):在连续触发中按固定间隔执行,单位时间最多执行一次(“限速”)。
How
1 | function debounce(fn,wait = 300) { |
Why
- 防抖:解决“抖动触发导致的重复计算/重复请求”,减少无效开销(如输入联想、频繁点击)。
- 节流:解决“持续高频触发导致的性能压力/掉帧”,让回调执行频率可控(如滚动、鼠标移动、拖拽)。
为什么要return function,因为function是一个函数,需要返回值,这里的返回值就是一个函数
遍历找到D盘的两个重复文件?
How
- 文件夹有递归嵌套的结构,每一个文件夹就像是树一样,遍历树就使用DFS/BFS/递归
- 构建 Map<size, filePaths[]>,只保留 filePaths.length >= 2 的桶作为候选(不同 size 必不相同)。
- 对候选文件用流式读取计算内容 hash(如 SHA-256/BLAKE3),构建 Map<hash, filePaths[]>;同一 hash 下的文件认为“高度可能相同”。
深拷贝和浅拷贝
What
- 浅拷贝:只复制一层属性;若属性值是
对象/数组/函数等引用类型,复制的是引用,因此会共享同一份深层数据。 - 深拷贝:递归复制各层级,让嵌套对象/数组变成新的引用,通常不共享深层引用。
Object.create 不是拷贝,是“以某对象作为原型”创建新对象。
Why
- React/Redux 等依赖
不可变更新 + 浅比较(===)做渲染与 memo 判断:你只需要“沿修改路径拷贝”(结构共享),而不是全量深拷贝。 - 全量深拷贝会带来:CPU/内存开销、GC 压力、缓存失效(引用每次都变,memo 失效)。
How
深拷贝
1 | //方法1:使用structuredClone |
浅拷贝
1 | //方法1:展开运算符 |
对React的理解
What
- JSX 不是 HTML,它会被编译成
React.createElement(...)的调用,产出的是一棵React Element树(普通 JS 对象描述 UI)。它是“UI 的描述”,不是浏览器 DOM。 - React 以组件为单位,由
state/props驱动渲染:每次 render 本质是“根据当前 state/props 计算下一棵 element 树”。
How
- 当
state/props变化时,React 会重新执行组件函数得到next element tree,再和prev element tree做 reconciliation(协调/对比),决定哪些地方需要更新。
所以React是一个声明式的UI框架,依托于Props/State,JSX会被编译为createElement,得到一个Element树,当状态变化时就会render一颗新树,然后通过diff算法来比较计算最小开销,最后把差异提交到真实的DOM
Bad
虚拟DOM也有一些不好的地方,它不是免费的抽象,在一些场景里会引入额外成本/复杂度。
- 额外的计算与内存开销:每次更新 React 都要重新执行组件、生成 element/fiber、做 reconciliation,再 commit。对超高频更新(动画、拖拽、实时渲染)可能比“直接命令式改 DOM/Canvas/WebGL”更重。
- 启发式 diff 不是全局最优:React 的对比依赖规则(同层、同类型、key),不是最小编辑距离;key 写错/用 index 会导致错误复用、状态串位或不必要重建,性能和正确性都受影响。
- 误用会导致性能问题:不稳定引用、无意义 re-render、巨型列表不做虚拟滚动、在 render 里做重计算——这些都会放大虚拟 DOM 的成本,所以工程上常配 memo/useMemo/useCallback、列表虚拟化、拆分组件。
useMemo和useCallback是什么?(react优化相关)
What
useMemo:记住“计算出来的值”(数组/对象/数字都行),依赖不变就复用上次结果。useCallback:记住“函数本身”(保持函数引用稳定)。它等价于:useCallback(fn, deps) ≈ useMemo(() => fn, deps) 它们都不会阻止组件 re-render;组件仍会 render,只是你拿到的值/函数不重新创建。就类似于,你唯一的输入一定会得到唯一的输出
How
1 | const list = useMemo(() => expensiveFilter(data, keyword), [data, keyword]) |
Why
- 省计算:过滤/排序/聚合很贵时,用 useMemo 避免每次 render 重算。
- 稳定引用,配合浅比较优化:用 useMemo/useCallback 让引用稳定,避免“不必要的触发”。
“数组/对象/函数每次 render 都会创建新引用;如果它们作为 props/依赖,会让浅比较认为变了,从而触发不必要更新。useMemo/useCallback 用来稳定这些引用。”
浅比较
- 对基本类型(Number,Boolean,String):比 值(===)。
- 对引用类型(arr[],obj{},function()):比 引用地址(===),不看里面内容。
1 | const a = { x: 1 } |
在 React 里常见浅比较场景:
React.memo默认用浅比较 props:只要某个 prop 引用变了,就认为“变了”。useEffect/useMemo/useCallback 的依赖数组比较:每一项用 Object.is(和 === 很像)做浅比较。
数组怎么合并?
How
1 | //方法1:concat拼接 |
如何写一个间隔一秒自动切换颜色的正方形
How
- 间隔:setInterval
- 自动切换 <==>
状态监听<==> 使用hook(useState) - 清除定时器 <==>
useEffect,需要返回清除函数 - react里传样式传的是一个
JSON
1 | import { useEffect, useState } from "react" |
useEffect是什么?
What
useEffect是 React 的“副作用hook”:让你在组件渲染到页面之后去做一些事,并且能在需要时清理这些事。- 副作用
- 定时器 setInterval/setTimeout
- 订阅/事件监听 addEventListener
- 发请求 fetch/axios
- 操作 DOM、设置标题 document.title = …
这些都不应该放在组件函数体里反复执行。
1 | useEffect(() => { |
How
依赖数组 [deps] 决定“执行频率”
- 不写第二个参数:每次渲染后都执行(很少用,容易反复绑定/请求)
- 写 []:只在组件首次挂载后执行一次(适合建定时器/订阅一次)
- 写 [a, b]:a 或 b 变化时执行(常用于“根据某个 state/prop 变化去请求/同步”)
return 的清理函数什么时候执行
- 组件卸载时执行一次
- 或者依赖变化导致 effect 重新执行时:先清理上一次,再执行新的
内存泄漏有哪些场景?
Where
- 循环依赖,A依赖B, B依赖A
- 定时器/轮询没有清理 (useEffect return 一个clearInterval)
- 闭包长期持有对象
数组有哪些遍历方法?
- 正常的for循环
- forEach:用于执行回调函数,但是一般不修改原数组内容,用于打印值
1 | let arr = [1,2,3] |
- map: 用于创建一个新的数组,本质也是遍历
1 | let arr = [1,4,9] |
- for … of / for…in 用于遍历值
对Promise的理解
Why
- 避免回调地狱的出现,在没有promise之前,需要很多个if else来列举出全部的情况,现在只需要成功或失败
- Promise 把异步结果抽象成一个对象,提供统一的组合方式(then 链、catch、all 等),让控制流更清晰。
What
- Promise是未来承诺会有返回值,然后去执行对应的逻辑
- 三个状态:pending、fulfilled、rejected。状态不可逆
- then去执行fulfilled的回调,catch去执行rejected的回调
How
1 | const p = new Promise((resolve,reject)=>{ |
两个API
- Promise.all: 所有成功才会走then,如果有一个失败了,就会报错;
1 | const p1 = Promise.resolve('data1') |
- Promise.allSettled: 为什么会出现他,如果我们主页有四个图表,肯定不想因为某一个挂了就不渲染其他正常的地方,allSettled只会返回一个then,附上它的status
1 | const p1 = Promise.resolve('成功') |
对async和await的理解
What
- async 保证函数返回 Promise;return x 等价 Promise.resolve(x),throw err 等价 Promise.reject(err);await 会等待一个 Promise,成功返回值,失败抛异常。
Why
async和await是Promise的语法糖,将.then拍平,用try/catch包裹promise的执行结果- async function f(){}:保证这个函数返回 Promise
- 在 async 里 return x,等价于 return Promise.resolve(x)
- 在 async 里 throw err,等价于 return Promise.reject(err)
await后面接受任何值,但如果是 Promise 才会“等待”;不是 Promise 就立刻得到该值(等价于 await Promise.resolve(x))。这个async函数里就需要await后面的Promise对象的状态,如果是fulfilled,走try里面的逻辑,如果是rejected,走catch里的逻辑
How
1 | async function requestJSON(url) { |
受控组件和非受控组件
What
- 受控组件(Controlled):表单值 由 React state 控制。value/checked 绑定 state,onChange 更新 state。单一数据源:state。
- 非受控组件(Uncontrolled):表单值 由 DOM 自己维护。React 不用 state 绑住值,需要时用 ref 读取(或表单提交时读)。数据源:DOM。
浏览器存数据有哪些方式? localStorage和sessionStorage有什么区别
What
- Cookie:最早的存储方式,容量小(约 4KB),每次请求都会带在 HTTP 头部,主要用于身份验证(Session ID)。
- Web Storage:包含 localStorage 和 sessionStorage。
- IndexedDB:浏览器内置的“非关系型数据库”,可以存储大量结构化数据(甚至是文件和二进制数据),且支持索引和事务。
- Cache API:主要用于 Service Worker,用来缓存网络资源(如 HTML、JS、图片),实现离线访问。
Why
- localStorage
- 永久有效。除非你手动删除或用户清理浏览器缓存,否则关闭浏览器后数据依然存在。
- 同源共享。同一域名下的所有页面、标签页都可以读写同一个 localStorage。
- sessionStorage
- 会话级有效。一旦标签页(Tab)或窗口被关闭,数据就会被清空。
- 标签页独立。即使是同一个网站,在不同标签页打开,它们的 sessionStorage 也是互不干扰的。
状态码问题
如果前端报404该如何排查?
- 确认请求地址:看看url是不是拼错了
- 查看方法是不是请求错了:如果接口是
post,但是使用了get - 确认前端代理:nginx或者next.config是不是正确代理到了api
- 确认后端接口是否存在
- 路径部署可能错误
全栈开发遇到500如何排查?
- 500几乎一定是服务端问题,先看日志或者控制台
- 用
postman/curl去直接调接口 - 检查请求参数列表:JSON格式,字段名,类型
- 代码逻辑错误
- 中间件炸了或者没开:SQL、Redis、第三方api
502和504
- 502网关有问题
- 504超时
评论如果有很多的话,如何处理?
- 作为后端
- 设置分页查询,将热门的评论放到redis中
- 作为前端
- 虚拟列表
What
- 虚拟列表的核心思想是只渲染可视区域的DOM
- 传统方法,假设10000条评论,那就会渲染出10000个DOM,会很卡,浏览器真正慢的是 DOM,不是 JS。
1
comments.map(c => <Comment />)
- 当DOM很多的时候,Layout计算慢,滚动也需要重排
How
- 如果用户屏幕大小就这么多,为什么要去渲染全部的评论,只需要用
评论区容器的px / 每条评论容器的px就是渲染出来的评论数。 - 滚动时候用scrollTop,它表示滚动了多少px,1600px / 80px = 20条
1
startIndex = Math.floor(scrollTop / itemHeight)
所以只需要渲染 startIndex + 评论数
- 虚拟列表
- 核心只有 4 步:
- 获取滚动距离 scrollTop
- 计算起始 index start = scrollTop / itemHeight
- 计算需要多少条 visibleCount = containerHeight / itemHeight
- 渲染 data.slice(start, start + visibleCount)
手写一个倒计时的拓展
1 | import { useEffect, useState } from 'react' |
What
- useEffect(fn,[]),[]表示依赖列表为空,意思是:
effect 不依赖任何 state / props,因此只在组件首次挂载时执行一次。
- 所以第一次,如果使用函数式的更新,即使依赖项为[],也会拿到最新的prev
- 第二次由于没填写依赖项,永远拿的是第一个值,因为没有依赖,所以不会重新执行
- console.log两次是因为开发时有了strict模式,默认走两次,会让副作用出现。
React 的通信方式有哪些?
- Props
- Callback:父组件传函数,子组件调用
- 兄弟组件通信:把状态提升到共同父组件
- 全局状态管理
能把hook写在条件判断里吗?
- 不能,从React的渲染机制来看,Props/State改变了就会重新渲染,如果我们写在了某一个循环判断里,然后成功渲染了,但是由于数据改变了,又会调用App()去渲染,这样就会多次渲染,会报错。
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
图片的懒加载是怎么实现的?
- 原生:img标签里的loading属性设置为lazy
- Intersection Observer API
1 | const images = document.querySelectorAll('img[data-src]'); |
预热加载: 在 Intersection Observer 中设置 rootMargin: “200px”,让图片在进入视口前 200 像素就开始加载,用户几乎感知不到加载过程。
大模型这种“蹦字”的效果,前端是怎么拿到的?底层协议是什么?
- 通过fetch 配合 ReadableStream,fetch配合ReadableStream
- 使用 fetch 请求大模型接口时,返回的 response.body 是一个流
1 | const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ prompt }) }); |
响应式布局?
What
- 页面根据设备尺寸自动调整布局(媒体查询、flex、grid)
Why
- 适配手机、平板、PC提升用户体验
How
- 使用 @media、flex/grid(使用弹性布局替代固定像素。)、百分比布局、rem/vw(相对单位)、移动端优先设计。
State和Props的区别
What
- 都是js对象,用于保存信息并触发渲染,Props是组件的配置参数(外部传入),State是组件的私有状态(内部管理)
- Props只读,State可变
如何区分一个变量是数组还是对象?
How
- typeof是没用的,只能认出基本类型,typeof {}和typeof []的结果都是object
- 用instanceof在原型链上找
1 | const a = {} |
- API,isArray()
- Object.prototype.toString.call()
浏览器缓存?
What
- 浏览器缓存 = 浏览器把请求过的资源(HTML / JS / CSS / 图片)存到本地,下次访问时直接使用,而不是重新请求服务器。
Why
- 因为很多资源 不会频繁变化:React打包后的JS、图片、CSS
- 分为两类
- 强制缓存:浏览器直接用本地缓存,不发请求到服务器。
- 协商缓存:浏览器会 询问服务器资源有没有变化。如果没变化:服务器返回 304 Not Modified,浏览器继续使用缓存。协商缓存主要通过
Last-Modified / ETag实现
- 为什么有了 ETag 还要 Cache-Control?
Cache-Control 和 ETag 解决的是不同层级的问题。Cache-Control 用于控制缓存是否过期,如果缓存未过期浏览器会直接使用本地资源,不会发送请求;而 ETag 是协商缓存机制,用于在缓存过期后让服务器判断资源是否发生变化。因此两者通常配合使用,先通过 Cache-Control 减少请求,再通过 ETag 判断资源是否需要重新下载。
How
- 服务器返回响应头:Cache-Control: max-age=3600(资源在3600秒内直接使用缓存)
- Expires(旧方案):Expires: Wed, 21 Oct 2026 07:28:00 GMT(依赖客户端时间)
- 对于静态资源,缓存用一年,配合文件名hash,重新打包上传如果文件hash值改了就重新请求。
- 什么时候走缓存
- 回车访问:先走强缓存,强缓存失效才走协商缓存(只认为是用户普通访问)
- F5刷新:走协商缓存(用户想要最新的资源)
- ctrl+F5:强制请求,两个缓存都不走
React打包后:文件内容变化 → hash变化 → 文件名变化
SPA(Single page Application)做缓存应该怎么做?
How
- 静态资源缓存
- JS、CSS、图片,打包后基本不变,文件名带hash,如果hash不变cache-control没到期就一直请求强缓存。
- 接口数据缓存
- 比如用户信息、文章列表、商品列表,一般会存在内存里/localstorage/sessionstorage/indexedDB这类缓存层。
- 页面缓存状态
- 比如你点击商品进入详情页,回退到上一页还回到相同的页面。
如何设置 cookie 和 session 的过期时间
- cookie
- expire是相对时间,max-age是绝对时间
- session
- 服务端设置的
闭包的实际应用场景
What
回调函数
防抖节流
封装私有变量
缓存结果
useEffect和useCallback
