对象
用键值对把数据组织得明明白白
🎯 学习目标
学完本章,你将能够:
- 能够创建、读取、更新、删除对象的属性和方法
- 能够使用对象解构和简写语法简化代码
- 能够使用 Object.keys/values/entries 遍历对象,以及 Object.assign 合并对象
- 能够解释引用类型的内存模型,区分浅拷贝与深拷贝
- 能够识别并避免常见的引用类型陷阱(如 const 对象的修改)
📌 有编程基础?JS 对象 ≈ Python 字典 / Java 的 Map,键值对你都懂。本章最关键的概念是引用类型:对象赋值是"传引用"不是"传值",const obj 锁不住内部属性——这是 JS 最常见的 bug 来源之一。其余语法可略读。
你有没有遇到过
一个人有名字、年龄、地址、电话……存了七八个变量,传参时漏了一个?
比如你正在写一个用户管理功能。每个用户有名字、年龄、邮箱、城市。你用四个变量分别存储。
一个用户还好。十个用户呢?四十个变量?传给函数时要排四个参数?漏一个就全乱。
用对象把这些信息打包在一起,一个变量搞定一个人。传参时只传一个对象,清晰又安全。
什么是对象
定义 → 白话 → 代码
定义:对象是属性的集合,每个属性由键(字符串/Symbol)和值组成。
白话:对象就像一张名片。名片上有姓名、电话、邮箱——每个信息前面有个标签(键),后面是对应的内容(值)。你通过标签找到对应的信息。
这个比喻只是为了帮你建立直观感受。在计算机内部,对象实际上是一块内存区域,引擎用哈希表或隐藏类来存储键值映射关系,通过键计算哈希值来快速定位值。
let person = {
name: "张三",
age: 25,
city: "北京"
};
console.log(person.name); // "张三"
console.log(person["age"]); // 25
增删改查
对象的四种基本操作
let user = { name: "张三" };
// 增
user.age = 25;
user["email"] = "zhang@example.com";
// 改
user.name = "李四";
// 查
console.log("name" in user); // true
console.log(user.unknown); // undefined
// 删
delete user.email;
console.log(user); // { name: "李四", age: 25 }
两种访问方式:点表示法 obj.key 和方括号 obj["key"]。键名包含空格或特殊字符时,只能用方括号。
动态属性名
用变量作为对象的键
// 用变量访问属性
let key = "name";
let person = { name: "张三", age: 25 };
console.log(person[key]); // "张三"
// ES6 计算属性名
let propName = "color";
let obj = {
[propName]: "red",
[`${propName}Code`]: "#FF0000"
};
console.log(obj.color); // "red"
console.log(obj.colorCode); // "#FF0000"
简写与解构
ES6 让对象的创建和提取更简洁
// 属性简写:变量名和键名相同时
let name = "张三";
let age = 25;
let person = { name, age };
// 方法简写
let calc = {
add(a, b) { return a + b; },
sub(a, b) { return a - b; }
};
// 解构赋值:从对象中提取值
let { name: n, age: a } = person;
console.log(n, a); // "张三" 25
// 重命名 + 默认值
let { name: userName, gender = "未知" } = person;
console.log(gender); // "未知"
检查点练习
练习一
任务:创建一个对象 book,包含 title、author、year 三个属性。然后用解构提取 title 和 author,给 year 设默认值为 2024。
// 你的代码:
参考答案
let book = {
title: "JavaScript入门",
author: "张三",
year: 2025
};
let { title, author, year = 2024 } = book;
console.log(title, author, year);
// "JavaScript入门" "张三" 2025(有值就不用默认值)
Part 2: 遍历与合并
对象不像数组那样有索引,怎么遍历它的所有属性?
对象的增删改查你都会了,但如果对象有 20 个属性,总不能一个一个手写吧?遍历对象需要特殊的方法——Object.keys、Object.values、Object.entries。两个对象要合成一个怎么办?展开运算符 ... 一行搞定。这一节都是高频操作,后面写项目天天用。
遍历对象
三种方式拿到键、值或键值对
let user = { name: "张三", age: 25, city: "北京" };
// for...in(包括原型链上的属性)
for (let key in user) {
if (user.hasOwnProperty(key)) {
console.log(key, user[key]);
}
}
// Object.keys() → 键数组
console.log(Object.keys(user));
// ["name", "age", "city"]
// Object.values() → 值数组
console.log(Object.values(user));
// ["张三", 25, "北京"]
// Object.entries() → 键值对数组
for (let [k, v] of Object.entries(user)) {
console.log(`${k}: ${v}`);
}
推荐用 Object.keys/values/entries + for...of,比 for...in 更安全,不会遍历到原型链上的属性。
合并与冻结
拼对象、锁对象
// 合并对象
let a = { x: 1 };
let b = { y: 2, z: 3 };
// 方式1:展开运算符(推荐)
let merged = { ...a, ...b, extra: 4 };
// 方式2:Object.assign
Object.assign(a, b);
// 冻结对象:不可增删改
let frozen = Object.freeze({ x: 1 });
frozen.x = 2; // 静默失败(严格模式报错)
// 密封对象:可改,不可增删
let sealed = Object.seal({ x: 1 });
sealed.x = 2; // 允许
sealed.y = 3; // 不允许
Part 3: 引用与拷贝
对象是引用类型,复制对象不是复制内容而是复制"地址"
前面的代码里你可能已经踩过坑了:改了 obj2 的属性,obj1 也跟着变了。这不是 bug,而是 JS 引用类型的正常行为。赋值只是复制了地址,两个变量指向同一份数据。要想真正独立,得用浅拷贝或深拷贝。这一节把引用机制讲透,再教你用 structuredClone 做真正的深拷贝。
Object 方法速查
处理对象的好帮手,一眼看全
let user = { name: "张三", age: 25, city: "北京" };
// 获取键、值、键值对——最常用的三个
Object.keys(user); // ["name", "age", "city"]
Object.values(user); // ["张三", 25, "北京"]
Object.entries(user); // [["name","张三"], ["age",25], ...]
// 判断属性存在(两种方式有区别)
user.hasOwnProperty("name"); // true(只看自身属性)
"name" in user; // true(会查原型链)
// 合并——注意目标对象会被修改
let target = { a: 1 };
Object.assign(target, { b: 2, a: 99 });
console.log(target); // { a: 99, b: 2 }
// 安全比较——比 === 更精确
Object.is(NaN, NaN); // true(NaN === NaN 是 false)
Object.is(0, -0); // false(0 === -0 是 true)
记住这个口诀:keys 拿键、values 拿值、entries 拿键值对。配合 for...of 遍历,比 for...in 更安全。
内存模型
基本类型存值,引用类型存地址

图:基本类型与引用类型的内存布局对比
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左右两列对比布局,左侧标题"基本类型(栈内存)",右侧标题"引用类型(栈 + 堆内存)" - 左侧(基本类型):画一个简洁的栈内存表格,3 行: - 变量名 `age` → 值 `25`(标注"直接存值") - 变量名 `name` → 值 `"张三"`(标注"直接存值") - 变量名 `active` → 值 `true`(标注"直接存值") - 下方用醒目文字标注:"赋值 = 复制值,互不影响" - 用静态对比示意:左侧展示 `let b = a` 赋值前后的栈内存状态(a=25, b=25),右侧标注"b 改变不影响 a" - 右侧(引用类型):分为上下两部分: - 上部分是"栈内存"表格,2 行: - 变量名 `obj1` → 值 `0x1A2B`(标注"存的是地址") - 变量名 `obj2` → 值 `0x1A2B`(标注"同一个地址!",用红色高亮框标注) - 下部分是"堆内存"区域,画一个方块,地址标注 `0x1A2B`,内容 `{ name: "张三" }` - 栈中的两个地址用实线箭头指向堆中的同一个方块 - 下方用醒目文字标注:"赋值 = 复制地址,指向同一个对象" - 中间用一个大大的 VS 分隔,上方标题"值 vs 引用" - 在右侧底部画一个小的对比示意:`obj2.name = "李四"` 后,箭头指向的对象内容变为 `{ name: "李四" }`,旁边用红色感叹号标注"obj1.name 也变了!" - 配色建议:浅灰背景,栈内存用浅蓝色,堆内存用浅绿色,地址/指针箭头用橙色,关键警示用红色 - 画面比例:4:3(内容较多,需要纵向空间) - 避免:过度装饰、3D 效果、文字过小 - 标注文字用中文,变量名和地址用等宽字体
引用类型陷阱
改了副本,原件也跟着变了?
let obj1 = { name: "张三" };
let obj2 = obj1; // 不是复制,是"指向同一个对象"
obj2.name = "李四";
console.log(obj1.name); // "李四" — 意外!
用白话说:obj2 = obj1 就像给了别人你家门的钥匙。他进屋把沙发换了,你回家发现沙发也变了——因为你们进的是同一间屋子。
这个比喻只是为了帮你建立直观感受。在计算机内部,变量存储的是内存地址(引用),赋值操作复制的是地址,不是数据本身。
浅拷贝与深拷贝
真正复制对象的三种方式
let original = { a: 1, b: { c: 2 } };
// 浅拷贝:只复制第一层
let shallow = { ...original };
shallow.b.c = 999;
console.log(original.b.c); // 999 — 嵌套对象还是引用!
// 深拷贝方式1:JSON(不支持函数/循环引用)
let deep1 = JSON.parse(JSON.stringify(original));
// 深拷贝方式2:structuredClone(推荐)
let deep2 = structuredClone(original);
deep2.b.c = 999;
console.log(original.b.c); // 2 — 不受影响
原因:用了浅拷贝(展开运算符或
Object.assign),嵌套对象仍然是共享引用。
修复:用
structuredClone() 做深拷贝,或者手动递归复制。
const 不可变吗
const 锁的是引用,不是内容
const user = { name: "张三" };
user.name = "李四"; // 允许!改属性
user.age = 25; // 允许!加属性
delete user.age; // 允许!删属性
// user = {}; // TypeError!不能换引用
// 真正不可变要冻结
const frozen = Object.freeze({ name: "张三" });
frozen.name = "李四"; // 无效
console.log(frozen.name); // "张三"
检查点练习
练习二
任务:创建一个对象 config,包含 host 和嵌套的 db(含 port 属性)。然后创建它的深拷贝 configCopy,修改 configCopy.db.port,确认 config.db.port 没有变化。
// 你的代码:
参考答案
let config = {
host: "localhost",
db: { port: 3306 }
};
let configCopy = structuredClone(config);
configCopy.db.port = 5432;
console.log(config.db.port); // 3306
console.log(configCopy.db.port); // 5432
综合练习
练习三:用户管理器
任务:用闭包创建一个用户管理器。它能添加用户、按名字查找、统计总数。
const mgr = createUserManager();
mgr.add({ name: "张三", age: 25 });
mgr.add({ name: "李四", age: 30 });
console.log(mgr.find("张三")); // { name: "张三", age: 25 }
console.log(mgr.count()); // 2
提示:用闭包保存一个 users 数组,返回的对象包含 add、find、count 方法。
参考答案
function createUserManager() {
let users = [];
return {
add: (user) => users.push(user),
find: (name) => users.find(u => u.name === name),
count: () => users.length
};
}
常见错误诊断
Cannot read property 'xxx' of undefined
原因:访问了嵌套属性,但中间某个对象是
undefined。比如 user.address.city,但 address 不存在。
修复:用可选链
user.address?.city,或者访问前先判断。
原因:两个变量指向同一个对象(引用赋值),不是独立的副本。
修复:需要独立副本时,用
structuredClone() 或展开运算符(浅拷贝场景)。
=== 比较两个内容相同的对象,结果是 false
原因:
=== 比较的是引用(内存地址),不是内容。两个新对象地址不同。
修复:用
JSON.stringify(a) === JSON.stringify(b) 做内容比较,或逐个属性对比。
📝 要点总结
本章核心知识点
CRUD 操作
增删改查对象属性的基本方法
解构与简写
ES6 语法让对象操作更简洁
遍历方法
keys、values、entries 三种遍历方式
深浅拷贝
浅拷贝只复制一层,深拷贝递归复制所有层级
引用陷阱
const 只绑定地址,不冻结内容
下一章
对象学会了用键值对组织数据。下一章学数组——用有序列表管理一组数据,搭配对象威力翻倍。