人人都会AI编程

4.1 对象的本质:属性集合与内部方法

更新时间:2026-07-11

在 JavaScript 中,对象是除了原始类型之外最核心的数据结构。任何稍微复杂一点的程序,都离不开对象的参与。要理解原型、继承、this 绑定这些进阶主题,必须先看清对象的底层本质:它到底是什么?

4.1.1 对象是一个动态的属性集合

从表面上看,对象就是一组键值对的集合(也称为“属性的容器”):

const person = {
  name: "张三",
  age: 25,
  greet() {
    console.log(`你好,我是${this.name}`);
  }
};

然而,这个看似简单的集合背后,远不止“键值对”三个字能概括。真正让对象拥有强大能力的,是每个属性背后隐藏的属性描述符以及对象内置的内部方法

4.1.2 属性的真实结构:属性描述符

每当我们给对象添加一个属性,JavaScript 引擎并非简单地存储一个值,而是创建一个属性描述符,它可能包含以下“元信息”:

  • value:属性的值(数据属性独有)。
  • writable:属性的值是否可以被修改。
  • enumerable:属性是否出现在 for...in 循环和 Object.keys() 中。
  • configurable:属性是否可以被删除,以及其描述符(除 valuewritable 外)是否可以被修改。

除了最常见的数据属性,还有一类访问器属性,用 getset 函数替代 value

  • get:读取属性时调用的函数。
  • set:设置属性时调用的函数。
const obj = {};

// 定义一个不可枚举的数据属性
Object.defineProperty(obj, 'secret', {
  value: 42,
  writable: false,
  enumerable: false,
  configurable: false
});

// 定义一个访问器属性
Object.defineProperty(obj, 'fullName', {
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  set(value) {
    [this.firstName, this.lastName] = value.split(' ');
  },
  enumerable: true,
  configurable: true
});

这意味着:对象不仅仅存储值,还精确控制每个属性的访问、修改、枚举和删除行为。这层元编程能力是对象灵活性的重要来源,也是 Vue 2.x 中 Object.defineProperty 实现响应式的基础。

4.1.3 隐藏的内部方法:决定对象行为的底层机制

除了外在的属性,每个对象还拥有一组内部方法,这些方法并不直接暴露给开发者,但决定了对象在各种操作下的实际行为。根据 ECMAScript 规范,这些内部方法用双中括号 [[...]] 表示,常见的有:

  • [[Get]](P, Receiver):读取属性 P 时的内部操作。当属性不存在时,会触发原型链查找,最终可能返回 undefined
  • [[Set]](P, Value, Receiver):设置属性 P 值时的内部操作。同样可能触发原型链上的 setter。
  • [[HasProperty]](P):判断对象是否拥有属性 P"P" in obj 的底层实现)。
  • [[Delete]](P):删除属性(delete obj[P] 的背后逻辑)。
  • [[GetPrototypeOf]]() / [[SetPrototypeOf]](O):获取和设置原型(Object.getPrototypeOf / Object.setPrototypeOf 的背后支持)。
  • [[Call]]() / [[Construct]]():分别用于函数调用和构造函数调用,区别了普通调用和 new 调用。

这些内部方法的存在,统一了解释引擎如何处理属性的存取、删除等操作。更重要的是,它们揭示了原型链的实现原理:当 [[Get]] 方法在自身找不到属性时,会通过 [[GetPrototypeOf]] 去原型上继续调用 [[Get]],形成查找链。这也就是原型继承的底层串联方式。

4.1.4 对象本质 = 属性集合 + 内部方法

结合以上两点,我们可以给出一个贴近引擎实现的对象定义:

在 JavaScript 中,对象是一组属性的逻辑集合,每个属性通过属性描述符精细控制其行为(值、可写性、可枚举性、可配置性、getter/setter)。同时,对象内置了一套内部方法,定义了引擎如何与对象交互,它们在原型查找、属性访问、函数调用等环节发挥着核心作用。

理解了这个本质,接下来学习原型链就不会感到突兀:原型之所以能被“继承”属性,正是因为 [[Get]] 内部方法会自动沿着 [[Prototype]] 链向上搜索。同样,理解 Object.definePropertyProxy 等高级特性,也是直接与属性描述符和内部方法打交道。

从实用的角度看,日常工作里你很可能不需要手动定义属性描述符,但知道它们的存在,能帮你解释一些“奇怪”的现象:比如为什么循环遍历时有些属性不出现(enumerable: false),为什么 console.log(Math.PI) 能看到值但改不了(writable: false),为什么某些库能拦截你对普通对象的读写操作。这些都不是魔法,只是属性的真实结构在起作用。