在 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:属性是否可以被删除,以及其描述符(除value和writable外)是否可以被修改。
除了最常见的数据属性,还有一类访问器属性,用 get 和 set 函数替代 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.defineProperty 和 Proxy 等高级特性,也是直接与属性描述符和内部方法打交道。
从实用的角度看,日常工作里你很可能不需要手动定义属性描述符,但知道它们的存在,能帮你解释一些“奇怪”的现象:比如为什么循环遍历时有些属性不出现(enumerable: false),为什么 console.log(Math.PI) 能看到值但改不了(writable: false),为什么某些库能拦截你对普通对象的读写操作。这些都不是魔法,只是属性的真实结构在起作用。