Skip to content

对象基础:构建复杂数据的蓝图

打开你的钱包,里面可能有身份证、银行卡、驾照和一些现金。每样东西都有其特定的信息:身份证有姓名、性别、出生日期;银行卡有卡号、有效期、持卡人姓名。如果用数组来存储这些信息,你需要记住"第 0 项是姓名,第 1 项是性别...",这既不直观也容易出错。对象提供了一种更自然的方式——就像给每个信息贴上标签,通过有意义的名称而非数字索引来访问数据。

什么是对象

对象(Object)是 JavaScript 中最重要的数据类型之一。它是一个键值对(key-value pairs)的集合,可以存储和组织相关的数据和功能。每个键(也称为属性名)都关联着一个值,这个值可以是任何 JavaScript 数据类型。

数组适合存储有序的列表,而对象则适合存储具有描述性属性的实体。让我们看一个简单的例子:

javascript
// 用数组存储用户信息 - 不够直观
let userArray = ["Alice", 25, "[email protected]", "New York"];
console.log(userArray[0]); // "Alice" - 需要记住索引

// 用对象存储用户信息 - 清晰明了
let user = {
  name: "Alice",
  age: 25,
  email: "[email protected]",
  city: "New York",
};
console.log(user.name); // "Alice" - 语义清晰

对象让代码更具可读性。user.nameuserArray[0] 更能表达意图,任何人看到代码都能立即理解这是在访问用户的名字。

创建对象的方式

JavaScript 提供了多种创建对象的方法,每种都有其适用场景。

对象字面量(最常用)

使用花括号 {} 创建对象是最直观、最常用的方式:

javascript
// 创建空对象
let emptyObject = {};

// 创建包含属性的对象
let book = {
  title: "JavaScript Guide",
  author: "John Smith",
  year: 2024,
  pages: 320,
  available: true,
};

// 属性值可以是任何类型
let product = {
  name: "Laptop",
  price: 999,
  specs: {
    // 嵌套对象
    cpu: "Intel i7",
    ram: "16GB",
    storage: "512GB SSD",
  },
  tags: ["electronics", "computer", "portable"], // 数组
  getDescription: function () {
    // 函数
    return `${this.name} - $${this.price}`;
  },
};

对象字面量可以包含任意数量的属性,属性之间用逗号分隔。每个属性由键和值组成,用冒号连接。

使用 new Object()

虽然较少使用,但 new Object() 也能创建对象:

javascript
let person = new Object();
person.name = "Bob";
person.age = 30;
person.greet = function () {
  return `Hello, I'm ${this.name}`;
};

console.log(person.name); // "Bob"
console.log(person.greet()); // "Hello, I'm Bob"

这种方式较为冗长,通常只在特定情况下使用。对象字面量在大多数情况下更简洁明了。

使用构造函数

构造函数允许创建具有相同结构的多个对象:

javascript
function User(name, email) {
  this.name = name;
  this.email = email;
  this.isActive = true;
  this.login = function () {
    console.log(`${this.name} logged in`);
  };
}

let user1 = new User("Alice", "[email protected]");
let user2 = new User("Bob", "[email protected]");

console.log(user1.name); // "Alice"
console.log(user2.name); // "Bob"
user1.login(); // "Alice logged in"

构造函数名通常以大写字母开头,以区别于普通函数。使用 new 关键字调用构造函数会创建一个新对象,并将 this 绑定到这个新对象。

使用 Object.create()

Object.create() 允许你基于现有对象创建新对象:

javascript
let personPrototype = {
  greet: function () {
    return `Hello, I'm ${this.name}`;
  },
  getAge: function () {
    return new Date().getFullYear() - this.birthYear;
  },
};

let alice = Object.create(personPrototype);
alice.name = "Alice";
alice.birthYear = 1995;

console.log(alice.greet()); // "Hello, I'm Alice"
console.log(alice.getAge()); // 计算年龄

let bob = Object.create(personPrototype);
bob.name = "Bob";
bob.birthYear = 1990;

console.log(bob.greet()); // "Hello, I'm Bob"

这种方式创建的对象会继承原型对象的方法,但不会复制这些方法,从而节省内存。

访问对象属性

有两种主要方式访问对象属性:点标记法和方括号标记法。

点标记法(Dot Notation)

这是最常用、最直观的方式:

javascript
let car = {
  brand: "Toyota",
  model: "Camry",
  year: 2024,
  color: "silver",
};

// 读取属性
console.log(car.brand); // "Toyota"
console.log(car.year); // 2024

// 修改属性
car.color = "blue";
console.log(car.color); // "blue"

// 添加新属性
car.owner = "Sarah";
console.log(car.owner); // "Sarah"

点标记法简洁易读,是访问对象属性的首选方式。

方括号标记法(Bracket Notation)

方括号标记法更加灵活,特别是在属性名包含特殊字符或需要动态访问时:

javascript
let user = {
  name: "Michael",
  age: 28,
  "favorite color": "green", // 属性名包含空格
  "home-address": "123 Main St", // 属性名包含连字符
};

// 访问包含空格的属性名
console.log(user["favorite color"]); // "green"
console.log(user["home-address"]); // "123 Main St"

// 动态访问属性
let propertyName = "age";
console.log(user[propertyName]); // 28

// 使用变量作为属性名
let field = "name";
console.log(user[field]); // "Michael"

// 计算属性名
let prefix = "user";
let id = 123;
let key = prefix + id; // "user123"
user[key] = "some value";
console.log(user.user123); // "some value"

方括号标记法在以下情况特别有用:

  • 属性名包含特殊字符(空格、连字符等)
  • 属性名存储在变量中
  • 需要动态生成属性名
  • 属性名是数字
javascript
let settings = {
  volume: 50,
  brightness: 80,
};

// 动态更新设置
function updateSetting(settingName, value) {
  settings[settingName] = value;
}

updateSetting("volume", 75);
console.log(settings.volume); // 75

检查属性是否存在

在访问对象属性之前,有时需要检查该属性是否存在。

使用 in 操作符

javascript
let product = {
  name: "Phone",
  price: 699,
  inStock: true,
};

console.log("name" in product); // true
console.log("color" in product); // false
console.log("price" in product); // true

in 操作符会检查对象自身及其原型链上是否存在该属性。

使用 hasOwnProperty()

hasOwnProperty() 只检查对象自身的属性,不包括继承的属性:

javascript
let animal = {
  species: "dog",
  sound: "bark",
};

console.log(animal.hasOwnProperty("species")); // true
console.log(animal.hasOwnProperty("toString")); // false
// toString 是继承自 Object.prototype 的方法

直接检查值

有时我们只需要检查属性值是否为 undefined

javascript
let config = {
  theme: "dark",
  fontSize: 14,
};

if (config.theme !== undefined) {
  console.log("Theme is set:", config.theme);
}

// 简写形式(但要注意 falsy 值)
if (config.theme) {
  console.log("Theme exists");
}

// 使用可选链操作符(现代 JavaScript)
console.log(config.language?.toUpperCase()); // undefined(不会报错)
console.log(config.theme?.toUpperCase()); // "DARK"

嵌套对象

对象可以包含其他对象作为属性值,形成嵌套结构,这对于表示复杂的数据非常有用。

javascript
let company = {
  name: "TechCorp",
  founded: 2015,
  headquarters: {
    country: "USA",
    city: "San Francisco",
    address: {
      street: "123 Tech Avenue",
      zipCode: "94105",
    },
  },
  ceo: {
    name: "Emma Johnson",
    age: 45,
    email: "[email protected]",
  },
  departments: [
    {
      name: "Engineering",
      employees: 120,
      head: "David Lee",
    },
    {
      name: "Sales",
      employees: 80,
      head: "Lisa Chen",
    },
  ],
};

// 访问嵌套属性
console.log(company.headquarters.city); // "San Francisco"
console.log(company.headquarters.address.street); // "123 Tech Avenue"
console.log(company.ceo.name); // "Emma Johnson"
console.log(company.departments[0].name); // "Engineering"
console.log(company.departments[1].head); // "Lisa Chen"

// 修改嵌套属性
company.headquarters.address.zipCode = "94106";
company.departments[0].employees = 125;

安全访问深层嵌套属性

访问深层嵌套的属性时,如果中间某个层级不存在,会导致错误:

javascript
let user = {
  name: "Alice",
  // 注意:没有 address 属性
};

// ❌ 这会报错
// console.log(user.address.city); // TypeError: Cannot read property 'city' of undefined

// ✅ 传统安全访问方式
if (user.address && user.address.city) {
  console.log(user.address.city);
}

// ✅ 使用可选链操作符(ES2020)
console.log(user.address?.city); // undefined(不会报错)
console.log(user.address?.city ?? "Unknown"); // "Unknown"(提供默认值)

可选链操作符 ?. 让代码更简洁安全。如果链中的任何部分是 nullundefined,整个表达式会短路并返回 undefined

对象与引用

对象是引用类型,这是理解 JavaScript 对象行为的关键。

引用赋值

变量不存储对象本身,而是存储对象在内存中的引用(地址):

javascript
let person1 = {
  name: "Alice",
  age: 25,
};

// person2 引用同一个对象
let person2 = person1;

// 修改 person2 会影响 person1
person2.age = 26;
console.log(person1.age); // 26
console.log(person2.age); // 26

// 它们指向同一个对象
console.log(person1 === person2); // true

对象比较

两个对象即使内容完全相同,只要是不同的对象,比较结果就是 false

javascript
let obj1 = { x: 1, y: 2 };
let obj2 = { x: 1, y: 2 };
let obj3 = obj1;

console.log(obj1 === obj2); // false - 不同的对象
console.log(obj1 === obj3); // true - 指向同一对象

// 要比较对象内容,需要自己实现
function areObjectsEqual(obj1, obj2) {
  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (let key of keys1) {
    if (obj1[key] !== obj2[key]) {
      return false;
    }
  }

  return true;
}

console.log(areObjectsEqual(obj1, obj2)); // true

复制对象

由于对象是引用类型,直接赋值只会复制引用。要创建对象的副本,需要特殊处理:

javascript
let original = {
  name: "John",
  age: 30,
  skills: ["JavaScript", "Python"],
};

// ❌ 这不是复制,只是引用
let notACopy = original;

// ✅ 浅拷贝 - 使用展开运算符
let shallowCopy1 = { ...original };
shallowCopy1.age = 31;
console.log(original.age); // 30 - 不受影响

// ✅ 浅拷贝 - 使用 Object.assign()
let shallowCopy2 = Object.assign({}, original);

// ⚠️ 但浅拷贝对嵌套对象仍是引用
shallowCopy1.skills.push("Java");
console.log(original.skills); // ["JavaScript", "Python", "Java"] - 被修改了

// ✅ 深拷贝 - 使用 JSON 方法(有限制)
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.skills.push("Ruby");
console.log(original.skills.length); // 3 - 不受影响
console.log(deepCopy.skills.length); // 4

// ✅ 深拷贝 - 使用 structuredClone(现代浏览器)
let deepCopy2 = structuredClone(original);

JSON 方法的限制:

  • 无法复制函数、undefinedSymbol
  • 无法处理循环引用
  • 会丢失原型链

structuredClone() 是更好的选择,但需要较新的浏览器支持。

对象方法

对象不仅可以存储数据,还可以包含函数(称为方法)。

javascript
let calculator = {
  value: 0,

  add: function (n) {
    this.value += n;
    return this;
  },

  subtract: function (n) {
    this.value -= n;
    return this;
  },

  multiply: function (n) {
    this.value *= n;
    return this;
  },

  getValue: function () {
    return this.value;
  },

  reset: function () {
    this.value = 0;
    return this;
  },
};

// 使用方法
calculator.add(10).multiply(2).subtract(5);
console.log(calculator.getValue()); // 15

calculator.reset().add(100);
console.log(calculator.getValue()); // 100

方法简写语法

ES6 提供了更简洁的方法定义语法:

javascript
let user = {
  name: "Alice",
  age: 25,

  // 传统方法定义
  greet: function () {
    return `Hello, I'm ${this.name}`;
  },

  // ES6 简写语法
  introduce() {
    return `I'm ${this.name}, ${this.age} years old`;
  },

  celebrateBirthday() {
    this.age++;
    return `Happy birthday! Now I'm ${this.age}`;
  },
};

console.log(user.greet()); // "Hello, I'm Alice"
console.log(user.introduce()); // "I'm Alice, 25 years old"
console.log(user.celebrateBirthday()); // "Happy birthday! Now I'm 26"

this 关键字

在对象方法中,this 指向调用该方法的对象:

javascript
let person = {
  firstName: "John",
  lastName: "Doe",
  fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  updateName(first, last) {
    this.firstName = first;
    this.lastName = last;
  },
};

console.log(person.fullName()); // "John Doe"
person.updateName("Jane", "Smith");
console.log(person.fullName()); // "Jane Smith"

实际应用场景

1. 用户配置管理

javascript
let userPreferences = {
  theme: "dark",
  language: "en",
  notifications: {
    email: true,
    push: false,
    sms: true,
  },
  privacy: {
    profileVisible: true,
    showEmail: false,
    showPhone: false,
  },

  updateTheme(newTheme) {
    this.theme = newTheme;
    console.log(`Theme updated to ${newTheme}`);
  },

  toggleNotification(type) {
    if (type in this.notifications) {
      this.notifications[type] = !this.notifications[type];
      return this.notifications[type];
    }
  },

  getNotificationStatus() {
    return Object.keys(this.notifications).filter(
      (key) => this.notifications[key]
    );
  },
};

userPreferences.updateTheme("light");
console.log(userPreferences.toggleNotification("push")); // true
console.log(userPreferences.getNotificationStatus()); // ["email", "push", "sms"]

2. 产品库存管理

javascript
let inventory = {
  items: [
    { id: 1, name: "Laptop", quantity: 15, price: 999 },
    { id: 2, name: "Mouse", quantity: 50, price: 25 },
    { id: 3, name: "Keyboard", quantity: 30, price: 75 },
  ],

  findItem(id) {
    return this.items.find((item) => item.id === id);
  },

  updateQuantity(id, quantity) {
    let item = this.findItem(id);
    if (item) {
      item.quantity = quantity;
      return true;
    }
    return false;
  },

  getTotalValue() {
    return this.items.reduce(
      (total, item) => total + item.quantity * item.price,
      0
    );
  },

  getLowStock(threshold = 20) {
    return this.items.filter((item) => item.quantity < threshold);
  },
};

console.log(inventory.getTotalValue()); // 34635
console.log(inventory.getLowStock()); // [{ id: 1, name: "Laptop", ... }]
inventory.updateQuantity(2, 45);
console.log(inventory.findItem(2)); // { id: 2, name: "Mouse", quantity: 45, ... }

3. 表单验证器

javascript
let formValidator = {
  rules: {
    email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    phone: /^\d{3}-\d{3}-\d{4}$/,
    zipCode: /^\d{5}$/,
  },

  errors: {},

  validate(field, value) {
    if (!this.rules[field]) {
      return true;
    }

    let isValid = this.rules[field].test(value);

    if (!isValid) {
      this.errors[field] = `Invalid ${field} format`;
    } else {
      delete this.errors[field];
    }

    return isValid;
  },

  validateForm(formData) {
    this.errors = {};

    for (let field in formData) {
      this.validate(field, formData[field]);
    }

    return Object.keys(this.errors).length === 0;
  },

  getErrors() {
    return { ...this.errors };
  },
};

let formData = {
  email: "[email protected]",
  phone: "123-456-7890",
  zipCode: "12345",
};

console.log(formValidator.validateForm(formData)); // true

formData.email = "invalid-email";
console.log(formValidator.validateForm(formData)); // false
console.log(formValidator.getErrors()); // { email: "Invalid email format" }

常见陷阱与最佳实践

1. 属性名命名规范

javascript
// ✅ 好的属性名
let user = {
  firstName: "John", // 驼峰命名
  lastName: "Doe",
  emailAddress: "[email protected]",
  isActive: true, // 布尔值用 is/has 前缀
};

// ❌ 避免的属性名
let badUser = {
  "first name": "John", // 包含空格,访问不便
  LastName: "Doe", // 不一致的命名风格
  email_address: "[email protected]", // JavaScript 通常不用下划线
};

2. 避免修改不属于你的对象

javascript
// ❌ 不要修改内置对象的原型
Object.prototype.myMethod = function () {
  /* ... */
}; // 危险!

// ✅ 创建自己的对象
let myObject = {
  myMethod() {
    /* ... */
  },
};

3. 对象字面量中的尾随逗号

现代 JavaScript 允许(并推荐)在最后一个属性后加逗号:

javascript
// ✅ 推荐:便于添加新属性
let config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3, // 最后一个逗号
};

// 添加新属性时,git diff 更清晰

4. 属性简写

当属性名和变量名相同时,可以使用简写:

javascript
let name = "Alice";
let age = 25;
let city = "New York";

// ❌ 冗余
let user1 = {
  name: name,
  age: age,
  city: city,
};

// ✅ 简写
let user2 = {
  name,
  age,
  city,
};

总结

对象是 JavaScript 中组织和管理复杂数据的基础工具。我们学习了:

  • 对象概念 - 键值对集合,用于表示具有属性的实体
  • 创建方式 - 对象字面量、构造函数、Object.create()
  • 属性访问 - 点标记法和方括号标记法,各有适用场景
  • 嵌套对象 - 表示复杂数据结构,注意安全访问
  • 引用特性 - 理解对象赋值和比较的行为
  • 对象方法 - 为对象添加功能,使用 this 访问对象属性
  • 实际应用 - 配置管理、数据组织、功能封装

掌握对象基础是学习 JavaScript 的关键一步。对象不仅用于存储数据,更是构建应用程序的基本单元。在后续文章中,我们将深入学习对象的高级操作,包括对象方法、属性操作和遍历技巧。