HTTP 状态码
- 200 OK:请求成功,服务器返回数据。
- 301 Moved Permanently:资源已被永久移动到新位置。通常是网站重定向时使用。
- 302 Found:临时重定向,表示请求的资源暂时被移到其它地方,常用于处理短期的重定向。
- 400 Bad Request:请求无效,通常是由于请求格式不正确或者缺少必要的参数。
- 401 Unauthorized:未授权,需要用户登录或者提供有效的身份认证信息。
- 403 Forbidden:禁止访问,服务器理解请求,但拒绝执行。
- 404 Not Found:请求的资源不存在,常见的“页面未找到”错误。
- 500 Internal Server Error:服务器内部错误,通常表示服务器遇到意外情况,无法完成请求。
- 502 Bad Gateway:网关错误,通常指上游服务器出现问题,无法响应请求。
- 503 Service Unavailable:服务不可用,通常是服务器过载或者正在维护。
Babel
Babel,巴别塔
Babel 是一个 JS 编译器,它把我们写的 ES6+ 代码转换成兼容 ES5 的代码。比如箭头函数、类、模块语法都能转换。如果代码中用到了像
Promise这种 ES6+ 的全局对象,还需要配合core-js加 polyfill。
Babel 解决了什么问题?
老浏览器(如 IE)只支持 ES5,而我们现在写的 JS 常常是 ES6+(比如:箭头函数、let/const、class、Promise、async/await 等)。所以要翻译(transpile) 成 ES5,让旧浏览器也能运行。
🔁 示例:ES6 转换前后
ES6 代码:
const greet = (name) => {
console.log(`Hello, ${name}`);
};Babel 转换后(ES5):
var greet = function (name) {
console.log('Hello, ' + name);
};Babel 如何处理 Promise 等高级功能?
- Babel 只能转换语法,不会自动引入运行所需的“功能” (比如:Promise、Map、Set)
- 这些叫做“polyfill”(垫片)——你要手动加或者用 Babel 插件自动加
想支持 Promise 怎么办?
你可以用:
npm install core-js然后在 Babel 中配置:
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}这样写:
new Promise((resolve) => resolve(123));Babel 就会自动引入 core-js 里的 Promise polyfill,确保旧浏览器也能运行。
Webpack
Webpack 是一个模块打包工具,它把前端的所有资源当作模块来处理,从入口文件出发分析依赖,通过 Loader 和 Plugin 实现模块转换与优化,最终输出浏览器可以使用的打包文件。我们可以配置它支持 Babel、CSS、图片等,也可以结合 DevServer 实现热更新。
📦 什么是 Webpack?
Webpack 是一个现代 JavaScript 应用的模块打包器(bundler) 。它会把你的项目里所有的 JS、CSS、图片、字体等资源当作模块处理,打包成一个(或多个)可以部署上线的文件。
✅ 一句话理解 Webpack
把各种前端资源 打碎 → 分析依赖 → 打包成浏览器能用的 JS 文件。
🧱 Webpack 的核心概念(面试必须掌握)
| 名称 | 作用解释 |
|---|---|
| Entry | 入口文件(webpack 从哪开始构建依赖图) |
| Output | 打包后的文件输出位置和文件名 |
| Loaders | 让 webpack 能理解非 JS 文件(如 CSS、图片、TS 等) |
| Plugins | 用于执行更复杂的构建逻辑(比如压缩、生成 HTML) |
| Mode | 构建环境(development开发 orproduction生产) |
| DevServer | 开发时的本地服务器,支持热更新 |
🔁 打包流程图
src/index.js <-- Entry
↓
依赖分析 <-- JS、CSS、图片等模块
↓
Loaders 转换 <-- Babel、CSS-loader 等
↓
Plugins 优化 <-- HTMLPlugin、压缩、环境变量等
↓
dist/main.js <-- Output 输出结果✍️ 一个最简单的 webpack.config.js 配置
module.exports = {
entry: './src/index.js', // 入口
output: {
path: __dirname + '/dist', // 输出目录
filename: 'bundle.js', // 输出文件
},
module: {
rules: [
{
test: /\.css$/, // 遇到 .css 用以下 loader
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [],
mode: 'development', // 或 'production'
};Webpack 的编译构造是什么样的?
1. 概述编译过程的几个阶段
Webpack 编译过程通常分为以下几个阶段:
初始化阶段:
- Webpack 会读取配置文件(
webpack.config.js或通过命令行参数传递的配置),并根据配置初始化 Compiler 对象,设置整个构建过程的基本配置(如入口文件、输出路径等)。
- Webpack 会读取配置文件(
构建阶段:
- Webpack 会遍历所有的依赖模块,依次解析每个模块。
- 模块解析:Webpack 会根据
entry配置,找到入口文件,并递归地处理所有依赖的模块。这些模块可以是 JavaScript、CSS、图片、字体等,Webpack 会根据不同的文件类型使用不同的 Loader 来处理。
生成阶段:
- Webpack 会把解析好的模块转化为一个个的模块对象,然后根据这些模块生成打包后的文件。这里还会根据配置的插件(Plugins)做一些优化、注入等操作。
输出阶段:
- 经过构建和生成后,Webpack 会把最终的文件(如 bundle.js、style.css 等)输出到指定的目录(通常由
output.path配置定义)。
- 经过构建和生成后,Webpack 会把最终的文件(如 bundle.js、style.css 等)输出到指定的目录(通常由
2. 详细步骤
你可以进一步深入每个阶段,具体描述其步骤:
初始化阶段:
- Webpack 初始化配置,创建一个
Compiler实例。 Compiler会根据配置文件的内容(例如,entry和output),以及插件和 Loaders 的配置,准备好接下来的构建任务。
- Webpack 初始化配置,创建一个
编译阶段:
- 入口文件解析:Webpack 从
entry配置中指定的文件开始,递归地分析文件的依赖关系。 - 模块化处理:每个文件通过不同的 Loaders 进行处理,转化为 Webpack 可以理解的模块(例如,JS 文件会经过 Babel 编译,CSS 文件会通过
style-loader和css-loader进行处理)。 - 依赖树构建:Webpack 会根据模块之间的依赖关系,建立一棵“依赖树”。
- 代码分割:Webpack 可以根据配置(如
splitChunks)对模块进行拆分,生成多个文件以实现懒加载。
- 入口文件解析:Webpack 从
生成阶段:
- 模块转换:每个模块会被转化为 Webpack 的内部表示形式,并通过
module.factory创建模块对象。 - 插件处理:Webpack 会根据配置的插件(如
HtmlWebpackPlugin、CleanWebpackPlugin)在此阶段对输出结果做进一步的处理,优化、注入资源等。
- 模块转换:每个模块会被转化为 Webpack 的内部表示形式,并通过
输出阶段:
- Webpack 根据配置的
output字段,把构建结果写入到指定的输出路径。 - 如果配置了
filename,Webpack 会根据文件名规则生成文件名。
- Webpack 根据配置的
3. 总结
Webpack 编译过程从初始化配置开始,解析入口文件及其依赖,逐个模块进行转换,最后生成最终输出文件。在这一过程中,Loader 负责文件转化,Plugins 负责优化和其他功能。
游览器
JS 是单线程,异步任务通过事件循环机制执行。
每轮事件循环会先执行同步代码,然后清空微任务队列,最后执行一个宏任务。
微任务包括 Promise 和 MutationObserver,宏任务包括定时器、事件、消息通道等。
内存结构
| 区域 | 说明 | 举例 |
|---|---|---|
| 🧱栈内存(Stack) | 存储基本类型变量(如 number、string)和函数调用顺序 | let a = 10; |
| 🧠堆内存(Heap) | 存储引用类型对象(如 object、array、function) | let obj = { name: 'Tom' } |
- 栈内存小、快,用于存储执行上下文
- 堆内存大、慢,用于存放复杂数据结构
基本类型放栈里,对象类型放堆里。
异步机制 & 事件循环(Event Loop)
浏览器是单线程的,也就是说一次只能做一件事。
那怎么处理异步任务(比如 setTimeout、Promise、DOM 事件)呢?
👉 事件循环(Event Loop)机制出现了!
执行流程图:
调用栈(主线程) ─────► 执行同步代码
↓
遇到异步任务(如 setTimeout)→ 加入任务队列
↓
主线程空了 → 开始处理队列任务(宏/微任务)宏任务 vs 微任务
| 类型 | 举例 | 执行时机 |
|---|---|---|
| 微任务 | Promise.then,MutationObserver |
当前宏任务结束后立即执行 |
| 宏任务 | setTimeout,setInterval,setImmediate,message |
下一轮事件循环 |
执行顺序规则:
一次事件循环:先执行同步代码 → 微任务队列 → 一个宏任务 → 再循环。
例子:
console.log('1');
setTimeout(() => {
console.log('2'); // 宏任务
});
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4');输出结果:
1
4
3 ← 微任务比宏任务先执行
2为什么是 1432 而不是 1324?
这是因为 JavaScript 是单线程的,执行时有一个任务队列(或事件循环机制),不同类型的任务有不同的优先级。
- 同步任务(console.log(‘1’) 和 console.log(‘4’)) 会在当前执行栈中立即执行。
- setTimeout 设置了一个延迟为 0 毫秒的异步任务。它会将回调放入宏任务队列(macro task queue),但必须等到当前执行栈清空后才会执行。
- Promise.resolve().then() 是一个微任务(micro task),它会在当前执行栈清空后、宏任务之前执行。
因此,执行顺序是这样的:
- 先执行同步代码
console.log('1')和console.log('4'),它们立即被打印。 - 然后微任务队列中的
Promise.resolve().then()会被执行,打印3。 - 最后,宏任务队列中的
setTimeout会被执行,打印2。
所以输出顺序是:1 4 3 2
es-lint
ESLint是一个用于 JavaScript(及其超集如 TypeScript)代码静态分析的工具,主要用于发现和修复代码中的问题,包括语法错误、风格问题以及潜在的 bug。
规则
ESLint 的规则大致可以分为以下几类:
- Possible Errors(可能的错误)
防止常见的 JavaScript 错误。
no-console:禁止使用console.logno-debugger:禁止使用debuggerno-dupe-args:函数参数不能重名no-extra-semi:禁止多余的分号no-unsafe-finally:禁止在finally中使用控制流语句
- Best Practices
推荐的编程实践和模式。
eqeqeq:强制使用===和!==curly:强制所有控制语句使用大括号no-eval:禁止使用eval()no-alert:禁止使用alert,confirm,prompt
- 严格模式
关于使用 JavaScript 严格模式的规则。
strict:控制是否需要使用"use strict"
- Variables
关于变量声明和使用的规则。
no-undef:禁止使用未声明的变量no-unused-vars:禁止定义了但未使用的变量no-use-before-define:禁止变量在定义之前使用
- Stylistic Issues(代码风格)
代码书写风格相关的规则。
indent:统一缩进风格(如 2 或 4 空格)quotes:使用单引号或双引号semi:强制使用分号或禁止使用分号camelcase:变量命名是否强制使用驼峰命名法space-before-function-paren:函数名和括号之间是否加空格
- ECMAScript 6(ES6+)
有关 ES6(及之后)语法的规则。
prefer-const:优先使用const而不是let,当变量不会被重新赋值时no-var:禁止使用vararrow-spacing:箭头函数中箭头前后是否要有空格no-duplicate-imports:禁止重复导入模块
常见官方规则配置规范
ESLint 提供了一些预设的规则配置(extends):
- eslint:recommended
官方推荐的基础规则集合,启用最重要的规则。
module.exports = {
extends: ['eslint:recommended'],
};- eslint:all
启用 ESLint 所有规则(不推荐,过于严格)。
社区流行的规则集(第三方配置)
e.g. Prettier
用于代码格式化,通常与 ESLint 一起使用,避免风格类规则冲突。
npm install --save-dev eslint-config-prettierextends: ["prettier"]自定义规则
你可以在 .eslintrc.js 或 .eslintrc.json 文件中设置规则:
module.exports = {
rules: {
semi: ['error', 'always'],
quotes: ['warn', 'single'],
'no-console': 'off',
},
};"off"或0:关闭规则"warn"或1:作为警告处理(不会导致构建失败)"error"或2:作为错误处理(会导致构建失败)
ts 的 interface
interface 是 TypeScript 中用于定义对象的结构的一种语法工具。它可以用来描述一个对象有哪些属性、方法、类型等。简单来说,它是用来给“对象”做“类型约束”的。
语法
interface Person {
name: string;
age: number;
}
const user: Person = {
name: 'Alice',
age: 25,
};在这个例子中:
Person是一个接口(interface),- 它规定了一个对象必须有
name(字符串)和age(数字)这两个属性, user对象就是按照这个接口来定义的。
接口的用途:
- 定义对象类型
- 类实现接口
- 函数类型约束
- 继承扩展其他接口
一些进阶特性:
可选属性:
interface User {
username: string;
email?: string; // 可选属性
}只读属性:
interface Point {
readonly x: number;
readonly y: number;
}接口继承:
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}接口也可以描述函数类型:
interface Add {
(x: number, y: number): number;
}
const add: Add = (a, b) => a + b;interface 和 type 有什么区别?
| 特性 | interface |
type |
|---|---|---|
| 可扩展(extend) | ✅ | ✅(使用交叉类型) |
| 兼容类的实现 | ✅ | ❌ |
| 联合类型 | ❌ | ✅ |
| 适合做对象描述 | ✅ | ✅ |
通常来说:
- 如果你是要定义“对象结构”或“类的契约”,优先用
interface。 - 如果你需要用到复杂类型(如联合类型、交叉类型等),用
type会更灵活。
nginx 反向代理的原理
就像你去饭店点餐,不是你直接跑到厨房,而是把菜单交给服务员,由他去厨房取菜。你只和服务员打交道,完全不知道后面厨房是几个人、谁做的、是不是换厨师了。
Nginx 在这里就像这个“服务员”,它:
- 接收用户的请求(比如浏览器发来的)
- 然后把请求转发到后端的服务器(比如真正处理数据的 Java、Python、Node 服务)
- 再把后端的响应结果转回来给用户
为什么叫“反向”代理?
因为:
- “正向代理”是用户找代理(比如翻墙软件)去帮忙访问网站
- “反向代理”是网站这边用代理(Nginx)来接收用户请求,帮忙把请求分发给后端服务
用户根本不知道真实后端长啥样,只知道有个 Nginx。
e.g. 你访问一个网站:
https://example.com/api/user这个网址其实是访问 Nginx,但 Nginx 配置了反向代理,比如把 /api 的请求转发到:
http://localhost:8080/user这个 8080 端口可能是 Java Spring Boot 服务在跑。
Nginx 做反向代理能带来什么好处?
- 隐藏真实后端(安全性)
- 负载均衡(多个后端,Nginx 帮你平均分配请求)
- 缓存静态资源(提升速度)
- 统一入口(前后端、多个服务都通过 Nginx 暴露一个统一的接口)
图示:
[ 用户浏览器 ] ---> [ Nginx ] ---> [ 后端服务A ]
|
---> [ 后端服务B ]权限控制 should be 放在后端
我在简历里写了用前端代码控制用户的权限按钮(因为工作中也确实要求我这么做的),面试官表示很奇怪: 用户可以 f12,改变 js 代码 ->改变权限,甚至还会攻击 api
面试官提出的问题是 非常合理的。你在简历中写「用前端代码控制用户权限按钮」会让人感觉你把权限控制的逻辑放在了前端,这是一个严重的安全问题。
为什么前端控制权限是错误的?
前端是完全暴露给用户的:
- 用户可以随时打开 DevTools(F12)修改 HTML、JS。
- 任何「隐藏按钮」或「禁止点击」的功能都能被轻松绕过。
攻击风险:
- 如果用户篡改前端代码后能成功访问接口(例如删库、改配置),说明你的后端权限控制是失效的。
- 黑客可以模拟任意角色调用 API,造成严重后果。
正确的做法应该是:
| 层级 | 功能 | 示例 |
|---|---|---|
| ✅ 后端 | 权限验证的核心逻辑 | 接口校验用户权限:不能访问就直接返回 403 Forbidden |
| ✅ 前端 | 仅作 UI 展示控制 | 根据权限信息决定是否展示某个按钮,但不能依赖它来真正控制访问 |
面试官可能在考察你什么?
- 你是否理解前后端权限控制的边界。
- 你是否能意识到安全问题,比如 XSS、CSRF、越权访问。
- 你是否有实际项目经验或只是停留在代码层面。
搞破坏的过程
普通用户(比如网站访问者)如何修改线上生产环境的 JS 代码?
用户可以通过浏览器的 开发者工具(F12) 来修改运行在自己浏览器里的 JS 代码,但无法改变服务器上的真实文件。也就是说,用户只能修改“本地浏览器中的 JS 行为” ,不能影响其他人,也不能直接修改生产服务器代码。
用户修改 JS 行为的几种方式:
- 打开开发者工具(F12)直接修改
进入「控制台(Console)」:
可以重写页面上的函数:
javascriptwindow.fetch = () => { alert('我拦截了请求'); };可以执行任意 JS 代码来模拟行为、绕过限制。
修改 HTML、按钮属性、class、JS 执行逻辑等。
- 改写 JS 文件(本地缓存)
- 进入「Sources」面板,找到加载的
.js文件。 - 可以在断点处修改变量,或覆盖函数。
这只影响本次会话中的 JS 执行逻辑,刷新页面就会恢复原样。
- 使用浏览器插件 / 用户脚本扩展(如 Tampermonkey)
写一个用户脚本,修改页面行为:
javascript// ==UserScript== // @name 修改权限按钮 // @match https://example.com/* // ==/UserScript== document.querySelector('#delete-button').style.display = 'block';这个方法适合持续对某个网站进行“外挂操作”。
- 拦截并修改网络请求
- 使用 Fiddler、Charles、Burp Suite、Postman 之类的工具,模拟接口调用。
- 即使前端隐藏按钮,用户也可以直接调用后端 API,如果 API 没权限控制就危险了。
总结:
| 前端行为 | 用户可以怎么绕过 |
|---|---|
| 按钮隐藏 | F12 改 DOM,显示按钮 |
| 禁用按钮 | 删除disabled属性 |
| 判断角色不执行操作 | 重写函数或变量 |
| 禁止访问某接口 | 手动调用接口 |
如果后端不进行权限验证,用户就可以执行原本不允许的操作,这就是越权漏洞(Insecure Direct Object Reference,IDOR)
举个例子
我们来演示一个完整的例子:如何用户通过浏览器控制台绕过前端权限限制,调用原本不应该调用的 API
场景设定(假设的网站)
一个简单的网站管理后台:
https://admin.example.com/前端根据当前用户角色来决定是否显示“删除用户”按钮。你是普通用户,前端隐藏了按钮:
<!-- 删除按钮在普通用户中被隐藏 -->
<button id="delete-user-btn" style="display: none;">删除用户</button>后台 API 接口如下:
DELETE https://admin.example.com/api/users/12345开发者错把权限控制只写在前端
比如这样 👇:
// 当前用户角色
const role = 'user';
if (role === 'admin') {
document.querySelector('#delete-user-btn').style.display = 'inline-block';
}但是后台接口没有判断角色权限,这就是关键问题。
🧑💻 攻击者如何绕过前端权限限制?
第一步:打开开发者工具(F12)
在 Console 里执行:
document.querySelector('#delete-user-btn').style.display = 'inline-block';按钮出现,用户点击也没用了?别担心,我们继续。
第二步:直接调用 API 接口
在控制台中直接发送请求:
fetch('https://admin.example.com/api/users/12345', {
method: 'DELETE',
headers: {
Authorization: 'Bearer [用户的 JWT Token]',
'Content-Type': 'application/json',
},
})
.then((res) => res.json())
.then(console.log);💥 如果后端没验证你是否是管理员,这条请求就会成功删掉用户!
正确的后端做法应该是:
在 API 层加上权限验证:
// Node.js 示例(Express)
app.delete('/api/users/:id', (req, res) => {
const currentUser = req.user; // 从 token 解析出的用户信息
if (currentUser.role !== 'admin') {
return res.status(403).json({ message: 'Forbidden' });
}
// 执行删除操作
});总结
前端隐藏 ≠ 安全
| 错误做法 | 正确做法 |
|---|---|
| 只在前端判断权限 | 后端也要做角色权限校验 |
| 隐藏按钮防止操作 | 即使接口暴露,必须权限校验 |
| 靠前端控制逻辑 | 前端是参考,不是防线 |
- ✅ 用户可以用浏览器控制台篡改页面或调用 API。
- ❌ 前端控制权限按钮 ≠ 真正的安全措施。
- ✅ 权限一定要在后端控制,否则非常容易被恶意操作。
NodeJS
会用 Node.js 做开发辅助工具 + 简单后端接口,以下是常见的面试考点 👇
一、Node.js 基础理解(一定要掌握)
| 题目 | 解答方向 |
|---|---|
| Node.js 是什么?和浏览器 JS 有什么不同? | JS 运行在服务器端、可以访问文件/网络,和浏览器中的 BOM/DOM 不同 |
| 什么是 CommonJS?什么是 ESM? | require/module.exportsvsimport/export,默认模块机制 |
| Node 是单线程还是多线程?如何处理并发? | 单线程 + 异步非阻塞(事件循环模型) |
这些题考你对 前端工具链的运行环境理解清不清楚
二、前端开发工具链(Webpack、Vite)背后原理
Node 在这部分用得很多,前端面试官可能会问:
| 题目 | 涉及内容 |
|---|---|
| 你了解 Webpack/Vite 的原理吗?他们是怎么启动开发服务器的? | Webpack/Vite 本质是 Node 写的,搭了一个基于http的本地开发服务器 |
| 怎么用 Node 写一个简单的本地静态文件服务器? | 用http和fs实现读取文件和返回响应 |
什么是 CLI 脚本?你写过node script.js这样的脚本吗? |
用来做构建、打包、转换等 |
建议你会写一个简单的 Node.js 静态服务器 + 命令行脚本(比如自动生成页面模板)
三、前端项目中常见的 Node 应用场景
| 使用场景 | 面试点 |
|---|---|
| 启动本地服务器(比如 Vite、webpack-dev-server) | 本质上是 Node 起的 http 服务 |
| 项目构建(babel/webpack 插件、打包压缩) | 写构建脚本、文件转换 |
| mock 本地接口 | express起个本地 mock 服务,前端联调不用等后端 |
| 自动化脚本 | 比如node build.js做资源处理、生成路由配置等 |
这些问题其实不太会正面问你“怎么用 Node”,而是间接看你是否用过/看得懂前端项目里的这些东西。
四、Express 简单使用
有能力自己 mock 接口是非常加分的,面试常问:
| 题目 | 掌握程度 |
|---|---|
| 你用过 Express 吗?怎么搭一个接口? | 基本路由、res.send() |
| 你怎么模拟一个后端 API 接口? | 用 Express 写接口,提供 JSON 响应 |
| 怎么处理 CORS(跨域)问题? | 设置res.setHeader('Access-Control-Allow-Origin', '*')或用cors中间件 |
node 和 npm 是什么关系
Node.js 是 JavaScript 的运行环境,npm 是 Node.js 附带的包管理器。也就是说,npm 是跟着 Node.js 一起装进你电脑的一个小工具
| 工具 | 作用 |
|---|---|
node |
让你可以在终端里执行.js文件,比如运行后端代码、脚本 |
npm |
让你可以下载别人写好的 JavaScript 库/工具,比如express、lodash、vite |
node xxx.js |
是“运行程序” |
TCP
Q: 建立一条 tcp 连接需要几个步骤?关闭需要几个?
A: 在 TCP(传输控制协议)中,建立连接和关闭连接分别是通过著名的三次握手(Three-Way Handshake)和 四次挥手(Four-Way Handshake)完成的。
一、建立 TCP 连接:3 个步骤(”三次握手”)
客户端 → 服务端:发送 SYN(同步)报文,表示请求建立连接
- 报文标志位:
SYN = 1 - 初始序号:
seq = x
- 报文标志位:
服务端 → 客户端:收到 SYN 后,回应一个 SYN + ACK 报文,表示同意建立连接
- 报文标志位:
SYN = 1,ACK = 1 - 回复确认号:
ack = x + 1 - 自己的初始序号:
seq = y
- 报文标志位:
客户端 → 服务端:收到 ACK+SYN 后,发送一个 ACK 报文表示确认。
- 报文标志位:
ACK = 1 - 确认号:
ack = y + 1
- 报文标志位:
👉 连接建立完成,双方可以开始传输数据。
二、关闭 TCP 连接:4 个步骤(”四次挥手”)
客户端 → 服务端:发送 FIN 报文,表示主动关闭连接。
- 报文标志位:
FIN = 1 - 此时客户端进入
FIN_WAIT_1状态
- 报文标志位:
服务端 → 客户端:收到 FIN 后,发送 ACK 报文,表示“收到关闭请求”
- 报文标志位:
ACK = 1 - 客户端此时进入
FIN_WAIT_2,等待服务端关闭
- 报文标志位:
服务端 → 客户端:准备关闭后,也发送 FIN 报文,表示可以关闭连接了
- 报文标志位:
FIN = 1 - 服务端进入
LAST_ACK状态
- 报文标志位:
客户端 → 服务端:收到 FIN 后发送 ACK 确认,连接完全关闭
- 报文标志位:
ACK = 1 - 客户端进入
TIME_WAIT状态,等待一段时间后关闭
- 报文标志位:
👉 连接关闭完成,服务端进入 CLOSED 状态。
总结
| 操作 | 步骤数 | 描述 |
|---|---|---|
| 建立连接 | 3 次 | 三次握手(SYN、SYN+ACK、ACK) |
| 关闭连接 | 4 次 | 四次挥手(FIN、ACK、FIN、ACK) |
cookie 和 session
🍪 一、什么是 Cookie?
打个比方:你去一家奶茶店,第一次买奶茶的时候,店员给你发了一个“会员卡”(上面写着你的名字和喜好),你随身带着这张卡。以后每次来这家店,你都把这张卡递给店员,店员一看就知道你是谁、你爱喝什么、要不要加糖。
在网页里的意思:
- Cookie 就像这个“会员卡”,是浏览器保存的一小段数据(文本)。
- 它由服务器设置或前端设置,保存在你自己的浏览器里。
- 以后你每次访问这个网站时,浏览器都会自动把这个 Cookie 发给服务器。
Cookie 里通常存:
- 登录状态(比如登录成功后发个 token)
- 用户偏好(比如黑暗模式、语言)
- 简单的追踪信息(比如你访问了哪些页面)
🧾 二、什么是 Session?
打个比方:你去了银行办业务,前台给你发了一个“号码牌”,同时在系统里记录了你今天的办理信息。这个“号码牌”在你今天逛银行时一直有效,但你一走出银行,或者时间久了没操作,这个号码牌就作废了。
在网页里的意思:
- Session 是服务器端的“你是谁”的记录,存在服务器内存或数据库里。
- 一般你登录后,服务器会创建一个 session,给你一个 session id(就像银行的“号码牌”),这个 id 会存到你的 Cookie 里。
- 你之后访问网站时,浏览器会带着这个 id,服务器通过它知道“啊,这个用户是你”。
对比
| 特性 | Cookie | Session |
|---|---|---|
| 存储位置 | 浏览器(客户端) | 服务器(后端) |
| 大小限制 | 每个 Cookie 最大 4KB,总量也有限 | 没啥硬性限制(取决于服务器) |
| 安全性 | 比较低,容易被盗取 | 安全性高一点(因为数据不在浏览器) |
| 持久性 | 可以设置过期时间,支持长期保存 | 通常短暂保存(关闭浏览器或超时就失效) |
| 典型用途 | 保存登录状态、用户偏好、广告追踪等 | 服务器识别用户、登录验证等 |
e.g. 你登录一个网站,登录后:
- 服务器生成一个 session,里面记录了你是“张三”;
- 然后把 session id 存到你的 Cookie 里;
- 下一次你访问网页,浏览器带上这个 session id;
- 服务器一看这个 id,知道“噢,是张三”,就让你继续保持登录状态。
跨域问题通常是怎么解决?
前端开发中,跨域问题(CORS, Cross-Origin Resource Sharing)是一个非常常见的难题,主要发生在前端代码请求不同域名、端口或协议的后端接口时。浏览器出于安全考虑,默认会限制这种跨域请求。
一、常见跨域场景
举例说明什么叫跨域:
当前网页地址:http://example.com:3000
请求的接口地址:http://api.example.com:8080这个就是跨域,因为:
- 主域名不同(example.com vs api.example.com)
- 或端口不同(3000 vs 8080)
- 或协议不同(http vs https)
二、常见的解决方式(重点)
1. 后端设置 CORS 允许跨域(最根本、最推荐的方式)
原理:后端服务器在响应头里加上Access-Control-Allow-Origin等字段。
示例(Node.js Express) :
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*'); // 或指定来源
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
next();
});注意:真实项目中不要写*,而是指定来源更安全。
2. 前端通过代理转发请求(开发环境常用)
适用于前后端分离开发、本地调试阶段,通过前端开发服务器(如 Vite、Webpack Dev Server)将请求“伪装”为同源。
示例(Vite 配置) :
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://backend.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};访问 /api/users 实际会被代理为 http://backend.example.com/users,从浏览器视角看是同源的。
3. JSONP(仅支持 GET,已很少用了)
原理:通过 <script> 标签可以绕过 CORS,因为它不受同源策略限制。
使用方式:
<script src="http://api.example.com/jsonp?callback=handleData"></script>
<script>
function handleData(data) {
console.log(data);
}
</script>缺点:
- 只能用于 GET 请求
- 安全性差
- 后端必须支持 JSONP 格式
4. 使用 Nginx 做反向代理(生产环境也常见)
类似前端代理,但部署在服务器端。
Nginx 示例:
location /api/ {
proxy_pass http://backend.example.com/;
proxy_set_header Host $host;
}这样前端访问 http://yourdomain.com/api/user 实际由 Nginx 转发到后端。
5. 服务端中转(更高级的场景)
前端请求自己的后端服务器(同源),由后端再请求第三方接口,然后再把数据返回给前端。
这种方式常用于请求外部 API,比如微信、支付宝、OpenAI API 等。
三、建议与总结
| 方式 | 场景 | 优点 | 缺点 |
|---|---|---|---|
| CORS | 后端可控 | 最标准、可靠 | 需后端配合 |
| 前端代理(Dev) | 本地开发 | 简单快速 | 仅开发可用 |
| Nginx 反向代理 | 生产部署 | 通用、安全 | 需配置服务器 |
| JSONP | 旧系统或兼容需求 | 无需改后端 | 仅 GET,安全性差 |
| 服务端中转 | 请求第三方 API 或安全控制 | 安全、灵活 | 增加开发和服务器负担 |
实战
一、跨域问题在 React 项目中怎么出现的?
假设你是用 create-react-app 脚手架起的项目,本地开发时你的前端跑在:
http://localhost:3000而你要请求后端接口,比如:
http://localhost:8080/api/user这就跨域了:端口不同(3000 vs 8080) 。浏览器会拦住你,说:“不行,不允许跨域。”
二、React 中怎么解决跨域(本地开发)
配置前端代理(开发时最推荐!):React 脚手架(create-react-app)已经内置了代理功能。你只需要在 package.json 里加一行:
"proxy": "http://localhost:8080"然后你的代码就可以放心写成这样 👇:
// 3000端口的前端请求
fetch('/api/user')
.then((res) => res.json())
.then((data) => console.log(data));背后 React dev server 会把 /api/user 转发到 http://localhost:8080/api/user,浏览器就认为你“没跨域”,问题就解决了。
注意事项:
- 这个方法只在开发时生效(npm start 的时候)
- 上线之后不能靠这个,生产环境不能用这个代理
三、上线后(生产环境)怎么搞?
后端设置 CORS 响应头:如果你的 React 项目部署好了,比如放在 https://www.myapp.com,要请求的后端接口在 https://api.myapp.com,那就必须后端来允许你访问。后端加上这段响应头就行(以 Express 为例):
res.setHeader('Access-Control-Allow-Origin', 'https://www.myapp.com'); // 只允许你的前端访问
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');这样浏览器才不会报跨域错。
or 部署时用 Nginx 代理(更高级、更推荐):如果你是全栈工程师或者有部署权,可以在 Nginx 里配置“反向代理”
location /api/ {
proxy_pass http://backend-server:8080/;
}然后你的 React 就可以请求同源的 /api/user,而 Nginx 会帮你偷偷转发过去,跨域就不存在了。
四、总结
| 场景 | 怎么搞 | 背后原理 |
|---|---|---|
| React 本地开发 | package.json里加"proxy" |
React dev server 帮你转发 |
| React 正式上线 | 后端设置Access-Control-Allow-Origin |
服务器说“我允许你访问” |
| 有运维或部署权限 | 配 Nginx 反向代理 | 浏览器以为是同源,其实偷偷转发 |
- 开发环境用前端代理(
proxy) - 生产环境靠 CORS 或 Nginx
- JSONP 不推荐,已过时
- 安全控制复杂时用服务端中转
跨域时前端向后端发送 option 请求?
在处理 跨域问题 时,前端会先发送一个 OPTIONS 请求,这个请求通常叫做 “预检请求” (Preflight Request),用于确认服务器是否允许当前的跨域请求。你可以把它理解为前端向后端“打个招呼”,问一下: “我可以跨域请求你这个资源吗?”
为什么要发 OPTIONS 请求?
当浏览器发起一个 跨域请求(即请求的域名、协议、端口不同)时,浏览器会先发一个 OPTIONS 请求,来判断目标服务器是否允许这种跨域请求。这个请求的目的是为了确保安全性,防止恶意网站通过跨域请求危害用户的数据或资源。
OPTIONS 请求的过程:
前端请求触发
比如,前端发送了一个POST请求,且请求头中包含了非常规的头部(如Content-Type: application/json),这时浏览器会认为这个请求属于 复杂请求,必须先通过 预检请求 来确认。发送
OPTIONS 请求
浏览器会自动发送一个OPTIONS请求到目标服务器,询问是否允许跨域操作。这个请求一般不会带有请求体,主要内容是请求头,表示它是一个预检请求。后端响应
OPTIONS 请求
服务器收到OPTIONS请求后,会根据跨域资源共享(CORS)策略做出响应。比如,返回:Access-Control-Allow-Origin: 指定允许跨域的来源(*或者具体域名)Access-Control-Allow-Methods: 允许的方法(如GET, POST, PUT等)Access-Control-Allow-Headers: 允许的请求头(比如Content-Type、Authorization等)
浏览器决定是否发送实际请求
如果OPTIONS请求的响应允许当前的跨域请求(例如,服务器返回了允许的跨域头部),浏览器就会继续发起实际的请求。否则,浏览器会阻止真正的请求发出。
例子
假设你的前端网站 https://www.frontend.com 想请求后端 https://api.backend.com,并且请求是一个 POST 请求,且带有 Content-Type: application/json 的头部,这属于 复杂请求,浏览器就会先发送一个 OPTIONS 请求。
前端(浏览器)发出的 OPTIONS 请求:
OPTIONS /some-api-endpoint HTTP/1.1
Host: api.backend.com
Origin: https://www.frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type后端(服务器)回应:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.frontend.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type如果响应中包含了允许跨域的头部,浏览器会接着发送实际的 POST 请求。
什么时候不需要 OPTIONS 请求?
简单请求(Simple Request):对于一些不涉及复杂请求头、请求方法(如 GET、POST 和一些基本的头部)等情况,浏览器就不会发送
OPTIONS请求。例如:- 请求方法是
GET或POST,且请求头是标准的(比如Content-Type是application/x-www-form-urlencoded、multipart/form-data或text/plain)。 - 不涉及跨域的自定义头部。
- 请求方法是
总结
OPTIONS 请求是浏览器为了确认服务器是否允许跨域请求而发出的“预检请求”。它通常是 跨域请求的一部分,特别是在请求比较复杂或涉及到自定义请求头时。服务器根据请求头来决定是否允许实际的跨域请求。
原型链
“原型链” 是 JavaScript 面试中非常经典的问题之一,涉及到 JS 的继承机制和对象查找机制。我们来系统地回顾一下原型链的概念、原理、常见面试问法,以及如何作答。
一、什么是原型链(Prototype Chain)?
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](在代码中可以通过 __proto__ 访问),它指向创建该对象的构造函数的 prototype 属性。
当你访问一个对象的属性时,如果该属性不在对象自身上,JavaScript 引擎就会沿着这个“原型链”向上查找,直到找到为止,或者到达最顶端的 null。
const obj = {};
console.log(obj.toString); // 找不到会沿着原型链找 Object.prototype.toString二、举个例子
function Person() {}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true这个查找链条:
person --> Person.prototype --> Object.prototype --> null这就是“原型链”。
三、原型链图解(文字版)
以一个例子为主:
function Animal() {}
Animal.prototype.sayHi = function () {
console.log('Hi');
};
const cat = new Animal();
cat.sayHi(); // Hi查找过程如下:
- 查找
cat.sayHi cat对象本身没有sayHi属性- 去
cat.__proto__(即Animal.prototype)找到了sayHi - 于是调用成功
四、常见面试题与应答技巧
1. 原型链和继承的关系?
答:原型链是实现继承的一种方式。子类的 prototype 是父类实例,或者其原型链最终指向父类 prototype,从而实现继承。
2. class 的继承和原型链有关吗?
答:是的。class 是基于原型的语法糖,底层依然是原型链。
class A {}
class B extends A {}
const b = new B();
console.log(b.__proto__ === B.prototype); // true
console.log(B.prototype.__proto__ === A.prototype); // true3. 如何实现一个原型继承?
function Parent() {
this.name = 'parent';
}
Parent.prototype.say = function () {
console.log('hello');
};
function Child() {}
Child.prototype = new Parent();
const c = new Child();
c.say(); // hello这就是原型链继承的基本实现方式(ES5 写法)。
五、注意点
__proto__是非标准属性,但几乎所有浏览器都支持;标准做法是用Object.getPrototypeOf(obj)Object.prototype.__proto__ === null,是原型链的终点- 原型链只会在读取属性时使用,设置属性不会沿原型链设置
六、面试官问:什么是原型链?
原型链是 JavaScript 实现继承的一种机制。每个对象都有一个内部的 [[Prototype]] 属性(可以通过 __proto__ 访问),当访问对象的属性时,如果自身没有,就会从它的原型对象查找,一直往上,直到原型链的尽头 null。这种查找机制就是原型链。原型链也是实现继承的核心机制,例如构造函数的 prototype 就是新对象的原型,实现了属性和方法的共享。