对象

用键值对把数据组织得明明白白

🎯 学习目标

学完本章,你将能够:

  • 能够创建、读取、更新、删除对象的属性和方法
  • 能够使用对象解构和简写语法简化代码
  • 能够使用 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 更安全。

内存模型

基本类型存值,引用类型存地址

图:基本类型与引用类型的内存布局对比

引用类型陷阱

改了副本,原件也跟着变了?

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 只绑定地址,不冻结内容

下一章

对象学会了用键值对组织数据。下一章学数组——用有序列表管理一组数据,搭配对象威力翻倍。

1 / X