一、前言

在 JavaScript 中,对象是核心数据结构,几乎所有复杂功能的实现都离不开对象。与传统面向对象语言(如 Java)不同,JS 没有类(ES6 之前)的概念,而是通过工厂模式构造函数模式原型模式等灵活机制创建对象,并基于原型链实现属性与方法的共享。

二、对象的本质与核心概念

2.1 什么是 JavaScript 对象?

  • 定义:对象是 “键值对” 的无序集合,键(属性名)为字符串或 Symbol,值(属性值)可以是基本类型(string/number)、引用类型(数组 / 函数)或其他对象。
  • 本质:可以理解为 “散列表”,用于存储相关联的数据与功能(方法),例如:

    javascript

    运行

    // 简单对象示例
    var person = {
      name: "张三", // 基本类型属性
      age: 18,
      sayName: function() { // 方法(函数作为属性值)
        console.log("我的名字是:" + this.name);
      },
      hobbies: ["篮球", "编程"] // 引用类型属性
    };
  • 核心特性
    1. 动态性:可随时添加 / 删除属性(如 person.gender = "男")。
    2. 原型继承:每个对象都有原型(__proto__),可继承原型上的属性与方法。
    3. 属性特性:支持控制属性是否可读写、可枚举、可删除(通过 Object.defineProperty())。

三、对象的创建模式:从简单到高级

JS 提供多种创建对象的方式,各有优劣,需根据场景选择。

3.1 工厂模式:批量创建对象的 “流水线”

3.1.1 核心思想

通过函数封装对象创建过程,接收参数动态设置属性与方法,批量生成结构相同的对象,解决 “重复创建相似对象” 的问题。

3.1.2 代码实现与运行结果

javascript

运行

// 工厂函数:接收参数,返回对象
function createPerson(name, age, gender) {
  // 1. 创建空对象
  var person = new Object();
  // 2. 动态添加属性与方法
  person.name = name;
  person.age = age;
  person.gender = gender;
  person.sayName = function() {
    console.log("姓名:" + this.name + ",年龄:" + this.age);
  };
  // 3. 返回创建的对象
  return person;
}
// 批量创建对象
var person1 = createPerson("张三", 18, "男");
var person2 = createPerson("李四", 20, "女");
// 测试
person1.sayName(); // 调用方法
person2.sayName();
console.log("person1 类型:", typeof person1); // 查看对象类型

运行结果

plaintext

姓名:张三,年龄:18
姓名:李四,年龄:20
person1 类型: object
3.1.3 优缺点
  • 优点:简单高效,适合批量创建结构一致的对象。
  • 缺点:无法区分对象类型(所有对象均为 Object 类型),无法通过 instanceof 判断对象归属(如无法区分 “人” 和 “动物” 对象)。

3.2 构造函数模式:可识别类型的 “蓝图”

3.2.1 核心思想

通过自定义构造函数(首字母大写,约定俗成),结合 new 关键字创建对象,将属性与方法绑定到 this(指向新对象),解决工厂模式 “无法区分类型” 的问题。

3.2.2 代码实现与运行结果

javascript

运行

// 构造函数(首字母大写,标识为构造器)
function Person(name, age, gender) {
  // this 指向新创建的对象
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function() {
    console.log("姓名:" + this.name);
  };
}
// 使用 new 关键字创建实例
var person1 = new Person("张三", 18, "男");
var person2 = new Person("李四", 20, "女");
// 测试
person1.sayName(); // 调用方法
console.log("person1 是 Person 实例吗?", person1 instanceof Person); // 类型判断
console.log("person1 是 Object 实例吗?", person1 instanceof Object); // 所有对象继承自 Object
console.log("person1 与 person2 方法是否相同?", person1.sayName === person2.sayName); // 方法独立性

运行结果

plaintext

姓名:张三
person1 是 Person 实例吗? true
person1 是 Object 实例吗? true
person1 与 person2 方法是否相同? false
3.2.3 new 关键字的执行过程
  1. 在内存中创建一个新的空对象。
  2. 新对象的 [[Prototype]] 属性(隐式原型)指向构造函数的 prototype 属性(显式原型)。
  3. 构造函数内部的 this 指向新对象。
  4. 执行构造函数内部代码(给新对象添加属性与方法)。
  5. 如果构造函数没有返回非空对象,则返回新创建的对象。
3.2.4 优缺点
  • 优点:可通过 instanceof 判断对象类型,语义清晰,适合创建同一 “类” 的对象。
  • 缺点:每个实例的方法都是独立函数(person1.sayName !== person2.sayName),造成内存浪费(相同逻辑的方法被重复创建)。

3.3 原型模式:共享方法的 “优化方案”

3.3.1 核心思想

利用原型链特性,将对象的共享方法定义在构造函数的 prototype(显式原型)上,所有实例通过原型链共享这些方法,解决构造函数模式 “方法重复创建” 的问题。

3.3.2 代码实现与运行结果

javascript

运行

// 构造函数:仅初始化实例属性(每个实例独立)
function Person() {}
// 原型对象:定义共享属性与方法(所有实例共享)
Person.prototype.name = "默认姓名";
Person.prototype.age = 18;
Person.prototype.sayName = function() {
  console.log("姓名:" + this.name);
};
// 创建实例
var person1 = new Person();
var person2 = new Person();
// 测试 1:共享方法
console.log("person1 与 person2 方法是否相同?", person1.sayName === person2.sayName); // 方法共享
// 测试 2:实例属性覆盖原型属性
person1.name = "张三"; // 实例属性覆盖原型属性
console.log("person1 姓名:", person1.name); // 读取实例属性
console.log("person2 姓名:", person2.name); // 读取原型属性
// 测试 3:原型链查找
console.log("person1 有 sayName 实例属性吗?", person1.hasOwnProperty("sayName")); // false(方法在原型上)
console.log("sayName 在 person1 的原型上吗?", "sayName" in person1); // true(原型链查找)

运行结果

plaintext

person1 与 person2 方法是否相同? true
person1 姓名: 张三
person2 姓名: 默认姓名
person1 有 sayName 实例属性吗? false
sayName 在 person1 的原型上吗? true
3.3.3 原型链查找规则
  1. 访问对象属性时,先查找实例自身的属性。
  2. 若实例无该属性,则查找原型对象constructor.prototype)。
  3. 若原型对象无该属性,则继续向上查找原型链(直到 Object.prototype)。
  4. 若最终未找到,返回 undefined
3.3.4 优缺点
  • 优点:方法共享,节省内存;支持原型链继承,可扩展对象功能。
  • 缺点:原型上的引用类型属性(如数组)会被所有实例共享,易造成数据污染(如一个实例修改数组,所有实例受影响)。

3.4 组合模式(推荐):兼顾性能与灵活性

3.4.1 核心思想

结合构造函数模式(定义实例属性,确保独立性)和原型模式(定义共享方法,节省内存),是 JS 中最常用的对象创建模式。

3.4.2 代码实现与运行结果

javascript

运行

// 构造函数:定义实例属性(独立属性,含引用类型)
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.friends = ["王五", "赵六"]; // 引用类型属性(每个实例独立)
}
// 原型模式:定义共享方法
Person.prototype.sayName = function() {
  console.log("姓名:" + this.name);
};
Person.prototype.showFriends = function() {
  console.log(this.name + " 的朋友:" + this.friends);
};
// 创建实例
var person1 = new Person("张三", 18, "男");
var person2 = new Person("李四", 20, "女");
// 测试 1:引用类型属性独立性
person1.friends.push("孙七"); // 仅修改 person1 的 friends
person1.showFriends();
person2.showFriends();
// 测试 2:方法共享
console.log("person1 与 person2 方法是否相同?", person1.sayName === person2.sayName);
// 测试 3:类型判断
console.log("person1 是 Person 实例吗?", person1 instanceof Person);

运行结果

plaintext

张三 的朋友:王五,赵六,孙七
李四 的朋友:王五,赵六
person1 与 person2 方法是否相同? true
person1 是 Person 实例吗? true
3.4.3 核心优势
  1. 实例属性独立:引用类型属性(如数组)定义在构造函数中,避免数据污染。
  2. 方法共享:方法定义在原型上,所有实例复用同一函数,节省内存。
  3. 类型可识别:支持 instanceof 判断,语义清晰,符合面向对象思想。

四、对象的高级特性:属性控制与序列化

4.1 数据属性与访问器属性:精细化控制属性

JS 中对象的属性分为两类:数据属性(存储值)和访问器属性(通过 get/set 方法控制访问),可通过 Object.defineProperty() 自定义属性特性(如是否可读写、可枚举)。

4.1.1 数据属性特性
特性名描述默认值(直接定义属性时)
configurable是否可通过 delete 删除属性、修改特性或转为访问器属性true
enumerable是否可通过 for...in 循环枚举true
writable是否可修改属性值true
value属性的实际值undefined
4.1.2 访问器属性特性
特性名描述默认值
configurable是否可删除或修改特性false
enumerable是否可通过 for...in 循环枚举false
get读取属性时调用的函数,返回属性值undefined
set修改属性时调用的函数,接收新值作为参数undefined
4.1.3 代码实现与运行结果

javascript

运行

// 1. 定义数据属性:控制属性可读写性
var person = {};
Object.defineProperty(person, "name", {
  value: "张三",
  writable: false, // 不可修改
  enumerable: true, // 可枚举
  configurable: true // 可删除
});
console.log("修改前 name:", person.name);
person.name = "李四"; // 尝试修改(writable: false,修改无效)
console.log("修改后 name:", person.name);
// 2. 定义访问器属性:监听属性变化(模拟数据双向绑定)
var user = {
  _age: 18 // 下划线表示“私有”属性,建议通过访问器访问
};
Object.defineProperty(user, "age", {
  get: function() {
    console.log("读取 age 属性");
    return this._age;
  },
  set: function(newAge) {
    console.log("修改 age 属性:" + newAge);
    if (newAge >= 0 && newAge <= 150) { // 校验新值
      this._age = newAge;
    } else {
      console.log("年龄无效!");
    }
  }
});
// 测试访问器属性
console.log("user.age:", user.age); // 触发 get 方法
user.age = 20; // 触发 set 方法(有效)
user.age = 200; // 触发 set 方法(无效)
console.log("修改后 user.age:", user.age);

运行结果

plaintext

修改前 name: 张三
修改后 name: 张三
读取 age 属性
user.age: 18
修改 age 属性:20
修改 age 属性:200
年龄无效!
读取 age 属性
修改后 user.age: 20

4.2 对象序列化:JSON 转换与深拷贝

对象序列化是将对象状态转换为字符串(JSON.stringify()),反序列化是将字符串还原为对象(JSON.parse()),常用于数据存储、接口传输与深拷贝。

4.2.1 核心规则
  • 可序列化类型stringnumberbooleanarray、普通对象(不含函数、Symbol)。
  • 不可序列化类型functionSymbolundefinedRegExpErrorDate(会被转为 ISO 字符串)。
4.2.2 代码实现与运行结果

javascript

运行

// 1. 对象序列化(转为 JSON 字符串)
var person = {
  name: "张三",
  age: 18,
  hobbies: ["篮球", "编程"],
  birthday: new Date("2005-01-01")
};
var jsonStr = JSON.stringify(person);
console.log("序列化结果:", jsonStr);
console.log("序列化结果类型:", typeof jsonStr);
// 2. 对象反序列化(JSON 字符串转为对象)
var parsedPerson = JSON.parse(jsonStr);
console.log("反序列化结果:", parsedPerson);
console.log("反序列化后 birthday 类型:", typeof parsedPerson.birthday); // Date 被转为字符串
// 3. 深拷贝(解决引用类型浅拷贝问题)
var obj1 = { a: 1, b: { c: 2 } };
var obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝
obj2.b.c = 3; // 修改 obj2 的引用属性
console.log("obj1.b.c:", obj1.b.c); // 原对象不受影响
console.log("obj2.b.c:", obj2.b.c);

运行结果

plaintext

序列化结果: {"name":"张三","age":18,"hobbies":["篮球","编程"],"birthday":"2004-12-31T16:00:00.000Z"}
序列化结果类型: string
反序列化结果: {
  name: '张三',
  age: 18,
  hobbies: [ '篮球', '编程' ],
  birthday: '2004-12-31T16:00:00.000Z'
}
反序列化后 birthday 类型: string
obj1.b.c: 2
obj2.b.c: 3

4.3 原型链与继承:对象间的 “传承”

4.3.1 原型链基础
  • 原型链:实例的 __proto__ → 构造函数的 prototype → 父构造函数的 prototype → ... → Object.prototype(原型链顶端)。
  • 继承本质:通过原型链让子对象访问父对象的属性与方法,是 JS 实现继承的核心机制。
4.3.2 代码实现与运行结果

javascript

运行

// 父构造函数
function Animal(name) {
  this.name = name;
}
// 父原型方法
Animal.prototype.sayName = function() {
  console.log("动物名称:" + this.name);
};
// 子构造函数:通过原型链继承 Animal
function Dog(name) {
  Animal.call(this, name); // 盗用构造函数,继承实例属性
}
Dog.prototype = new Animal(); // 原型链继承,继承原型方法
Dog.prototype.constructor = Dog; // 修复 constructor 指向(否则指向 Animal)
// 子原型方法:扩展功能
Dog.prototype.sayWang = function() {
  console.log("汪汪汪!");
};
// 测试继承
var dog = new Dog("小黑");
dog.sayName(); // 继承父原型方法
dog.sayWang(); // 子原型方法
console.log("dog 是 Dog 实例吗?", dog instanceof Dog);
console.log("dog 是 Animal 实例吗?", dog instanceof Animal);

运行结果

plaintext

动物名称:小黑
汪汪汪!
dog 是 Dog 实例吗? true
dog 是 Animal 实例吗? true

五、实战案例:封装一个 “学生信息管理对象”

基于组合模式与原型链,实现一个支持 “添加学生、查询学生、修改成绩” 的管理对象,展示对象高级特性的实战应用。

5.1 代码实现

javascript

运行

// 1. 学生构造函数(实例属性)
function Student(name, studentId, score) {
  this.name = name;
  this.studentId = studentId;
  this.score = score; // 成绩:{ math: 0, chinese: 0, english: 0 }
}
// 2. 学生原型方法(共享方法)
Student.prototype.calcAverage = function() {
  // 计算平均分
  var total = this.score.math + this.score.chinese + this.score.english;
  return (total / 3).toFixed(2);
};
Student.prototype.showInfo = function() {
  console.log(`学号:${this.studentId},姓名:${this.name},平均分:${this.calcAverage()}`);
};
// 3. 管理对象(单例模式,无需实例化)
var StudentManager = {
  students: [], // 存储学生实例
  // 添加学生
  addStudent: function(student) {
    if (student instanceof Student) {
      this.students.push(student);
      console.log(`成功添加学生:${student.name}`);
    } else {
      console.error("添加失败:必须传入 Student 实例");
    }
  },
  // 根据学号查询学生
  findStudentById: function(studentId) {
    return this.students.find(student => student.studentId === studentId);
  },
  // 修改学生成绩
  updateScore: function(studentId, subject, newScore) {
    var student = this.findStudentById(studentId);
    if (student && student.score[subject]) {
      student.score[subject] = newScore;
      console.log(`成功修改 ${student.name} 的 ${subject} 成绩为 ${newScore}`);
    } else {
      console.error("修改失败:学生不存在或科目无效");
    }
  }
};
// 4. 测试
// 添加学生
var student1 = new Student("张三", "2025001", { math: 90, chinese: 85, english: 95 });
var student2 = new Student("李四", "2025002", { math: 88, chinese: 92, english: 80 });
StudentManager.addStudent(student1);
StudentManager.addStudent(student2);
// 查询并展示学生信息
var foundStudent = StudentManager.findStudentById("2025001");
foundStudent.showInfo();
// 修改成绩
StudentManager.updateScore("2025002", "math", 95);
var updatedStudent = StudentManager.findStudentById("2025002");
updatedStudent.showInfo();

5.2 运行结果

plaintext

成功添加学生:张三
成功添加学生:李四
学号:2025001,姓名:张三,平均分:90.00
成功修改 李四 的 math 成绩为 95
学号:2025002,姓名:李四,平均分:92.33

六、总结与开发建议

  1. 对象创建模式选择

    • 简单对象(无复用需求):用对象字面量(var obj = {})。
    • 批量创建相似对象:用组合模式(构造函数 + 原型),兼顾独立性与性能。
    • 单例对象(如管理类):用对象字面量直接定义(var Manager = {})。
  2. 属性控制要点

    • 需保护的属性(如 “年龄”):用访问器属性(get/set)添加校验逻辑。
    • 不可修改的常量:用 Object.defineProperty() 设置 writable: false
    • 避免枚举的属性:设置 enumerable: false(如内部方法)。
  3. 序列化与继承避坑

    • 序列化时注意不可序列化类型(如函数、Symbol),需提前过滤。
    • 继承时修复 constructor 指向,避免类型判断错误。
    • 原型上避免定义引用类型属性,防止数据污染。