HTML面试题汇总
-
结构与语义:语义化标签、文档流、空元素。
-
通信与存储:本地存储、Web Worker、跨文档通信、表单enctype。
-
渲染与性能:渲染引擎原理、defer/async、性能优化。
-
安全:同源策略、XSS/CSRF、
data-*属性。
一、广义的 HTML5 新增了哪些东西?
HTML5 不仅指新的 HTML 标记语言标准,更是一个技术集合,为 Web 开发带来了革命性变化。
1. 语义化标签 (Semantic Tags)
HTML5 引入了大量具有明确含义的标签,取代了过去到处都是 <div> 的局面,提高了代码的可读性、SEO 和无障碍访问性。
-
结构标签:
<header>、<footer>、<nav>、<section>、<article>、<aside>、<main>
-
其他语义标签:
<figure>(插图)、<figcaption>(插图标题)、<time>、<mark>(高亮)
2. 多媒体支持 (Multimedia)
在 HTML5 之前,播放视频或音频通常需要第三方插件(如 Flash)
-
<video> 和 <audio> :原生支持流媒体播放,支持 controls、autoplay、loop 等属性
-
<track> :为媒体文件添加字幕(WebVTT 格式)
3. 表单增强 (Forms 2.0)
大大简化了前端表单验证和交互逻辑
-
新的 Input 类型:
email、url、number、range(滑块)、date、time、color、search、tel
-
新属性:
placeholder(占位符)、required(必填)、autofocus(自动聚焦)、multiple(多选)、pattern(正则匹配)
-
新元素:
<datalist>(输入建议列表)、<output>(计算结果输出)
4. 强大的绘图与图形 (Graphics)
Web 不再只是静态的图文,而是可以进行高性能渲染
-
Canvas API:使用 JavaScript 在网页上绘制 2D 图形(适合游戏、动态图表)
-
SVG 内联:支持在 HTML 中直接嵌入和操作可伸缩矢量图形
-
WebGL:基于 Canvas 的 3D 渲染接口(常配合 Three.js 使用)
5. 本地存储 (Client-Side Storage)
解决了 Cookie 存储空间小(4KB)、性能差的问题
-
localStorage:永久存储数据,除非手动删除
-
sessionStorage:会话级存储,关闭窗口后失效
-
IndexedDB:浏览器端的高性能 NoSQL 数据库,用于存储大量结构化数据
6. 新的 JavaScript API
这是广义 HTML5 最强大的部分,让 Web 应用的功能接近原生 App
-
地理定位 (Geolocation API) :获取用户的经纬度坐标
-
拖放 API (Drag and Drop) :原生支持元素拖拽
-
Web Workers:允许在后台线程运行 JS,不阻塞 UI 渲染(多线程处理)
-
WebSockets:全双工通信协议,实现真正的实时数据交互(如聊天、实时报价)
-
History API:
pushState 和 replaceState,允许不刷新页面修改 URL(单页应用 SPA 的基础)
-
通知 (Notifications API) :向用户发送桌面弹窗通知
-
离线缓存 (Service Workers / Cache API) :替代了早期的 AppCache,让网页在无网环境下也能运行(PWA 核心)
7. CSS3 (广义 HTML5 的一部分)
虽然 CSS3 是独立标准,但常被归入 H5 范畴
-
布局:Flexbox(弹性盒子)、Grid(网格布局)
-
视觉:圆角 (
border-radius)、阴影 (box-shadow)、渐变 (gradient)、透明度 (rgba)
-
动画:Transition(过渡)、Animation(关键帧动画)、Transform(旋转、缩放、位移)
-
响应式:Media Queries(媒体查询),实现一套代码适配手机和电脑
8. 设备访问 (Device Access)
-
Device Orientation:访问陀螺仪、重力感应
-
Camera/Microphone API:通过
getUserMedia 调用摄像头和麦克风
-
Battery Status API:获取设备电量
总结
广义 HTML5 的核心价值在于:
-
脱离插件:干掉了 Flash
-
移动优先:完美适配手机浏览器
-
应用化:让网页不再只是文档,而是能离线、能绘图、能定位、能实时通信的 "Web App"
HTML5 的核心目标是减少浏览器对插件的依赖,并提高 Web 应用的性能和用户体验。
二、HTML 文档的基本结构是什么?
一个符合 HTML5 标准的基础结构如下:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页标题</title>
</head>
<body>
<h1>这是一个标题</h1>
<p>这是一个段落。</p>
</body>
</html>
各部分详细解释:
1. <!DOCTYPE html> (文档类型声明)
- 必须位于 HTML 文档的第一行
- 告诉浏览器当前文档使用的是 HTML5 标准
- 它不是 HTML 标签,而是一个声明
2. <html> 标签 (根元素)
- 所有其他 HTML 元素的容器(除了
<!DOCTYPE>)
-
lang="zh-CN" 属性用于指定网页的语言(此处为简体中文),有助于搜索引擎优化(SEO)和屏幕阅读器
3. <head> 标签 (元数据区)
4. <body> 标签 (主体内容区)
- 包含网页上所有可见的内容,如文本、图片、链接、视频、表格、按钮等
- 用户在浏览器窗口中看到的每一处细节都写在这里
四、语义化标签是什么?
1. 形象对比
无语义写法(像"套娃") : 你用一堆盒子装东西,每个盒子上都只写着"盒子1"、"盒子2"。你想找勺子,必须把盒子一个个打开看。
html
<div id="header">这是头部</div>
<div id="nav">这是导航</div>
<div id="main">这是内容</div>
<div id="footer">这是底部</div>
语义化写法(有标签的盒子) : 你在盒子上清楚地标明"餐具盒"、"急救箱"。一眼看过去就知道里面是什么。
html
<header>这是头部区域</header>
<nav>这是导航区域</nav>
<main>这是网页的主体内容</main>
<footer>这是页脚区域</footer>
2. 常见的语义化标签(HTML5 引入)
这些标签在布局中非常常用:
html
<header>:页眉/头部
<nav>:导航链接部分
<main>:页面的主要内容(一个页面通常只有一个)
<article>:独立的文章内容(如博客文章、新闻)
<section>:文档中的某个章节或区块
<aside>:侧边栏(与主体内容间接相关的内容)
<footer>:页脚/底部
<figure> 和 <figcaption>:用于图片及其说明
<time>:表示日期和时间
3. 为什么要使用语义化标签?
使用语义化标签不仅仅是为了好看,它有三个核心价值:
① 对搜索引擎友好(SEO)
搜索引擎的爬虫(如 Google, 百度)在"阅读"你的网页时,会根据语义化标签来判断权重。比如,它知道 <main> 里的内容比 <footer> 重要,这有助于提升网页的搜索排名。
② 提升可访问性(Accessibility)
可访问性(Accessibility,通常缩写为 a11y)是指确保网页内容能够被所有人(包括有残障的人士)平等地获取和使用。
对于视障人士,他们使用"屏幕阅读器"来听网页内容。阅读器会告诉用户:"现在进入导航栏","现在是正文"。如果全是 div,而且没有起形象的类名的话,屏幕阅读器就会把所有 div 都读出来,用户会迷失在代码中。
深入理解 Accessibility,可以从以下四个核心维度展开:
-
核心指导原则:POUR 原则 这是国际标准 WCAG(网页内容可访问性指南)的基础:
-
P (Perceivable) 可感知性:用户必须能通过视觉、听觉或触觉感知到信息(例如:图片要有文字描述,视频要有字幕)
-
O (Operable) 可操作性:用户必须能操作界面(例如:不能只有鼠标能点,键盘也得能控制;操作时间要充足)
-
U (Understandable) 可理解性:内容和操作必须清晰(例如:错误提示要明确,语言要简单)
-
R (Robust) 健壮性:内容必须能被各种技术(如不同的浏览器、屏幕阅读器)稳定解析
-
ARIA 技术:HTML 的补丁 ARIA (Accessible Rich Internet Applications) 是一组特殊的 HTML 属性,用来增强标签的语义。
-
键盘导航 (Keyboard Navigation) 很多肢体残障人士或极客用户不使用鼠标,只使用 Tab 键切换。
-
焦点管理 (Focus) :
- 所有的交互元素(链接、按钮、输入框)必须能通过 Tab 键选中
- 不要去掉焦点框!很多设计师喜欢用
outline: none 去掉那个"难看"的蓝色边框,但这对于键盘用户来说是灾难,因为他们不知道现在选到哪了
-
Tabindex:
-
tabindex="0":让原本不能选中的元素(如 div)可以被 Tab 选中
-
tabindex="-1":元素不能被 Tab 选中,但可以用脚本聚焦
-
视觉设计细节 Accessibility 不仅仅是代码,也关乎视觉设计。
-
屏幕阅读器 (Screen Readers) 的工作方式 了解视障人士如何"看"网页:
-
按标题跳转:阅读器用户通常会按快捷键在
<h1> 到 <h6> 之间跳转来快速了解大意。所以标题等级严禁跳跃(不要从 h1 直接跳到 h3)
-
地标区域 (Landmarks) :阅读器会识别
<header>、<nav>、<main>。用户可以一键跳到"主内容区",这就是为什么语义化标签对 a11y 至关重要
-
如何测试 Accessibility?
-
Lighthouse:Chrome 浏览器自带,在"开发者工具"里有一个 Accessibility 评分
-
WAVE:一个非常著名的插件,能直接在页面上标出哪里对比度不够,哪里缺标签
-
尝试只用键盘控制你的网页:如果你发现自己无法完成登录或提交表单,说明 a11y 做得不够好
③ 提高代码可读性和维护性
当其他开发者(或者几个月后的你自己)阅读代码时,语义化标签能让他们迅速理解页面结构,而不需要从大量的 class 名中去猜这块代码的功能。
总结
语义化标签就是用正确的标签做正确的事。
- 不要滥用
<div> 来搭建所有结构
- 如果是文章,就用
<article>
- 如果是导航,就用
<nav>
- 如果是页脚,就用
<footer>
五、块级元素和内联元素有什么区别?
1. 块级元素 (Block) —— "霸道总裁"
代表标签:<div>、<p>、<h1>~<h6>、<ul>、<header>、<footer>
特性:
-
换行:非常霸道,必须独占一行。即便你给它设置了很小的宽度,它后面的元素也必须另起一行
-
尺寸:默认宽度是父容器的 100%。你可以随意设置
width(宽)和 height(高)
-
边距:四个方向的
margin(外边距)和 padding(内边距)完全有效,能把周围的元素推开
-
用途:网页的骨架(如侧边栏、导航条、文章区块)
2. 行级元素 (Inline) —— "邻家女孩"
代表标签:<span>、<a>、<strong>、<em>
特性:
-
换行:很随和,不换行。它们会像文字一样,一个接一个地排在同一行,直到排不下才会折行
-
尺寸:无法设置宽度和高度。它的宽高度完全由包裹的内容(文字或图片)撑开。你写 width: 100px; 是会被浏览器直接忽略的
-
边距:
-
水平方向(左右):
margin-left/right 和 padding-left/right 有效
-
垂直方向(上下):设置
margin-top/bottom 无效;设置 padding-top/bottom 视觉上有颜色,但不会推开上下行的文字(会产生重叠)
-
用途:修改段落里的局部样式(如给某个词加粗或变红)
3. 行内块元素 (Inline-block) —— "全能选手"
代表标签:<img>、<input>、<button>,或通过 display: inline-block 转换的元素
特性:
-
换行:像行级元素一样,不换行,可以和别人并排站
-
尺寸:像块级元素一样,可以自由设置
width 和 height
-
边距:四个方向的
margin 和 padding 全部有效,且能完美推开周围的元素
-
奇点:这种元素在代码里如果换行写,浏览器会在它们之间产生一个微小的空隙(这是因为换行符被当成了空格)
-
用途:制作并排的导航菜单、商品卡片列表
核心区别对比表
| 特性 |
块级元素 (Block) |
行级元素 (Inline) |
行内块元素 (Inline-block) |
| 换行 |
独占一行 |
不换行,可并排 |
不换行,可并排 |
| 设置宽高 |
✅ 可以 |
❌ 不可以 |
✅ 可以 |
| margin/padding |
全部有效 |
水平有效,垂直无效 |
全部有效 |
| 默认宽度 |
父元素宽度 |
内容宽度 |
内容宽度(可设置) |
| 常见标签 |
<div>, <p>, <h1>~<h6>
|
<span>, <a>, <strong>
|
<img>, <input>, <button>
|
补充:如何互相转换?
在 CSS 中,你可以通过 display 属性让它们"变身":
css
/* 想让 span 变高变宽? */
span {
display: block;
/* 或 display: inline-block; */
}
/* 想让几个 div 并排显示? */
div {
display: inline-block;
/* 或者使用现代的 Flex 布局 */
}
/* 想让链接 a 像按钮一样有间距? */
a {
display: inline-block;
padding: 10px 20px;
}
总结
-
Block:独占一行,能定大小
-
Inline:挤在一起,不能定大小,上下边距没用
-
Inline-block:挤在一起,但能定大小,边距全有用
六、什么是空元素?
空元素(Void Elements)也叫自闭和标签,是指在 HTML 中不需要闭合标签的元素。它们通常用于插入某种内容或资源到文档中,而不包裹任何内容。
常见的空元素:
html
<img src="image.jpg" alt="描述文字">
<input type="text" name="username">
<br>
<hr>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
特性:
-
不能包含任何内容:空元素内部不能有子元素或文本内容
-
不需要闭合标签:在 HTML5 中,不需要写成
<img /> 形式(虽然 XML 风格也兼容)
- 用于引入资源或表示结构性分隔
正确写法:
html
<!-- HTML5 推荐写法(简洁) -->
<img src="photo.jpg" alt="照片">
<br>
<input type="email">
<!-- XHTML/XML 风格(也兼容) -->
<img src="photo.jpg" alt="照片" />
<br />
<input type="email" />
常见的空元素列表:
-
<area>:图像映射中的区域
-
<base>:文档中所有相对 URL 的基准 URL
-
<br>:换行符
-
<col>:表格列的属性
-
<embed>:外部内容的容器
-
<hr>:水平分隔线
-
<img>:图像
-
<input>:输入控件
-
<link>:链接到外部资源
-
<meta>:文档元数据
-
<param>(已废弃):对象参数
-
<source>:媒体元素的媒体资源
-
<track>:媒体元素的文本轨道
-
<wbr>:可选的换行点
七、data-* 自定义属性的作用是什么?如何在 JavaScript 中访问?
1. data-* 自定义属性的作用
【官方定义】 :data-* 属性(Custom Data Attributes)是 HTML5 引入的一种规范,允许我们在标准语义标签上,嵌入自定义的私有元数据,且不会被浏览器解析为布局或样式逻辑。
“data-* 属性是 HTML 语义化的延伸,它在 DOM 节点上建立了一个结构化的私有数据存储空间。通过 dataset API,我们可以实现数据与视图的轻量级绑定,尤其在处理事件委托、样式状态联动以及 SSR 初始化数据注入时,它是比频繁操作 ClassList 更加语义化、更易于静态分析的方案。
(1) 核心价值:语义化的数据存储
<!-- 语义化的数据声明,易于理解和维护 -->
<div
data-user='{"id": 101, "role": "admin"}'
data-theme="dark"
data-interaction-state="active"
data-validation-rules='{"required": true, "minLength": 3}'
>
用户控制面板
</div>
2. 核心应用场景
(1) 事件委托中的轻量级数据绑定
<!-- 列表项统一委托处理,避免为每个元素单独绑定事件 -->
<ul id="task-list" onclick="handleTaskClick(event)">
<li data-task-id="t1" data-priority="high" data-status="pending">
紧急任务
</li>
<li data-task-id="t2" data-priority="medium" data-status="in-progress">
进行中任务
</li>
<li data-task-id="t3" data-priority="low" data-status="completed">
已完成任务
</li>
</ul>
<script>
function handleTaskClick(event) {
const target = event.target.closest('li[data-task-id]');
if (!target) return;
// 从 data-* 属性获取完整上下文
const taskData = {
id: target.dataset.taskId,
priority: target.dataset.priority,
status: target.dataset.status,
timestamp: Date.now()
};
// 统一的事件处理逻辑
console.log('任务点击:', taskData);
// 进一步处理...
}
</script>
(2) CSS 样式状态联动
<!-- 通过 data-* 属性控制 CSS 样式,实现状态驱动UI -->
<div class="progress-container">
<div
class="progress-bar"
data-progress="75"
data-status="warning"
style="--progress: 75%;"
>
<span data-progress-text="75%">75%</span>
</div>
</div>
<style>
/* CSS 可以通过属性选择器响应 data-* 状态变化 */
.progress-bar[data-status="normal"] {
--color: #4CAF50;
}
.progress-bar[data-status="warning"] {
--color: #FF9800;
}
.progress-bar[data-status="error"] {
--color: #F44336;
}
.progress-bar::before {
content: '';
display: block;
width: var(--progress);
height: 100%;
background-color: var(--color);
transition: width 0.3s ease;
}
/* 通过 CSS 计数器显示 data-* 内容 */
.progress-bar::after {
content: attr(data-progress) '%';
position: absolute;
right: 10px;
color: white;
}
</style>
(3) SSR(服务端渲染)初始化数据注入
<!-- 服务端渲染时将初始状态注入到 data-* 属性中 -->
<div id="app"
data-initial-state='{"user": {"name": "张三", "role": "admin"}, "theme": "dark"}'
data-config='{"apiEndpoint": "/api", "features": ["ssr", "pwa"]}'
>
<!-- 客户端 JS 可以直接读取,无需二次请求 -->
</div>
<script>
// 客户端直接读取 SSR 注入的数据
const appElement = document.getElementById('app');
const initialState = JSON.parse(appElement.dataset.initialState);
const config = JSON.parse(appElement.dataset.config);
// 初始化应用状态
window.APP_STATE = {
...initialState,
hydrationTime: Date.now()
};
// 基于配置启用功能
if (config.features.includes('pwa')) {
// 注册 Service Worker...
}
</script>
3. 现代框架中的最佳实践
(1) 与 Vue 3 的整合
<!-- Vue 3 组件中使用 data-* 属性 -->
<template>
<div
:data-user-id="user.id"
:data-user-role="user.role"
:data-component-state="state"
@click="handleClick"
>
{{ user.name }}
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const user = ref({ id: 1, name: '张三', role: 'admin' });
const state = ref('active');
// 在 Vue 中访问其他元素的 data-* 属性
function handleClick() {
const element = document.querySelector('[data-user-id="2"]');
if (element) {
console.log('其他用户状态:', element.dataset);
}
}
</script>
(2) 与 React 的整合
// React 组件中使用 data-* 属性
function UserCard({ user, status }) {
return (
<div
data-user-id={user.id}
data-user-role={user.role}
data-component-state={status}
data-user-info={JSON.stringify(user)}
onClick={handleClick}
className="user-card"
>
{user.name}
</div>
);
function handleClick(event) {
// React 中通过原生方式访问
const element = event.currentTarget;
const userId = element.dataset.userId;
const userInfo = JSON.parse(element.dataset.userInfo);
// 或者使用自定义 hook
const data = useDataset(element);
console.log(data);
}
}
// 自定义 hook 封装 dataset 操作
function useDataset(elementRef) {
const [dataset, setDataset] = useState({});
useEffect(() => {
if (!elementRef.current) return;
const observer = new MutationObserver(() => {
setDataset({ ...elementRef.current.dataset });
});
observer.observe(elementRef.current, {
attributes: true,
attributeFilter: Object.keys(elementRef.current.dataset).map(k => `data-${k}`)
});
return () => observer.disconnect();
}, [elementRef]);
return dataset;
}
4. 静态分析与工具支持
(1) TypeScript 类型定义
// 定义 data-* 属性的类型约束
interface CustomDataAttributes {
'data-user-id'?: string;
'data-user-role'?: 'admin' | 'user' | 'guest';
'data-component-state'?: 'active' | 'inactive' | 'loading';
'data-validation-state'?: 'pending' | 'valid' | 'invalid';
'data-config'?: string; // JSON 字符串
}
// 扩展 HTML 元素类型
declare global {
interface HTMLElement {
dataset: DOMStringMap & {
userId?: string;
userRole?: string;
componentState?: string;
config?: string;
};
}
}
// 使用时的类型提示
const element = document.getElementById('app')!;
element.dataset.userId = '123'; // 有类型提示
element.dataset.userRole = 'admin'; // 只能赋值 'admin' | 'user' | 'guest'
八、defer和async属性有什么区别?
1. 核心概念
defer 和 async 都是 <script> 标签的布尔属性,用于控制外部 JavaScript 脚本的加载和执行时机,主要目的是优化页面加载性能,避免脚本阻塞页面渲染。
<!-- 普通脚本 - 阻塞渲染 -->
<script src="normal.js"></script>
<!-- 异步脚本 -->
<script async src="async.js"></script>
<!-- 延迟脚本 -->
<script defer src="defer.js"></script>
2. 三种加载模式对比
(1) 普通脚本(无属性)
执行流程:
HTML解析 → 遇到<script> → 暂停解析 → 下载脚本 → 执行脚本 → 继续解析HTML
<script src="script.js"></script>
<!-- 后续的DOM元素需要等待脚本执行完毕才能渲染 -->
(2) async 异步加载
执行流程:
HTML解析开始
↓
同时下载async脚本
↓
脚本下载完成 → 立即暂停HTML解析 → 执行脚本 → 继续解析HTML
↓
HTML解析完成
<script async src="script1.js"></script>
<script async src="script2.js"></script>
<!-- 脚本下载完成后立即执行,执行顺序不确定 -->
(3) defer 延迟执行
执行流程:
HTML解析开始
↓
同时下载defer脚本
↓
HTML解析完成 → 按顺序执行所有defer脚本 → 触发DOMContentLoaded
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
<!-- 所有defer脚本在HTML解析完成后,按顺序执行 -->
3. 详细对比表格
| 特性 |
普通脚本 |
async 脚本 |
defer 脚本 |
| HTML解析是否阻塞 |
❌ 立即暂停 |
✅ 并行进行 |
✅ 并行进行 |
| 脚本下载时机 |
遇到即下载 |
异步下载 |
异步下载 |
| 脚本执行时机 |
下载后立即执行 |
下载后立即执行 |
HTML解析完成后执行 |
| 执行顺序保证 |
✅ 文档顺序 |
❌ 不保证(先下载完先执行) |
✅ 严格文档顺序 |
| DOMContentLoaded |
执行后才触发 |
可能阻塞或并行 |
执行前触发 |
| 适合场景 |
必要的初始化脚本 |
独立第三方脚本 |
依赖DOM的操作脚本 |
4. 总结
选择指南
使用 async 当:
- 脚本完全独立,不依赖其他脚本
- 不操作 DOM,或操作可以安全延迟
- 主要用于收集数据或跟踪
- 执行顺序不重要
使用 defer 当:
- 脚本需要操作 DOM
- 脚本之间有依赖关系
- 需要在 DOM 完全加载后执行
- 希望保持执行顺序
默认选择: 现代 Web 开发中,如果没有特殊要求,优先使用 defer,因为它提供了最佳的性能和可预测性。
记忆口诀
text
async:异步下载,立即执行,顺序不管
defer:异步下载,延迟执行,顺序不乱
普通:阻塞下载,立即执行,顺序照办
九、enctype属性的三种值分别代表什么?什么时候multipart/form-data?
1. enctype 是什么?
控制表单数据如何编码发送到服务器
<form method="post" enctype="值">
<!-- 表单内容 -->
</form>
2. 三种值的区别
| 值 |
用途 |
特点 |
|
application/x-www-form-urlencoded (默认值) |
普通文本表单 |
- 键值对编码 - 特殊字符转义 - 适合用户名、密码等 |
| multipart/form-data |
文件上传表单 |
- 支持二进制数据 - 每个字段独立部分 - 数据量较大 |
| text/plain |
纯文本调试 |
- 简单纯文本 - 人类可读 - 很少使用 |
3. 具体解释
(1) 默认值:application/x-www-form-urlencoded
<form method="post">
<!-- 或 enctype="application/x-www-form-urlencoded" -->
<input type="text" name="user" value="张三">
<input type="password" name="pwd" value="123">
</form>
发送的数据:
user=%E5%BC%A0%E4%B8%89&pwd=123
✅ 中文会转码,&符号连接字段
(2) 文件上传:multipart/form-data
<form method="post" enctype="multipart/form-data">
<input type="text" name="title" value="头像">
<input type="file" name="image">
</form>
发送的数据:
------边界字符串
Content-Disposition: form-data; name="title"
头像
------边界字符串
Content-Disposition: form-data; name="image"; filename="pic.jpg"
Content-Type: image/jpeg
[二进制图片数据]
------边界字符串--
(3) 纯文本:text/plain
<form method="post" enctype="text/plain">
<input type="text" name="name" value="测试">
</form>
发送的数据:
name=测试
⚠️ 实际开发基本不用
4. 什么时候必须用 multipart/form-data?
只要表单中有文件上传,就必须用!
<!-- ✅ 正确:有文件,用 multipart/form-data -->
<form method="post" enctype="multipart/form-data">
<input type="text" name="username">
<input type="file" name="avatar"> <!-- 文件字段 -->
<button>提交</button>
</form>
<!-- ❌ 错误:有文件但用了默认编码 -->
<form method="post">
<!-- enctype 默认是 application/x-www-form-urlencoded -->
<input type="file" name="avatar"> <!-- 文件会上传失败! -->
</form>
十、本地存储有哪几种方式?它们的区别是什么?
1. 核心概念:所有存储都在客户端
重要说明:以下所有存储方式的数据都保存在用户自己的设备上(浏览器中),不在网站服务器上。网站无法直接访问这些数据(除非你主动发送)。
2. 五种存储方式对比总表
| 方式 |
存储位置 |
容量 |
生命周期 |
是否自动发到服务器 |
数据结构 |
主要用途 |
| Cookie |
客户端硬盘+内存 |
4KB |
可设置过期时间 |
✅ 每次请求自动带 |
字符串键值 |
登录状态、会话 |
| LocalStorage |
客户端硬盘 |
5-10MB |
永久(手动清除) |
❌ 需主动发送 |
字符串键值 |
用户偏好设置 |
| SessionStorage |
客户端内存 |
5-10MB |
标签页关闭消失 |
❌ 需主动发送 |
字符串键值 |
临时表单数据 |
| IndexedDB |
客户端硬盘 |
≥250MB |
永久(手动清除) |
❌ 需主动发送 |
对象数据库 |
离线应用数据 |
| Cache API |
客户端硬盘 |
动态 |
可编程控制 |
❌ 需主动发送 |
请求/响应 |
PWA离线缓存 |
3. 详细说明与代码示例
(1) Cookie 🍪 - "服务员的小纸条"
特点:数据小,每次请求都自动带给服务器
javascript
// 设置Cookie(客户端操作)
document.cookie = "username=张三; max-age=3600"; // 1小时过期
// 读取
console.log(document.cookie); // "username=张三"
// 服务器也能设置(通过HTTP响应头)
// Set-Cookie: sessionid=abc123; HttpOnly
适用场景:
数据流向:
text
你的浏览器 → 自动附带 → 服务器
↑ ↓
←───── 响应时带回 ←─────
(2) LocalStorage 📦 - "你的私人抽屉"
特点:只在你电脑里,网站刷新、关闭都还在
javascript
// 存数据(会一直保留)
localStorage.setItem('theme', 'dark');
localStorage.setItem('user', JSON.stringify({name: '张三'}));
// 取数据
const theme = localStorage.getItem('theme'); // "dark"
const user = JSON.parse(localStorage.getItem('user')); // {name: "张三"}
// 删数据
localStorage.removeItem('theme');
localStorage.clear(); // 清空所有
适用场景:
- 网站主题设置
- 记住登录用户名(非密码)
- 表单草稿保存
(3) SessionStorage 💼 - "临时办公桌"
特点:只存在当前标签页,关了就没
javascript
// 和LocalStorage用法一样
sessionStorage.setItem('cart', JSON.stringify(['苹果', '香蕉']));
// 但:开新标签页就访问不到了
// 刷新页面还在,关闭标签页就消失
适用场景:
- 购物车商品(当前会话)
- 多步骤表单暂存
- 页面间临时传值
(4) IndexedDB 🗄️ - "大型文件柜"
特点:能存大量数据,支持复杂查询
javascript
// 1. 打开数据库
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建"表"
db.createObjectStore('products', { keyPath: 'id' });
};
// 2. 存数据
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction('products', 'readwrite');
const store = transaction.objectStore('products');
// 存对象
store.add({ id: 1, name: '手机', price: 2999, stock: 100 });
};
适用场景:
(5) Cache API 📚 - "网站备份本"
特点:存网页资源,没网也能看
javascript
// 存网页到缓存(在Service Worker中)
caches.open('v1').then(cache => {
cache.addAll([
'/index.html',
'/style.css',
'/logo.png'
]);
});
// 没网时从缓存读取
caches.match('/index.html').then(response => {
if (response) {
return response; // 有缓存,显示缓存内容
}
return fetch(event.request); // 没缓存,尝试网络
});
适用场景:
- PWA应用(如微博、Twitter移动版)
- 离线阅读文章
- 弱网环境优化
4. 简单选择指南
根据需求选:
| 你想存什么? |
选这个 |
原因 |
| 登录状态、记住我 |
Cookie |
自动带给服务器验证 |
| 主题、字体大小 |
LocalStorage |
永久保存偏好 |
| 购物车商品 |
SessionStorage |
关了网页就不要了 |
| 大量离线数据 |
IndexedDB |
容量大,能查询 |
| 让网站离线能用 |
Cache API |
专门干这个的 |
容量对比图:
text
容量从小到大:
Cookie (4KB) → LocalStorage (5MB) → IndexedDB (250MB+)
🍪 📦 🗄️
很小 中等 很大
自动发送 自己存 自己存+能查询
5. 安全注意事项 ⚠️
什么不能存?
javascript
// ❌ 绝对不要存!
localStorage.setItem('password', '123456');
localStorage.setItem('creditCard', '6225888888888888');
localStorage.setItem('token', 'jwt-secret-token');
// ✅ 可以存的
localStorage.setItem('theme', 'dark'); // 界面设置
localStorage.setItem('fontSize', '16px'); // 显示设置
localStorage.setItem('history', JSON.stringify(['搜索1', '搜索2'])); // 非敏感历史
敏感数据怎么存?
javascript
// 方案1:用HttpOnly Cookie(服务器设置)
// 响应头:Set-Cookie: auth=token123; HttpOnly; Secure
// JavaScript读不到,防XSS攻击
// 方案2:短期SessionStorage
sessionStorage.setItem('tempToken', 'short-lived-token');
// 关了网页就消失
// 方案3:加密后存
const encrypted = btoa('sensitive-data'); // Base64编码(只是简单演示)
localStorage.setItem('encryptedData', encrypted);
6. 实际例子:用户设置系统
html
<!DOCTYPE html>
<html>
<head>
<title>用户设置</title>
</head>
<body>
<select id="theme">
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
<input type="range" id="fontSize" min="12" max="24" value="16">
<button onclick="saveSettings()">保存设置</button>
<button onclick="clearSettings()">清除设置</button>
<script>
// 页面加载时恢复设置
window.onload = function() {
const theme = localStorage.getItem('theme') || 'light';
const fontSize = localStorage.getItem('fontSize') || '16';
document.getElementById('theme').value = theme;
document.getElementById('fontSize').value = fontSize;
applySettings(theme, fontSize);
};
// 保存设置
function saveSettings() {
const theme = document.getElementById('theme').value;
const fontSize = document.getElementById('fontSize').value;
// 存到LocalStorage
localStorage.setItem('theme', theme);
localStorage.setItem('fontSize', fontSize);
applySettings(theme, fontSize);
alert('设置已保存!');
}
// 应用设置
function applySettings(theme, fontSize) {
document.body.className = theme;
document.body.style.fontSize = fontSize + 'px';
}
// 清除设置
function clearSettings() {
localStorage.removeItem('theme');
localStorage.removeItem('fontSize');
location.reload(); // 重新加载页面
}
</script>
<style>
body.light { background: white; color: black; }
body.dark { background: #333; color: white; }
</style>
</body>
</html>
7. 总结:一句话选择
-
Cookie:需要服务器知道的数据(如登录)
-
LocalStorage:想永久保存的数据(如主题)
-
SessionStorage:临时用用的数据(如购物车)
-
IndexedDB:数据很多很复杂时(如离线邮件)
-
Cache API:想让网站没网也能用时(如PWA)
记住:所有数据都存在你自己的电脑/手机里,不在网站服务器上。想给服务器,得主动发送(Cookie除外)。
十一、 Web Worker 是什么?
1. 官方定义 (The Law)
Web Worker 是 HTML5 标准引入的一种让脚本运行在后台线程的能力。它允许主线程(UI 线程)创建子线程,将耗时任务分配给后者,从而实现并行计算。
2. 白话翻译 (The Logic)
你可以把浏览器想象成一个 “手术室” :
-
主线程(UI 线程) :是主刀医生。他必须时刻保持专注,观察患者情况(渲染 UI)、对监护仪的点击做出反应(处理交互)。如果医生去搬运沉重的氧气瓶(计算大批量数据),手术台就会“断档”,发生医疗事故(界面卡死)。
-
Web Worker(子线程) :是巡回护士或设备工程师。他在旁边默默处理耗时工作(比如整理过去 4 小时的麻醉记录、计算药物泵注速率),处理完后通过对讲机(postMessage)告诉医生结果。这样医生(主线程)永远不会被琐事卡住。
3. 底层内幕 (The Metal)
-
线程隔离:Worker 运行在另一个全局上下文中(DedicatedWorkerGlobalScope),与主线程完全独立。
-
无 DOM 访问:由于不在主线程,它无法操作 DOM、无法访问 window、parent。
-
通信机制:基于序列化拷贝(Structured Clone)或所有权转移(Transferable Objects)的通信。
进阶必杀:手术麻醉系统啥时候用?
麻醉系统中有三个典型的“夺命场景”,必须使用 Web Worker:
场景 1:高频实时波形数据的处理(如 ECG/压力波形)
痛点:麻醉监护仪上传的波形数据频率极高(如心电图 250Hz - 500Hz)。如果主线程每秒处理 500 个点并计算心率均值,同时还要绘制 Canvas 动画,界面会出现肉眼可见的掉帧。
-
Worker 方案:子线程负责接收原始二进制流,进行 滤波算法处理、基线漂移校正、峰值检测。计算出“干净”的点坐标后再传给主线程绘图。
2. 复杂药代动力学/药效学(PK/PD)模型计算
痛点:麻醉医生需要实时观察“靶控泵注(TCI)”的血药浓度预测曲线。这涉及到复杂的微分方程计算,计算量随时间轴非线性增长。
-
Worker 方案:将数学模型丢入子线程。主线程输入药物剂量,子线程实时计算未来 30 分钟的浓度走向,确保 UI 响应时间(Response Time)恒定在 16ms 以内。
3. 大容量历史病历/术中记录的离线解析与检索
痛点:大型手术可能持续十几个小时,术中采集的数据点(生命体征、给药、插管事件)可能有数万条。在生成“麻醉单 PDF”预览或进行趋势分析时,大数组的遍历和排序会直接让页面假死。
-
Worker 方案:子线程在后台处理这些大数组,执行全量搜索或统计分析。
实战代码:架构师级优化写法
我们要展示如何利用 Transferable Objects(所有权转移)来处理麻醉监测数据,避免大对象克隆带来的性能损耗。
主线程 (main.js)
// 创建子线程处理麻醉机原始数据
const dataWorker = new Worker('data-processor.js');
// 模拟从监护仪获取的高频原始数据(Uint8Array 二进制流)
const rawData = new Uint8Array(1024 * 1024);
// 【优化点】:使用第二个参数 [rawData.buffer],实现内存所有权转移
// 这样数据不会被拷贝,而是直接“瞬移”到子线程,效率极高
dataWorker.postMessage({ buffer: rawData.buffer }, [rawData.buffer]);
dataWorker.onmessage = (e) => {
const { heartRate, bloodPressure } = e.data;
console.log(`主线程收到精准体征:心率 ${heartRate}, 血压 ${bloodPressure}`);
// 更新 UI 仪表盘...
};
子线程 (data-processor.js)
self.onmessage = function(e) {
// 1. 获取主线程传来的 Buffer
const buffer = e.data.buffer;
const view = new DataView(buffer);
// 2. 执行复杂的滤波算法(模拟耗时操作)
let result = complexMedicalAlgorithm(view);
// 3. 将计算结果返回给医生(主线程)
self.postMessage({
heartRate: result.hr,
bloodPressure: result.bp
});
};
function complexMedicalAlgorithm(data) {
// 这里执行微分方程、傅里叶变换等耗时逻辑
return { hr: 75, bp: 120 };
}
性能优化与架构师思考 (The Differentiator)
在麻醉系统这种高可靠性软件中,你需要多考虑一步:
-
通信成本预估:Web Worker 通信本身有开销。如果任务太小(比如只是把 1+1 发过去计算),通信耗时可能大于计算耗时,得不偿失。只有处理大批量数据或超过 50ms 的逻辑才上 Worker。
-
线程池管理:不要无限制创建 Worker。通常建立一个 Worker Pool(线程池) ,数量保持在 navigator.hardwareConcurrency(CPU 核心数)左右。
-
异常兜底:如果子线程报错(如算法溢出),必须捕获 onerror。在麻醉系统中,如果子线程挂了,主线程应有备选方案(如显示最近一次缓存的数值),并在日志中记录。
-
OffscreenCanvas (终极杀招) :在现代浏览器中,你可以把 canvas.transferControlToOffscreen() 传给 Worker。这意味着连绘图逻辑都可以不在主线程跑,彻底实现 UI 零阻塞。
面试通关词典
-
Q: Web Worker 会阻塞主线程吗?
-
A: 不会。它是真正的操作系统级线程。但要注意,如果子线程疯狂进行 I/O 或占用大量内存,可能会导致宿主进程不稳定。
-
Q: 手术室场景下,页面刷新了 Worker 会怎样?
-
A: Worker 会随之销毁。在麻醉系统中,建议配合 SharedWorker 或 Service Worker 实现多标签页共享数据或离线状态保持,防止医生误刷页面导致监测中断。
结论:在手术麻醉系统中,Web Worker 是保障生命线数据流畅的最后一道技术屏障。
十二、浏览器渲染引擎全链路探秘
在前端圈有一句名言: “不懂渲染引擎,优化全靠撞大运。” 当你在浏览器输入 URL 到页面显示的这几百毫秒里,渲染引擎内部经历了一场极其复杂的“工业化流水线”协作。

1、 灵魂拷问:为什么我们要懂渲染引擎?
-
现状:大多数人只知道“HTML 转 DOM,CSS 转 CSSOM”。
-
痛点:为什么 transform 比 left 性能好?为什么 JS 会阻塞渲染?为什么 will-change 不能乱用?
-
本质:渲染引擎决定了 Web 应用的性能天花板。
1.1 为什么 transform 比 left 性能好?(线程与流水线视角)
这是面试中最能拉开档次的问题。很多人的回答止步于“transform 开启了硬件加速”。
【底层深度解构】 :
【吊打面试官金句】 :
“left 的性能瓶颈在于它耦合了主线程的 Layout 任务,受 JS 执行阻塞;而 transform 实现了逻辑与渲染的分离,通过合成线程在 GPU 层面完成位图变换,绕过了重排与重绘,这才是高性能动画的本质。”
1.2 为什么 JS 会阻塞渲染?(临界资源与一致性视角)
面试官可能会问:“为什么浏览器不能一边跑 JS 一边渲染?”
【底层深度解构】 :
-
单一主线程机制:浏览器的渲染进程中,JS 引擎和渲染引擎共用一个主线程。这是为了保证 DOM 的一致性。
-
JS 的“特权” :
-
修改权:JS 可以通过 document.write 或 appendChild 改变 DOM 结构。
-
查询权:JS 可以通过 getComputedStyle 查询最新的样式。
-
浏览器的“保守策略” : 由于 JS 具备随时改变 DOM 和样式的能力,渲染引擎在执行 JS 时必须暂停所有工作。如果 JS 还没跑完,渲染引擎就开始绘制,那么绘制出来的可能是“过时”的内容。
-
CSS 的并发阻塞: 如果 JS 前面有一个 CSS 资源正在下载,JS 也会被阻塞(因为 JS 可能会访问样式,必须等 CSSOM 构建完)。这形成了一个 CSS -> JS -> Rendering 的阻塞链。
【白话举例】 : 这就像装修时,施工队(渲染引擎) 必须等设计师(JS) 改完图纸才能动工。如果设计师还在改方案,施工队强行开工,最后拆改的成本更高。
1.3 为什么 will-change 不能乱用?(内存与层爆炸视角)
很多人以为加了 will-change 页面就快了,实际上滥用它会导致浏览器直接崩溃。
【底层深度解构】 :
【架构师建议】 :
-
动态开关:在动画开始前(如 hover 或 mousedown)添加 will-change,动画结束立即移除。
-
针对性使用:只给那些确实有复杂变换且引起卡顿的元素加。
1.4 总结:性能天花板的本质
当你理解了上述三点,你就能向面试官输出这个终极结论:
“Web 应用的性能治理,本质上是对 渲染管线同步点 的管理。
- 我们要利用 transform 这种属性将压力从主线程转移到合成线程;
- 我们要通过 defer/async 或 Web Worker 减少 JS 对主线程渲染周期的霸占;
- 我们要通过按需层提升避免 GPU 显存溢出。
只有理解了引擎如何‘搬运像素’,我们才能真正触达 Web 性能的最优解。”
【自测追问】 : 如果面试官接着问:“既然 transform 这么好,那为什么不把所有元素都设为合成层?” 神回复: “那就像是把整本书的每一行字都单独印在一张透明胶片上。虽然你想动哪一行都很方便,但这本书的厚度(内存占用)和翻页时的校准(合成计算)会拖垮整个浏览器。”
2、 第一章:剥茧抽丝——渲染引擎是什么?
2.1. 官方定义 (The Law)
渲染引擎(Rendering Engine),也常被称为“浏览器内核”,负责取得网页的内容(HTML、XML、图像等)、整理讯息(加入 CSS 等),以及计算网页的显示方式,然后输出至显示器。
-
常见引擎:Blink (Chrome/Edge)、WebKit (Safari)、Gecko (Firefox)。
2.2 白话翻译 (The Logic)
想象渲染引擎是一个高级装修施工队:
-
HTML 是客户给的装修清单。
-
CSS 是设计图纸。
-
JS 是智能家居脚本。
-
渲染引擎 就是带班工头,他要把清单变成实物,还要确保开关(交互)灵敏,且墙皮(像素)不掉色。
3、 第二章:探究本质——渲染流水线 (The Pipeline)
渲染引擎的工作流程通常被称为 关键渲染路径(Critical Rendering Path) 。
3.1 构建对象模型 (Parsing)
-
DOM (Document Object Model) :引擎将 HTML 字节流解析为 Token,再转换为 Node,最后组成树状结构。
-
CSSOM (CSS Object Model) :解析 CSS 样式表,计算出每个节点的样式。
-
【面试杀手锏 - Preload Scanner】 : 官方版:浏览器在解析 HTML 时,会启动一个轻量级的扫描器,提前下载后续的 JS/CSS。 架构师话术: “渲染引擎并不是死板地线性解析。Preload Scanner 解决了解析阻塞时的带宽浪费,这是现代浏览器首屏优化的核心机制。”
3.2 构建渲染树 (Render Tree)
-
过程:将 DOM 和 CSSOM 合并。
-
细节:不可见节点(如 display: none)不会进入渲染树,但 visibility: hidden 的节点会。
3.3 布局 (Layout / Reflow)
-
任务:计算每个节点在屏幕上的确切几何坐标(位置和大小)。
-
白话:确定每个家具摆在客厅的哪个角落,占多大地方。
3.4 绘制 (Paint / Raster)
-
任务:将计算好的节点转换为实际的像素点。涉及颜色、阴影、边框等绘制指令。
3.5 合成 (Compositing) —— 【重难点】
-
原理:现代浏览器会将页面拆分为多个层(Layers) 。合成线程(Compositor Thread)负责将这些层合并并输出到屏幕。
-
GPU 加速:合成阶段主要在 GPU 中完成,这就是为什么 transform 动画流畅的原因——它跳过了布局和绘制,直接在 GPU 操纵层位移。
4、 第三章:进阶必杀——性能优化与底层逻辑
4.1 重排 vs 重绘 (Reflow vs Repaint)
| 概念 |
触发原因 |
性能开销 |
白话类比 |
| 重排 (Reflow) |
几何属性改变(宽高、位置、DOM 增删) |
极大(需重新计算整个布局流) |
拆掉承重墙,重新规划户型 |
| 重绘 (Repaint) |
视觉属性改变(颜色、背景色) |
中等 |
墙皮旧了,重新刷个漆 |
4.2 架构师级优化:避开主线程
【专业技巧】 : 传统的动画通过修改 top/left 触发重排,由主线程计算,主线程一旦忙碌(JS 执行长任务),动画就卡顿。 优化方案:使用 transform 或 opacity。 原因:这两个属性会触发合成层提升。它们在 Compositor Thread 运行,完全不占用主线程,通过 GPU 直接渲染。
【代码详解】 :
// ❌ 新手版:频繁触发重排,性能差
element.style.left = '100px';
// ✅ 架构师版:跳过布局与绘制,直接交给合成线程
element.style.transform = 'translateX(100px)';
// 提示:配合 will-change: transform 提前告知引擎提升层,但不可滥用,否则会耗尽显存。
5、 第四章:现代浏览器架构 (Modern Architecture)
5.1 多进程架构
面试中如果能提到渲染进程(Renderer Process) 与 GPU 进程 的分离,是巨大的加分项。
-
渲染进程:每个标签页一个(沙箱环境),包含主线程、合成线程、解析线程。
-
安全隔离:如果一个标签页崩溃,不会影响整个浏览器。
5.2 事件循环与渲染的节律
浏览器通常 16.7ms (60fps) 刷新一次。 底层逻辑:JS 代码执行 -> 微任务处理 -> RequestAnimationFrame -> 布局/绘制 -> 渲染。 如果你的 JS 执行超过 16ms,渲染引擎就会“丢帧”,用户就会感到卡顿。
6、 面试通关词典 (Interview Prep)
【吊打面试官话术】 :
“深入理解渲染引擎,本质上是在理解 关键渲染路径(CRP) 的资源调度。我不仅关注 DOM 的构建,更关注 合成线程(Compositor Thread) 的独立性。在高性能场景下,我会通过属性提升策略避开 Layout 和 Paint,直接利用 GPU 执行 Composite-only 动画。同时,我会监控 Long Tasks,确保主线程不会因为过载而导致渲染引擎的帧调度失效。”
【神回复追问】 :
-
问:既然 CSS 不阻塞 DOM 解析,为什么还要建议把 CSS 放在头部?
-
答: “CSS 虽不阻塞 DOM 解析,但它会阻塞渲染树的构建和 JS 的执行(因为 JS 可能查询样式)。如果不把 CSS 放在头部,浏览器可能会先渲染出无样式的内容(FOUC),造成糟糕的用户体验。这属于渲染引擎的‘预加载策略’与‘渲染一致性’权衡。”
十三、 浏览器兼容性 (全链路治理:从“填坑”到“工程化闭环”)
在大多数开发者眼中,兼容性是写不完的 CSS Hack 和没完没了的 Polyfill。但在架构师眼中,兼容性是一场Web 标准的超前性与宿主环境滞后性之间的博弈。
如果我们只停留在“修 Bug”层面,永远无法触及性能的天花板。
1、 知识图谱:兼容性治理的五层防御体系
首先要建立起宏观的防御模型,而不仅仅是罗列工具。
| 层次 |
防御手段 |
核心价值 |
| 第五层:体验策略 |
渐进增强 (PE) vs 优雅降级 (GD) |
决定业务底线与上限 |
| 第四层:运行时拦截 |
特性检测 (Feature Detection) + 动态 Polyfill |
解决“API 存在性”问题 |
| 第三层:渲染兼容 |
PostCSS + Autoprefixer + CSS Fallback |
解决布局与视觉偏差 |
| 第二层:工具链转译 |
Babel + Browserslist + Core-js |
解决语法兼容的工业化标准 |
| 第一层:基线决策 |
Browserslist + ROI 决策模型 |
唯一真相来源,控制工程成本 |
2、 核心解析:是什么?为什么?怎么做?
1. 现状剖析:为什么兼容性是“性能杀手”?
-
痛点:为了兼容 1% 的 IE 用户,全量打包了庞大的 ES5 转换代码和 Polyfill,导致 99% 的现代浏览器用户多下载了 30% 的冗余包。
-
本质:这是 “兼容性开销”对“现代性能”的霸凌。
2. 特性检测 (Feature Detection) —— 【白话版】
-
白话版:就像你进一家饭店,不要问“你是哪年哪月开业的(UA 探测)”,而是问“你们这儿能刷医保卡吗(特性检测)”。能刷就刷,不能刷就付现金(降级)。
-
官方逻辑:不要依赖不稳定的 navigator.userAgent,而要直接判断 API 是否在 window 或 Element.prototype 上。
-
代码详解:
// 架构师级写法:不检测浏览器,只检测能力
if ('IntersectionObserver' in window) {
// 只有支持该特性的浏览器才执行高性能观察逻辑
} else {
// 降级为监听 scroll 事件的传统方案
}
3、 进阶必杀:架构师级的工程方案
1. Browserslist:全链路的“唯一真相”
-
深度解析:很多项目在 Babel 里写一套,PostCSS 里写一套。架构师要求必须在 .browserslistrc 中统一配置。
-
底层逻辑:它是连接“业务需求”与“编译工具”的纽带,确保语法转译和前缀补全遵循同一套基线。
2. 差异化打包 (Differential Serving) —— 【吊打点】
这是区分架构师与高级开发的关键。
-
是什么:针对现代浏览器和旧版浏览器生成两套独立的 JS 包。
-
为什么:现代浏览器原生支持 const/await/class,不需要转译和垫片,执行效率极高。
-
怎么做:
<!-- 现代浏览器加载:不带垫片、不转译、代码量极小 -->
<script type="module" src="app.modern.js"></script>
<!-- 旧版浏览器加载:全量转译、带庞大 Polyfill -->
<script nomodule src="app.legacy.js"></script>
3. 按需 Polyfill:动态垫片服务
-
技术原理:利用 Polyfill.io 类似的原理,根据浏览器请求头的 UA 动态下发该环境缺失的补丁。
-
优势:避免了在 Bundle 包中硬编码 Polyfill,将兼容性成本从“前端包体积”转移到“CDN 动态分发”。
4、 性能优化与个人思考:ROI 决策模型
在面试中,谈论兼容性一定要带上商业视角:
-
四象限法则:
-
高流量+低成本(如 Chrome 前缀):必须做。
-
低流量+高成本(如 IE8 兼容):坚决不做,引导用户升级或提供纯文版降级。
-
CSS 逻辑回退(Fallback) : 利用 CSS 的解析忽略机制,实现零开销的降级。
.container {
display: block; /* 降级方案 */
display: flex; /* 现代方案:如果浏览器不认识 flex,会自动忽略上一行,保持 block */
}
5、 面试通关词典 (Interview Prep)
【金句总结】 :
“解决兼容性不应是‘打补丁’,而应是 ‘构建治理闭环’。
首先,通过 Browserslist 建立统一的环境基线; 其次,利用 PostCSS 和 Babel 实现编译时的工业化转换; 接着,通过 特性检测 结合 差异化打包(Differential Serving) ,将兼容性开销精准限制在老旧设备上; 最后,建立 RUM(真实用户监控) ,动态分析不同环境下的白屏率,用数据驱动兼容性决策的迭代。
这种‘现代优先、向后兼容’的弹性架构,才是解决浏览器碎裂化的最优解。”
【神回复追问】 :
-
问:如果某个新特性完全无法 Polyfill 怎么办?
-
神回复: “我会采用‘功能裁剪’策略。核心业务逻辑(如支付)走普通路径,增强型体验(如 WebGPU 动画)在不支持的环境下直接‘静默失效’。我们要兼容的是用户的使用权,而不是强制视觉像素的 100% 一致。”
十四、 跨文档通信全景解构:打破浏览器的“孤岛效应”
在现代 Web 应用中,跨文档通信本质上是解决 “多个窗口、多个标签页、或多个 Iframe 之间如何互通有无” 的问题。
1、 核心底座:为什么要通信?
-
现状:为了安全,浏览器通过“同源策略”将每个标签页隔离在独立的“沙箱”里。
-
痛点:用户在 A 标签页登录了,B 标签页如何实时更新头像?点击扫码登录后,主页面如何感知并跳转?
-
本质:这是分布式 UI 状态同步的挑战。
2、 方案解构:从“暴力黑客”到“优雅总线”
跨文档通信分为两大战场:跨域通信(Cross-origin)和 同源通信(Same-origin) 。
1. 跨域通信的“唯一真理”:postMessage
这是 W3C 定义的唯一合法跨域通信 API。
-
底层内幕:基于结构化克隆算法(Structured Clone Algorithm) 。它不是简单的 JSON 序列化,而是能够处理循环引用、Date、Blob 等复杂对象的引擎级克隆。
-
吊打点(安全性) :一定要提到 origin 校验。如果不校验 event.origin,就等于给 XSS 攻击开了后门。
-
白话版:就像两个敌对国家(不同源)通信,必须通过外交部(postMessage)并在信封上盖好国家公章(Origin),对方确认公章后才开信。
2. 同源通信的“现代班车”:BroadcastChannel
-
是什么:专门为同源页面设计的“发布/订阅”总线。
-
为什么吊:它比 postMessage 更简洁,不需要获取 window 对象的引用,只要频道名称(Channel Name)一致,所有页面都能收到。
-
架构价值:非常适合做多页面的状态同步(如全站静音、主题切换)。
3. 同源通信的“隐形大脑”:SharedWorker
-
是什么:多个同源标签页共享同一个后台线程。
-
深度解构:它是所有标签页的“中央控制器”。数据存放在 SharedWorker 的内存里,所有页面通过 port 连进来取。
-
高级感:这能解决重复请求问题。多个页面都要拿配置数据,只需一个 Worker 去请求,然后分发给所有页面。
4. 同源通信的“被动监听”:StorageEvent
-
做法:监听 window.addEventListener('storage', ...)。
-
细节:只有当 localStorage 的值被改变且是在另一个窗口改变时,才会触发。
3、 进阶必杀:架构师级的选型与思考
1. 通信成本与性能
-
痛点:频繁通信会导致主线程卡顿。
-
优化:如果是传输超大数据(如图片像素、大数据量表格),不要直接发,要用 Transferable Objects(可转移对象) 。
-
本质:直接转移内存控制权,零拷贝,性能炸裂。
2. 安全性决策 (Security)
-
永远不要信任来源:任何通信进来的数据都要做严格的 Schema 校验。
-
敏感信息:永远不要通过 postMessage 传输密码或 Token。
4、 面试通关词典 (Interview Prep)
【吊打话术总结】 :
“跨文档通信的选型取决于 **‘信任边界’**和 ‘实时性要求’。
- 如果涉及到跨域(如嵌入第三方 Iframe) ,postMessage 是唯一的安全选择,但必须严格遵守 Origin Check 闭环;
- 如果是同源多页同步,我会优先考虑 BroadcastChannel,因为它提供了最纯粹的观察者模式实现;
- 如果需要更复杂的中央状态管理或减少网络冗余,我会引入 SharedWorker 作为所有标签页的‘脑干’;
- 在处理极端性能要求时,我会利用 Transferable Objects 绕过序列化开销,实现内存级的快速周转。
这种‘分场景治理’的思想,才是构建健壮多页应用的基础。”
🎨 技术对比一览表(记这个就行)
| 技术 |
范围 |
特点 |
架构师评价 |
| postMessage |
跨域/同源 |
需持有窗口引用 |
全能选手,安全第一 |
| BroadcastChannel |
同源 |
发布订阅,无需引用 |
多标签页同步首选 |
| SharedWorker |
同源 |
中央集权,共享状态 |
重型架构,减少冗余 |
| localStorage |
同源 |
事件驱动 |
兼容性老旧方案 |
| Service Worker |
同源 |
拦截网络,中转数据 |
离线架构下的副产品 |
面试官追问: “如果我关掉了主页面,SharedWorker 还会存在吗?” 神回复: “只要还有一个关联的标签页存活,SharedWorker 就不会销毁。它是真正的‘最后一个人关灯’模式。”
十五、如何实现拖放功能?
1、 核心解构:实现三步走
1. 赋予身份:谁能拖?
在 HTML 标签上加个“通行证”:draggable="true" 。
2. 托运货物:带什么走?
在 dragstart 事件里,把数据塞进浏览器的“快递盒”—— dataTransfer。
codeJavaScript
source.ondragstart = (e) => {
e.dataTransfer.setData('text/plain', '这是我的业务ID'); // 贴标签
};
3. 接收安检:准不准落?(最关键的一步)
在目标区域监听 dragover,并执行 e.preventDefault() 。
-
吊打点:为什么要执行 preventDefault?因为浏览器的默认脾气是“拒绝在任何地方丢垃圾”。你拦截了默认行为,就是告诉浏览器:“这里是合法的投放区”。
2、 实战全流程(极简逻辑)
-
Source(源) : 监听 dragstart,存入 ID。
-
Target(目标) :
- 监听 dragover,阻止默认行为(允许落下)。
- 监听 drop,取出 ID,执行业务逻辑(比如移动 DOM 或调接口)。
3、 架构师级的深度“加分项”
如果你能随口提下面这几点,面试官会觉得你很有实操深度:
1. 性能优化:为什么原生 DnD 比较“丝滑”?
-
内核机制:原生的拖拽“鬼影”(Ghost Image)是由浏览器在独立进程/合成线程中生成的。它不占用主线程的 JS 逻辑,所以即使页面很卡,拖拽的那个虚影依然是流畅的。
-
对比:如果你用 mousedown 模拟拖拽,所有位移计算都在主线程,页面一卡,拖拽就掉帧。
2. 安全保护模式(DataTransfer Protected Mode)
-
冷知识:在 dragover 事件中,你是读不到 getData() 里的具体内容的。
-
原因:这是浏览器的安全隐私保护。只有在最终的 drop 瞬间,数据才会对目标开放。防止你拖着一段密码经过一个恶意广告位时,数据被偷走。
3. 跨文档/跨系统(核心优势)
-
场景:原生 DnD 最强的地方在于跨界。你可以把一张桌面的图片直接拖进浏览器,或者把 A 网页的文本拖进 B 网页。
-
实现:只需要检查 e.dataTransfer.files 是否存在,就能直接对接 File API 实现文件上传。
4、 面试官反问话术
问: “如果我想让拖拽的虚影更好看,或者换个形状怎么办?” 答: “我会使用 e.dataTransfer.setDragImage(element, x, y) 。它可以指定任何一个 DOM 节点(甚至是隐藏的)作为拖拽时的视觉反馈,这比手动写定位跟随要高效得多,而且利用了 GPU 加速。”
总结: 实现拖放就是:draggable 启身份,dataTransfer 传数据,preventDefault 准降落。 剩下的样式和逻辑,不过是基于这个协议的填空题。
十六、什么是同源策略?
同源策略(Same-Origin Policy, SOP) 不是一个 Bug 的解决方案,它是浏览器最核心、最基本的安全隔离机制。如果没有它,互联网将处于完全的“丛林状态”。
1、 剥茧抽丝:什么是同源策略? (The Essence)
1. 官方定义 (The Law)
同源必须同时满足三个条件:
-
协议相同 (Protocol, 如 http/https)
-
域名相同 (Domain, 如 example.com)
-
端口相同 (Port, 如 80/443)
2. 架构师视角的白话翻译 (The Logic)
同源策略本质上是定义了 “信任边界” 。
-
白话版:就像你住在酒店里,你的房卡只能开你自己的房门(同源)。如果没有这个策略,意味着隔壁房间的人(恶意网站)可以随时走进你的房间,翻你的行李,甚至在你的床头柜里放监听器。
-
本质:它是浏览器为了防止不同来源的文档相互干扰而建立的一套“防撬锁”机制。
2、 核心限制:它到底拦住了什么? (Restrictions)
同源策略主要在三个层面建立防火墙:
-
DOM 层面:不能访问非同源页面的 DOM。
-
痛点:如果没有它,你在 A 网站(钓鱼站)里嵌套一个 B 网站(银行)的 Iframe,A 就可以通过 JS 读取你输入银行页面的密码。
-
数据交互层面:AJAX / Fetch 请求受限。
-
存储层面:无法读取非同源的 Cookie、LocalStorage、IndexedDB。
-
痛点:这防止了恶意网站盗取你的 Session Token。
3、 探究本质:浏览器是如何“拦截”的? (Deep Dive)
这是最体现深度的地方,请记住这个底层逻辑:
4、 关键转折:为什么脚本和图片可以跨域? (The Loophole)
你一定见过
-
架构师解析:同源策略允许 “跨域嵌入(Cross-origin embedding)” ,但限制了 “跨域读取(Cross-origin reading)” 。
-
原因:Web 的本质是“链接”。如果连图片和 JS 都不给引用,互联网就退化成一个个孤岛了。
-
安全风险(CSRF 根源) :正因为图片和表单可以跨域发送请求,才导致了 CSRF(跨站请求伪造) 。攻击者虽然读不到你的数据,但他可以借用你的 Cookie 发起一次“点击”。
5、 现代治理方案:如何优雅地“打破”同源?
在实际业务中,前后端分离必须跨域,我们有这套“武器库”:
-
CORS (跨域资源共享) :官方钦定的标准。通过服务器返回 Access-Control-Allow-Origin 头,明确告诉浏览器:“这个邻居是我的朋友,让他进来”。
-
Proxy (代理) :通过 Webpack 或 Nginx 转发。浏览器认为是在访问同源服务器,实际上服务器在后台帮你偷偷取了数据(服务器之间没有同源策略)。
-
postMessage:用于不同窗口/Iframe 之间的跨域通信。
6、 面试通关词典 (Interview Prep)
【吊打话术总结】 :
“同源策略是 Web 安全的底座。它通过对 协议、域名、端口 的强匹配,在浏览器内部建立了一套严格的物理隔离机制。
深入底层来看,同源策略并不是阻止请求的发出,而是通过浏览器引擎在 数据回流阶段 的拦截,防止了非授信来源读取敏感数据。
它的核心哲学是‘限制读取而非限制嵌入’。这也导致了 CSRF 等安全风险。在现代微服务架构中,我会通过 CORS 的精细化配置或 Nginx 反向代理 来平衡安全性与灵活性,同时利用 HttpOnly Cookie 进一步加固同源边界以外的安全。”
【神回复追问】 :
-
问:既然有同源策略,为什么还需要 CSRF Token?
-
神回复: “因为同源策略只能防止‘读’,不能完全防止‘写’(比如表单提交)。攻击者不需要读到你的响应结果,他只要让你的浏览器发出一笔转账请求并带上你的 Cookie 就够了。所以 CSRF Token 是为了验证请求的‘自愿性’,它是对同源策略在防御‘写操作’上的有力补充。”
十七、XSS和CRSF
1、 XSS (跨站脚本攻击) :评论区里的“内鬼”
业务场景: 你在开发一个电商网站的商品评论功能。
1. 攻击过程(白话版):
-
黑客的操作:黑客在评论框里不写“好评”,而是写了一段代码:
-
网站的失误:你的后端没检查,直接把这段话存到了数据库。
-
受害者的遭遇:普通用户张三打开这个商品页面想看评价。浏览器下载了这条评论,发现这是一段 了。
-
结果:张三的登录 Cookie 瞬间被发到了黑客的服务器,黑客拿着 Cookie 就能直接登录张三的账号。
2. 架构师深挖(为什么会成功?):
-
本质:浏览器分不清哪些代码是开发者写的,哪些是用户写的。它把用户输入的“剧毒脚本”当成了正常的业务逻辑去执行。
3. 吊打级解决方案:
-
方案 A(最稳) :HttpOnly。给 Cookie 加这个属性,JS 就读不到它了,黑客就算成功运行了代码也拿不走身份令牌。
-
方案 B(标准) :输入脱敏/输出转义。把 < 变成 <。这样脚本就不会被执行,而是像普通文字一样显示出来。
2、 CSRF (跨站请求伪造) :诱导点击的“远程遥控”
业务场景: 你在开发银行的转账功能。转账接口是:bank.com/transfer?to…。
1. 攻击过程(白话版):
-
前提:受害者张三刚刚登录了银行网站,没退出,浏览器里存着银行的登录 Cookie。
-
黑客的操作:黑客发了一封邮件给张三,标题是“恭喜中奖,点击领钱”,诱导张三点开一个网页 evil.com。
-
内幕:这个恶意网页里隐藏了一个看不见的图片:
-
结果:张三点开网页的瞬间,浏览器尝试加载图片。因为它发现地址是 bank.com 的,于是自动带上了张三的银行 Cookie。银行服务器一看:请求合法,Cookie 正确,确认是张三本人,于是划走了 1 万块。
2. 架构师深挖(为什么会成功?):
-
本质: “傻瓜式”的 Cookie 携带机制。浏览器在发请求时,只要地址匹配,就会自动带上该域下的 Cookie,它根本不管这个请求是你在银行页面点的,还是在黑客页面点的。
3. 吊打级解决方案:
-
方案 A(现代) :SameSite 属性。设置 Cookie 为 SameSite=Lax。这样从黑客网站发起的跨站请求,浏览器就不会自动带上 Cookie 了。
-
方案 B(经典) :CSRF Token。每次转账时,页面必须带一个随机生成的 Token。黑客可以借用你的 Cookie,但他拿不到你页面里的 Token(受同源策略保护),请求就会失败。
3、 总结:一张表看清业务差异
| 维度 |
XSS (内鬼) |
CSRF (遥控) |
| 攻击载体 |
恶意脚本(在你的页面里跑) |
恶意链接/请求(在别的页面发起) |
| 黑客是否需要拿到 Cookie |
是(通过脚本偷走) |
否(不需要拿到,直接借用) |
| 攻击发生的地点 |
你的网站内部 |
你的网站外部 |
| 打个比方 |
坏人混进你的公司,偷走了你的钥匙
|
坏人趁你在家,伪造你的签名去银行取钱 |
💡 吊打面试官的总结话术(建议背诵):
“在实际业务中,防范 XSS 的核心是 ‘隔离与不信任’。我会强制开启 CSP (内容安全策略) 限制脚本来源,并对所有 Cookie 开启 HttpOnly,从源头切断脚本窃取敏感信息的可能。
而防范 CSRF 的核心是 ‘来源确认’。因为跨域请求会自动携带 Cookie,我们不能仅依赖 Cookie 鉴权。我会引入 SameSite 属性来限制第三方 Cookie 传递,并配合 双重 Cookie 校验或自定义 Header Token。因为黑客虽然能伪造请求,但他受制于同源策略(SOP),无法读取我们页面内的私密 Token,从而实现逻辑闭环。”
十八、性能优化之HTML篇
当面试官问"HTML5对性能优化有什么帮助?"
黄金回答框架:
"HTML5不是简单的标签更新,而是一整套性能优化原生方案:
第一层:资源加载优化
-
loading="lazy":原生懒加载,省掉所有懒加载JS库
-
preload/prefetch:资源优先级管理,首屏提速30%
第二层:渲染优化
- 语义化标签:浏览器内置渲染优化
- 响应式图片:
srcset和<picture>,节省50%图片流量
第三层:计算优化
- Web Workers:复杂计算移出主线程
- Service Worker:离线缓存,重复访问秒开
第四层:现代API
- Intersection Observer:替代scroll监听,性能提升100倍
- Resize/Mutation Observer:高效监听DOM变化
总的来说,HTML5让很多需要JS实现的优化变成了一行HTML属性,这是质的飞跃。"
展现深度的追问回答:
追问:"那具体怎么选择用哪个优化方案呢?"
回答:
"我遵循性能优化金字塔:
-
最底层(必须做) :语义化标签 + 懒加载 + 资源预加载
-
中间层(应该做) :响应式图片 + Service Worker缓存
-
顶层(高级优化) :Web Workers + 现代Observer API
具体执行时,我先测量再优化:
- Lighthouse跑分,看具体瓶颈
- WebPageTest分析加载瀑布图
- 真实用户监控(RUM)看实际情况
比如发现LCP(最大内容绘制)慢,就优先用preload和loading=lazy;发现CPU占用高,就考虑Web Workers。"
** 实战性能数据对比**
| 优化项 |
优化前 |
优化后 |
性能提升 |
| 图片懒加载(JS实现) |
首屏2.5s |
首屏1.8s |
28% |
| 图片懒加载(原生) |
首屏1.8s |
首屏1.5s |
17% |
| 资源预加载 |
FCP 1.2s |
FCP 0.8s |
33% |
| Service Worker缓存 |
重复访问2s |
重复访问0.3s |
85% |
| Web Workers(计算) |
UI卡顿300ms |
UI流畅0ms |
100% |
一句话总结
"HTML5性能优化的核心思想:把性能优化从'JS补救'变成'HTML原生',用一行属性替代一堆代码,让浏览器原生能力为我们工作。"
记住这个口诀:
-
加载:
lazy延迟,preload优先
-
渲染:语义标签,响应图片
-
计算:Worker分担,主线程轻松
-
缓存:Service Worker,离线能用