第50课:对象与原型(上)——对象字面量、属性描述符与 Object 静态方法
在 JavaScript 中,对象(Object) 是核心的复合数据类型。几乎所有非原始值(包括函数、数组、正则表达式)都是对象。理解对象如何创建、如何组织属性、以及属性本身的元信息如何控制对象行为,是深入 JavaScript 语言的关键一步。本节课将聚焦于对象字面量的增强语法、属性描述符(Property Descriptor)的精确控制、以及 Object 构造函数上最重要的静态方法。原型链将在第 51 课详述。
1. 对象字面量:创建对象的最简方式
对象字面量 {} 是 JavaScript 中创建对象最直观、最常用的语法。ES6 之后,对象字面量得到了多项增强,使代码更简洁、表达能力更强。
1.1 属性值简写
当属性名与变量名相同时,可以省略冒号和值,直接写变量名。
1 | const name = 'Alice'; |
1.2 方法简写
省略 : function,直接写方法名和函数体。这样定义的方法与普通函数方法有细微的 this 行为差异(但在此处不展开)。
1 | // ES5 写法 |
注意:方法简写定义的函数不能用作构造函数(不能 new),而 function 定义的方法可以。这是两者在行为上的关键差异。
1.3 计算属性名
使用方括号 [] 包裹一个表达式,将该表达式的计算结果作为属性名。这使得动态生成属性名变得极其简单。
1 | const key = 'favoriteColor'; |
计算属性名内的表达式在对象创建时求值,任何能产生字符串或 Symbol 值的表达式均可使用。
1.4 展开运算符 ...(对象展开)
ES2018 引入了对象的展开运算符,可以将一个对象的自有可枚举属性浅拷贝到另一个对象中。
1 | const defaults = { theme: 'light', fontSize: 14 }; |
浅拷贝意味着:如果属性值是对象或数组,展开运算符只复制引用,新旧对象将共享嵌套的可变值。
1 | const original = { nested: { value: 1 } }; |
2. 属性描述符:属性的元信息
对象的每个属性都有一套属性描述符,它定义了该属性在特定操作下的行为。ES5 之前这些内部特性不可直接操作,现在可以通过 Object.defineProperty 和 Object.defineProperties 精确控制。
2.1 两种描述符类型
属性描述符分为数据描述符和存取描述符,两者互斥(不能同时设置 value/writable 和 get/set)。
| 描述符属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
value |
数据描述符 | undefined |
属性的值。 |
writable |
数据描述符 | false |
若为 false,属性值只读(严格模式下赋值会抛出 TypeError)。 |
get |
存取描述符(getter) | undefined |
取值函数,无参数,读取属性时调用。 |
set |
存取描述符(setter) | undefined |
存值函数,接收一个参数,设置属性时调用。 |
enumerable |
通用 | false |
若为 true,属性可被 for...in 和 Object.keys() 遍历。 |
configurable |
通用 | false |
若为 true,属性可被删除(delete)且描述符可修改(writable 可从 true 改为 false)。 |
2.2 Object.defineProperty:定义单个属性
1 | const user = {}; |
重要默认值:当使用字面量 { name: 'Alice' } 创建属性时,writable、enumerable、configurable 均默认为 true。但使用 Object.defineProperty 添加属性时,这三个布尔描述符**默认都为 false**。这是极易出错的差异点。
1 | const obj = {}; |
2.3 Getter 和 Setter(存取描述符)
通过 get 和 set 定义计算属性,是封装内部状态和控制访问的常用手段。
1 | const person = { |
使用 Object.defineProperty 定义 getter/setter:
1 | const product = { _price: 0 }; |
2.4 Object.defineProperties:批量定义
1 | const obj = {}; |
3. Object 静态方法:操作对象的利器
Object 构造函数上提供了一组静态方法,用于获取属性信息、控制对象行为、进行对象级别操作。
3.1 属性查询与遍历
| 方法 | 返回值 | 范围 |
|---|---|---|
Object.keys(obj) |
string[] |
自身可枚举非 Symbol 属性键。 |
Object.values(obj) |
any[] |
自身可枚举非 Symbol 属性值。 |
Object.entries(obj) |
[string, any][] |
自身可枚举非 Symbol 键值对。 |
Object.getOwnPropertyNames(obj) |
string[] |
自身所有非 Symbol 属性键(含不可枚举)。 |
Object.getOwnPropertySymbols(obj) |
symbol[] |
自身所有 Symbol 属性键。 |
1 | const sym = Symbol('id'); |
3.2 属性描述符相关
Object.getOwnPropertyDescriptor(obj, prop):获取单个属性的描述符对象。Object.getOwnPropertyDescriptors(obj):获取所有自有属性的描述符对象集合(ES2017)。
1 | const obj = { count: 10 }; |
Object.getOwnPropertyDescriptors 配合 Object.defineProperties 可用来创建对象的完整浅拷贝(包括 getter/setter 和不可枚举属性):
1 | const source = { |
3.3 对象控制与保护
| 方法 | 说明 | 可添加属性 | 可删除属性 | 可修改属性值 | 可修改描述符 |
|---|---|---|---|---|---|
Object.preventExtensions(obj) |
阻止添加新属性。 | ❌ | ✅ | ✅ | ✅ |
Object.seal(obj) |
阻止添加和删除属性,并将所有现有属性的 configurable 设为 false。 |
❌ | ❌ | ✅ | ❌ |
Object.freeze(obj) |
阻止添加、删除、修改属性值,并将所有现有属性的 writable 和 configurable 设为 false。 |
❌ | ❌ | ❌ | ❌ |
1 | const obj = { name: 'Alice' }; |
对应检测方法:
Object.isExtensible(obj)→ 如果未调用preventExtensions,返回true。Object.isSealed(obj)→ 如果调用了seal或freeze,返回true。Object.isFrozen(obj)→ 如果调用了freeze,返回true。
重要限制:这三种方法都是浅层的——只影响对象的直接属性。嵌套对象内部的属性不受影响,仍可修改。
1 | const user = { profile: { name: 'Alice' } }; |
实现深冻结需要递归处理:
1 | function deepFreeze(obj) { |
3.4 对象比较与转换
Object.is(value1, value2):比较两个值是否相同。与===的主要区别在于对NaN和-0的处理。
1 | console.log(NaN === NaN); // false |
Object.assign(target, ...sources):将源对象的自有可枚举属性浅拷贝到目标对象。这是修改目标对象并返回它。
1 | const target = { a: 1 }; |
Object.assign 会触发目标对象的 setter(如果有),而展开运算符 { ...obj } 不会触发 setter。这是两者在使用上需要注意的细微差异。
4. 可选链 ?. 与空值合并 ?? 在对象访问中的应用
这两项现代语法在处理深层嵌套对象访问时提供了极大的安全性和便利性。
1 | const user = null; |
可选链也可用于方法调用和动态属性访问:
1 | const obj = null; |
课后练习
一、概念自测(选择题 / 填空题)
(单选) 使用
Object.defineProperty(obj, 'prop', { value: 1 })定义的属性,其writable默认值是?
A.true
B.false
C.undefined
D. 与字面量定义一致(单选) 以下哪个方法可以同时获取对象的自有 Symbol 属性和非 Symbol 属性(包括不可枚举属性)?
A.Object.keys()
B.Object.getOwnPropertyNames()
C.Object.getOwnPropertyNames()+Object.getOwnPropertySymbols()
D.Object.entries()(填空) 要创建一个对象的浅拷贝,同时保留所有属性描述符(包括 getter/setter),应使用
Object.getOwnPropertyDescriptors()配合______方法。(多选) 以下哪些操作会被
Object.freeze()阻止?
A. 添加新属性
B. 删除现有属性
C. 修改现有属性的值
D. 修改嵌套对象的属性值
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 JavaScript 函数 createReadOnlyProxy(obj),它接收一个对象并返回一个新的对象,该对象的所有一级属性都是只读的(无法修改、无法添加、无法删除),同时保留对原有嵌套对象的正常读写。要求如下:
- 使用
Object.defineProperties配合Object.getOwnPropertyDescriptors实现。 - 原对象的属性如果是对象,应保持其可变性(即浅层只读)。
- 使用
const声明函数,变量命名遵循 camelCase。 - 添加 JSDoc 注释说明参数和返回值。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定使用的方法组合、浅层只读的语义、以及对 getter/setter 的保留。
三、面试真题与参考答案
题目(腾讯前端面试题):
请详细解释
Object.freeze()、Object.seal()和Object.preventExtensions()三者的区别,并说明它们在实际开发中的典型应用场景。另外,为什么说const声明的对象仍然是可变的,而Object.freeze()才能真正实现不可变?
参考答案:
1. 三者区别:
| 方法 | 添加新属性 | 删除属性 | 修改属性值 | 修改属性描述符 |
|---|---|---|---|---|
Object.preventExtensions() |
❌ | ✅ | ✅ | ✅ |
Object.seal() |
❌ | ❌ | ✅ | ❌ |
Object.freeze() |
❌ | ❌ | ❌ | ❌ |
2. 典型应用场景:
Object.freeze():定义应用程序常量配置对象,防止意外修改;在 Redux 中确保状态不可变(配合深冻结或 Immutable.js)。Object.seal():固定对象的“形状”(键集合),但允许更新值。例如,某个 API 返回的数据结构必须包含固定字段,但字段值可刷新。Object.preventExtensions():防止对象被注入意外属性,但允许修改和删除现有属性。在库的内部维护对象结构时使用。
3. const vs Object.freeze():const 声明的变量是绑定不可变,即变量名不能重新赋值为另一个值。但它指向的对象内部的属性仍可随意修改。Object.freeze() 则是值不可变,它冻结对象本身,阻止任何对对象属性的修改、添加或删除。两者作用于不同层次:const 保护引用,freeze 保护对象内容。真正的不可变对象需要两者结合——用 const 声明并用 Object.freeze() 冻结。
4. 浅冻结限制:Object.freeze() 仅冻结对象的第一层属性。嵌套对象仍可被修改。要实现深度不可变,需要递归调用 Object.freeze()(深冻结),或使用专门的不可变数据库(如 Immer、Immutable.js)。
课后练习答案
一、概念自测答案
B
- 解析:
Object.defineProperty添加属性时,writable、enumerable、configurable均默认为false,与字面量创建属性(均为true)不同。
- 解析:
C
- 解析:
getOwnPropertyNames获取所有非 Symbol 属性,getOwnPropertySymbols获取所有 Symbol 属性,两者组合覆盖所有自有属性(含不可枚举)。
- 解析:
Object.defineProperties- 解析:
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source))是保留属性描述符的浅拷贝模式。
- 解析:
A、B、C
- 解析:
Object.freeze()阻止添加、删除和修改一级属性的值。D 错误——嵌套对象不受影响,仍可修改其内部属性。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数createReadOnlyProxy(obj),接收一个对象并返回其浅层只读的版本。要求:
- 使用
Object.getOwnPropertyDescriptors(obj)获取原对象所有自有属性的描述符。- 遍历这些描述符,将每个属性的
writable设为false、configurable设为false(若原来有 setter,则保留 setter 但不设置 writable)。- 使用
Object.defineProperties({}, modifiedDescriptors)创建新对象。- 最终对返回对象调用
Object.preventExtensions()防止添加新属性。- 浅层只读:原对象的嵌套对象属性值保持不变(不递归)。
- 使用
const声明函数,添加 JSDoc 注释。- 提供使用示例并验证修改失败。直接输出完整代码。”