JavaScript 对象

对象是 JS 里最重要的数据结构——它用"键值对"组织数据,几乎一切皆对象:用户、商品、配置、API 返回值,甚至函数、数组本身也是对象。这一章我们彻底搞懂对象的创建、操作、复制、方法。

1. 对象基础

对象用 {} 创建,里面是"键: 值"对,逗号分隔。键必须是字符串(或 Symbol),值可以是任何类型:

// 对象字面量:键值对的集合
const user = {
  name: "小明",
  age: 20,
  isStudent: true,
  hobbies: ["读书", "写代码"]
};

// 1. 点语法访问属性
console.log(user.name);        // "小明"
console.log(user.age);         // 20

// 2. 方括号语法(键是变量或特殊字符时用)
console.log(user["name"]);     // "小明"
const key = "age";
console.log(user[key]);        // 20

// 3. 修改属性
user.age = 21;
user.city = "北京";             // 添加新属性
delete user.isStudent;          // 删除属性

// 4. 检查属性是否存在
console.log("name" in user);           // true
console.log(user.hasOwnProperty("name")); // true

访问属性有两种方式:

不存在的属性返回 undefined(不报错),这是 JS 对象的一个特殊行为。

2. ES6 对象简写与计算属性

// 属性简写:变量名和键名相同时
const name = "小明";
const age = 20;
const user = { name, age };     // 等价于 { name: name, age: age }
console.log(user);              // { name: "小明", age: 20 }

// 方法简写(更简洁)
const calc = {
  add(a, b) { return a + b; },
  sub(a, b) { return a - b; }
};
// 等价于
const calc2 = {
  add: function (a, b) { return a + b; },
  sub: function (a, b) { return a - b; }
};

// 计算属性名(键名是表达式)
const field = "score";
const student = {
  name: "小红",
  [field]: 95,                  // 动态键名
  [`total${field}`]: "优秀"    // 模板字符串键
};
console.log(student.score);     // 95

当变量名和键名相同时,可以只写一个;方法可以省略 function 关键字。这些简写在现代 JS 里大量使用,尤其 React 组件、对象配置等场景。

3. 对象解构赋值

解构是从对象里"挑出"需要的字段,赋给变量。这是 ES6 最常用的特性之一:

// 对象解构:从对象中"挑出"属性
const user = { name: "小明", age: 20, city: "北京" };

// 基本解构
const { name, age } = user;
console.log(name, age);         // 小明 20

// 重命名(原属性名:新名)
const { name: userName } = user;
console.log(userName);          // 小明

// 默认值(属性不存在时使用)
const { phone = "未填写" } = user;
console.log(phone);             // 未填写

// 嵌套解构
const person = {
  info: { name: "小明", address: { city: "北京" } }
};
const { info: { address: { city } } } = person;
console.log(city);              // 北京

// 函数参数解构(超常用)
function greet({ name, age = 18 }) {
  console.log(`你好 ${name},${age} 岁`);
}
greet({ name: "小明" });        // 你好 小明,18 岁

解构在函数参数里特别有用——可以模拟命名参数,调用时不需要记参数顺序,默认值也清晰。React 的 props、各种 API 都大量使用。

2.5 实用:可选链 ?.

const user = { name: "小明", address: { city: "北京" } };

// ❌ 老写法:层层判断
const zip = user && user.address && user.address.zip;

// ✅ 可选链:任何一层是 null/undefined 就返回 undefined,不报错
const zip2 = user?.address?.zip;
console.log(zip2);   // undefined(不报错)

// 也可以用在方法上
const result = obj?.method?.();
// 等价于:如果 obj 存在且 obj.method 是函数,才调用

// 数组下标也支持
const first = arr?.[0];

4. 对象方法与 this

对象里可以放函数,称为"方法"。方法内的 this 指向调用对象:

// 对象方法中的 this
const user = {
  name: "小明",
  hobbies: ["读书", "写代码"],
  // 方法:用普通函数,this 指向调用对象
  greet() {
    console.log(`我是 ${this.name}`);
  },
  // ❌ 箭头函数:this 是外层的(window/undefined)
  badGreet: () => {
    console.log(this.name);    // undefined
  },
  // 方法里嵌套回调要用箭头函数,才能拿到正确的 this
  showHobbies() {
    this.hobbies.forEach(h => {
      console.log(`${this.name} 喜欢 ${h}`);
    });
  }
};

user.greet();          // 我是小明
user.badGreet();       // undefined
user.showHobbies();    // 小明 喜欢 读书 / 小明 喜欢 写代码

箭头函数与对象方法的搭配坑:对象方法用普通函数(greet() {}),this 是对象;但方法内部的回调用箭头函数,才能保留外层 this。这条规则记牢。

5. 展开运算符 ...

... 是复制和合并对象的利器:

// 展开运算符 ...:复制和合并对象(浅拷贝)
const user = { name: "小明", age: 20 };

// 复制对象(浅拷贝)
const copy = { ...user };
copy.name = "小红";
console.log(user.name);   // 仍然是"小明"(不影响原对象)

// 合并对象
const defaults = { theme: "light", font: "sans" };
const userPrefs = { theme: "dark" };
const merged = { ...defaults, ...userPrefs };
console.log(merged);      // { theme: "dark", font: "sans" }(后者覆盖前者)

// 添加 / 覆盖属性
const updated = { ...user, age: 21, city: "北京" };
console.log(updated);
// { name: "小明", age: 21, city: "北京" }

注意:展开是浅拷贝——只复制一层,嵌套的对象仍是引用(下面会讲)。

6. Object 静态方法

// Object 静态方法
const user = { name: "小明", age: 20, city: "北京" };

// 1. Object.keys():所有键组成的数组
console.log(Object.keys(user));   // ["name", "age", "city"]

// 2. Object.values():所有值组成的数组
console.log(Object.values(user)); // ["小明", 20, "北京"]

// 3. Object.entries():键值对组成的二维数组
console.log(Object.entries(user));
// [["name","小明"], ["age",20], ["city","北京"]]

// 4. 遍历对象(经典写法)
for (const [key, value] of Object.entries(user)) {
  console.log(key, value);
}

// 5. Object.assign():合并对象(等价于展开运算符)
const merged = Object.assign({}, { a: 1 }, { b: 2 });
console.log(merged);    // { a: 1, b: 2 }

// 6. Object.freeze():冻结对象,不可改
const config = Object.freeze({ host: "localhost" });
config.host = "x";       // 静默失败 / 严格模式报错

Object.keys/values/entries 是遍历对象的标准三件套——因为 for-in 会带上继承的属性,而这套方法只返回对象自身的可枚举属性,更安全。

7. 浅拷贝 vs 深拷贝

这是 JS 最经典的坑之一:对象是引用类型,赋值只是复制地址,改一边影响另一边:

// 浅拷贝 vs 深拷贝
const obj = {
  name: "小明",
  hobbies: ["读书", "写代码"],
  info: { age: 20 }
};

// 1. 浅拷贝:只复制一层
const shallow1 = { ...obj };
const shallow2 = Object.assign({}, obj);
shallow1.hobbies.push("游泳");
console.log(obj.hobbies);    // ["读书","写代码","游泳"] ⚠️ 原对象也被改了!

// 2. 深拷贝:全部复制,互不影响
// 方法 A:JSON(简单但会丢失函数、Date、undefined 等)
const deep1 = JSON.parse(JSON.stringify(obj));

// 方法 B:structuredClone(推荐,现代浏览器都支持)
const deep2 = structuredClone(obj);
deep2.hobbies.push("游泳");
console.log(obj.hobbies);    // ["读书","写代码"](原对象不受影响)

8. 原型链入门

JS 的继承靠原型链。每个对象都有一个隐藏属性 __proto__,指向它的"原型对象"。访问属性时,先在对象本身找,找不到就去原型找,层层向上。

const obj = { name: "小明" };

// obj 自己没有 toString,但它从 Object.prototype 继承了
console.log(obj.toString());    // "[object Object]"
console.log(obj.__proto__ === Object.prototype);   // true
console.log(obj.__proto__.hasOwnProperty("toString"));  // true

// 原型链示意:
// obj --> Object.prototype --> null

// 数组原型链:
// [] --> Array.prototype --> Object.prototype --> null
console.log([1,2,3].__proto__ === Array.prototype);   // true

原型链就是 JS 实现"继承"的机制——你调用 [1,2,3].map() 时,map 不在数组本身,而在 Array.prototype。理解这点,才能读懂各种继承模式、框架源码。

9. 实战:对象数组操作

const users = [
  { name: "小明", age: 20, score: 88 },
  { name: "小红", age: 22, score: 95 },
  { name: "小刚", age: 19, score: 76 }
];

// 找到成绩最高的人
const top = users.reduce((max, u) => u.score > max.score ? u : max);
console.log(top.name);    // 小红

// 筛选成年人
const adults = users.filter(u => u.age >= 20);
console.log(adults.length);    // 2

// 取出所有名字组成新数组
const names = users.map(u => u.name);
console.log(names);    // ["小明","小红","小刚"]

// 按成绩排序(注意 sort 会改原数组)
const sorted = [...users].sort((a, b) => b.score - a.score);
console.log(sorted[0].name);    // 小红

"对象数组"是真实业务里最高频的数据结构——API 返回的列表、表格数据都是这种。熟练用 map/filter/reduce 操作它,等于掌握了 JS 一半的实战能力。

小结

下一篇我们看 JS 的数组——和对象并列的常用数据结构。

← 上一篇 JavaScript 作用域与闭包

下一篇 JavaScript 数组

✈️💬