前端基础知识


HTTP 状态码

  1. 200 OK:请求成功,服务器返回数据。
  2. 301 Moved Permanently:资源已被永久移动到新位置。通常是网站重定向时使用。
  3. 302 Found:临时重定向,表示请求的资源暂时被移到其它地方,常用于处理短期的重定向。
  4. 400 Bad Request:请求无效,通常是由于请求格式不正确或者缺少必要的参数。
  5. 401 Unauthorized:未授权,需要用户登录或者提供有效的身份认证信息。
  6. 403 Forbidden:禁止访问,服务器理解请求,但拒绝执行。
  7. 404 Not Found:请求的资源不存在,常见的“页面未找到”错误。
  8. 500 Internal Server Error:服务器内部错误,通常表示服务器遇到意外情况,无法完成请求。
  9. 502 Bad Gateway:网关错误,通常指上游服务器出现问题,无法响应请求。
  10. 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 会根据 entry 配置,找到入口文件,并递归地处理所有依赖的模块。这些模块可以是 JavaScript、CSS、图片、字体等,Webpack 会根据不同的文件类型使用不同的 Loader 来处理。
  • 生成阶段

    • Webpack 会把解析好的模块转化为一个个的模块对象,然后根据这些模块生成打包后的文件。这里还会根据配置的插件(Plugins)做一些优化、注入等操作。
  • 输出阶段

    • 经过构建和生成后,Webpack 会把最终的文件(如 bundle.js、style.css 等)输出到指定的目录(通常由 output.path 配置定义)。

2. 详细步骤

你可以进一步深入每个阶段,具体描述其步骤:

  • 初始化阶段

    • Webpack 初始化配置,创建一个 Compiler 实例。
    • Compiler 会根据配置文件的内容(例如,entryoutput),以及插件和 Loaders 的配置,准备好接下来的构建任务。
  • 编译阶段

    • 入口文件解析:Webpack 从 entry 配置中指定的文件开始,递归地分析文件的依赖关系。
    • 模块化处理:每个文件通过不同的 Loaders 进行处理,转化为 Webpack 可以理解的模块(例如,JS 文件会经过 Babel 编译,CSS 文件会通过 style-loadercss-loader 进行处理)。
    • 依赖树构建:Webpack 会根据模块之间的依赖关系,建立一棵“依赖树”。
    • 代码分割:Webpack 可以根据配置(如 splitChunks)对模块进行拆分,生成多个文件以实现懒加载。
  • 生成阶段

    • 模块转换:每个模块会被转化为 Webpack 的内部表示形式,并通过 module.factory 创建模块对象。
    • 插件处理:Webpack 会根据配置的插件(如 HtmlWebpackPluginCleanWebpackPlugin)在此阶段对输出结果做进一步的处理,优化、注入资源等。
  • 输出阶段

    • Webpack 根据配置的 output 字段,把构建结果写入到指定的输出路径。
    • 如果配置了 filename,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 是单线程的,执行时有一个任务队列(或事件循环机制),不同类型的任务有不同的优先级。

  1. 同步任务(console.log(‘1’) 和 console.log(‘4’)) 会在当前执行栈中立即执行。
  2. setTimeout 设置了一个延迟为 0 毫秒的异步任务。它会将回调放入宏任务队列(macro task queue),但必须等到当前执行栈清空后才会执行。
  3. Promise.resolve().then() 是一个微任务(micro task),它会在当前执行栈清空后、宏任务之前执行。

因此,执行顺序是这样的:

  1. 先执行同步代码 console.log('1')console.log('4'),它们立即被打印。
  2. 然后微任务队列中的 Promise.resolve().then() 会被执行,打印 3
  3. 最后,宏任务队列中的 setTimeout 会被执行,打印 2

所以输出顺序是:1 4 3 2


es-lint

ESLint是一个用于 JavaScript(及其超集如 TypeScript)代码静态分析的工具,主要用于发现和修复代码中的问题,包括语法错误、风格问题以及潜在的 bug。

规则

ESLint 的规则大致可以分为以下几类:

  1. Possible Errors(可能的错误)

防止常见的 JavaScript 错误。

  • no-console:禁止使用 console.log
  • no-debugger:禁止使用 debugger
  • no-dupe-args:函数参数不能重名
  • no-extra-semi:禁止多余的分号
  • no-unsafe-finally:禁止在 finally 中使用控制流语句
  1. Best Practices

推荐的编程实践和模式。

  • eqeqeq:强制使用 ===!==
  • curly:强制所有控制语句使用大括号
  • no-eval:禁止使用 eval()
  • no-alert:禁止使用 alert, confirm, prompt
  1. 严格模式

关于使用 JavaScript 严格模式的规则。

  • strict:控制是否需要使用 "use strict"
  1. Variables

关于变量声明和使用的规则。

  • no-undef:禁止使用未声明的变量
  • no-unused-vars:禁止定义了但未使用的变量
  • no-use-before-define:禁止变量在定义之前使用
  1. Stylistic Issues(代码风格)

代码书写风格相关的规则。

  • indent:统一缩进风格(如 2 或 4 空格)
  • quotes:使用单引号或双引号
  • semi:强制使用分号或禁止使用分号
  • camelcase:变量命名是否强制使用驼峰命名法
  • space-before-function-paren:函数名和括号之间是否加空格
  1. ECMAScript 6(ES6+)

有关 ES6(及之后)语法的规则。

  • prefer-const:优先使用 const 而不是 let,当变量不会被重新赋值时
  • no-var:禁止使用 var
  • arrow-spacing:箭头函数中箭头前后是否要有空格
  • no-duplicate-imports:禁止重复导入模块

常见官方规则配置规范

ESLint 提供了一些预设的规则配置(extends):

  1. eslint:recommended

官方推荐的基础规则集合,启用最重要的规则。

module.exports = {
  extends: ['eslint:recommended'],
};
  1. eslint:all

启用 ESLint 所有规则(不推荐,过于严格)。

社区流行的规则集(第三方配置)

e.g. Prettier

用于代码格式化,通常与 ESLint 一起使用,避免风格类规则冲突。

npm install --save-dev eslint-config-prettier
extends: ["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 对象就是按照这个接口来定义的。

接口的用途:

  1. 定义对象类型
  2. 类实现接口
  3. 函数类型约束
  4. 继承扩展其他接口

一些进阶特性:

可选属性:

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;

interfacetype 有什么区别?

特性 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 做反向代理能带来什么好处?

  1. 隐藏真实后端(安全性)
  2. 负载均衡(多个后端,Nginx 帮你平均分配请求)
  3. 缓存静态资源(提升速度)
  4. 统一入口(前后端、多个服务都通过 Nginx 暴露一个统一的接口)

图示:

[ 用户浏览器 ]  --->  [ Nginx ]  --->  [ 后端服务A ]
                                   |
                                   --->  [ 后端服务B ]

权限控制 should be 放在后端

我在简历里写了用前端代码控制用户的权限按钮(因为工作中也确实要求我这么做的),面试官表示很奇怪: 用户可以 f12,改变 js 代码 ->改变权限,甚至还会攻击 api

面试官提出的问题是 非常合理的。你在简历中写「用前端代码控制用户权限按钮」会让人感觉你把权限控制的逻辑放在了前端,这是一个严重的安全问题。

为什么前端控制权限是错误的?

  1. 前端是完全暴露给用户的:

    • 用户可以随时打开 DevTools(F12)修改 HTML、JS。
    • 任何「隐藏按钮」或「禁止点击」的功能都能被轻松绕过。
  2. 攻击风险:

    • 如果用户篡改前端代码后能成功访问接口(例如删库、改配置),说明你的后端权限控制是失效的。
    • 黑客可以模拟任意角色调用 API,造成严重后果。

正确的做法应该是:

层级 功能 示例
✅ 后端 权限验证的核心逻辑 接口校验用户权限:不能访问就直接返回 403 Forbidden
✅ 前端 仅作 UI 展示控制 根据权限信息决定是否展示某个按钮,但不能依赖它来真正控制访问

面试官可能在考察你什么?

  • 你是否理解前后端权限控制的边界
  • 你是否能意识到安全问题,比如 XSS、CSRF、越权访问。
  • 你是否有实际项目经验或只是停留在代码层面。

搞破坏的过程

普通用户(比如网站访问者)如何修改线上生产环境的 JS 代码?
用户可以通过浏览器的 开发者工具(F12)修改运行在自己浏览器里的 JS 代码,但无法改变服务器上的真实文件。也就是说,用户只能修改“本地浏览器中的 JS 行为” ,不能影响其他人,也不能直接修改生产服务器代码。

用户修改 JS 行为的几种方式:

  1. 打开开发者工具(F12)直接修改
  • 进入「控制台(Console)」:

    • 可以重写页面上的函数:

      window.fetch = () => {
        alert('我拦截了请求');
      };
    • 可以执行任意 JS 代码来模拟行为、绕过限制。

  • 修改 HTML、按钮属性、class、JS 执行逻辑等。

  1. 改写 JS 文件(本地缓存)
  • 进入「Sources」面板,找到加载的 .js 文件。
  • 可以在断点处修改变量,或覆盖函数。

这只影响本次会话中的 JS 执行逻辑,刷新页面就会恢复原样。

  1. 使用浏览器插件 / 用户脚本扩展(如 Tampermonkey)
  • 写一个用户脚本,修改页面行为:

    // ==UserScript==
    // @name         修改权限按钮
    // @match        https://example.com/*
    // ==/UserScript==
    
    document.querySelector('#delete-button').style.display = 'block';
  • 这个方法适合持续对某个网站进行“外挂操作”。

  1. 拦截并修改网络请求
  • 使用 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 写一个简单的本地静态文件服务器? httpfs实现读取文件和返回响应
什么是 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 库/工具,比如expresslodashvite
node xxx.js 是“运行程序”

TCP

Q: 建立一条 tcp 连接需要几个步骤?关闭需要几个?
A: 在 TCP(传输控制协议)中,建立连接和关闭连接分别是通过著名的三次握手(Three-Way Handshake)和 四次挥手(Four-Way Handshake)完成的。

一、建立 TCP 连接:3 个步骤(”三次握手”)

  1. 客户端 → 服务端:发送 SYN(同步)报文,表示请求建立连接

    • 报文标志位:SYN = 1
    • 初始序号:seq = x
  2. 服务端 → 客户端:收到 SYN 后,回应一个 SYN + ACK 报文,表示同意建立连接

    • 报文标志位:SYN = 1ACK = 1
    • 回复确认号:ack = x + 1
    • 自己的初始序号:seq = y
  3. 客户端 → 服务端:收到 ACK+SYN 后,发送一个 ACK 报文表示确认。

    • 报文标志位:ACK = 1
    • 确认号:ack = y + 1

👉 连接建立完成,双方可以开始传输数据。

二、关闭 TCP 连接:4 个步骤(”四次挥手”)

  1. 客户端 → 服务端:发送 FIN 报文,表示主动关闭连接。

    • 报文标志位:FIN = 1
    • 此时客户端进入 FIN_WAIT_1 状态
  2. 服务端 → 客户端:收到 FIN 后,发送 ACK 报文,表示“收到关闭请求”

    • 报文标志位:ACK = 1
    • 客户端此时进入 FIN_WAIT_2,等待服务端关闭
  3. 服务端 → 客户端:准备关闭后,也发送 FIN 报文,表示可以关闭连接了

    • 报文标志位:FIN = 1
    • 服务端进入 LAST_ACK 状态
  4. 客户端 → 服务端:收到 FIN 后发送 ACK 确认,连接完全关闭

    • 报文标志位:ACK = 1
    • 客户端进入 TIME_WAIT 状态,等待一段时间后关闭

👉 连接关闭完成,服务端进入 CLOSED 状态。

总结

操作 步骤数 描述
建立连接 3 次 三次握手(SYN、SYN+ACK、ACK)
关闭连接 4 次 四次挥手(FIN、ACK、FIN、ACK)

🍪 一、什么是 Cookie?

打个比方:你去一家奶茶店,第一次买奶茶的时候,店员给你发了一个“会员卡”(上面写着你的名字和喜好),你随身带着这张卡。以后每次来这家店,你都把这张卡递给店员,店员一看就知道你是谁、你爱喝什么、要不要加糖。

在网页里的意思:

  • Cookie 就像这个“会员卡”,是浏览器保存的一小段数据(文本)。
  • 它由服务器设置或前端设置,保存在你自己的浏览器里
  • 以后你每次访问这个网站时,浏览器都会自动把这个 Cookie 发给服务器。

Cookie 里通常存:

  • 登录状态(比如登录成功后发个 token)
  • 用户偏好(比如黑暗模式、语言)
  • 简单的追踪信息(比如你访问了哪些页面)

🧾 二、什么是 Session?

打个比方:你去了银行办业务,前台给你发了一个“号码牌”,同时在系统里记录了你今天的办理信息。这个“号码牌”在你今天逛银行时一直有效,但你一走出银行,或者时间久了没操作,这个号码牌就作废了。

在网页里的意思:

  • Session 是服务器端的“你是谁”的记录,存在服务器内存或数据库里
  • 一般你登录后,服务器会创建一个 session,给你一个 session id(就像银行的“号码牌”),这个 id 会存到你的 Cookie 里。
  • 你之后访问网站时,浏览器会带着这个 id,服务器通过它知道“啊,这个用户是你”。

对比

特性 Cookie Session
存储位置 浏览器(客户端) 服务器(后端)
大小限制 每个 Cookie 最大 4KB,总量也有限 没啥硬性限制(取决于服务器)
安全性 比较低,容易被盗取 安全性高一点(因为数据不在浏览器)
持久性 可以设置过期时间,支持长期保存 通常短暂保存(关闭浏览器或超时就失效)
典型用途 保存登录状态、用户偏好、广告追踪等 服务器识别用户、登录验证等

e.g. 你登录一个网站,登录后:

  1. 服务器生成一个 session,里面记录了你是“张三”;
  2. 然后把 session id 存到你的 Cookie 里;
  3. 下一次你访问网页,浏览器带上这个 session id;
  4. 服务器一看这个 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 请求的过程:

  1. 前端请求触发
    比如,前端发送了一个 POST 请求,且请求头中包含了非常规的头部(如 Content-Type: application/json),这时浏览器会认为这个请求属于 复杂请求,必须先通过 预检请求 来确认。

  2. 发送 OPTIONS 请求
    浏览器会自动发送一个 OPTIONS 请求到目标服务器,询问是否允许跨域操作。这个请求一般不会带有请求体,主要内容是请求头,表示它是一个预检请求。

  3. 后端响应 OPTIONS 请求
    服务器收到 OPTIONS 请求后,会根据跨域资源共享(CORS)策略做出响应。比如,返回:

    • Access-Control-Allow-Origin: 指定允许跨域的来源(* 或者具体域名)
    • Access-Control-Allow-Methods: 允许的方法(如 GET, POST, PUT 等)
    • Access-Control-Allow-Headers: 允许的请求头(比如 Content-TypeAuthorization 等)
  4. 浏览器决定是否发送实际请求
    如果 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 请求。例如:

    • 请求方法是 GETPOST,且请求头是标准的(比如 Content-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/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

查找过程如下:

  1. 查找 cat.sayHi
  2. cat 对象本身没有 sayHi 属性
  3. cat.__proto__(即 Animal.prototype)找到了 sayHi
  4. 于是调用成功

四、常见面试题与应答技巧

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); // true

3. 如何实现一个原型继承?

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 就是新对象的原型,实现了属性和方法的共享。


文章作者: Citrus
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Citrus
  目录