第5天:JavaScript 对象详解
学习时间:约 1 小时
前置知识:第3天的变量、类型,第4天的函数
目录
- 对象是什么
- 创建对象
- 访问属性
- 增删改属性
- 计算属性名
- 对象遍历
- 对象解构
- 展开运算符(对象展开)
- 嵌套对象与可选链
- [Object 常用静态方法](#Object 常用静态方法)
- 对象是引用类型
- 常用模式:对象与数组互转
- 练习
- 常见陷阱
1. 对象是什么
1.1 最简单的理解
对象 = 一组键值对的集合。
每个键(也叫属性名)对应一个值,就像一个字典:
js
// 一个"用户"对象
const user = {
name: '小明', // 键是 name,值是 '小明'
age: 25, // 键是 age,值是 25
email: 'xm@example.com'
}
你可以把对象想象成一张表格:
| 键 | 值 |
|---|---|
| name | '小明' |
| age | 25 |
| 'xm@example.com' |
1.2 对象和其他语言的对比
java
// Java:必须先定义类,再创建对象
public class User {
private String name;
private int age;
private String email;
// 还要写构造函数、getter、setter...
}
User user = new User("小明", 25, "xm@example.com");
js
// JS:直接创建对象,不需要类
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
| Java | JavaScript |
|---|---|
| 必须先定义类 | 直接创建对象,不需要类 |
| 属性类型固定 | 属性值可以是任意类型 |
| 属性在编译时确定 | 属性可以随时增删 |
用 . 访问属性 |
用 . 或 [] 访问属性 |
1.3 对象可以包含什么
对象的值可以是任何类型------数字、字符串、布尔、数组、函数,甚至另一个对象:
js
const product = {
name: 'iPhone 15', // 字符串
price: 7999, // 数字
inStock: true, // 布尔
tags: ['手机', '苹果'], // 数组
specs: { // 另一个对象
weight: '171g',
color: '黑色'
},
getInfo() { // 函数(方法)
return `${this.name} - ¥${this.price}`
}
}
2. 创建对象
2.1 对象字面量(最常用)
用 {} 直接定义:
js
// 空对象
const obj = {}
// 带属性的对象
const user = {
name: '小明',
age: 25
}
2.2 new Object()
js
const user = new Object()
user.name = '小明'
user.age = 25
几乎不用这种写法------字面量更简洁、更直观。
2.3 什么时候用哪种?
| 方式 | 推荐度 | 场景 |
|---|---|---|
字面量 {} |
⭐⭐⭐ | 99% 的场景 |
new Object() |
⭐ | 几乎不用 |
3. 访问属性
3.1 点语法(最常用)
js
const user = {
name: '小明',
age: 25
}
console.log(user.name) // '小明'
console.log(user.age) // 25
3.2 方括号语法
js
const user = {
name: '小明',
age: 25
}
console.log(user['name']) // '小明'
console.log(user['age']) // 25
什么时候用方括号?
当属性名是变量、包含特殊字符、或者动态拼接时:
js
const user = {
name: '小明',
'user-age': 25 // 属性名包含连字符
}
// ❌ 点语法访问不了连字符属性名
console.log(user.user-age) // NaN(JS 理解为 user.user 减 age)
// ✅ 方括号可以
console.log(user['user-age']) // 25
// 动态属性名
const key = 'name'
console.log(user[key]) // '小明'(key 是变量,值是 'name')
3.3 访问不存在的属性
js
const user = {
name: '小明',
age: 25
}
console.log(user.email) // undefined(不报错,返回 undefined)
console.log(user.address) // undefined
和 Java 的区别:
java
// Java:访问不存在的字段会编译错误
user.email // 编译错误:User 类没有 email 字段
js
// JS:访问不存在的属性返回 undefined
user.email // undefined(运行时不报错)
3.4 检查属性是否存在
js
const user = {
name: '小明',
age: 25
}
// 方式 1:in 操作符
console.log('name' in user) // true
console.log('email' in user) // false
// 方式 2:!== undefined
console.log(user.name !== undefined) // true
console.log(user.email !== undefined) // false
// 方式 3:hasOwnProperty
console.log(user.hasOwnProperty('name')) // true
console.log(user.hasOwnProperty('email')) // false
4. 增删改属性
4.1 添加属性
js
const user = {
name: '小明'
}
// 直接赋值即可添加
user.age = 25
user.email = 'xm@example.com'
console.log(user)
// { name: '小明', age: 25, email: 'xm@example.com' }
4.2 修改属性
js
const user = {
name: '小明',
age: 25
}
// 直接赋值即可修改
user.age = 26
user.name = '大明'
console.log(user)
// { name: '大明', age: 26 }
4.3 删除属性
js
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
// 用 delete 关键字删除属性
delete user.email
console.log(user)
// { name: '小明', age: 25 }(email 被删除了)
4.4 动态属性名(变量作为属性名)
js
const user = {}
const fieldName = 'age'
const fieldValue = 25
// 用方括号语法动态设置属性
user[fieldName] = fieldValue
console.log(user) // { age: 25 }
// 等同于
user['age'] = 25
5. 计算属性名
5.1 ES6 计算属性名
ES6 允许用表达式作为属性名,用 [] 包起来:
js
const prefix = 'user'
// 属性名用表达式计算
const data = {
[prefix + 'Name']: '小明',
[prefix + 'Age']: 25,
[`${prefix}Email`]: 'xm@example.com'
}
console.log(data)
// { userName: '小明', userAge: 25, userEmail: 'xm@example.com' }
5.2 实际场景
js
// 根据条件设置不同的属性名
const field = 'status'
const value = 'active'
const update = {
[field]: value,
[`${field}UpdatedAt`]: new Date().toISOString()
}
console.log(update)
// { status: 'active', statusUpdatedAt: '2026-06-04T...' }
6. 对象遍历
6.1 for...in 循环
遍历对象的所有可枚举属性(包括继承的):
js
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
for (const key in user) {
console.log(`${key}: ${user[key]}`)
}
// name: 小明
// age: 25
// email: xm@example.com
注意:for...in 会遍历原型链上的属性 ,所以遍历对象时一般用 Object.keys() 更安全。
6.2 Object.keys() --- 获取所有键
js
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
const keys = Object.keys(user)
console.log(keys) // ['name', 'age', 'email']
6.3 Object.values() --- 获取所有值
js
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
const values = Object.values(user)
console.log(values) // ['小明', 25, 'xm@example.com']
6.4 Object.entries() --- 获取所有键值对
把对象变成二维数组,每个元素是 [key, value]:
js
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
const entries = Object.entries(user)
console.log(entries)
// [ ['name', '小明'], ['age', 25], ['email', 'xm@example.com'] ]
配合解构遍历:
js
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`)
}
// name: 小明
// age: 25
// email: xm@example.com
6.5 三种方法的对比
| 方法 | 返回值 | 用途 |
|---|---|---|
Object.keys(obj) |
['name', 'age'] |
只需要键 |
Object.values(obj) |
['小明', 25] |
只需要值 |
Object.entries(obj) |
[['name','小明'], ['age',25]] |
键值对都要 |
7. 对象解构
7.1 基础解构
从对象中提取属性,赋值给同名变量:
js
const user = {
name: '小明',
age: 25,
email: 'xm@example.com'
}
// 普通写法
const name = user.name
const age = user.age
const email = user.email
// ✅ 解构写法(一行搞定)
const { name, age, email } = user
console.log(name) // '小明'
console.log(age) // 25
console.log(email) // 'xm@example.com'
7.2 重命名变量
如果不想用属性名作为变量名:
js
const user = {
name: '小明',
age: 25
}
// 把 name 重命名为 userName
const { name: userName, age: userAge } = user
console.log(userName) // '小明'
console.log(userAge) // 25
// ❌ name 变量不存在了
console.log(name) // ReferenceError: name is not defined
7.3 默认值
属性不存在时使用默认值:
js
const user = {
name: '小明',
age: 25
}
// email 不存在,使用默认值
const { name, age, email = '未填写' } = user
console.log(name) // '小明'
console.log(age) // 25
console.log(email) // '未填写'(user 没有 email,用了默认值)
7.4 解构 + 重命名 + 默认值组合
js
const user = {
name: '小明',
age: 25
}
// 提取 name,重命名为 userName,默认值 '匿名'
// 提取 email,重命名为 userEmail,默认值 '无'
const { name: userName = '匿名', email: userEmail = '无' } = user
console.log(userName) // '小明'(有值,不用默认)
console.log(userEmail) // '无'(没有值,用默认)
7.5 嵌套解构
从嵌套对象中提取属性:
js
const response = {
code: 200,
data: {
user: {
name: '小明',
address: {
city: '北京',
street: '朝阳路'
}
}
}
}
// 一层层解构
const { data: { user: { name, address: { city } } } } = response
console.log(name) // '小明'
console.log(city) // '北京'
7.6 函数参数解构
Day 4 已经讲过,这里再回顾一下:
js
// 普通写法
function greet(user) {
console.log(`你好,${user.name}!你${user.age}岁了。`)
}
// ✅ 解构写法
function greet({ name, age }) {
console.log(`你好,${name}!你${age}岁了。`)
}
greet({ name: '小明', age: 25 }) // 你好,小明!你25岁了。
8. 展开运算符(对象展开)
8.1 浅拷贝对象
js
const original = {
name: '小明',
age: 25
}
// 用 ... 创建一个新对象(浅拷贝)
const copy = { ...original }
console.log(copy) // { name: '小明', age: 25 }
console.log(copy === original) // false(是不同的对象)
8.2 合并对象
js
const defaults = {
theme: 'light',
fontSize: 14,
language: 'zh'
}
const userSettings = {
theme: 'dark',
fontSize: 16
}
// 合并:后面的覆盖前面的
const settings = { ...defaults, ...userSettings }
console.log(settings)
// { theme: 'dark', fontSize: 16, language: 'zh' }
// userSettings 的 theme 覆盖了 defaults 的 theme
// userSettings 没有 language,所以保留 defaults 的
8.3 添加/覆盖属性
js
const user = {
name: '小明',
age: 25
}
// 添加新属性
const updated = { ...user, email: 'xm@example.com' }
console.log(updated)
// { name: '小明', age: 25, email: 'xm@example.com' }
// 覆盖已有属性
const older = { ...user, age: 26 }
console.log(older)
// { name: '小明', age: 26 }
8.4 展开的顺序很重要
js
const base = { a: 1, b: 2, c: 3 }
// 后面的覆盖前面的
const result = { ...base, b: 99 }
console.log(result) // { a: 1, b: 99, c: 3 }
// 前面的不会覆盖后面的
const result2 = { b: 99, ...base }
console.log(result2) // { a: 1, b: 2, c: 3 }
9. 嵌套对象与可选链
9.1 嵌套对象
对象的属性值可以是另一个对象:
js
const order = {
id: 1001,
customer: {
name: '小明',
address: {
city: '北京',
street: '朝阳路 123 号'
}
},
items: [
{ name: 'iPhone', price: 7999 },
{ name: 'AirPods', price: 1999 }
]
}
// 访问嵌套属性
console.log(order.customer.name) // '小明'
console.log(order.customer.address.city) // '北京'
console.log(order.items[0].name) // 'iPhone'
9.2 可选链操作符 ?.(ES2020)
当不确定嵌套的某一层是否存在时,用 ?. 安全访问:
js
const user = {
name: '小明'
// 没有 address 属性
}
// ❌ 普通访问会报错
// console.log(user.address.city) // TypeError: Cannot read property 'city' of undefined
// ✅ 可选链:如果 address 是 null 或 undefined,直接返回 undefined
console.log(user.address?.city) // undefined(不报错)
// 也可以用在方法调用
console.log(user.getName?.()) // undefined(方法不存在,不报错)
// 也可以用在数组
console.log(user.hobbies?.[0]) // undefined(属性不存在,不报错)
9.3 可选链的链式使用
js
const response = {
code: 200,
data: {
user: {
name: '小明'
// 没有 address
}
}
}
// 安全地访问深层属性
const city = response.data?.user?.address?.city
console.log(city) // undefined(不报错)
// 配合空值合并运算符 ?? 提供默认值
const cityOrDefault = response.data?.user?.address?.city ?? '未知城市'
console.log(cityOrDefault) // '未知城市'
9.4 ?. 和 && 的对比
js
const user = { name: '小明' }
// 以前的写法(用 && 短路)
const city1 = user.address && user.address.city
// ✅ 现在的写法(可选链)
const city2 = user.address?.city
// 两者效果一样,但 ?. 更简洁、更易读
10. Object 常用静态方法
10.1 Object.keys() / values() / entries()
前面已经讲过,这里做个小总结:
js
const user = { name: '小明', age: 25, city: '北京' }
Object.keys(user) // ['name', 'age', 'city']
Object.values(user) // ['小明', 25, '北京']
Object.entries(user) // [['name','小明'], ['age',25], ['city','北京']]
10.2 Object.assign() --- 合并对象
js
const target = { a: 1, b: 2 }
const source = { b: 3, c: 4 }
// 把 source 的属性合并到 target(会修改 target)
Object.assign(target, source)
console.log(target) // { a: 1, b: 3, c: 4 }
console.log(source) // { b: 3, c: 4 }(source 不变)
实际场景:给对象添加默认值
js
const user = { name: '小明' }
// 给 user 添加默认属性(user 已有的不会被覆盖)
Object.assign(user, { age: 0, role: '用户', email: '' })
console.log(user)
// { name: '小明', age: 0, role: '用户', email: '' }
10.3 Object.freeze() --- 冻结对象
冻结后,对象的属性不能被修改、添加、删除:
js
const config = {
apiUrl: 'https://api.example.com',
timeout: 3000
}
Object.freeze(config)
// ❌ 修改无效(严格模式下会报错)
config.apiUrl = 'https://hack.com'
console.log(config.apiUrl) // 'https://api.example.com'(没变)
// ❌ 添加无效
config.newProp = 'hello'
console.log(config.newProp) // undefined(没加上)
// ❌ 删除无效
delete config.timeout
console.log(config.timeout) // 3000(没删掉)
10.4 Object.is() --- 严格比较
js
// === 和 Object.is() 大部分情况一样
console.log(1 === 1) // true
console.log(Object.is(1, 1)) // true
// 但有两个特例
console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
10.5 方法速查表
| 方法 | 作用 | 示例 |
|---|---|---|
Object.keys(obj) |
获取所有键 | ['name', 'age'] |
Object.values(obj) |
获取所有值 | ['小明', 25] |
Object.entries(obj) |
获取所有键值对 | [['name','小明']] |
Object.assign(target, source) |
合并对象(修改 target) | { a:1, b:3 } |
Object.freeze(obj) |
冻结对象(不可修改) | 冻结后的对象 |
Object.is(a, b) |
严格相等比较 | true / false |
11. 对象是引用类型
11.1 值类型 vs 引用类型
js
// 值类型(基本类型):赋值时复制值
let a = 10
let b = a // b 是 a 的副本
b = 20
console.log(a) // 10(a 不受影响)
// 引用类型(对象):赋值时复制引用(指针)
let obj1 = { name: '小明' }
let obj2 = obj1 // obj2 和 obj1 指向同一个对象
obj2.name = '大明'
console.log(obj1.name) // '大明'(obj1 也被改了!)
和 Java 的对比:
java
// Java:对象变量也是引用
User user1 = new User("小明");
User user2 = user1; // user2 和 user1 指向同一个对象
user2.setName("大明");
System.out.println(user1.getName()); // "大明"(一样)
11.2 为什么 obj2 改了 obj1 也变了?
因为 obj2 = obj1 不是复制对象,而是复制了对象的"地址"。两个变量指向同一块内存:
obj1 ──→ ┌──────────────┐
│ name: '小明' │
obj2 ──→ └──────────────┘
// 改 obj2.name 就是改了这块内存里的 name
// obj1 读的也是这块内存,所以也变了
11.3 浅拷贝 vs 深拷贝
浅拷贝:只复制第一层,嵌套的还是共享引用:
js
const original = {
name: '小明',
address: {
city: '北京'
}
}
// 浅拷贝(用展开运算符)
const copy = { ...original }
copy.name = '大明'
console.log(original.name) // '小明'(第一层不受影响)
copy.address.city = '上海'
console.log(original.address.city) // '上海'(嵌套对象被影响了!)
深拷贝:完全复制,互不影响:
js
const original = {
name: '小明',
address: {
city: '北京'
}
}
// 深拷贝(用 JSON,最简单的方式)
const deepCopy = JSON.parse(JSON.stringify(original))
deepCopy.address.city = '上海'
console.log(original.address.city) // '北京'(不受影响)
JSON 深拷贝的限制:
- 不能复制函数(会被丢弃)
- 不能复制
undefined(会被丢弃) - 不能复制
Date(会变成字符串) - 不能处理循环引用
更可靠的方式(ES2024):
js
// structuredClone --- 浏览器原生深拷贝
const deepCopy = structuredClone(original)
12. 常用模式:对象与数组互转
12.1 对象转数组
js
const user = { name: '小明', age: 25, city: '北京' }
// 转成键数组
const keys = Object.keys(user) // ['name', 'age', 'city']
// 转成值数组
const values = Object.values(user) // ['小明', 25, '北京']
// 转成键值对数组
const entries = Object.entries(user) // [['name','小明'], ['age',25], ['city','北京']]
12.2 数组转对象
js
// 用 Object.fromEntries 把键值对数组转成对象
const entries = [['name', '小明'], ['age', 25], ['city', '北京']]
const obj = Object.fromEntries(entries)
console.log(obj) // { name: '小明', age: 25, city: '北京' }
12.3 实际场景:对象属性映射
js
const user = { name: '小明', age: 25, city: '北京' }
// 把所有值变成大写
const upper = Object.fromEntries(
Object.entries(user).map(([key, value]) => [key, String(value).toUpperCase()])
)
console.log(upper) // { name: '小明', age: '25', city: '北京' }
// 过滤掉值为空的属性
const cleaned = Object.fromEntries(
Object.entries(user).filter(([key, value]) => value !== '' && value !== null)
)
13. 练习
练习 1(初级):创建和访问对象
js
// 创建一个商品对象,包含 name、price、stock 三个属性
// 然后用两种方式(点语法、方括号)分别访问每个属性
const product = {
// 你的代码
}
console.log(product.name) // 点语法
console.log(product['price']) // 方括号语法
练习 2(初级):对象解构
js
const response = {
code: 200,
message: 'success',
data: {
id: 1,
name: 'iPhone 15',
price: 7999,
tags: ['手机', '苹果']
}
}
// 用解构提取 id、name、price
// 然后用模板字符串输出:"商品 iPhone 15 (ID: 1) 售价 ¥7999"
// 你的代码
练习 3(中级):对象遍历 + 数组方法
js
const products = {
'p001': { name: 'iPhone', price: 7999, stock: 10 },
'p002': { name: 'iPad', price: 3499, stock: 0 },
'p003': { name: 'AirPods', price: 1999, stock: 50 },
'p004': { name: 'MacBook', price: 12999, stock: 5 }
}
// 1. 用 Object.values() + filter 找出有库存的商品
// 2. 用 Object.entries() + map 生成 "商品名: ¥价格" 格式的数组
// 3. 用 Object.values() + reduce 计算所有商品的总库存
练习 4(中级):展开运算符 + 合并
js
const defaultConfig = {
theme: 'light',
fontSize: 14,
showSidebar: true,
language: 'zh-CN'
}
const userConfig = {
theme: 'dark',
fontSize: 16
}
// 1. 用展开运算符合并两个配置(用户配置覆盖默认配置)
// 2. 在合并结果中添加一个新属性 version: '1.0'
// 你的代码
练习 5(综合):商品数据处理
js
const shop = {
name: '数码商城',
products: {
'p001': { name: 'iPhone 15', price: 7999, category: '手机' },
'p002': { name: 'iPad Pro', price: 6799, category: '平板' },
'p003': { name: 'AirPods Pro', price: 1999, category: '配件' },
'p004': { name: 'MacBook Pro', price: 14999, category: '电脑' },
'p005': { name: 'Apple Watch', price: 2999, category: '配件' }
}
}
// 1. 获取所有商品的名字列表(用 Object.values + map)
// 2. 找出所有价格超过 5000 的商品(用 Object.entries + filter)
// 3. 按 category 分组,生成 { 手机: [...], 平板: [...], 配件: [...], 电脑: [...] }
// 4. 计算所有商品的平均价格
14. 常见陷阱
陷阱 1:忘记对象是引用类型
js
const user = { name: '小明', age: 25 }
// ❌ 以为 copy 是独立的副本
const copy = user
copy.age = 26
console.log(user.age) // 26(user 也被改了!)
// ✅ 用展开运算符创建新对象
const copy2 = { ...user }
copy2.age = 27
console.log(user.age) // 26(user 不受影响)
陷阱 2:for...in 遍历原型链
js
// 给所有对象添加一个方法(不推荐,只是演示)
Object.prototype.greet = function() { console.log('hello') }
const user = { name: '小明', age: 25 }
// ❌ for...in 会遍历到 greet
for (const key in user) {
console.log(key)
}
// 输出:name, age, greet(greet 是继承来的)
// ✅ 用 Object.keys() 只遍历自己的属性
Object.keys(user).forEach(key => {
console.log(key)
})
// 输出:name, age
陷阱 3:解构时变量名不匹配
js
const user = { name: '小明', age: 25 }
// ❌ 变量名和属性名不匹配
const { username, userage } = user
console.log(username) // undefined(user 没有 username 属性)
console.log(userage) // undefined
// ✅ 用重命名
const { name: username, age: userage } = user
console.log(username) // '小明'
console.log(userage) // 25
陷阱 4:可选链和空值混用
js
const user = { name: '小明', age: 0 }
// ❌ age 是 0,但 0 是 falsy,|| 会跳过
const age1 = user.age || 18
console.log(age1) // 18(0 被当成 falsy 了)
// ✅ 用 ??(空值合并运算符),只在 null/undefined 时才用默认值
const age2 = user.age ?? 18
console.log(age2) // 0(0 不是 null/undefined,保留原值)
陷阱 5:展开运算符只做浅拷贝
js
const original = {
name: '小明',
scores: [90, 85, 92]
}
const copy = { ...original }
// 第一层是独立的
copy.name = '大明'
console.log(original.name) // '小明'(不受影响)
// 但数组是引用类型,还是共享的
copy.scores.push(100)
console.log(original.scores) // [90, 85, 92, 100](被影响了!)
// ✅ 要深拷贝嵌套的数组/对象,需要单独处理
const deepCopy = {
...original,
scores: [...original.scores] // 数组也展开
}
附:今日速查
js
// 创建对象
const obj = { name: '小明', age: 25 }
// 访问属性
obj.name // 点语法
obj['name'] // 方括号语法(支持变量和特殊字符)
// 增删改
obj.email = '...' // 添加/修改
delete obj.email // 删除
'name' in obj // 检查属性是否存在
// 遍历
Object.keys(obj) // ['name', 'age']
Object.values(obj) // ['小明', 25]
Object.entries(obj) // [['name','小明'], ['age',25]]
// 解构
const { name, age } = obj // 基础解构
const { name: userName } = obj // 重命名
const { email = '无' } = obj // 默认值
const { a: { b } } = nested // 嵌套解构
// 展开运算符
const copy = { ...obj } // 浅拷贝
const merged = { ...defaults, ...userSettings } // 合并
// 可选链
user?.address?.city // 安全访问嵌套属性
user?.getName?.() // 安全调用方法
// 对象 ↔ 数组
Object.keys/values/entries(obj) // 对象 → 数组
Object.fromEntries(entries) // 数组 → 对象
// 深拷贝
JSON.parse(JSON.stringify(obj)) // JSON 深拷贝
structuredClone(obj) // 原生深拷贝(ES2024)
// 冻结
Object.freeze(obj) // 不可修改