1. FormData 和原生 AJAX 有什么区别?
FormData 是数据容器,AJAX 是传输通道,它们是"子弹"和"枪"的关系。
✅ FormData 职责:
- 构建
multipart/form-data
格式数据 - 自动处理文件上传、二进制流
- 支持
append(key, value, filename)
添加字段
js
const fd = new FormData();
fd.append('name', '张三');
fd.append('avatar', fileInput.files[0]); // 文件自动处理
✅ 原生 AJAX(XHR)职责:
- 创建请求:
new XMLHttpRequest()
- 发送数据:
xhr.send(fd)
- 监听状态:
xhr.onreadystatechange
js
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/submit');
xhr.onload = () => console.log(xhr.response);
xhr.send(fd); // 发送 FormData
Q1: 能直接 send({name: '张三'}) 吗?
A1: 不能!必须 JSON.stringify
+ 设置 Content-Type: application/json
,否则后端收不到 ❌
Q2: 为什么上传文件必须用 FormData?
A2: 普通 key=value
编码无法处理二进制流,multipart/form-data
才能分段传输文件 💡
👉继续看?Q3: jQuery 的 serialize() 和 FormData 冲突吗?
A3: 冲突!serialize()
输出 a=1&b=2
,而 FormData 是二进制格式,混用会导致后端解析失败 🤯
2. 表单提交方式 & FormData 的角色
✅ 传统同步提交(刷新页面)
html
<form action="/submit" method="post">
<input name="name" />
<button type="submit">提交</button>
</form>
浏览器自动构建请求,但整页刷新,用户体验差。
✅ 异步提交(无刷新)→ FormData 登场
js
// 阻止默认提交
form.addEventListener('submit', e => {
e.preventDefault();
const fd = new FormData(form); // 直接从 form 构建!
ajaxSubmit(fd);
});
new FormData(form)
可自动提取所有带name
属性的表单控件
Q4: 如何获取 checkbox 多选值?
A4: 多个同名 name="hobby"
的 checkbox,FormData 会自动 append 多次,后端用数组接收 ✅
8. 异步方案演进史(回调 → Promise → async/await)
阶段一:回调地狱
js
ajax('/step1', () => {
ajax('/step2', () => {
ajax('/step3', () => {
console.log('完成');
});
});
});
阶段二:Promise 扁平化
js
ajax('/step1')
.then(() => ajax('/step2'))
.then(() => ajax('/step3'))
.then(() => console.log('完成'));
阶段三:async/await 伪同步
js
async function run() {
await ajax('/step1');
await ajax('/step2');
await ajax('/step3');
console.log('完成');
}
Q5: Promise 如何解决回调地狱?
A5: 通过 .then
链式调用,将嵌套转为线性结构,错误统一捕获 💥
9. Promise 如何实现 .then 处理?
手写简化版 Promise:
js
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = null;
this.callbacks = []; // 存储 then 回调
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
this.callbacks.forEach(cb => cb.onFulfilled(value));
};
executor(resolve, () => {});
}
then(onFulfilled) {
return new MyPromise((resolve) => {
if (this.state === 'pending') {
this.callbacks.push({ onFulfilled: () => {
const result = onFulfilled(this.value);
resolve(result); // 支持链式调用
}});
}
if (this.state === 'fulfilled') {
const result = onFulfilled(this.value);
resolve(result);
}
});
}
}
⚠️这代码能处理异步 resolve 吗?
Q6: 为什么 then 要返回新 Promise?
A6: 实现链式调用,且支持 return
值传递给下一个 then 💡
Q7: 如何实现 catch?
A7: catch(onRejected)
等价于 then(null, onRejected)
✅
13. 如何优化相对路径引用?
问题场景:
js
// a/b/c.js 中引用 util
import utils from '../../utils/index.js'; // 容易出错
✅ 解决方案:
- Webpack/Vite 配置 alias
js
// vite.config.js
export default {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@utils': path.resolve(__dirname, 'src/utils')
}
}
}
js
import utils from '@/utils'; // 清晰稳定
- 使用绝对路径(Node.js)
js
import path from 'path';
import utils from path.resolve(__dirname, '../utils');
- ESLint + Import 插件校验路径
json
"rules": {
"import/no-unresolved": "error"
}
Q8: alias 在生产环境生效吗?
A8: 生效!构建工具会在打包时替换为真实路径,不影响运行时 ✅
14. Node.js 文件查找优先级
当 require('module')
时,Node 按顺序查找:
fs/path等] B -->|否| D{node_modules/.bin?} D -->|是| E[返回命令行工具] D -->|否| F[当前目录node_modules] F --> G[向上递归查找] G --> H{找到模块文件?} H -->|是| I[按扩展名尝试] H -->|否| K[抛出MODULE_NOT_FOUND] I --> J1[.js] I --> J2[.json] I --> J3[.node] I --> J4[.mjs] J2 --> L[自动parse JSON] J3 --> M[返回C++扩展] J1 --> N[执行JS代码] J4 --> O[ES Module处理] style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#b9f,stroke:#333,stroke-width:1px style E fill:#ff9,stroke:#333,stroke-width:1px style J2 fill:#9f9,stroke:#333,stroke-width:1px style J3 fill:#f99,stroke:#333,stroke-width:1px style K fill:#f99,stroke:#333,stroke-width:1px
特殊规则:
require('./utils')
:先找utils.js
,再找utils.json
,最后找utils/index.js
package.json
中的main
字段指定入口
Q9: 如何强制加载 .mjs?
A9: 文件名用 .mjs
后缀,或在 package.json 中设置 "type": "module"
💥
15. npm2 与 npm3+ 的区别(依赖扁平化革命)
npm2:嵌套依赖(树状结构)
kotlin
node_modules/
├── lodash@1.0.0
└── request@2.0.0
└── lodash@0.9.0 // 重复安装!
→ 依赖深度大,磁盘占用高,Maximum call stack size exceeded
风险
npm3+:扁平化依赖(革命性优化)
kotlin
node_modules/
├── lodash@1.0.0 // 共享
├── request@2.0.0
└── underscore@1.8.0
npm 会尝试将所有依赖提升到根目录,只要版本兼容
npm5+ 增强:
- 引入
package-lock.json
→ 锁定依赖树,保证一致性 - 默认使用扁平化 + 符号链接(symlink)处理冲突
Q10: 为什么还需要 yarn/pnpm?
A10: pnpm 使用硬链接 + 内容寻址存储,进一步节省磁盘空间,适合大型 mono-repo 🌟