表单序列化
在 JavaScript 中,表单数据的序列化(Form Serialization),将 HTML 表单中的数据转换为可传输的格式(如 URL 查询字符串、JSON 等),是 Web 开发中非常常见且重要的操作。
下面以清晰结构说明:
✅ 一、什么是“表单序列化”?
表单序列化(Form Serialization):将 HTML 表单中所有成功控件(successful controls)的
name=value对,转换为一种便于发送到服务器的格式(如查询字符串或 JSON 对象)。
成功控件包括:
- 有
name属性的<input>、<select>、<textarea> - 未被禁用(
disabled)的控件 - 被选中的复选框/单选按钮
- 文件上传控件(需特殊处理)
✅ 二、JavaScript 中常见的表单序列化方式(表格对比)
| 方法 | 代码示例 | 输出格式 | 特点 | 兼容性 |
|---|---|---|---|---|
1. FormData 对象(推荐) |
| FormData 实例(键值对,支持文件) | - 自动处理所有表单控件 - 支持文件上传 - 可直接用于 fetch 或 XMLHttpRequest- 值为 File 或 string | ✅ 现代浏览器(IE10+) |
| 2. 序列化为 URL 查询字符串 |
| application/x-www-form-urlencoded 字符串 | - 适合 AJAX 发送或拼接到 URL - 不支持文件 | ✅ 现代浏览器 |
| 3. 手动转为普通对象(Object) |
| { name: "Alice", age: "30" } | - 方便转 JSON - 多个同名字段会被覆盖(如复选框) | ✅ 需 Object.fromEntries()(ES2019,现代浏览器) |
4. 使用 jQuery .serialize()(传统) |
| 查询字符串 | - 老项目常用 - 不支持文件 | ⚠️ 依赖 jQuery |
5. 手动遍历 elements |
| 自定义对象 | - 灵活但易出错 - 需处理复选框、多选等特殊情况 | ✅ 全浏览器 |
✅ 三、详细说明与示例
1. 使用 FormData(最强大)
<form id="myForm">
<input name="username" value="alice">
<input name="hobby" type="checkbox" value="reading" checked>
<input name="hobby" type="checkbox" value="coding" checked>
<input name="avatar" type="file">
</form>
const form = document.getElementById('myForm');
const fd = new FormData(form);
// 获取所有值(注意:同名字段会保留多个)
for (let [key, value] of fd.entries()) {
console.log(key, value);
// username alice
// hobby reading
// hobby coding
// avatar [object File](如果选择了文件)
}
// 直接用于 fetch
fetch('/api', { method: 'POST', body: fd });
✅
FormData自动处理:
- 复选框/多选下拉(保留多个值)
- 文件上传
- 编码(UTF-8)
2. 转为查询字符串(用于 GET 请求或日志)
const queryString = new URLSearchParams(new FormData(form)).toString();
// 结果: "username=alice&hobby=reading&hobby=coding"
⚠️ 注意:文件无法转为字符串,会被忽略或报错。
3. 转为普通 JavaScript 对象(适合 JSON API)
// ⚠️ 警告:同名字段会被覆盖!
const obj = Object.fromEntries(new FormData(form));
// { username: "alice", hobby: "coding" } ← "reading" 丢失!
// 安全方式:手动聚合数组
const safeObj = {};
for (let [key, value] of new FormData(form)) {
if (safeObj[key]) {
safeObj[key] = Array.isArray(safeObj[key]) ? [...safeObj[key], value] : [safeObj[key], value];
} else {
safeObj[key] = value;
}
}
✅ 四、常见误区
| 误区 | 正确理解 |
|---|---|
“FormData 可以直接 JSON.stringify()” | ❌ FormData 不是普通对象,需先转为 entries |
“表单序列化会包含 disabled 字段” | ❌ disabled 控件不会被包含 |
“<input type="button"> 会被序列化” | ❌ 只有序列化“成功控件”(submit/reset/button 不包含) |
✅ 五、总结:如何选择?
| 场景 | 推荐方式 |
|---|---|
| 提交完整表单(含文件) | new FormData(form) + fetch |
| 发送纯数据到 REST API(JSON) | Object.fromEntries(FormData) + 处理多值 |
| 构造 URL 查询参数(GET) | URLSearchParams(new FormData(form)) |
| 老项目兼容 jQuery | $(form).serialize() |
表单状态持久化到 localStorage
Qwen3-Max
将表单状态持久化到 localStorage 是一种常见的前端优化技术,用于在用户刷新页面、意外关闭或返回时自动恢复已填写的内容,提升用户体验(例如注册/问卷中途离开后回来仍保留输入)。
下面以清晰结构说明实现方法、注意事项和最佳实践。
✅ 一、核心思路
- 监听表单变化(如
input、change事件) - 将当前表单数据序列化(转为 JSON 字符串)
- 存入
localStorage - 页面加载时读取并回填表单
✅ 二、完整实现示例(原生 JavaScript)
<form id="myForm">
<input name="username" placeholder="用户名">
<input name="email" type="email" placeholder="邮箱">
<textarea name="bio" placeholder="个人简介"></textarea>
<label><input type="checkbox" name="subscribe" value="1"> 订阅 newsletter</label>
<button type="reset">清空</button>
</form>
<script>
const form = document.getElementById('myForm');
const STORAGE_KEY = 'form-state-myForm';
// 1. 页面加载时恢复表单
function restoreForm() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const data = JSON.parse(saved);
Object.entries(data).forEach(([name, value]) => {
const field = form.elements[name];
if (field) {
if (field.type === 'checkbox') {
field.checked = value === 'true'; // 注意:localStorage 存的是字符串
} else if (field.type === 'radio') {
if (field.value === value) field.checked = true;
} else {
field.value = value;
}
}
});
}
}
// 2. 保存表单状态
function saveForm() {
const formData = new FormData(form);
const data = {};
for (let [name, value] of formData.entries()) {
// 复选框/单选按钮:存储 checked 状态而非值
const field = form.elements[name];
if (field.type === 'checkbox' || field.type === 'radio') {
data[name] = field.checked.toString();
} else {
data[name] = value;
}
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
// 3. 绑定事件
form.addEventListener('input', saveForm);
form.addEventListener('change', saveForm); // 覆盖 select、checkbox 等
window.addEventListener('pageshow', restoreForm); // 包括从缓存返回(如后退按钮)
</script>
✅ 优点:支持文本、多行文本、复选框、单选按钮、下拉框等常见控件。
✅ 三、关键细节处理
| 问题 | 解决方案 |
|---|---|
| 复选框/单选按钮状态丢失 | 不要直接用 FormData 的值,而应存储 .checked 布尔值(转为字符串) |
| 多个同名字段(如 hobby[]) | 需特殊处理:存储为数组 ["reading", "coding"] |
| 敏感信息(密码、银行卡) | ❌ 不要保存!可通过 data-persist="false" 标记跳过 |
| localStorage 容量限制 | ≤5MB(不同浏览器),避免存大文件或 Base64 图片 |
| 隐私模式/无痕浏览 | localStorage 可能不可用,需 try-catch |
🔒 跳过敏感字段示例:
// 保存时过滤
for (let el of form.elements) {
if (el.name && !el.dataset.persist === 'false') {
// 保存逻辑
}
}
<input name="password" type="password" data-persist="false">
✅ 四、高级优化建议
| 优化点 | 说明 |
|---|---|
| 防抖保存 | 避免频繁写入(如每 500ms 保存一次) |
| 设置过期时间 | 存储时加时间戳,超过 7 天自动清除 |
| 按用户隔离 | 若有登录态,用 userId + formId 作为 key |
使用 sessionStorage | 仅需会话级持久化(关闭标签页即清除) |
结合 beforeunload | 提醒用户“离开将丢失未保存内容” |
示例:带过期机制
const TTL = 7 * 24 * 60 * 60 * 1000; // 7天
function saveForm() {
const payload = {
data: {/* 表单数据 */},
timestamp: Date.now()
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
}
function restoreForm() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const { data, timestamp } = JSON.parse(saved);
if (Date.now() - timestamp < TTL) {
// 回填逻辑
} else {
localStorage.removeItem(STORAGE_KEY); // 过期清理
}
}
}
✅ 五、为什么不直接用 FormData + JSON.stringify?
FormData无法区分未选中的复选框(根本不会出现在 entries 中)- 多个同名字段会被覆盖(如多个 hobby 复选框)
- 无法存储布尔状态(checked/unchecked)
✅ 正确做法:遍历
form.elements,按控件类型分别处理
✅ 六、替代方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
localStorage | 持久化、简单 | 容量小、同步阻塞、不安全(明文) |
sessionStorage | 自动清理 | 关闭标签页即丢失 |
| IndexedDB | 容量大、异步 | API 复杂 |
| Cookie | 可随请求发送 | 容量极小(4KB)、影响性能 |
| 服务端保存草稿 | 安全、跨设备 | 需后端支持 |
💡 对于简单表单,
localStorage是最轻量高效的选择。
✅ 七、总结:最佳实践清单
- ✅ 只保存非敏感字段
- ✅ 正确处理 checkbox/radio/select
- ✅ 使用
pageshow而非load事件恢复(兼容后退缓存) - ✅ 添加过期机制避免陈旧数据
- ✅ 提供“清空草稿”按钮(调用
localStorage.removeItem) - ❌ 不要保存密码、身份证、支付信息等
如何处理表单中的复杂数据结构?
Qwen3-Max
在 Web 开发中,表单中的“复杂数据结构” 通常指以下情况:
- 多层级嵌套字段(如
user.profile.name) - 动态字段(如可增删的地址列表、商品项)
- 数组型输入(如多个邮箱、标签)
- 带关联关系的数据(如订单 + 商品明细)
- 非字符串值(如日期对象、布尔值、数字)
由于 HTML 表单原生只支持 name=value 的扁平键值对(且 value 总是字符串),要处理复杂结构,需通过 命名约定 + 序列化/反序列化逻辑 来实现。
✅ 一、核心策略:使用命名规范编码结构
1. 点号表示法(Dot Notation)
<input name="user.name" value="Alice">
<input name="user.age" value="30">
<input name="user.hobbies[0]" value="reading">
<input name="user.hobbies[1]" value="coding">
2. 方括号表示法(PHP 风格)
<input name="user[name]" value="Alice">
<input name="user[age]" value="30">
<input name="user[hobbies][]" value="reading">
<input name="user[hobbies][]" value="coding">
💡 浏览器本身不解析这些名称——解析逻辑需由 JavaScript 或后端实现。
✅ 二、JavaScript 解析复杂表单数据(前端序列化)
方法:遍历 form.elements,按命名规则重建对象
function parseFormData(form) {
const data = {};
// 辅助函数:将扁平 key(如 "user.name")转为嵌套路径
function setNestedValue(obj, key, value) {
const keys = key.replace(/\[(\d+)\]/g, '.$1').split('.'); // 将 [0] 转为 .0
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!current[k]) {
// 判断下一个是否为数字 → 应该是数组
const next = keys[i + 1];
current[k] = /^\d+$/.test(next) ? [] : {};
}
current = current[k];
}
const lastKey = keys[keys.length - 1];
if (Array.isArray(current)) {
current.push(value);
} else {
current[lastKey] = value;
}
}
// 收集所有成功控件
for (const field of form.elements) {
if (!field.name || field.disabled) continue;
let value;
if (field.type === 'checkbox' || field.type === 'radio') {
if (!field.checked) continue;
value = field.value === 'on' ? true : field.value; // 可自定义
} else if (field.type === 'select-multiple') {
value = Array.from(field.selectedOptions).map(opt => opt.value);
} else {
value = field.value;
}
// 类型转换(可选)
if (typeof value === 'string') {
if (/^\d+$/.test(value)) value = Number(value);
else if (value === 'true') value = true;
else if (value === 'false') value = false;
}
setNestedValue(data, field.name, value);
}
return data;
}
使用示例:
<form id="myForm">
<input name="user.name" value="Alice">
<input name="user.age" value="30">
<input name="user.isActive" value="true">
<input name="user.tags[]" value="dev">
<input name="user.tags[]" value="js">
<input name="addresses[0].city" value="Beijing">
<input name="addresses[1].city" value="Shanghai">
</form>
const form = document.getElementById('myForm');
const result = parseFormData(form);
console.log(result);
/*
{
user: {
name: "Alice",
age: 30,
isActive: true,
tags: ["dev", "js"]
},
addresses: [
{ city: "Beijing" },
{ city: "Shanghai" }
]
}
*/
✅ 三、反向操作:从复杂对象回填表单(反序列化)
function populateForm(form, data, prefix = '') {
for (const [key, value] of Object.entries(data)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === 'object' && !Array.isArray(value)) {
// 递归处理嵌套对象
populateForm(form, value, fullKey);
} else if (Array.isArray(value)) {
// 处理数组:动态生成字段或匹配现有字段
value.forEach((item, index) => {
const arrayKey = `${fullKey}[${index}]`;
if (typeof item === 'object') {
populateForm(form, item, arrayKey);
} else {
setFieldValue(form, arrayKey, item);
}
});
} else {
setFieldValue(form, fullKey, value);
}
}
}
function setFieldValue(form, name, value) {
const field = form.elements.namedItem(name);
if (!field) return;
if (field.type === 'checkbox' || field.type === 'radio') {
field.checked = value === true || value === field.value;
} else if (field.tagName === 'SELECT' && field.multiple) {
// 多选下拉
for (const option of field.options) {
option.selected = value.includes(option.value);
}
} else {
field.value = String(value);
}
}
⚠️ 注意:动态表单(如可增删行)需先确保 DOM 结构存在,或结合模板引擎生成。
✅ 四、常见复杂场景处理方案
| 场景 | 推荐方案 |
|---|---|
| 动态增删字段(如“添加更多地址”) | - 用模板(<template>)克隆- 为每个新字段分配唯一 name(如 addresses[2].city) |
| 文件上传 + 元数据 | - 文件单独用 FormData- 元数据用 JSON 字段存储(如 files[0].description) |
| 富文本编辑器 | - 监听编辑器 change 事件 - 将内容同步到隐藏 <input type="hidden"> |
| 日期/时间选择器 | - 存储 ISO 字符串(如 "2025-12-01")- 回填时转为 Date 对象或格式化字符串 |
| 关联选择(级联下拉) | - 用 data-* 属性存储层级关系- 不依赖 name 嵌套,而是用独立字段 |
✅ 五、与后端协同建议
-
统一命名规范
前后端约定使用user[name]或user.name,避免解析不一致。 -
后端自动解析
- Express.js:可用
body-parser+qs库解析user[name]=Alice - PHP:原生支持
user[name]格式 - Python(Django/Flask):需手动解析或使用库(如
webargs)
- Express.js:可用
-
API 优先方案
对于 SPA,可直接提交 JSON:fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(parseFormData(form)) });
✅ 六、现成工具推荐
| 工具 | 用途 |
|---|---|
| FormData | 原生支持文件 + 键值对,但仍是扁平结构 |
| qs | 强大的查询字符串解析/序列化(支持嵌套) |
| form-serialize | 轻量级表单序列化库 |
| React Hook Form / Vue FormKit | 框架级表单管理,内置复杂数据支持 |
✅ 总结
HTML 表单是扁平的,但你的数据可以是复杂的——关键在于:
- 设计合理的
name命名规则- 编写健壮的序列化/反序列化逻辑
- 前后端保持解析一致性
Object.fromEntries()方法详解
Qwen3-Max
Object.fromEntries() 是 ECMAScript 2019(ES10)引入的一个静态方法,用于将可迭代的键值对集合(如 Map、Array 等)转换为普通对象(Plain Object)。它是 Object.entries() 的逆操作。
✅ 一、基本语法
Object.fromEntries(iterable)
- 参数:
iterable
一个可迭代对象,其每个元素必须是一个包含两个元素的数组(即[key, value]形式)。 - 返回值:一个新的普通对象,其属性由
iterable中的键值对定义。
✅ 二、核心用途与示例
1. 将 Map 转换为普通对象
const map = new Map([
['name', 'Alice'],
['age', 30],
['city', 'Beijing']
]);
const obj = Object.fromEntries(map);
console.log(obj);
// { name: "Alice", age: 30, city: "Beijing" }
💡 这是最常见用法:当你从
fetch、FormData或其他 API 得到Map时,快速转为对象以便访问属性(如obj.name)。
2. 将键值对数组转为对象
const entries = [
['a', 1],
['b', 2],
['c', 3]
];
const obj = Object.fromEntries(entries);
console.log(obj); // { a: 1, b: 2, c: 3 }
3. 与 Object.entries() 配合:过滤/转换对象属性
const original = { a: 1, b: 2, c: 3, d: 4 };
// 过滤出值大于 2 的属性
const filtered = Object.fromEntries(
Object.entries(original).filter(([key, val]) => val > 2)
);
console.log(filtered); // { c: 3, d: 4 }
// 将所有值转为字符串
const stringified = Object.fromEntries(
Object.entries(original).map(([k, v]) => [k, String(v)])
);
console.log(stringified); // { a: "1", b: "2", c: "3", d: "4" }
✅ 这是函数式编程中修改对象的常用模式(因为对象本身不可变)。
4. 从 FormData 创建对象(常用于表单处理)
const form = document.querySelector('form');
const formData = new FormData(form);
// 转为普通对象(注意:同名字段会被覆盖!)
const obj = Object.fromEntries(formData);
console.log(obj); // { username: "alice", email: "a@example.com" }
⚠️ 警告:如果表单有多个同名字段(如复选框),只有最后一个值会被保留。需手动处理多值:
const obj = {}; for (let [key, value] of formData) { obj[key] = obj[key] ? [...(Array.isArray(obj[key]) ? obj[key] : [obj[key]]), value] : value; }
✅ 三、支持的可迭代对象类型
| 可迭代源 | 示例 | 是否有效 |
|---|---|---|
Map | new Map([['k','v']]) | ✅ |
Array | [['k','v']] | ✅ |
Set | new Set([['k','v']]) | ✅(但 Set 元素必须是 [key, value] 数组) |
String | 'ab' → [['a','b']]? | ❌(每个字符是单个元素,不是 [k,v] 对) |
| 自定义迭代器 | 实现 Symbol.iterator 返回 [k,v] | ✅ |
✅ 四、注意事项与限制
1. 键必须是字符串或 Symbol
- 如果键是其他类型(如数字、对象),会被强制转换为字符串:
Object.fromEntries([[1, 'one'], [true, 'yes']]); // { "1": "one", "true": "yes" }
2. 重复键会被覆盖
Object.fromEntries([['a', 1], ['a', 2]]);
// { a: 2 } ← 后出现的值覆盖前面的
3. 不能处理非 [key, value] 结构
Object.fromEntries([['a'], ['b', 1, 2]]);
// { a: undefined, b: 1 } ← 多余元素被忽略,缺失值为 undefined
4. 不适用于深层嵌套转换
- 它只做一层扁平转换,不会递归处理嵌套结构。
✅ 五、浏览器兼容性
| 浏览器 | 支持版本 |
|---|---|
| Chrome | 73+ |
| Firefox | 63+ |
| Safari | 12.1+ |
| Edge | 79+ |
| Node.js | 12.0+ |
💡 对于旧环境,可使用 polyfill(基于
Array.reduce):
if (!Object.fromEntries) {
Object.fromEntries = function(iterable) {
return Array.from(iterable).reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {});
};
}
✅ 六、对比其他转换方式
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
Object.fromEntries(map) | Map → 对象 | 简洁、标准 | 仅 ES2019+ |
{...map} | Map → 对象 | 语法糖 | 无效!展开运算符对 Map 不生成对象属性 |
Array.reduce | 通用 | 兼容性好 | 代码冗长 |
| 手动循环 | 精细控制 | 灵活 | 易出错 |
❌ 常见错误:
const obj = { ...myMap }; // 得到 { "size": 2 } ❌(不是你想要的!)
✅ 七、典型应用场景总结
| 场景 | 代码片段 |
|---|---|
| 表单数据转对象 | Object.fromEntries(new FormData(form)) |
| 过滤对象属性 | Object.fromEntries(Object.entries(obj).filter(...)) |
| Map 转 JSON | JSON.stringify(Object.fromEntries(map)) |
| URLSearchParams 转对象 | Object.fromEntries(new URLSearchParams(location.search)) |
| 转换计算属性 | Object.fromEntries(keys.map(k => [k, compute(k)])) |
✅ 总结
Object.fromEntries()是现代 JavaScript 中连接“可迭代键值对”与“普通对象”的桥梁。
它简洁、语义清晰,特别适合与Map、FormData、URLSearchParams等 API 协同工作,是函数式对象操作的重要工具。
合理使用它,可以大幅减少手动循环和临时变量,写出更声明式、更易读的代码。
函数式对象是什么意思
Qwen3-Max
“函数式对象”并不是 JavaScript 中一个严格定义的术语,但它通常出现在讨论 函数式编程(Functional Programming)风格操作对象 的上下文中。它的含义可以从两个角度理解:
✅ 一、主流理解:以函数式风格操作/转换的对象
这是最常见的用法——指通过纯函数、不可变性、高阶函数等方式来创建或处理的对象,而不是通过命令式修改。
🌰 例子对比:
| 命令式(非函数式) | 函数式风格 |
|---|---|
|
|
|
|
✅ “函数式对象操作”强调:
- 不可变性(不修改原对象)
- 使用纯函数(如
map、filter、reduce)- 链式/声明式表达
🔧 典型函数式对象操作工具:
// 1. 过滤属性
const filtered = Object.fromEntries(
Object.entries(obj).filter(([k, v]) => v > 0)
);
// 2. 映射值
const doubled = Object.fromEntries(
Object.entries(obj).map(([k, v]) => [k, v * 2])
);
// 3. 合并对象(无副作用)
const merged = { ...obj1, ...obj2 };
💡 这里的 “函数式对象” 指的是 用函数式方法生成或变换的对象,而非对象本身是“函数”。
❌ 二、可能的误解:对象本身就是函数?
在 JavaScript 中,函数也是对象(typeof func === 'function',但 func instanceof Object 为 true),所以 technically 所有函数都是“可调用的对象”。
例如:
function greet() { return "Hi"; }
greet.name = "Alice"; // 函数可以有属性!
console.log(greet()); // "Hi"
console.log(greet.name); // "Alice"
但这通常不叫“函数式对象”,而是称为:
- 可调用对象(Callable Object)
- 带属性的函数
- 函数与对象的混合体
⚠️ 注意:这不是“函数式编程”中的概念,而是 JavaScript 语言特性。
✅ 三、函数式编程中“对象”的正确使用原则
在函数式编程范式中,对对象的处理遵循以下规则:
| 原则 | 说明 |
|---|---|
| 不可变性(Immutability) | 永远不修改原对象,总是返回新对象 |
| 纯函数(Pure Functions) | 对象转换函数无副作用、相同输入必得相同输出 |
| 避免状态共享 | 对象不包含隐藏状态或可变引用 |
| 使用代数数据类型(ADT)思想 | 用普通对象表示结构化数据(类似 record) |
✅ 函数式风格的对象工厂:
// 纯函数创建对象
const createUser = (name, age) => ({ name, age, active: true });
// 纯函数更新对象
const activateUser = (user) => ({ ...user, active: true });
✅ 四、常见误区澄清
| 说法 | 正误 | 说明 |
|---|---|---|
| “函数式对象 = 对象里存了函数” | ❌ | 这只是普通对象的用法(如 { fn: () => {} }) |
| “函数式对象 = 用函数式方法处理的对象” | ✅ | 正确理解 |
| “React 的 state 对象必须是函数式更新” | ✅(部分) | 指使用 setState(prev => ({...})) 避免闭包问题,属于函数式思想应用 |
✅ 五、总结
“函数式对象” ≈ “以函数式编程原则创建或操作的对象”
它的核心是:
- 不可变
- 由纯函数生成或变换
- 避免直接赋值/删除属性
它不是一种特殊对象类型,而是一种编程风格和数据处理方式。
📌 记忆口诀:
“对象不变,函数来变;旧的不去,新的不来。”
如果你看到文档或教程中提到“函数式对象”,大概率是指 用 Object.fromEntries、解构、展开运算符等无副作用方式处理的对象,而不是某种神秘的新数据类型 😊

被折叠的 条评论
为什么被折叠?



