JavaScript 对象与属性描述符:从原理到实战
背景:为什么要深入理解对象?
在日常开发中,我们经常会遇到这样的困惑:
- 为什么有些对象属性用
for-in遍历不出来? - 为什么
delete有时能删除属性,有时却失效? - Vue2 的响应式原理到底是怎么"劫持"属性访问的?
这些问题的答案都指向同一个核心概念:属性描述符。它是 JavaScript 对象系统的底层机制,掌握它不仅能让你理解框架源码,还能写出更精准、更可控的代码。
本文将从面向对象的本质出发,逐步深入到属性描述符的细节,并结合实际场景帮你建立完整的知识体系。
你将收获:
- 理解 JavaScript 面向对象的设计思想
- 掌握属性描述符的 6 种特性及应用场景
- 学会用
Object.defineProperty精准控制对象行为 - 具备阅读 MDN 文档和框架源码的基础能力
一、面向对象:用代码模拟现实世界
1.1 什么是面向对象?
面向对象编程(OOP)的核心思想是:用包含数据和行为的对象来模拟现实世界的实体。
举个例子:
- 一辆车(Car):有颜色、速度、品牌、价格等属性,有行驶、刹车等方法
- 一个人(Person):有姓名、年龄、身高等属性,有吃饭、跑步等方法
这种抽象方式让代码结构更清晰,也更贴近人类的思维方式。在 JavaScript 中,面向对象主要体现在两个方面:
- 封装:把相关数据和方法组织在一起(函数、模块、对象都是封装)
- 继承:通过原型链实现代码复用(这是 JS 的重点,后续会详细讲解)
1.2 JavaScript 中的对象设计
JavaScript 支持多种编程范式,对象被设计成属性的无序集合,类似哈希表:
{
key: value
}
- key:标识符名称(字符串或 Symbol)
- value:任意类型(基本类型、对象、函数等)
- 如果 value 是函数,我们称之为方法
1.3 创建对象的两种方式
方式一:new Object()(构造函数方式)
var person1 = new Object();
person1.name = "小吴";
person1.age = 18;
person1.greet = function() {
console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
};
person1.greet(); // 输出: Hello, my name is 小吴 and I am 18 years old.
适用场景:
- 需要动态添加属性的复杂逻辑
- 有 Java/C++ 等面向对象语言背景的开发者
历史背景: JavaScript 早期为了蹭 Java 的热度,在命名和语法上刻意模仿,导致很多 Java 开发者习惯用这种方式。
方式二:对象字面量(推荐)
var person2 = {
name: "小吴",
age: 18,
greet: function() {
console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
}
};
person2.greet(); // 输出: Hello, my name is 小吴 and I am 18 years old.
优势:
- 代码简洁,结构清晰
- 属性和方法内聚性强
- 性能略优(省略函数调用开销)
二、属性描述符:精准控制对象行为
2.1 为什么需要属性描述符?
通常我们直接定义属性:
var obj = {
name: "小吴",
age: 20,
sex: "男"
};
// 获取属性
console.log(obj.name); // 小吴
// 修改属性
obj.name = "XiaoWu";
console.log(obj.name); // XiaoWu
// 删除属性
delete obj.name;
console.log(obj); // { age: 20, sex: '男' }
但这种方式无法控制:
- 这个属性能否被
delete删除? - 这个属性能否被
for-in遍历? - 这个属性能否被重新赋值?
属性描述符就是用来解决这些问题的工具。
2.2 Object.defineProperty 基础用法
Object.defineProperty(obj, prop, descriptor)
参数说明:
-
obj:目标对象 -
prop:属性名(字符串或 Symbol) -
descriptor:属性描述符对象(核心)
返回值: 修改后的原对象(非纯函数)
示例:
var obj = {
name: "XiaoWu",
age: 20
};
Object.defineProperty(obj, "height", {
value: 1.75
});
console.log(obj); // Node 环境:{ name: 'XiaoWu', age: 20 }
疑问:为什么 height 没显示出来?
![]()
图 1:浏览器控制台显示了 height 属性
原因分析:
-
height默认是不可枚举的(enumerable: false) -
Node.js 的
console.log使用util.inspect(),默认只显示可枚举属性(遵循 ECMAScript 标准) - 浏览器控制台 为了调试方便,会显示所有属性(包括不可枚举属性)
验证属性确实存在:
console.log(obj.height); // 1.75(可以访问)
让属性可枚举:
Object.defineProperty(obj, "height", {
value: 1.75,
enumerable: true // 设置为可枚举
});
console.log(obj); // { name: 'XiaoWu', age: 20, height: 1.75 }
三、属性描述符的两种类型
属性描述符分为两大类,它们不能混用:
| 类型 | configurable | enumerable | value | writable | get | set |
|---|---|---|---|---|---|---|
| 数据描述符 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 存取描述符 | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
记忆口诀: 2 共用 + 2 可选,同时生效最多 4 种
3.1 为什么不能混用?
本质原因: 它们代表了两种完全不同的属性管理方式
- 数据描述符(静态):属性持有一个具体的值,可以直接读写
- 存取描述符(动态):属性值通过函数动态计算,每次访问可能不同
如果同时定义,JavaScript 引擎无法判断应该直接操作值还是调用函数,因此规范禁止混用。
类比理解:
- 数据描述符 = 名词(静态的"数据")
- 存取描述符 = 动词(动态的"存取"操作)
四、数据描述符详解
4.1 四大特性
[[Configurable]]:可配置性
控制属性是否可以:
- 被
delete删除 - 修改其他描述符特性
- 转换为存取描述符
默认值:
- 直接定义:
true - 通过描述符定义:
false
[[Enumerable]]:可枚举性
控制属性是否可以:
- 被
for-in遍历 - 被
Object.keys()返回
默认值:
- 直接定义:
true - 通过描述符定义:
false
[[Writable]]:可写性
控制属性值是否可以被修改。
默认值:
- 直接定义:
true - 通过描述符定义:
false
[[Value]]:属性值
属性的实际值。
默认值: undefined
4.2 实战案例
var obj = {
name: "XiaoWu",
age: 18
};
// 定义一个受控属性
Object.defineProperty(obj, "address", {
value: "福建省",
configurable: false, // 不可删除、不可重新配置
enumerable: true, // 可枚举
writable: false // 不可修改
});
// 测试 configurable
delete obj.name;
console.log(obj); // { age: 18, address: '福建省' }(name 被删除)
delete obj.address;
console.log(obj.address); // 福建省(删除失败)
// 测试 enumerable
console.log(Object.keys(obj)); // [ 'age', 'address' ]
for (var key in obj) {
console.log(key); // age, address
}
// 测试 writable
obj.address = "上海市";
console.log(obj.address); // 福建省(修改失败)
关键点:
- 直接定义的属性(
name、age)默认所有特性都是true - 通过描述符定义的属性(
address)默认所有特性都是false
五、存取描述符详解
5.1 四大特性
-
[[Configurable]]:同数据描述符 -
[[Enumerable]]:同数据描述符 -
[[Get]]:获取属性时执行的函数,默认undefined -
[[Set]]:设置属性时执行的函数,默认undefined
5.2 应用场景
场景一:隐藏私有属性
var obj = {
name: "小吴",
age: 18,
_address: "泉州市" // _ 开头表示私有属性(约定俗成)
};
Object.defineProperty(obj, "address", {
enumerable: true,
configurable: true,
get: function() {
return this._address; // 通过 address 访问 _address
},
set: function(value) {
this._address = value;
}
});
console.log(obj.address); // 泉州市
obj.address = "厦门市";
console.log(obj.address); // 厦门市
注意: ES6 后可以用 # 定义真正的私有属性(后续会讲)。
场景二:拦截属性访问(Vue2 响应式原理)
var obj = {
name: "小吴",
age: 18,
_address: "泉州市"
};
Object.defineProperty(obj, "address", {
enumerable: true,
configurable: true,
get: function() {
console.log("获取了一次 address 的值"); // 拦截读取
return this._address;
},
set: function(value) {
console.log("设置了一次 address 的值"); // 拦截写入
this._address = value;
}
});
console.log(obj.address);
// 输出:获取了一次 address 的值
// 泉州市
obj.address = "why";
// 输出:设置了一次 address 的值
console.log(obj.address);
// 输出:获取了一次 address 的值
// why
核心价值: 这就是 Vue2 响应式系统的底层原理——通过 get/set 拦截属性访问,实现依赖收集和派发更新。
六、学习属性描述符的实战意义
6.1 理解原生 API 的能力边界
所有原生对象的 API 都有属性描述符,这决定了它们的行为:
- 为什么
Array.prototype上的方法用for-in遍历不出来?(enumerable: false) - 为什么
Object.prototype.toString不能被删除?(configurable: false)
6.2 读懂技术文档
MDN 文档中大量使用属性描述符来描述 API 特性:
![]()
图 2:MDN 文档对 API 能力边界的描述
掌握这些概念后,你能:
- 快速理解 API 的使用限制
- 预判代码的行为边界
- 避免踩坑(比如误删不可配置的属性)
6.3 降低框架学习门槛
React、Vue 等框架文档中会用到这些术语:
![]()
图 3:React 文档中的专业术语
学完 JavaScript 高级后,这些词汇对你来说将不再陌生。
七、关键要点总结
- 属性描述符分两类:数据描述符(静态值)和存取描述符(动态函数),不能混用
-
默认值差异:直接定义的属性默认可配置/可枚举/可写,通过描述符定义的默认都是
false -
核心应用场景:
- 隐藏私有属性(用
get/set代理访问) - 拦截属性访问(实现响应式、日志、校验等)
- 精准控制对象行为(防删除、防修改、防遍历)
- 隐藏私有属性(用
- 实战价值:理解原生 API、读懂技术文档、掌握框架原理
八、下一步建议
团队落地建议:
- 在工具函数库中封装常用的属性控制逻辑(如冻结对象、只读属性等)
- Code Review 时关注属性描述符的使用是否合理
- 在复杂对象设计中主动使用描述符提升代码健壮性
后续学习方向:
- 批量定义属性描述符(
Object.defineProperties) - 对象方法补充(
Object.freeze、Object.seal等) - 工厂函数与构造函数
- 原型链与继承机制
下一篇我们将深入构造函数,探索更高效的对象创建方案。