前端学习资料库阅读 · 理解 · 练习

BUILD A SOLID FOUNDATION

前端基础 必背知识点

把框架背后的基础,讲得清楚、写得出来。
从页面结构到浏览器与网络,按主题查漏补缺。

HTML · CSS · JavaScript · 浏览器 · 网络49 张卡 · 47 个必背重点
掌握进度0 / 49

能脱稿回答、解释代码,再勾选已掌握。

每张卡按三步复习:先口述 30–60 秒,再核对要点,最后手写或推演代码。先理解条件和边界,再记结论;遇到薄弱项,点开官方原文补充。

共 49 个知识点

TOPIC 01 / 07

HTML 与可访问性

7 个知识点
01.01必背

HTML 语义化与文档结构

面试怎么问什么是 HTML 语义化?为什么不能把所有元素都写成 div?

查看背诵答案与示例
可以这样回答

语义化是按内容和用途选择 HTML 元素,让结构本身表达含义。导航用 nav,主要内容用 main,独立文章用 article,标题按层级组织。它有助于辅助技术定位区域,也让代码和搜索引擎更容易理解内容。视觉样式交给 CSS;标签选择依据内容关系,不依据默认字体或边距。

记住这几个点

  • main 表达页面主要内容;nav 表达主要导航区域。
  • article 适合可独立分发的内容,section 表达一个主题分区。
  • 标题级别反映内容层次,不能为了字号跳级。
  • 跳转使用带 href 的 a,执行操作使用 button。
关键示例 html
<header>前端复习</header>
<nav aria-label="主导航"><a href="/">首页</a></nav>
<main>
  <article>
    <h1>HTML 基础</h1>
    <section>
      <h2>语义化</h2>
      <p>按内容意义选择元素。</p>
    </section>
  </article>
</main>
易错点

语义标签不自动保证 SEO 排名或无障碍达标;错误标题、缺少名称和不可操作的交互仍需修复。

常见追问:section 和 div 怎么选?

有独立主题且通常可用标题概括时考虑 section;只为布局或样式分组、没有额外语义时使用 div。不要为了替换 div 而机械增加 section。

01.02必背

表单、label 与按钮类型

面试怎么问如何写一个可访问、行为正确的 HTML 表单?

查看背诵答案与示例
可以这样回答

表单控件要有可识别的名称,通常用 label 的 for 对应控件 id,也可将控件包在 label 内。合适的 input 类型可提供键盘和基础校验,提交字段依靠 name。button 应明确 type:提交用 submit,普通操作用 button,避免误提交。浏览器校验改善体验,服务器仍要验证输入。

记住这几个点

  • id 用于关联标签;name 决定普通表单提交时的字段名称。
  • placeholder 是输入提示,不能作为 label 的可靠替代。
  • email、required、min 等提供约束,但后端校验仍必需。
  • 表单内普通按钮明确 type="button"。
关键示例 html
<form action="/subscribe" method="post">
  <label for="email">邮箱</label>
  <input id="email" name="email"
         type="email" autocomplete="email" required>
  <button type="submit">订阅</button>
  <button type="button">查看说明</button>
</form>
易错点

在表单中省略 button 的 type 可能触发提交;disabled 控件通常不参与普通表单提交,readonly 控件仍可提交。

常见追问:label 的作用只是显示文字吗?

不是。正确关联会提供控件的可访问名称,点击标签也能把焦点转到控件或切换复选框,从而扩大操作区域;页面内 id 还应保持唯一。

01.03必背

script、defer、async 与 module

面试怎么问普通 script、defer、async 和 type=module 有什么区别?

查看背诵答案与示例
可以这样回答

解析器遇到普通经典脚本会暂停 HTML 解析,获取并执行后继续。外部经典脚本加 defer 可并行下载,解析结束后按文档顺序执行;async 下载就绪就执行,不保证多个脚本的顺序。模块脚本默认延后执行并加载依赖,通常不需 defer;模块加 async 后,依赖就绪即可提前执行。

记住这几个点

  • 经典外部 defer 脚本在 DOMContentLoaded 之前执行。
  • 经典 async 适合不依赖前后脚本顺序的独立代码。
  • defer 对内联经典脚本无效,不能用于延迟它。
  • 模块默认使用严格模式,跨域加载需满足 CORS。
关键示例 html
<!-- 两个外部经典脚本按文档顺序执行 -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>
<!-- 独立统计脚本:不依赖执行顺序 -->
<script async src="analytics.js"></script>
<!-- 模块默认延后执行 -->
<script type="module" src="main.js"></script>
易错点

并行下载不等于执行不占主线程;经典脚本同时写 async 与 defer 时,支持 async 的浏览器按 async 行为处理。

常见追问:DOMContentLoaded 会等待所有图片和 async 脚本吗?

不会。它等待 HTML 解析以及延后脚本执行完成;图片与 async 脚本不在其等待范围内。load 更晚,通常用于需要相关资源完成的场景。

01.04进阶

资源加载提示与懒加载

面试怎么问preload、preconnect 与 loading=lazy 分别解决什么问题?

查看背诵答案与示例
可以这样回答

preload 让浏览器提前获取本页即将使用但发现较晚的资源,需正确指定 as 和请求模式;它不会自动执行脚本或应用样式。preconnect 提前建立重要跨站连接,适合少量关键来源。loading=lazy 延后非首屏图片或 iframe 的加载。使用前应看网络瀑布,避免提示过多争抢关键资源带宽。

记住这几个点

  • preload 是提前获取,使用资源仍需相应的 link、script 或 CSS。
  • 字体 preload 需匹配字体请求的 CORS 模式,通常写 crossorigin。
  • preconnect 适用于真正会访问的重要域名,不能无限添加。
  • 首屏关键图片通常不应懒加载,非关键图片可按需延后。
关键示例 html
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preload" href="/fonts/ui.woff2"
      as="font" type="font/woff2" crossorigin>
<img src="/images/below-fold.jpg"
     width="640" height="360" loading="lazy"
     alt="复习资料书架">
易错点

preload 的 URL、as 或凭据模式与实际请求不匹配时可能无法复用,甚至重复下载;预加载越多不代表越快。

常见追问:prefetch 和 preload 是一回事吗?

不是。preload 针对当前页面较快需要的资源;prefetch 是对未来可能使用资源的提示,通常优先级较低,浏览器也可能不执行该提示。

01.05必背

文档元信息与 SEO 基础

面试怎么问一个页面最基础的文档元信息应该包含什么?

查看背诵答案与示例
可以这样回答

基础 HTML 应声明字符编码、语言、清晰且独特的 title,并用 description 简述页面内容。标题层次、真实链接和可理解正文帮助用户及搜索引擎理解页面;移动端用 viewport 配合响应式布局。SEO 还依赖可抓取性、内容和服务端响应,不能靠堆关键词或加几个 meta 就保证排名。

记住这几个点

  • title 描述当前页面,避免所有路由都用相同标题。
  • meta description 是摘要候选,搜索引擎可能展示其他内容。
  • html lang 标明内容语言,有助于辅助技术正确发音。
  • viewport 常用 width=device-width, initial-scale=1,不禁止用户缩放。
关键示例 html
<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>前端基础必背知识点 · 复习资料库</title>
  <meta name="description" content="按主题复习 HTML、CSS 和 JavaScript。">
</head>
<body><h1>前端基础必背知识点</h1></body>
</html>
易错点

meta keywords 不能替代高质量内容;描述不是排名承诺,也不保证作为搜索结果摘要。

常见追问:Vue 单页应用应该关注什么 SEO 问题?

要检查目标爬虫能否获得正文、各路由标题是否正确、链接是否可抓取以及错误 URL 是否返回合适状态;内容站可根据需求采用 SSR 或预渲染。

01.06必背

响应式图片与布局稳定

面试怎么问srcset、sizes、picture 和图片宽高分别有什么作用?

查看背诵答案与示例
可以这样回答

srcset 提供不同尺寸或像素密度的候选图片,使用宽度描述符时 sizes 描述预期显示宽度,浏览器结合环境选择资源。picture 适合不同断点裁切或格式回退。给图片提供匹配比例的 width、height,再用 CSS 自适应缩放,可在下载前预留空间,减少图片造成的布局偏移。

记住这几个点

  • srcset 的 400w 指图片固有宽度,不是 CSS 显示宽度。
  • sizes 提供选图提示,不直接设置图片的 CSS 宽度。
  • picture 内仍需 img 作为实际图片元素及回退。
  • 有意义图片写描述性 alt;纯装饰图片通常使用 alt=""。
关键示例 html
<img src="cover-800.jpg"
     srcset="cover-400.jpg 400w, cover-800.jpg 800w"
     sizes="(max-width: 600px) 90vw, 560px"
     width="800" height="450"
     style="width: min(90vw, 560px); height: auto"
     alt="前端知识点思维导图">
易错点

仅写 max-width:100% 不能在未知图片比例时充分预留高度;srcset 的候选尺寸描述必须符合实际图片尺寸。

常见追问:图片 width 和 height 写死后还能响应式吗?

可以。HTML 宽高可提供比例信息,CSS 的 width 或 max-width 配合 height:auto 决定实际显示尺寸;保持同一比例就能兼顾响应式与空间预留。

01.07必背

原生交互语义与键盘可访问性

面试怎么问给 div 加 role=button,就与原生 button 等价了吗?

查看背诵答案与示例
可以这样回答

不等价。ARIA 主要向辅助技术描述角色和状态,不会自动补齐焦点、键盘操作或按钮默认行为。原生 button 默认可通过键盘操作,并提供按钮语义;自制控件还要维护焦点、快捷键和状态。实现时优先选合适的原生元素,保留清楚的焦点样式,并让图标按钮拥有可访问名称。

记住这几个点

  • button 执行操作,a[href] 表达导航,交互含义要一致。
  • 原生按钮支持 Enter、Space 激活;role 本身不添加此行为。
  • tabindex=0 可进入常规 Tab 顺序,避免用正数强行重排。
  • aria-label 可给只有图标的按钮提供名称,状态要与实际行为同步。
关键示例 html
<button type="button" aria-label="关闭弹窗">
  <span aria-hidden="true">×</span>
</button>
<style>
  button:focus-visible {
    outline: 3px solid #087f72;
    outline-offset: 3px;
  }
</style>
易错点

删除 outline 却不提供替代焦点样式,会让键盘用户难以定位;aria-hidden 不能作为禁用或隐藏可聚焦控件的替代方案。

常见追问:无障碍检查最简单的第一步是什么?

先只用键盘完成主要流程:检查 Tab 顺序、焦点是否可见、Enter 或 Space 能否操作、弹窗关闭后焦点能否返回触发点,再结合辅助技术检查名称和状态。

TOPIC 02 / 07

CSS 与布局

7 个知识点
02.01必背

盒模型与 box-sizing

面试怎么问content-box 与 border-box 的宽度怎么计算?

查看背诵答案与示例
可以这样回答

CSS 盒子从内到外包含内容、内边距、边框和外边距。content-box 的 width 指内容宽度,实际边框盒宽度还需加两侧 padding 和 border;border-box 的 width 包含内容、padding 和 border,便于控制布局尺寸。两者都不把 margin 算进 width,溢出内容也可能超出盒子边界。

记住这几个点

  • content-box:边框盒宽度 = width + 左右 padding + 左右 border。
  • border-box:声明宽度包含 padding、border,内容区域随之缩小。
  • margin 是盒子外部间距,不属于声明的 border-box 宽度。
  • box-sizing 不默认继承,可用通配选择器包含伪元素统一设置。
关键示例 css
*, *::before, *::after { box-sizing: border-box; }
.card {
  width: 300px;
  padding: 20px;
  border: 2px solid #ccd8d6;
  margin: 16px;
}
/* 边框盒 300px,内容宽度为 256px */
易错点

border-box 不意味着包括 margin,也不保证内容永不溢出;过长单词、固定宽子元素仍需处理。

常见追问:为什么 width:100% 加 padding 容易溢出?

默认 content-box 把百分比宽度分配给内容区域,再额外增加 padding 和 border,可能超过父容器。使用 border-box 后这些部分包含在声明宽度内。

02.02必背

层叠、优先级与选择器权重

面试怎么问CSS 冲突时如何判断最终生效的规则?

查看背诵答案与示例
可以这样回答

先确认声明是否匹配,再看层叠来源、重要性和层级等条件;在有优先权的同一来源及层内才比较选择器权重。权重按 ID、类属性伪类、类型伪元素三列从左到右比较,不做十进制加法。权重仍相同时,作用域接近程度与书写顺序继续决定结果。调试应看开发者工具的实际覆盖链。

记住这几个点

  • ID 比较第一列,类、属性和多数伪类比较第二列,标签及伪元素比较第三列。
  • :where() 本身及其参数的权重为零。
  • :is()、:not()、:has() 的权重取参数列表中最高的选择器权重。
  • !important 改变层叠重要性,不是给选择器增加权重。
关键示例 css
/* 假设同一来源、同一层,都是普通声明 */
.card .title { color: #576; } /* 0-2-0 */
#app h2 { color: #087; }      /* 1-0-1,胜出 */
:where(#app) h2 { color: #999; } /* 0-0-1 */
/* 100 个 class 也不会进位成为 1 个 ID */
易错点

“写在后面就生效”只在前面的层叠条件也相同后成立;不能跳过 origin、!important、cascade layer 直接比 ID 数量。

常见追问:如何减少优先级打架?

保持选择器简洁,用组件类名和可控的层组织样式,避免多层 ID 嵌套;外部库可放在较低普通层,先查覆盖原因,再决定是否确实需要 !important。

02.03必背

Flex 主轴、交叉轴与尺寸分配

面试怎么问Flex 如何对齐和分配空间?为什么子项有时缩不下去?

查看背诵答案与示例
可以这样回答

Flex 是以一条轴为主的一维布局。flex-direction 决定主轴,justify-content 分配主轴剩余空间,align-items 控制交叉轴对齐。flex-grow、flex-shrink 和 flex-basis 共同决定子项尺寸。子项默认最小尺寸可能受内容约束,长文本时可按布局方向设置 min-width:0,配合换行或省略处理。

记住这几个点

  • 主轴不一定水平;direction 与书写方向都会影响轴和起点。
  • flex-basis 给出分配空间前的主尺寸基础,auto 会参考相应尺寸或内容。
  • flex-shrink 分配负空间时还会考虑项目的基础尺寸。
  • 换行后每一行独立分配空间;align-content 用于多行在交叉轴上的分布。
关键示例 css
.row { display: flex; align-items: center; gap: 12px; }
.avatar { width: 48px; flex: 0 0 48px; }
.content { flex: 1 1 0; min-width: 0; }
.title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
易错点

justify-content:center 不代表垂直居中;flex:1 也不保证最终等宽,最小尺寸、内容和其他约束仍可能影响结果。

常见追问:一行内容怎样水平垂直居中?

用 flex 容器设置 justify-content:center 和 align-items:center,并保证容器有可用空间;同时确认 flex-direction,主轴不一定水平。

02.04必背

Grid、fr 与 minmax

面试怎么问Grid 和 Flex 怎么选?fr 与 minmax 如何理解?

查看背诵答案与示例
可以这样回答

Grid 同时定义行和列,适合二维对齐、页面区域及卡片网格;Flex 更适合单轴上的排列和分配空间,两者常嵌套使用。fr 表示网格轨道分配可用空间的份额,并不直接等于容器宽度百分比。minmax 给轨道设置尺寸范围,结合 repeat 与 auto-fit 可以创建随容器宽度变化的列数。

记住这几个点

  • grid-template-columns 和 rows 定义显式轨道,自动放置可能生成隐式轨道。
  • gap 占据空间,会影响可分配给 fr 的剩余空间。
  • minmax(0,1fr) 可避免轨道默认最小内容尺寸撑宽布局。
  • auto-fit 会折叠未被占用的重复轨道,auto-fill 会保留这些轨道。
关键示例 css
.cards {
  display: grid;
  grid-template-columns:
    repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 16px;
}
.cards > * { min-width: 0; }
易错点

1fr 不总是容器的某个固定百分比;使用 Grid 或 Flex 改变视觉顺序不会同步改变 DOM 阅读和键盘导航顺序。

常见追问:三列 1fr 为什么还可能横向溢出?

轨道和子项的自动最小尺寸可能受长内容影响。可以按需求改为 repeat(3,minmax(0,1fr)),再给子项 min-width:0,并处理长文本或固定宽媒体。

02.05必背

BFC、浮动与外边距折叠

面试怎么问BFC 是什么?能解决哪些常见布局问题?

查看背诵答案与示例
可以这样回答

BFC 是块级格式化上下文,描述一个相对独立的块布局区域及浮动交互规则。新 BFC 会包含内部浮动,避免自身与外部浮动重叠,并阻止它与内部子元素之间的外边距折叠。常用 display:flow-root 创建。它不能消除所有 margin 折叠,同一上下文内相邻普通块的垂直外边距仍可能折叠。

记住这几个点

  • display:flow-root 明确建立 BFC,无需借 overflow 的裁切副作用。
  • 浮动、绝对定位、inline-block 等也可能创建 BFC。
  • 块元素 overflow 非 visible 且非 clip 时可建立 BFC。
  • Flex 与 Grid 容器建立各自的格式化上下文,项目外边距不按普通块方式折叠。
关键示例 css
.article { display: flow-root; }
.article img {
  float: left;
  width: 120px;
  margin-right: 16px;
}
/* article 的高度包含内部浮动图片 */
易错点

给父容器建 BFC 主要隔开父子折叠边界,不能据此断言内部所有相邻子元素的 margin 都不再折叠。

常见追问:为什么更推荐 flow-root 而不是 overflow:hidden 清浮动?

flow-root 直接表达创建格式化上下文的意图;overflow:hidden 虽可建立 BFC,但还会裁切溢出内容,可能误伤下拉菜单、阴影或其他需要溢出的元素。

02.06必背

定位、包含块与层叠上下文

面试怎么问absolute、fixed、sticky 有什么区别?z-index 为什么有时无效?

查看背诵答案与示例
可以这样回答

relative 保留原位置,absolute 脱离普通流并依据包含块定位;fixed 通常相对视口,但某些祖先属性会改变其包含块。sticky 保留流位置,达到指定阈值后在滚动范围内吸附。z-index 的比较受层叠上下文限制,子元素再大的数值也不能直接越过祖先所在层级,应先检查上下文和包含块。

记住这几个点

  • absolute 常以最近定位祖先为包含块,但 transform 等属性也可能建立包含块。
  • sticky 需要对应轴上非 auto 的 inset,且受滚动祖先与容器边界限制。
  • opacity 小于 1、transform 非 none 等可创建层叠上下文。
  • 层叠上下文作为整体参与父级排序;弹层可考虑 dialog 或合适的顶层机制。
关键示例 css
.card { position: relative; }
.badge { position: absolute; top: 8px; right: 8px; }
.section-title { position: sticky; top: 0; }
.low-layer { position: relative; z-index: 1; }
.low-layer .popup { position: absolute; z-index: 9999; }
.high-layer { position: relative; z-index: 2; }
/* popup 仍受 low-layer 所在层叠上下文限制 */
易错点

fixed 不永远相对视口;z-index 也不要求所有场景都配 position,Flex 或 Grid 项目在 z-index 非 auto 时也能创建层叠上下文。

常见追问:sticky 不生效先排查什么?

先看对应轴是否设置 top 等阈值,再查祖先 overflow 是否形成意外滚动容器、容器是否有足够滚动空间,以及元素是否已填满容器导致没有可移动范围。

02.07必背

响应式布局、单位与内容约束

面试怎么问如何实现响应式布局?px、em、rem、vw 和百分比怎么选?

查看背诵答案与示例
可以这样回答

响应式先让布局随可用空间伸缩,再在内容开始拥挤的位置设置断点,而不是只按设备型号设计。px 是 CSS 像素,rem 相对根字号,em 通常参考当前字号,在 font-size 上参考父字号;vw 相对视口宽度,百分比的参照随属性而变。结合弹性布局、max-width 和媒体查询,也要测试长内容与用户缩放。

记住这几个点

  • 移动端设置 viewport,布局宽度才能按预期对应设备 CSS 视口。
  • 断点依据内容需求,先做流式布局,再添加必要媒体查询。
  • min-width、max-width、clamp() 可限制尺寸范围并避免无限放大。
  • 保留缩放能力,测试窄屏、长单词、中文和大字号,而非只看一个分辨率。
关键示例 css
.page { width: min(100% - 32px, 1080px); margin-inline: auto; }
.title { font-size: clamp(1.5rem, 4vw, 2.5rem); }
.layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); }
@media (max-width: 760px) {
  .layout { grid-template-columns: minmax(0, 1fr); }
}
img { max-width: 100%; height: auto; }
易错点

rem 不会自动让整个页面适配手机,100vh 也未必等于移动浏览器当下可见高度;视口高度需求可评估 dvh、svh 等单位。

常见追问:为什么 em 容易出现嵌套放大?

当多层元素都用 em 设置 font-size 时,每层以父级字号为基准,比例会累乘。统一字号标尺可用 rem,局部随组件字号缩放的间距则可有意使用 em。

TOPIC 03 / 07

JavaScript 语言核心

7 个知识点
03.01必背

类型、转换与相等判断

面试怎么问JavaScript 有哪些类型?为什么日常优先用 ===?

查看背诵答案与示例
可以这样回答

JavaScript 有七种原始类型和对象类型。原始值不可变,对象通过引用访问。双等号可能先做隐式转换,严格相等不会转换类型;对象比较的是身份,不是结构。还要单独记住 NaN 不等于自身,正零和负零在严格相等下相等,Object.is 对这两种情况有不同规则。日常先显式转换输入,再比较,能减少边界错误。

记住这几个点

  • 原始类型:undefined、null、boolean、number、bigint、string、symbol;函数属于对象。
  • typeof null 是历史遗留的 object;判断数组用 Array.isArray。
  • === 不做类型转换;NaN 检测优先用 Number.isNaN。
  • Boolean([]) 和 Boolean({}) 都是 true;空数组并不是假值。
关键示例 js
// 浏览器或现代 Node.js
console.log('1' === 1);       // false
console.log(Number('1') === 1); // true
console.log({ a: 1 } === { a: 1 }); // false
console.log(NaN === NaN);     // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false
console.log(Array.isArray([])); // true
易错点

不要把 typeof 当作完整类型检测器,也不要把 == 简化为“先统一转换成数字”。不同类型组合有不同转换规则。

常见追问:Object.is 与 ===、Set 去重有什么区别?

Object.is 认为 NaN 与自身相同、正负零不同;=== 恰好相反。Set 使用 SameValueZero,认为 NaN 相同、正负零也相同;对象仍按身份比较。

03.02必背

var、let、const 与暂时性死区

面试怎么问var、let、const 的作用域和声明前访问有什么区别?

查看背诵答案与示例
可以这样回答

var 通常是函数作用域,声明前读到 undefined;let 和 const 是块作用域,进入作用域后到初始化前处于暂时性死区,访问会抛 ReferenceError。const 约束的是绑定不能重新赋值,并不冻结对象内容。三者的声明都会参与作用域建立,所以不能把 let 简单说成完全没有提升;准确说法是初始化前不可访问。

记住这几个点

  • var 可在同一作用域重复声明;let、const 不能重复声明同名绑定。
  • const 声明通常必须初始化;对象属性仍可修改。
  • TDZ 中使用 typeof 同样会抛错,不能套用“typeof 总是安全”。
  • 浏览器普通脚本顶层 var 可能成为全局对象属性,顶层 let、const 不会;ES 模块拥有模块作用域。
关键示例 js
// 浏览器或 Node.js:独立代码块
{
  console.log(a); // undefined
  var a = 1;
  try { console.log(b); }
  catch (error) { console.log(error.name); } // ReferenceError
  let b = 2;
  const user = { name: '小林' };
  user.name = '小张'; // 合法:绑定未重新赋值
}
易错点

“const 对象不可修改”和“let 完全没有提升”都不准确。Object.freeze 也只冻结对象自身,不自动递归冻结嵌套对象。

常见追问:for 循环中的 let 为什么能让回调记住每次的索引?

for 的 let 循环变量会为迭代建立独立绑定,回调闭包捕获各自绑定。var 共用同一个函数或全局绑定,异步执行时通常读到循环结束后的值。

03.03必背

作用域链与闭包

面试怎么问闭包是什么?它能解决什么问题,又可能产生什么成本?

查看背诵答案与示例
可以这样回答

闭包是函数与它创建时可访问的词法环境的组合。函数离开原来的执行过程后,仍能访问外层变量;查找变量按定义位置形成的作用域链进行,而不是调用位置。常见用途是封装私有状态、函数工厂和保留回调上下文。闭包捕获的是绑定,所以变量后续变化也可被读到;长期持有闭包可能让相关数据无法回收。

记住这几个点

  • 作用域决定变量在哪里可见;执行上下文是运行时执行代码的环境,两者不要混为一谈。
  • 闭包并不要求函数被 return;回调、事件监听器同样可能形成闭包。
  • 每次调用函数工厂通常会创建新的环境,实例之间可以独立保存状态。
  • 关闭页面功能时要清理监听器、计时器及不再需要的引用。
关键示例 js
// 浏览器或 Node.js
function createCounter() {
  let count = 0;
  return () => ++count;
}
const a = createCounter();
const b = createCounter();
console.log(a(), a(), b()); // 1 2 1
易错点

闭包不是把外层变量的值拍成快照,也不是一出现闭包就一定内存泄漏;关键是对象是否仍被不必要的引用保留。

常见追问:为什么事件监听器可能让已经移除的 DOM 留在内存里?

若长期存活的监听器或其他对象仍通过闭包引用该 DOM,垃圾回收器就可能继续认为它可达。移除节点后,还应按实际持有关系清理监听器及引用。

核对原文MDN:闭包
03.04必背

普通函数、this 与箭头函数

面试怎么问this 如何确定?箭头函数与普通函数有什么差别?

查看背诵答案与示例
可以这样回答

普通函数的 this 由调用方式决定:方法调用时指向接收者,call、apply 可指定本次调用的 this,bind 创建绑定函数。严格模式下独立调用的 this 是 undefined。箭头函数没有自己的 this,而是沿词法环境使用外层 this,call 和 bind 不能改写它。箭头函数不能作为构造函数,适合保留外层上下文的回调。

记住这几个点

  • 函数从对象上取出后再调用,原来的接收者不会自动保留。
  • call 逐个传参数,apply 传类数组参数,bind 返回新函数并可预置参数。
  • 箭头函数没有自己的 arguments,可使用剩余参数 ...args。
  • 浏览器 ES 模块顶层 this 是 undefined;普通脚本顶层行为不同,回答时要说明环境。
关键示例 js
// 浏览器或 Node.js
'use strict';
const box = {
  value: 2,
  makeReader() { return () => this.value; }
};
const reader = box.makeReader();
console.log(reader.call({ value: 9 })); // 2
function read() { return this?.value; }
console.log(read()); // undefined
console.log(read.call({ value: 9 })); // 9
易错点

对象字面量的花括号不会产生 this 作用域;把箭头函数直接当成依赖接收者的对象方法,常会拿到错误的 this。

常见追问:bind 后的普通函数还可以用 new 吗?

如果原函数可构造,绑定函数仍可用 new。构造调用会忽略 bind 固定的 this,由新实例作为 this;预置参数仍会参与调用,箭头函数则不可构造。

03.05必背

原型链、new 与 class

面试怎么问属性是如何沿原型链查找的?class 与原型是什么关系?

查看背诵答案与示例
可以这样回答

访问对象属性时,先检查对象自身,再沿内部原型逐级查找,直到找到属性或到达 null。构造函数的 prototype 通常成为 new 出来的实例的原型,实例共享放在原型上的方法。class 使用同一套原型继承机制,但还有严格模式、必须用 new 等语义。应区分对象内部的原型与构造函数的 prototype 属性,避免把两者混称为同一个东西。

记住这几个点

  • 用 Object.getPrototypeOf 查看对象原型,用 Object.hasOwn 判断自身属性。
  • 同名自身属性会遮蔽继承属性;in 运算符会检查原型链。
  • class 的实例方法通常位于 prototype 上,实例字段位于实例自身。
  • new 通常创建实例、连接原型、执行构造逻辑;构造函数显式返回对象时,结果可能是该对象。
关键示例 js
// 浏览器或现代 Node.js
class User {
  constructor(name) { this.name = name; }
  greet() { return this.name; }
}
const user = new User('小林');
console.log(Object.getPrototypeOf(user) === User.prototype); // true
console.log(Object.hasOwn(user, 'name')); // true
console.log(Object.hasOwn(user, 'greet')); // false
console.log(user.greet()); // 小林
易错点

不是每个函数都有可用于 new 的 prototype 属性;箭头函数不可构造。不要为业务功能直接修改 Object.prototype 等内建原型。

常见追问:instanceof 能否可靠判断任何环境中的数组?

不能。instanceof 根据构造函数的原型链判断,跨 iframe 等不同 realm 的数组可能不属于当前 Array 的原型链。判断数组应使用 Array.isArray。

03.06必背

浅拷贝、深拷贝与共享引用

面试怎么问展开运算符能否彻底隔离对象?深拷贝应该怎么选?

查看背诵答案与示例
可以这样回答

浅拷贝只复制第一层属性,嵌套对象仍共享引用,所以修改嵌套数据可能影响原对象。对象展开、Object.assign 和数组 slice 都是常见浅拷贝。深拷贝需要根据数据类型选择方案:现代浏览器的 structuredClone 支持循环引用和多种内建类型,但不能复制函数、DOM 节点,也不保留一般自定义原型。JSON 往返只适用于可接受序列化损失的数据。

记住这几个点

  • 复制容器不等于复制里面的每个对象;不可变更新需复制发生变化的各层。
  • JSON 往返会丢失或改变 undefined、函数、Date 等内容,循环引用还会导致报错。
  • structuredClone 对不支持的内容可能抛 DataCloneError,不能把它称为万能克隆。
  • 对象展开主要复制自身可枚举属性,不会完整保留属性描述符与原型。
关键示例 js
// 支持 structuredClone 的现代浏览器
const source = { user: { name: '小林' } };
const shallow = { ...source };
shallow.user.name = '小张';
console.log(source.user.name); // 小张
const deep = structuredClone(source);
deep.user.name = '小王';
console.log(source.user.name); // 小张
console.log(deep.user === source.user); // false
易错点

JSON.parse(JSON.stringify(value)) 不是通用深拷贝;深拷贝也不总是更好,大对象整体克隆有性能成本,局部不可变更新通常更合适。

常见追问:structuredClone 的 transfer 选项与普通克隆有什么不同?

transfer 会转移可转移对象的底层资源而非复制,例如转移 ArrayBuffer 后原缓冲区会被分离。它适合大数据跨线程传输,不能继续依赖原资源。

03.07必背

ES 模块与 live binding

面试怎么问ES 模块如何导入导出?导入的变量是一次性的副本吗?

查看背诵答案与示例
可以这样回答

ES 模块用 export 和 import 管理依赖,命名导出按名称导入,默认导出可自行命名。模块有独立作用域并自动使用严格模式。静态 import 是指向导出绑定的只读视图,导出方更新绑定后,导入方可看到新值,而不是拿到一次性的快照。动态 import 返回 Promise,可用于按需加载;循环依赖是否出错取决于读取时绑定是否已经初始化。

记住这几个点

  • 浏览器入口使用 script type="module",本地预览通常要经 HTTP 服务加载。
  • import 绑定不能在导入方重新赋值,但导入对象的可变属性仍可能被修改。
  • 静态 import、export 位于模块顶层;import() 可放在函数和条件分支中。
  • ES 模块顶层 this 为 undefined;加载请求和普通脚本有不同规则。
关键示例 js
// 浏览器经 HTTP 加载的两个 ES 模块文件
// counter.js
export let count = 0;
export function increment() { count += 1; }
// main.js(由 type="module" 的 script 加载)
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1:读取同一个导出绑定
易错点

只读导入不等于整个导入对象被冻结。循环依赖也不是必然失败,提前读取尚未初始化的绑定才是常见报错原因。

常见追问:Tree shaking 为什么通常更容易处理 ES 模块?

静态导入导出让构建工具更容易分析依赖和未使用导出,但删除代码仍需考虑副作用、包配置和构建模式;使用 ES 模块不保证产物一定很小。

TOPIC 04 / 07

异步、事件与数据

7 个知识点
04.01必背

浏览器事件循环与微任务

面试怎么问同步代码、Promise 回调和 setTimeout 的执行顺序如何判断?

查看背诵答案与示例
可以这样回答

浏览器先执行当前调用栈中的同步代码。Promise 的反应回调和 queueMicrotask 回调进入微任务队列,在微任务检查点持续执行到队列清空,再继续其他任务;期间加入的新微任务也会执行。setTimeout 回调属于后续任务,零毫秒只是请求尽早调度,不能立即执行。浏览器有多个任务队列,渲染也依赖调度机会,不能把整个循环背成一条固定流水线。

记住这几个点

  • Promise 构造器的执行器同步运行,then 回调异步运行。
  • 微任务一直追加可能阻塞后续任务与渲染。
  • setTimeout 的延迟是调度门槛,繁忙主线程和浏览器限制会让实际执行更晚。
  • 以下顺序例子限浏览器;Node.js 还涉及不同阶段和 process.nextTick。
关键示例 js
// 浏览器:在一个普通 script 中执行
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => {
  console.log('C');
  queueMicrotask(() => console.log('D'));
});
console.log('E');
// A E C D B
易错点

不能说“浏览器只有一个宏任务队列”,也不能承诺“每个任务结束一定立刻绘制一次”;规范和实际调度比这两句话更细。

常见追问:await Promise.resolve() 能让耗时计算释放页面绘制机会吗?

通常不能可靠做到。后续代码只是排入微任务,连续微任务仍可能占用主线程。较大计算应分块交给后续任务,或移到 Web Worker 执行。

04.02必背

Promise 链、返回值与错误传播

面试怎么问then、catch 怎样影响后续 Promise?为什么必须 return?

查看背诵答案与示例
可以这样回答

Promise 表示异步结果,从 pending 进入 fulfilled 或 rejected 后状态固定。then 返回新 Promise:返回普通值成为后续成功值,抛错让后续失败,返回 Promise 则等待其结果。catch 正常返回表示错误已恢复,重新抛出才继续传播。回调启动异步工作却忘记 return,会让外层链提前继续并可能漏掉错误。

记住这几个点

  • Promise 的执行器同步运行;成功和失败回调通过微任务调度。
  • catch 能处理它前面链路的拒绝与回调抛错,不能捕获所有无关异步异常。
  • then(success, fail) 的 fail 不能捕获同一次 success 自己抛出的错误。
  • finally 通常用于清理并保留原结果;它抛错或返回拒绝 Promise 时,会改变链结果。
关键示例 js
// 浏览器或 Node.js
Promise.resolve(2)
  .then(value => value * 3)
  .then(() => { throw new Error('失败'); })
  .catch(error => {
    console.log(error.message); // 失败
    return 0; // 恢复为成功状态
  })
  .then(value => console.log(value)); // 0
易错点

catch 中只打印错误再正常返回,会把失败恢复为成功。若调用方仍需要知道失败,应重新抛出,而不是无意吞掉异常。

常见追问:new Promise 的执行器里抛错和 setTimeout 回调里抛错一样吗?

不一样。执行器同步抛错会让该 Promise 拒绝;稍后计时器回调抛错属于另一段执行,需在回调内捕获并调用 reject,或改用已有 Promise API。

04.03必背

async / await 与并发控制

面试怎么问await 会阻塞线程吗?独立请求怎样并发并处理失败?

查看背诵答案与示例
可以这样回答

async 函数返回 Promise,await 暂停当前异步函数并交还控制权,不会在等待期间阻塞线程。独立操作应先启动,再用 Promise.all 等待;逐个启动后立即 await 会变成串行。all 需要全部成功,任意一项拒绝就拒绝;allSettled 收集所有结果。并发数量和失败策略按业务决定,聚合 Promise 不会取消仍在运行的操作。

记住这几个点

  • async 函数在首次 await 前仍同步执行;await 之后的续段异步调度。
  • 用 try/catch 捕获 await 得到的拒绝,必要时重新抛出。
  • Promise.all 的结果按输入顺序排列,并非按完成顺序。
  • forEach 不等待异步回调;并发映射可用 Promise.all(list.map(...)),串行用 for...of。
关键示例 js
// 浏览器或 Node.js:模拟两个独立异步操作
const work = (value, delay) => new Promise(resolve => {
  setTimeout(() => resolve(value), delay);
});
async function run() {
  const [a, b] = await Promise.all([work('A', 20), work('B', 5)]);
  console.log(a, b); // A B:按输入顺序
}
run().catch(console.error);
易错点

Promise.all 不会替你启动函数,数组中要放实际的 Promise 或值;失败后也不会自动中止其他请求,取消通常要配合 AbortController。

常见追问:Promise.all、allSettled、race、any 各适合什么场景?

all 用于全部依赖成功;allSettled 用于统计每项结果;race 取第一个完成或失败结果;any 取第一个成功结果,全部失败时抛 AggregateError。

04.04必背

事件传播与事件委托

面试怎么问捕获、目标、冒泡是什么?为什么列表常用事件委托?

查看背诵答案与示例
可以这样回答

DOM 事件经历捕获、目标和可能的冒泡阶段。addEventListener 默认通常监听冒泡方向,capture 为 true 监听捕获。事件委托在祖先上监听,利用冒泡及目标匹配处理子项,适合动态列表。target 是事件目标,currentTarget 是当前监听器所在对象;点击按钮内的图标时,通过 closest 找到按钮并检查所属容器。

记住这几个点

  • 不是所有事件都冒泡;例如 focus 不冒泡,可按场景使用捕获或 focusin。
  • stopPropagation 阻止继续传播;preventDefault 阻止可取消的默认行为,两者作用不同。
  • stopImmediatePropagation 还阻止当前对象上后续监听器执行。
  • passive 监听器不能有效调用 preventDefault;Shadow DOM 会影响 target 和传播路径。
关键示例 js
// 浏览器:页面已有 #list 和内部 [data-id] 按钮
const list = document.querySelector('#list');
list.addEventListener('click', event => {
  if (!(event.target instanceof Element)) return;
  const button = event.target.closest('button[data-id]');
  if (!button || !list.contains(button)) return;
  console.log(button.dataset.id);
  console.log(event.currentTarget === list); // true
});
易错点

委托不能只比较 target.tagName,点击嵌套元素会漏处理。阻止默认行为不等于阻止冒泡,非冒泡事件也不能直接套同一方案。

常见追问:移除监听器时为什么匿名函数经常移除失败?

removeEventListener 需要匹配原来的回调函数引用及捕获标志,重新写一个外观相同的匿名函数是新引用。可以保存回调,或用 AbortSignal 统一清理。

04.05必背

防抖、节流与清理

面试怎么问防抖和节流有什么区别?分别适合什么场景?

查看背诵答案与示例
可以这样回答

防抖把连续触发合并,常见尾沿实现是在最后一次调用后安静一段时间才执行,适合输入搜索和保存草稿。节流限制持续触发时的执行频率,适合滚动统计和拖动反馈。实现时应保留参数及 this,明确首次和末次是否执行,并提供取消能力。防抖仅减少触发次数,不解决网络响应乱序;搜索请求仍需要取消旧请求或检查请求标记。

记住这几个点

  • 尾沿防抖在每次调用时清除并重设计时器;节流按时间窗口控制执行。
  • 首沿、尾沿、最大等待时间属于策略选项,回答时先说明采用哪种。
  • 页面功能销毁时取消计时器,避免回调更新失效界面。
  • 需要跟随屏幕刷新的视觉更新可考虑 requestAnimationFrame;它与一般按毫秒节流并不等价。
关键示例 js
// 浏览器:仅尾沿执行的简化防抖
function debounce(fn, delay) {
  let timer;
  function wrapped(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  }
  wrapped.cancel = () => clearTimeout(timer);
  return wrapped;
}
const log = debounce(console.log, 200);
log('A'); log('B'); // 安静 200ms 后只输出 B
// 页面功能销毁时调用 log.cancel()
易错点

不要把防抖和节流都背成“固定时间执行一次”。尾沿防抖在持续触发时可能一直不执行,需要按需求加入最大等待时间。

常见追问:节流可以保证回调精确每隔 100 毫秒执行吗?

不能。主线程繁忙、计时器调度及页面后台限制都会改变实际执行时间。节流规定的是调用策略和频率上限,精确周期不是浏览器计时器的保证。

04.06必背

数组迭代与不可变更新

面试怎么问map、filter、reduce、forEach 怎样选择?哪些操作会改原数组?

查看背诵答案与示例
可以这样回答

map 映射元素,filter 筛选,reduce 累积结果,forEach 执行副作用且不收集返回值。push、splice、sort、reverse 修改原数组;slice、map、filter 生成新数组,但仍可能共享原来的对象元素,所以不可变更新需复制被修改的对象。现代环境可用 toSorted、toReversed、toSpliced 返回新数组。

记住这几个点

  • map 回调必须返回需要收集的值;只做副作用时不必创建无用结果数组。
  • 数字排序要提供比较函数,如 (a, b) => a - b;默认排序按字符串规则。
  • slice 是截取并返回新数组,splice 是增删改原数组,名称相近但语义不同。
  • forEach 无法用普通 break 提前结束;查找用 find,判断用 some、every,控制循环用 for...of。
关键示例 js
// 支持 toSorted 的现代浏览器或 Node.js
const items = [{ id: 1, done: false }, { id: 2, done: false }];
const next = items.map(item =>
  item.id === 1 ? { ...item, done: true } : item
);
console.log(items[0].done); // false
console.log(next[1] === items[1]); // true:未修改项复用
console.log([10, 2, 1].toSorted((a, b) => a - b)); // [1, 2, 10]
易错点

map 返回新数组并不意味着没有副作用;若回调修改元素对象,原数组中对应对象也会变化。toSorted 等较新 API 应核对目标运行环境。

常见追问:reduce 为什么通常建议显式给初始值?

初始值能明确累积类型及空数组行为。没有初始值时第一个元素作为累积器,空数组会抛 TypeError,也容易使不同输入得到不同的处理路径。

04.07进阶

Map、Set 与 WeakMap

面试怎么问Map 与普通对象、Set 与 WeakMap 分别解决什么问题?

查看背诵答案与示例
可以这样回答

Map 保存键值对,键可以是各种值,并按插入顺序迭代;普通对象的常规属性键是字符串或 Symbol。Set 保存不重复的值,常用于原始值去重。它们使用 SameValueZero 比较,NaN 可以去重,对象仍按身份区分。WeakMap 用可回收的键关联元数据,不强引用键且不可枚举,适合对象缓存;它不能保证立即回收,也不能替代所有需要统计或遍历的 Map。

记住这几个点

  • Map、Set 的 size 直接给出条目数量;普通对象需要另行统计自身属性。
  • Set([{ id: 1 }, { id: 1 }]) 仍有两项;按业务字段去重需自行建立索引。
  • WeakMap 键可为对象或非注册 Symbol;旧环境对 Symbol 键的支持需要核对。
  • WeakMap 没有 keys、entries、size,避免程序观察垃圾回收带来的不确定性。
关键示例 js
// 浏览器或 Node.js:使用对象键兼容常见环境
const values = new Set([1, 1, NaN, NaN]);
console.log(values.size); // 2
const key = { id: 1 };
const map = new Map([[key, '记录']]);
console.log(map.get({ id: 1 })); // undefined:不同对象
console.log(map.get(key)); // 记录
const metadata = new WeakMap();
metadata.set(key, { visited: true });
console.log(metadata.get(key).visited); // true
易错点

WeakMap 的弱引用针对键,不等于所有数据都立刻销毁;对象被其他引用持有时仍不会回收。也不要背“WeakMap 只接受对象”这一过时的绝对说法。

常见追问:Map 查询一定是 O(1) 吗?

不宜作绝对保证。规范要求平均访问时间与条目数相比为次线性,具体实现可能采用哈希表或其他结构;实际复杂度与运行引擎及使用方式有关。

TOPIC 05 / 07

浏览器与安全

7 个知识点
05.01必背

从输入 URL 到页面呈现

面试怎么问输入一个网址后,浏览器经历了哪些步骤?

查看背诵答案与示例
可以这样回答

浏览器先解析 URL,检查可用缓存,必要时解析域名并建立连接;HTTPS 还要协商 TLS。收到 HTML 后边下载边解析,构建 DOM,发现并获取 CSS、脚本和图片。随后计算样式、布局、绘制并合成画面。脚本执行和资源加载会影响过程,缓存命中、连接复用或 Service Worker 都可能跳过部分网络步骤。

记住这几个点

  • 网络:缓存、DNS、连接、TLS、HTTP。
  • 渲染:解析、样式、布局、绘制、合成。
  • HTML 可以增量解析,资源获取与解析常有交错。
  • 说明典型流程,再指出缓存和连接复用的分支。
易错点

不要把步骤说成每次都重新 DNS、TCP、TLS,也不要认为必须下载完整 HTML 才开始解析。

常见追问:DOMContentLoaded 和 load 有什么区别?

DOMContentLoaded 在 HTML 解析及延迟脚本执行完成后触发;load 还等待非懒加载的相关页面资源。不要用 load 代替具体组件的就绪条件。

05.02必背

重排、重绘与合成

面试怎么问重排和重绘有什么区别,怎么减少布局抖动?

查看背诵答案与示例
可以这样回答

重排是重新计算元素的尺寸和位置,重绘是重新生成颜色、边框等绘制内容;布局变化通常会引起后续绘制。改变背景颜色可能只重绘,某些 transform 动画能主要在合成阶段完成。性能问题常来自写入样式后立即读取几何信息,强制浏览器提前布局。应先集中读取,再批量写入,并用性能面板确认实际成本。

记住这几个点

  • 布局关注几何,绘制关注视觉内容,合成组合图层。
  • offsetWidth、getBoundingClientRect 等读取可能触发待处理布局。
  • 把 DOM 读取与写入分批,避免循环中交错。
  • 图层提升也有内存成本,不能给所有元素加 will-change。
关键示例 js
const box = document.querySelector('.box');
const items = [...document.querySelectorAll('.item')];
// 先读一次,再批量写,避免每轮重新测量
const width = box.getBoundingClientRect().width;
for (const item of items) {
  item.style.width = `${width}px`;
}
易错点

transform 不保证零成本;requestAnimationFrame 也不会自动消除回调里的强制布局。

常见追问:为什么读取 offsetWidth 有时会变慢?

若前面的修改已使布局失效,浏览器必须同步计算最新几何才能返回准确值。反复读写会把原本可合并的布局拆成多次。

05.03必背

DOM、CSSOM 与渲染阻塞

面试怎么问DOM 和渲染结构有什么区别,CSS 会阻塞 HTML 解析吗?

查看背诵答案与示例
可以这样回答

DOM 表示文档节点,CSSOM 提供浏览器可处理的样式信息,两者结合确定渲染所需结构、外观和布局。DOM 节点不一定参与渲染:display:none 的元素及其后代不占布局空间,visibility:hidden 的元素仍占空间。匹配的样式表通常阻塞首次渲染,但不直接暂停 HTML 解析;它可能让等待样式的脚本停住,再间接延缓后续解析。

记住这几个点

  • DOM 是文档结构,渲染还要考虑样式与可见性。
  • display:none 不销毁 DOM,脚本仍可找到这个节点。
  • visibility:hidden 保留布局空间,与移除布局不同。
  • 区分直接阻塞解析、阻塞渲染,以及通过脚本形成的间接等待。
关键示例 html
<style>
  .gone { display: none; }
  .invisible { visibility: hidden; }
</style>
<div class="gone">存在于 DOM,但不占布局空间</div>
<div class="invisible">不可见,但仍占空间</div>
<div>比较前两者对本元素位置的影响</div>
易错点

不能把渲染结构等同于所有 DOM 节点,也不能笼统说 CSS 直接阻塞 HTML 解析。

常见追问:样式表为什么可能阻塞脚本执行?

脚本可能读取计算样式或布局信息,浏览器需要先完成相关样式处理才能保证结果正确;解析阻塞脚本的等待会间接拖慢后续 HTML 解析。

05.04必背

Cookie 与 Web Storage

面试怎么问Cookie、localStorage 和 sessionStorage 应怎样选择?

查看背诵答案与示例
可以这样回答

Cookie 会按域、路径及安全策略随匹配请求发送,适合由服务端管理会话;HttpOnly 能限制脚本读取。localStorage 按源共享,通常跨浏览器重启保留;sessionStorage 按源和标签页会话隔离,刷新仍保留。两者保存字符串且 API 同步,不自动随请求发送。存储都可能受配额、隐私模式或用户清理影响,敏感信息不能只依靠前端存储保护。

记住这几个点

  • Cookie:关注 Secure、HttpOnly、SameSite 和有效期。
  • localStorage:适合少量持久偏好,操作要处理异常。
  • sessionStorage:适合当前标签页的临时状态。
  • 大量结构化数据优先考虑异步 IndexedDB。
关键示例 js
try {
  localStorage.setItem('theme', 'dark');
  const theme = localStorage.getItem('theme') ?? 'light';
  document.documentElement.dataset.theme = theme;
} catch {
  // 被禁用或配额不足时,仍让页面可用
  document.documentElement.dataset.theme = 'light';
}
易错点

HttpOnly 不会消除 XSS 的全部危害;localStorage 不会自动过期,也不是安全凭据保险箱。

常见追问:sessionStorage 在两个新标签页之间共享吗?

它按标签页会话隔离;存在 opener 时初始内容可能从打开者复制,随后各自修改独立。不能把它当跨标签页共享存储。

05.05必背

同源策略与 CORS

面试怎么问什么是跨域,为什么前端不能自行开启 CORS?

查看背诵答案与示例
可以这样回答

同源要求协议、主机和端口一致。同源策略限制脚本读取跨源响应等能力,CORS 通过服务端响应头授予浏览器读取权限。部分请求先发 OPTIONS 预检,检查方法和请求头是否允许。跨源携带凭据还要客户端配置及服务端明确许可,允许源不能用星号。开发代理能改变请求路径,但最终生产环境仍须通过同源部署或正确的服务端配置处理。

记住这几个点

  • 跨源发送与读取是不同问题,部分请求可发出但响应不可读。
  • JSON Content-Type、自定义请求头等常会触发预检。
  • 带凭据通常需明确 Allow-Origin 与 Allow-Credentials。
  • CORS 是浏览器访问控制机制,不能替代服务端鉴权。
关键示例 http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Content-Type: application/json

{"ok":true}
易错点

设置 mode:'no-cors' 不会获得可读的跨源 JSON;前端请求头写 Allow-Origin 不能授权自己。

常见追问:为什么跨域报错,但后端确实处理了请求?

符合无需预检条件的请求可能已经发送,浏览器随后拒绝把响应交给脚本。因此 CORS 不能被当作所有跨站写入的防线。

05.06必背

XSS 与 CSRF

面试怎么问XSS 和 CSRF 分别利用什么,怎么防护?

查看背诵答案与示例
可以这样回答

XSS 是不可信内容被当作代码执行,攻击者获得页面所在源的脚本能力;CSRF 是诱导浏览器携带已有身份发送非预期操作。XSS 应按输出上下文处理数据,普通文本用 textContent,需要富文本时使用可靠净化方案,并以 CSP 增加防线。CSRF 要在服务端结合令牌、Origin 校验与合适的 SameSite Cookie,状态修改不能使用 GET。

记住这几个点

  • 文本、HTML、URL 和脚本上下文需要不同处理。
  • 框架默认转义不覆盖绕过转义的原始 HTML 注入入口。
  • CSRF 令牌必须由服务端验证,不能只是前端添加固定值。
  • HttpOnly 保护 Cookie 读取,Secure 限制传输,SameSite 限制跨站发送。
关键示例 js
const output = document.querySelector('#message');
const userText = '<img src=x onerror=alert(1)>';
// 作为文本显示,不解释为 HTML
output.textContent = userText;
易错点

输入过滤、HttpOnly、CORS 或 SameSite 中任何一项都不是解决全部 XSS/CSRF 的万能开关。

常见追问:同站和同源是一个概念吗?

不是。不同子域可能同站但不同源,SameSite 的判断不能替代同源检查。应结合业务拓扑验证令牌和请求来源。

05.07必背

性能指标与测量

面试怎么问LCP、INP、CLS 各衡量什么,怎么定位问题?

查看背诵答案与示例
可以这样回答

LCP 衡量主要内容出现的速度,INP 衡量用户交互到下一次绘制的响应延迟,CLS 衡量意外布局偏移。良好阈值分别是 2.5 秒、200 毫秒和 0.1,通常看真实访问的第 75 百分位并区分设备。先用线上数据发现慢页面,再在 DevTools 或 Lighthouse 中复现并定位网络、长任务与布局瓶颈,优化后在同样条件下比较。

记住这几个点

  • LCP:关注主资源发现、下载与渲染延迟。
  • INP:关注事件处理、主线程排队与后续绘制。
  • CLS:为图片、广告等预留尺寸,避免意外插入。
  • 实验室数据利于定位,真实用户数据反映实际分布。
关键示例 js
// 安装 web-vitals 后,在应用入口采集
import { onLCP, onINP, onCLS } from 'web-vitals';
const report = metric => {
  console.log(metric.name, metric.value, metric.id);
};
onLCP(report);
onINP(report);
onCLS(report);
易错点

Lighthouse 总分不等于真实用户体验;无用户交互的加载测试不能直接测出真实 INP。

常见追问:为什么本地很快,用户反馈却很慢?

设备性能、网络、缓存、地理位置和交互方式都不同。应对真实用户指标分组,再模拟相近环境复现,而非只测开发机。

TOPIC 06 / 07

网络与 HTTP

7 个知识点
06.01必背

HTTP 方法、安全与幂等

面试怎么问GET、POST、PUT、DELETE 的语义和幂等性是什么?

查看背诵答案与示例
可以这样回答

GET 用于读取资源,POST 让目标资源处理提交内容,PUT 用给定表示创建或替换目标资源,DELETE 请求删除目标资源的关联。安全方法按语义不要求修改状态;幂等是相同请求执行多次与一次具有相同预期效果。GET、PUT、DELETE 等是幂等方法,POST 通常不是。幂等不要求响应状态码相同,重试仍要结合接口实现和并发条件判断。

记住这几个点

  • 安全关注是否请求修改状态,幂等关注重复执行的预期效果。
  • GET 不应设计为删除或转账,链接预取也可能触发它。
  • 重复 DELETE 可以第一次 204、第二次 404,仍可符合幂等。
  • POST 可以靠业务幂等键实现安全重试。
关键示例 http
PUT /profiles/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"name":"Lin"}
易错点

幂等不代表没有日志等附带效果;不能因为请求失败就认为服务端一定没有执行。

常见追问:支付 POST 超时能直接重试吗?

不能盲目重试。应由服务端识别同一业务的幂等键,并提供结果查询,防止第一次已执行但响应丢失造成重复支付。

06.02必背

常见 HTTP 状态码

面试怎么问面试中常见状态码应怎样解释?

查看背诵答案与示例
可以这样回答

状态码描述请求结果:2xx 成功,3xx 重定向或缓存相关,4xx 请求侧问题,5xx 服务端未完成请求。200 成功,201 已创建,204 无响应内容;304 告诉客户端复用验证过的缓存。401 缺少有效认证,403 拒绝访问,404 未找到,429 请求过多;500 为一般服务端错误,502 为网关收到无效上游响应,504 为网关等待上游超时。

记住这几个点

  • 204 没有响应内容,不应无条件调用 response.json()。
  • 401 通常需要认证流程,403 不等于重新登录就能解决。
  • 301/302 的历史行为与 307/308 保留方法的语义有区别。
  • HTTP 200 仍可能承载业务失败,应同时检查接口约定。
关键示例 js
async function readResult(response) {
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  if (response.status === 204) return null;
  return response.json();
}
易错点

304 不是普通成功 JSON;4xx 不一定由前端代码错误导致,接口契约和用户输入也会影响结果。

常见追问:为什么 502 和 504 要分开看?

两者常来自网关,但 502 强调上游响应无效,504 强调等待超时。需结合网关与上游日志排查,不能只改前端超时。

06.03必背

新鲜缓存与条件验证

面试怎么问强制缓存和协商缓存如何配合,no-cache 是什么意思?

查看背诵答案与示例
可以这样回答

缓存先判断响应是否新鲜,新鲜且允许复用时可直接使用,常由 max-age 控制;过期或要求验证时,可携带 If-None-Match 或 If-Modified-Since 请求服务端。未变化返回 304,变化则返回新内容。no-cache 允许存储但复用前必须验证,no-store 要求不存储。带哈希静态资源适合长缓存,HTML 通常需及时验证。

记住这几个点

  • ETag 对应 If-None-Match,Last-Modified 对应 If-Modified-Since。
  • 两种验证条件同时出现时,If-None-Match 优先。
  • private 限制共享缓存,不等于禁止浏览器缓存。
  • 缓存效果还受 Vary、请求方法及缓存键影响。
关键示例 http
GET /index.html HTTP/1.1
Host: example.com
If-None-Match: "page-v2"

HTTP/1.1 304 Not Modified
ETag: "page-v2"
Cache-Control: no-cache
易错点

no-cache 不等于不缓存;304 需要已有表示配合复用,不能把所有缓存命中都叫强缓存。

常见追问:发布后用户仍拿到旧 JS,怎么处理?

构建时给内容变化的文件生成新哈希 URL,并让 HTML 及时验证;检查 CDN 的缓存键和刷新策略,避免覆盖同名长缓存资源。

06.04必背

HTTPS 与 TLS

面试怎么问HTTPS 为什么安全,TLS 握手完成什么工作?

查看背诵答案与示例
可以这样回答

HTTPS 在 HTTP 传输中使用 TLS,为数据提供机密性、完整性和服务器身份验证。握手会协商协议和算法,验证证书与目标主机,通过密钥协商建立会话密钥,后续主要用对称加密传输。现代方案以 TLS 1.3 或安全配置的 1.2 为主。证书公钥并不意味着直接加密全部业务数据,HTTPS 也不能阻止站点自身的 XSS、越权或恶意业务。

记住这几个点

  • 机密性防窃听,完整性检测篡改,证书帮助认证服务器。
  • 校验证书链、有效期和主机名,不能随意忽略证书错误。
  • HTTP/3 使用 QUIC,并集成 TLS 1.3。
  • 0-RTT 早期数据有重放风险,需考虑操作是否安全。
易错点

不要背所有 TLS 都是 RSA 交换密钥;现代 TLS 1.3 的握手机制和旧版本不相同。

常见追问:用了 HTTPS,前端就能放心嵌入 HTTP 资源吗?

不能。混合内容会削弱安全,浏览器可能升级或阻止相关资源。页面、接口和关键子资源应使用 HTTPS,并检查实际加载结果。

06.05必背

HTTP/1.1、HTTP/2、HTTP/3

面试怎么问三个版本的主要区别是什么,HTTP/2 还有队头阻塞吗?

查看背诵答案与示例
可以这样回答

HTTP/1.1 支持持久连接,但单连接的请求响应调度有限,流水线响应还需按序。HTTP/2 用二进制分帧、多路复用和头部压缩,让多个流共享连接,减少应用层的排队;它基于 TCP,丢包仍可能暂停连接上的所有流。HTTP/3 基于 QUIC,提供独立流,避免 TCP 那种跨流的传输层队头阻塞。三个版本保留了方法、状态码等 HTTP 核心语义。

记住这几个点

  • HTTP/2 的多路复用解决不了 TCP 按序交付造成的跨流等待。
  • HTTP/3 基于 UDP 上的 QUIC,不是应用层裸用 UDP。
  • QUIC 提供可靠性、拥塞控制与安全传输能力。
  • 协议升级收益取决于服务器、网络、连接复用和资源分布。
易错点

HTTP/2 并非没有队头阻塞;HTTP/3 也不是永不等待或保证任何场景都更快。

常见追问:HTTP/2 下还要把所有文件合并为一个大包吗?

不必沿用减少请求数的旧经验。应兼顾首屏体积、缓存复用与请求开销,按实际加载路径分包,并用测量选择大小。

06.06必背

DNS 与 CDN

面试怎么问DNS 解析与 CDN 分别解决什么问题?

查看背诵答案与示例
可以这样回答

DNS 把域名解析为相应地址等记录,解析结果会按 TTL 缓存;首次解析通常经过递归解析器查询相关权威服务器。CDN 把内容分发到多个边缘节点,将请求引导到合适节点,减少访问延迟并分担源站压力。未命中时通常回源获取。DNS 负责名称解析,CDN 负责内容分发,两者常配合使用;更新域名或资源后,要分别检查 DNS 和 CDN 缓存。

记住这几个点

  • A 对应 IPv4,AAAA 对应 IPv6,CNAME 指向另一域名。
  • TTL 控制缓存期限,DNS 更新不保证全网瞬时生效。
  • CDN 可按 URL、请求头等构造缓存键,具体规则需核查。
  • 个性化响应不能按静态公共资源的方式随意共享缓存。
易错点

CDN 不是永久保存所有响应;DNS TTL、浏览器 HTTP 缓存和 CDN 缓存是不同层次。

常见追问:源站更新了,CDN 为什么还可能返回旧文件?

边缘缓存可能仍新鲜,应检查缓存头和节点命中情况。发布采用新哈希 URL,必要时按提供商规则刷新,避免依赖同名覆盖。

06.07必背

请求错误、重试与取消

面试怎么问怎样正确处理 fetch 的失败、超时和过时请求?

查看背诵答案与示例
可以这样回答

fetch 遇到网络失败或取消会拒绝 Promise,但收到 404、500 时通常仍返回 Response,要检查 ok 或 status。读取 JSON 也可能失败。取消用 AbortController,超时可由定时器触发取消,结束时清理。重试针对合适的暂时错误,限制次数并退避;写操作需要幂等保障。取消请求不保证服务端已停止处理。

记住这几个点

  • 区分网络错误、HTTP 错误、解析错误与业务错误。
  • 429 或 503 的重试可结合 Retry-After。
  • 新查询出现时取消旧请求,并防止旧结果覆盖新状态。
  • 用户主动取消通常不应显示成严重失败。
关键示例 js
async function loadJSON(url) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 5000);
  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}
// 调用方用 try/catch 区分取消、网络与业务错误
易错点

Promise.race 超时并不会自动取消底层 fetch;AbortController 也不能撤销服务端已经完成的写入。

常见追问:为什么搜索框还需要处理请求竞争?

请求可能乱序完成。取消旧请求能减少工作,但还应按请求序号或当前查询检查结果,只允许最新查询更新界面。

TOPIC 07 / 07

工程与实践

7 个知识点
07.01必背

依赖声明与锁文件

面试怎么问package.json 与 package-lock.json 有什么区别,CI 为什么用 npm ci?

查看背诵答案与示例
可以这样回答

package.json 声明脚本、依赖与版本范围;锁文件记录解析后的依赖树和具体版本,使安装更可复现。添加或更新依赖用 npm install 并提交相关文件;CI 常用 npm ci 按锁文件安装,不一致时失败,不修改锁。dependencies 和 devDependencies 表示用途,不能据此断言包是否进入浏览器产物。

记住这几个点

  • 应用项目通常同时提交 package.json 和锁文件。
  • 统一包管理器与重要安装配置,避免不同锁文件混用。
  • npm ci 会清理已有 node_modules 后进行干净安装。
  • 是否打进产物取决于导入与构建,开发依赖也可能参与产物生成。
关键示例 bash
# 本地添加运行时库
npm install dayjs
# 添加开发工具
npm install -D typescript
# CI 在已有且一致的锁文件基础上安装
npm ci
npm run build
易错点

锁文件不能保证跨所有系统和工具版本完全相同;只提交 package.json 容易让间接依赖漂移。

常见追问:devDependencies 能完全不安装就构建吗?

通常不能,构建器、类型检查和测试工具多在开发依赖中。可以在构建阶段安装完整依赖,再按部署形态交付产物或运行时依赖。

07.02必背

构建、Tree Shaking 与分包

面试怎么问打包器做什么,Tree Shaking 和代码分割有何区别?

查看背诵答案与示例
可以这样回答

构建工具处理模块依赖、资源、语法转换和产物输出。Tree Shaking 利用可分析的模块关系删除未使用代码,但必须保留有副作用的内容;ES 模块的静态结构有利于分析。代码分割把应用拆成多个产物,动态 import 常用于按需加载。前者减少不需要的代码,后者改变加载时机。分包要结合首屏路径、缓存复用和请求成本,不能只追求文件越小越多。

记住这几个点

  • 压缩、Tree Shaking、代码分割是相关但不同的优化。
  • CSS 导入、polyfill、全局注册可能有副作用。
  • 误标 sideEffects:false 可能导致生产样式或初始化丢失。
  • 动态导入需提供加载与失败反馈,并验证构建后的行为。
关键示例 js
async function openReport() {
  try {
    // 在支持分包的构建配置中按需加载
    const { renderReport } = await import('./report.js');
    renderReport();
  } catch (error) {
    console.error('报表模块加载失败', error);
  }
}
易错点

命名导入不保证库一定能被摇掉;副作用、模块格式和构建配置都会影响最终体积。

常见追问:开发正常,生产构建却没样式,应查哪里?

先检查 CSS 导入和依赖包的副作用声明,再确认构建产物与实际资源路径。Tree Shaking 误删 CSS 是一种可能,需用产物证据判断。

07.03必背

开发构建、生产构建与 Source Map

面试怎么问为什么上线前必须测生产产物,Source Map 有什么用?

查看背诵答案与示例
可以这样回答

开发模式偏重快速启动、调试与热更新,生产构建偏重可部署产物、优化和资源处理,两者行为可能不同。上线前应检查正式构建的路径、环境变量、懒加载和缓存策略。Source Map 把转换后代码映射到源代码位置,帮助还原错误堆栈;它不是加密。公开映射可能暴露源码,是否公开应按项目策略决定,也可只上传到受控的错误监控服务。

记住这几个点

  • 开发服务器能运行,不代表正式构建一定正确。
  • 客户端环境变量最终可被用户看到,不能放服务器密钥。
  • Vite 只转换 TypeScript,类型检查应单独运行。
  • 生产错误定位需要构建版本与对应 Source Map 一致。
关键示例 bash
# 纯 TypeScript 项目的检查示例
npx tsc --noEmit
npm run build
# Vite 项目可用 preview 验证产物
# 这是本地验证服务器,不是生产托管方案
npm run preview
易错点

压缩代码和关闭 Source Map 不能保护写进客户端的密钥;vite preview 不是生产服务器。

常见追问:构建通过能说明 TypeScript 没有类型错误吗?

不一定。Vite 的转换过程不承担完整类型检查,应在 CI 单独运行 tsc,Vue 项目可使用对应的 vue-tsc 检查。

07.04必背

Git 工作区、暂存区与提交

面试怎么问Git 的基本工作流是什么,怎样安全撤销变更?

查看背诵答案与示例
可以这样回答

工作区是当前文件,暂存区保存准备提交的快照,提交把暂存内容记录到历史。修改后先看 status 和 diff,再选择性 add,检查暂存 diff 后提交;分支用于隔离工作。撤销前要区分未提交修改和已有提交。共享历史通常用 revert 新增反向提交,重写历史会影响协作者;涉及丢弃内容的操作应先确认范围并保留可恢复副本。

记住这几个点

  • git diff 看未暂存修改,git diff --cached 看将被提交的内容。
  • git add 后继续编辑,新增修改不会自动进入原暂存快照。
  • 小而完整的提交便于审查、回退和定位回归。
  • 密钥和临时产物不要进入版本历史,提交前检查文件列表。
关键示例 bash
git status --short
git diff
git add src/app.js
git diff --cached
git commit -m "fix: handle empty search results"
# 撤销共享历史中的一个普通提交时
# 确认工作区干净,再执行 git revert <commit>
易错点

git reset --hard 会丢弃部分未提交内容;不要把它作为遇到冲突时的默认解决办法。

常见追问:revert 和 reset 的关键区别是什么?

revert 记录一个反向提交,保留已有历史;reset 移动当前分支位置,并按模式影响暂存区或工作区。共享历史优先考虑可审查的 revert。

07.05必背

测试层次与异步断言

面试怎么问前端测试怎样分层,异步测试容易漏掉什么?

查看背诵答案与示例
可以这样回答

单元测试验证独立逻辑,组件或集成测试验证模块协作,端到端测试验证真实浏览器中的关键用户流程。选择层次应看风险和成本,避免所有场景都走最慢的端到端。测试要断言用户可观察的结果,覆盖成功、失败与边界。异步操作必须 await 或返回 Promise,否则测试可能在断言执行前就结束;Mock 应控制外部依赖,并清理状态防止用例相互污染。

记住这几个点

  • 纯计算逻辑适合快的单元测试,关键业务链路需要端到端覆盖。
  • 异步错误分支、加载态和请求竞争也需要验证。
  • 优先验证行为,避免断言私有实现细节。
  • 使用假定时器时要正确推进时间并等待 Promise 完成。
关键示例 js
import { test, expect } from 'vitest';
async function doubleLater(value) {
  return value * 2;
}
test('等待异步结果后断言', async () => {
  const result = await doubleLater(3);
  expect(result).toBe(6);
});
易错点

测试全绿不保证覆盖关键风险;异步断言忘记等待,可能形成假阳性。

常见追问:为什么不应该把所有接口都 Mock 掉?

过度 Mock 会遗漏真实契约、序列化和模块协作问题。单元测试可隔离依赖,同时保留少量集成与端到端测试验证关键连接。

07.06必背

TypeScript 的运行时边界

面试怎么问TypeScript 能保证接口返回的数据一定正确吗?

查看背诵答案与示例
可以这样回答

TypeScript 的类型系统主要在静态检查阶段工作,类型注解、接口和类型断言通常在编译后被移除,不会自动验证网络数据。把 JSON 断言成 User 只是在告诉编译器信任你,数据仍可能缺字段或类型错误。外部输入应先视为 unknown,通过实际校验再缩小类型;类型检查与运行时校验配合,才能在开发阶段和真实执行阶段分别发现问题。

记住这几个点

  • unknown 要先缩小类型,any 会跳过很多检查。
  • as 和非空断言 ! 不会补字段,也不会阻止运行时异常。
  • 泛型表达类型关系,不会自动检查服务端响应。
  • 类型守卫或校验库必须真实验证需要的字段。
关键示例 ts
type User = { name: string };
function isUser(value: unknown): value is User {
  return typeof value === 'object'
    && value !== null
    && 'name' in value
    && typeof value.name === 'string';
}
function greet(value: unknown) {
  if (!isUser(value)) throw new Error('无效用户数据');
  return `你好,${value.name}`;
}
易错点

自定义类型守卫也可能写错;断言 response as User 不是校验,更不是数据转换。

常见追问:为什么说不是所有 TypeScript 语法都只存在于类型层?

类型和接口会被移除,但类、部分枚举等语法可能产生运行时代码。应区分类型信息与生成代码,不能笼统说所有 TS 内容都消失。

07.07必背

性能优化的完整过程

面试怎么问面试被问做过性能优化,怎样回答才有证据?

查看背诵答案与示例
可以这样回答

先说具体用户场景和指标基线,再说明测量发现的瓶颈,而不是先列工具。网络慢就查资源发现、体积、缓存和服务响应;主线程忙就查长任务、重复计算和 DOM 工作;布局不稳就查尺寸与插入时机。选择一个对应改动,在相同环境下对比前后结果,再用真实用户数据确认收益,并监控错误和功能回归,持续验证优化没有把成本转移到别处。

记住这几个点

  • 回答结构:场景、基线、定位、改动、结果、回归。
  • 使用中位数或分位数及多次采样,不挑最快的一次。
  • 首屏关键图片通常不应盲目懒加载,非关键图片可按需加载。
  • 虚拟列表、缓存或分包都要说明适用场景与代价。
关键示例 js
// User Timing 只测量这段同步计算
const items = ['Vue', 'React', 'JavaScript'];
const matchesQuery = item => item.includes('a');
performance.mark('filter:start');
const result = items.filter(matchesQuery);
performance.mark('filter:end');
performance.measure('filter', 'filter:start', 'filter:end');
const measures = performance.getEntriesByName('filter');
console.log(measures.at(-1)?.duration, result.length);
performance.clearMarks('filter:start');
performance.clearMarks('filter:end');
performance.clearMeasures('filter');
易错点

这个计时只测同步筛选,不包含浏览器绘制;不能拿它直接证明页面 INP 或整体加载改善。

常见追问:优化代码后单次计时更快,就能宣布成功吗?

不能。要排除设备、缓存和样本波动,并确认目标体验指标改善、功能无回归。局部耗时下降可能被网络或其他任务抵消。