JavaScript|第12章:属性配置与 getter/setter

JavaScript|第12章:属性配置与 getter/setter

属性配置与getter/setter

  • [JavaScript|第12章:属性配置与 getter/setter](#JavaScript|第12章:属性配置与 getter/setter)
    • 引言
    • [12.1 属性标志与属性描述符](#12.1 属性标志与属性描述符)
      • [12.1.1 属性不只是键值对](#12.1.1 属性不只是键值对)
      • [12.1.2 查看属性描述符](#12.1.2 查看属性描述符)
      • [12.1.3 writable --- 控制属性是否可写](#12.1.3 writable — 控制属性是否可写)
      • [12.1.4 enumerable --- 控制属性是否可枚举](#12.1.4 enumerable — 控制属性是否可枚举)
      • [12.1.5 configurable --- 控制属性是否可重新配置](#12.1.5 configurable — 控制属性是否可重新配置)
      • [12.1.6 属性标志速查表](#12.1.6 属性标志速查表)
    • [12.2 Object.defineProperty 与 Object.defineProperties](#12.2 Object.defineProperty 与 Object.defineProperties)
      • [12.2.1 Object.defineProperty 基本语法](#12.2.1 Object.defineProperty 基本语法)
      • [12.2.2 定义数据属性](#12.2.2 定义数据属性)
      • [12.2.3 defineProperty 的默认值陷阱](#12.2.3 defineProperty 的默认值陷阱)
      • [12.2.4 Object.defineProperties --- 批量定义](#12.2.4 Object.defineProperties — 批量定义)
      • [12.2.5 Object.defineProperty 修改已有属性](#12.2.5 Object.defineProperty 修改已有属性)
      • [12.2.6 属性的删除:delete 的条件](#12.2.6 属性的删除:delete 的条件)
    • [12.3 getter 与 setter](#12.3 getter 与 setter)
      • [12.3.1 什么是访问器属性?](#12.3.1 什么是访问器属性?)
      • [12.3.2 在对象字面量中定义 getter/setter](#12.3.2 在对象字面量中定义 getter/setter)
      • [12.3.3 getter/setter 与数据属性的关系](#12.3.3 getter/setter 与数据属性的关系)
      • [12.3.4 使用 Object.defineProperty 定义访问器属性](#12.3.4 使用 Object.defineProperty 定义访问器属性)
      • [12.3.5 只有 getter(只读计算属性)](#12.3.5 只有 getter(只读计算属性))
      • [12.3.6 class 中的 getter/setter](#12.3.6 class 中的 getter/setter)
      • [12.3.7 访问器属性的描述符](#12.3.7 访问器属性的描述符)
    • [12.4 属性的遍历与复制](#12.4 属性的遍历与复制)
      • [12.4.1 各种遍历方法的枚举性差异](#12.4.1 各种遍历方法的枚举性差异)
      • [12.4.2 Object.assign 与可枚举性](#12.4.2 Object.assign 与可枚举性)
      • [12.4.3 对象的冻结方法](#12.4.3 对象的冻结方法)
    • [12.5 实战](#12.5 实战)
      • [12.5.1 实战1:手写 Object.create](#12.5.1 实战1:手写 Object.create)
      • [12.5.2 实战2:实现数据响应式(Vue2 原理)](#12.5.2 实战2:实现数据响应式(Vue2 原理))
      • [12.5.3 实战3:实现可观察的属性代理](#12.5.3 实战3:实现可观察的属性代理)
      • [12.5.4 实战4:实现常量管理器](#12.5.4 实战4:实现常量管理器)
      • [12.5.5 实战5:防篡改的对象克隆](#12.5.5 实战5:防篡改的对象克隆)
    • [12.6 defineProperty 的限制与 Proxy 的对比](#12.6 defineProperty 的限制与 Proxy 的对比)
      • [12.6.1 defineProperty 的三大局限](#12.6.1 defineProperty 的三大局限)
      • [12.6.2 Proxy vs defineProperty](#12.6.2 Proxy vs defineProperty)
    • 本章小结

上篇回顾第11章:构造器与 new 操作符

下篇预告第13章:原始类型的方法


引言

在前面的章节中,我们学会了用对象字面量、构造函数等方式创建对象,并给它们添加属性。但你有没有想过:为什么 Object.keys() 能遍历到某些属性,却遍历不到另一些?为什么 Math.PI 是只读的,尝试修改它时不会报错也不会生效?为什么修改数组的 length 会影响数组内容?

这些问题的答案都指向 JavaScript 属性系统的底层机制------属性描述符

每个属性不只是"一个键值对",它还携带着一组隐藏的配置信息,控制着这个属性能否被修改、能否被遍历、能否被重新配置。掌握这些配置,你就能:

  • 保护关键属性不被意外修改
  • 隐藏内部属性不被遍历发现
  • 拦截属性的读写,实现数据响应式
  • 定义计算属性,让属性值自动依赖其他属性

#mermaid-svg-Ke9A1qzD718dgIog{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ke9A1qzD718dgIog .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ke9A1qzD718dgIog .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ke9A1qzD718dgIog .error-icon{fill:#552222;}#mermaid-svg-Ke9A1qzD718dgIog .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ke9A1qzD718dgIog .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ke9A1qzD718dgIog .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ke9A1qzD718dgIog .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ke9A1qzD718dgIog .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ke9A1qzD718dgIog .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ke9A1qzD718dgIog .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ke9A1qzD718dgIog .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ke9A1qzD718dgIog .marker.cross{stroke:#333333;}#mermaid-svg-Ke9A1qzD718dgIog svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ke9A1qzD718dgIog p{margin:0;}#mermaid-svg-Ke9A1qzD718dgIog .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ke9A1qzD718dgIog .cluster-label text{fill:#333;}#mermaid-svg-Ke9A1qzD718dgIog .cluster-label span{color:#333;}#mermaid-svg-Ke9A1qzD718dgIog .cluster-label span p{background-color:transparent;}#mermaid-svg-Ke9A1qzD718dgIog .label text,#mermaid-svg-Ke9A1qzD718dgIog span{fill:#333;color:#333;}#mermaid-svg-Ke9A1qzD718dgIog .node rect,#mermaid-svg-Ke9A1qzD718dgIog .node circle,#mermaid-svg-Ke9A1qzD718dgIog .node ellipse,#mermaid-svg-Ke9A1qzD718dgIog .node polygon,#mermaid-svg-Ke9A1qzD718dgIog .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ke9A1qzD718dgIog .rough-node .label text,#mermaid-svg-Ke9A1qzD718dgIog .node .label text,#mermaid-svg-Ke9A1qzD718dgIog .image-shape .label,#mermaid-svg-Ke9A1qzD718dgIog .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ke9A1qzD718dgIog .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ke9A1qzD718dgIog .rough-node .label,#mermaid-svg-Ke9A1qzD718dgIog .node .label,#mermaid-svg-Ke9A1qzD718dgIog .image-shape .label,#mermaid-svg-Ke9A1qzD718dgIog .icon-shape .label{text-align:center;}#mermaid-svg-Ke9A1qzD718dgIog .node.clickable{cursor:pointer;}#mermaid-svg-Ke9A1qzD718dgIog .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ke9A1qzD718dgIog .arrowheadPath{fill:#333333;}#mermaid-svg-Ke9A1qzD718dgIog .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ke9A1qzD718dgIog .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ke9A1qzD718dgIog .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ke9A1qzD718dgIog .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ke9A1qzD718dgIog .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ke9A1qzD718dgIog .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ke9A1qzD718dgIog .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ke9A1qzD718dgIog .cluster text{fill:#333;}#mermaid-svg-Ke9A1qzD718dgIog .cluster span{color:#333;}#mermaid-svg-Ke9A1qzD718dgIog div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ke9A1qzD718dgIog .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ke9A1qzD718dgIog rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ke9A1qzD718dgIog .icon-shape,#mermaid-svg-Ke9A1qzD718dgIog .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ke9A1qzD718dgIog .icon-shape p,#mermaid-svg-Ke9A1qzD718dgIog .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ke9A1qzD718dgIog .icon-shape .label rect,#mermaid-svg-Ke9A1qzD718dgIog .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ke9A1qzD718dgIog .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ke9A1qzD718dgIog .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ke9A1qzD718dgIog :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第12章:属性配置与 getter/setter
属性标志与描述符
Object.defineProperty
getter 与 setter
实战
value / writable

enumerable / configurable
getOwnPropertyDescriptor
定义新属性
修改已有属性
批量定义 defineProperties
访问器属性
class 中的 getter/setter
手写 Object.create
实现数据响应式

本章目标

  • 理解属性描述符的四个标志:valuewritableenumerableconfigurable
  • 掌握 Object.definePropertyObject.defineProperties 的使用
  • 理解数据属性与访问器属性的区别
  • 熟练定义 gettersetter,包括在 class 中的使用
  • 实战:手写 Object.create、实现一个简易的数据响应式系统

12.1 属性标志与属性描述符

12.1.1 属性不只是键值对

当我们用对象字面量创建一个属性时,JavaScript 在背后为这个属性附加了三组"开关",称为属性标志(property flags):

标志 默认值 含义
writable true 如果为 false,属性值不可被赋值修改(只读)
enumerable true 如果为 false,属性不出现在 for...inObject.keys()
configurable true 如果为 false,属性不可被 delete 删除,且标志不可被修改

再加上属性本身的值 value,这四个字段共同构成了一个属性描述符(Property Descriptor)对象。

javascript 复制代码
const user = {
  name: 'Alice',
  age: 25
};

// 查看属性的完整描述符
const desc = Object.getOwnPropertyDescriptor(user, 'name');
console.log(desc);
// {
//   value: 'Alice',
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

12.1.2 查看属性描述符

Object.getOwnPropertyDescriptor(obj, prop) 返回指定属性的直接描述符。

javascript 复制代码
const obj = {
  id: 1,
  name: 'test'
};

// 查看 'id' 属性的描述符
const desc = Object.getOwnPropertyDescriptor(obj, 'id');
console.log(desc.value);         // 1
console.log(desc.writable);     // true
console.log(desc.enumerable);   // true
console.log(desc.configurable); // true

// 查看不存在的属性 → undefined
const missing = Object.getOwnPropertyDescriptor(obj, 'toString');
console.log(missing); // undefined(toString 来自原型,不是自身属性)

Object.getOwnPropertyDescriptors(obj) 可以一次性获取所有自身属性的描述符:

javascript 复制代码
const user = {
  name: 'Alice',
  age: 25
};

const allDesc = Object.getOwnPropertyDescriptors(user);
console.log(allDesc);
// {
//   name: { value: 'Alice', writable: true, enumerable: true, configurable: true },
//   age: { value: 25, writable: true, enumerable: true, configurable: true }
// }

注意Object.getOwnPropertyDescriptor 只能查看自身属性(own properties),不包含原型链上继承的属性。

12.1.3 writable --- 控制属性是否可写

writable 设为 false 后,对属性的赋值操作将静默失败 (非严格模式)或抛出 TypeError(严格模式)。

javascript 复制代码
'use strict';

const user = {};

// 先用 defineProperty 设置只读属性
Object.defineProperty(user, 'name', {
  value: 'Alice',
  writable: false,
  enumerable: true,
  configurable: true
});

console.log(user.name); // "Alice"

// 尝试修改
// user.name = 'Bob'; // TypeError: Cannot assign to read only property 'name'

这就是 Math.PI 无法被修改的原因:

javascript 复制代码
const desc = Object.getOwnPropertyDescriptor(Math, 'PI');
console.log(desc);
// {
//   value: 3.141592653589793,
//   writable: false,      ← 只读
//   enumerable: false,     ← 不可枚举
//   configurable: false    ← 不可重新配置
// }

Math.PI = 3; // 静默失败,PI 仍然是 3.14159...

12.1.4 enumerable --- 控制属性是否可枚举

enumerable 设为 false,属性会被"隐藏"------不会出现在 for...in 循环和 Object.keys() 中,但仍然可以正常访问。

javascript 复制代码
const user = {
  name: 'Alice',
  age: 25
};

// 添加一个隐藏属性
Object.defineProperty(user, 'secretKey', {
  value: 'abc-123',
  writable: true,
  enumerable: false,
  configurable: true
});

console.log(user.secretKey); // "abc-123"(可以正常访问)

// 但不会被遍历到
console.log(Object.keys(user)); // ['name', 'age']
console.log(Object.values(user)); // ['Alice', 25]

for (const key in user) {
  console.log(key); // 'name', 'age'(不含 secretKey)
}

#mermaid-svg-y4LXnLHxTlYyjlRL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y4LXnLHxTlYyjlRL .error-icon{fill:#552222;}#mermaid-svg-y4LXnLHxTlYyjlRL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y4LXnLHxTlYyjlRL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y4LXnLHxTlYyjlRL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y4LXnLHxTlYyjlRL .marker.cross{stroke:#333333;}#mermaid-svg-y4LXnLHxTlYyjlRL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y4LXnLHxTlYyjlRL p{margin:0;}#mermaid-svg-y4LXnLHxTlYyjlRL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL .cluster-label text{fill:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL .cluster-label span{color:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL .cluster-label span p{background-color:transparent;}#mermaid-svg-y4LXnLHxTlYyjlRL .label text,#mermaid-svg-y4LXnLHxTlYyjlRL span{fill:#333;color:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL .node rect,#mermaid-svg-y4LXnLHxTlYyjlRL .node circle,#mermaid-svg-y4LXnLHxTlYyjlRL .node ellipse,#mermaid-svg-y4LXnLHxTlYyjlRL .node polygon,#mermaid-svg-y4LXnLHxTlYyjlRL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-y4LXnLHxTlYyjlRL .rough-node .label text,#mermaid-svg-y4LXnLHxTlYyjlRL .node .label text,#mermaid-svg-y4LXnLHxTlYyjlRL .image-shape .label,#mermaid-svg-y4LXnLHxTlYyjlRL .icon-shape .label{text-anchor:middle;}#mermaid-svg-y4LXnLHxTlYyjlRL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-y4LXnLHxTlYyjlRL .rough-node .label,#mermaid-svg-y4LXnLHxTlYyjlRL .node .label,#mermaid-svg-y4LXnLHxTlYyjlRL .image-shape .label,#mermaid-svg-y4LXnLHxTlYyjlRL .icon-shape .label{text-align:center;}#mermaid-svg-y4LXnLHxTlYyjlRL .node.clickable{cursor:pointer;}#mermaid-svg-y4LXnLHxTlYyjlRL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-y4LXnLHxTlYyjlRL .arrowheadPath{fill:#333333;}#mermaid-svg-y4LXnLHxTlYyjlRL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-y4LXnLHxTlYyjlRL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-y4LXnLHxTlYyjlRL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y4LXnLHxTlYyjlRL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-y4LXnLHxTlYyjlRL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y4LXnLHxTlYyjlRL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-y4LXnLHxTlYyjlRL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-y4LXnLHxTlYyjlRL .cluster text{fill:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL .cluster span{color:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-y4LXnLHxTlYyjlRL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-y4LXnLHxTlYyjlRL rect.text{fill:none;stroke-width:0;}#mermaid-svg-y4LXnLHxTlYyjlRL .icon-shape,#mermaid-svg-y4LXnLHxTlYyjlRL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y4LXnLHxTlYyjlRL .icon-shape p,#mermaid-svg-y4LXnLHxTlYyjlRL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-y4LXnLHxTlYyjlRL .icon-shape .label rect,#mermaid-svg-y4LXnLHxTlYyjlRL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y4LXnLHxTlYyjlRL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-y4LXnLHxTlYyjlRL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-y4LXnLHxTlYyjlRL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 对象属性
enumerable: true
enumerable: false
Object.keys 可见
for...in 可见
JSON.stringify 序列化
Object.keys 不可见
for...in 不可见
JSON.stringify 忽略
直接访问正常

应用场景 :在实现对象序列化时,可以用 enumerable: false 标记内部使用的辅助属性,使其不被 JSON.stringify 输出。

12.1.5 configurable --- 控制属性是否可重新配置

configurable: false 是一道"锁":一旦设为 false,就无法再修改该属性的任何标志,也无法删除该属性。

javascript 复制代码
const obj = {};

Object.defineProperty(obj, 'lock', {
  value: 'frozen',
  writable: true,
  enumerable: true,
  configurable: false  // ← 锁定配置
});

// 仍然可以修改 value(因为 writable 仍为 true)
obj.lock = 'changed';
console.log(obj.lock); // "changed"

// 但不能修改 writable
// Object.defineProperty(obj, 'lock', { writable: false });
// TypeError: Cannot redefine property: lock

// 不能删除
// delete obj.lock; // 严格模式下 TypeError

特别注意configurable: false 后,唯一还能做的事情是writabletrue 改为 false (单向收紧),但不能反过来从 false 改为 true。这是 JavaScript 规范的特殊设计。

javascript 复制代码
'use strict';

const obj = {};

Object.defineProperty(obj, 'x', {
  value: 1,
  writable: true,
  enumerable: true,
  configurable: false
});

// writable: true → false 是允许的
Object.defineProperty(obj, 'x', { writable: false });
obj.x = 99; // 静默失败
console.log(obj.x); // 1

// writable: false → true 是不允许的
// Object.defineProperty(obj, 'x', { writable: true });
// TypeError: Cannot redefine property: x

12.1.6 属性标志速查表

标志 true false 特殊约束
writable 可赋值修改 只读(赋值静默失败/报错) 可被 configurable:false 锁定
enumerable Object.keys / for...in 可见 隐藏,不可枚举 可被 configurable:false 锁定
configurable 可修改标志、可删除 不可修改标志、不可删除 一旦为 false,不可恢复

12.2 Object.defineProperty 与 Object.defineProperties

12.2.1 Object.defineProperty 基本语法

Object.defineProperty(obj, prop, descriptor) 是精确控制属性的核心方法。

javascript 复制代码
/**
 * Object.defineProperty 参数说明
 * @param {Object} obj - 目标对象
 * @param {string|Symbol} prop - 属性名
 * @param {Object} descriptor - 属性描述符
 * @returns {Object} 返回目标对象 obj
 */

描述符有两种形式:数据描述符访问器描述符

描述符类型 包含字段 用途
数据描述符 valuewritable 定义一个普通的数据属性
访问器描述符 getset 定义一个由函数控制的属性

互斥规则 :数据描述符和访问器描述符不能混用。一个描述符要么有 value/writable,要么有 get/set,不能同时拥有。

12.2.2 定义数据属性

javascript 复制代码
const user = {};

// 定义一个可读写、可枚举、可配置的属性
Object.defineProperty(user, 'name', {
  value: 'Alice',
  writable: true,
  enumerable: true,
  configurable: true
});

console.log(user.name); // "Alice"

// 定义一个只读属性
Object.defineProperty(user, 'id', {
  value: 1001,
  writable: false,
  enumerable: true,
  configurable: false
});

console.log(user.id); // 1001
// user.id = 2002; // TypeError(严格模式下)

12.2.3 defineProperty 的默认值陷阱

当使用 Object.defineProperty 定义一个新属性 时,如果描述符中省略了某些标志,它们的默认值是 false

javascript 复制代码
const obj = {};

// 只提供 value,省略其他标志
Object.defineProperty(obj, 'hidden', {
  value: 'secret'
});

const desc = Object.getOwnPropertyDescriptor(obj, 'hidden');
console.log(desc);
// {
//   value: 'secret',
//   writable: false,     ← 默认 false
//   enumerable: false,    ← 默认 false
//   configurable: false   ← 默认 false
// }

// 不可修改
// obj.hidden = 'changed'; // TypeError
// 不可枚举
console.log(Object.keys(obj)); // []
// 不可删除
// delete obj.hidden; // TypeError

这与字面量创建属性的默认值(全部为 true)完全相反,是很多初学者踩坑的地方。
#mermaid-svg-2bcp11tMBUmlh3sU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2bcp11tMBUmlh3sU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2bcp11tMBUmlh3sU .error-icon{fill:#552222;}#mermaid-svg-2bcp11tMBUmlh3sU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2bcp11tMBUmlh3sU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2bcp11tMBUmlh3sU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2bcp11tMBUmlh3sU .marker.cross{stroke:#333333;}#mermaid-svg-2bcp11tMBUmlh3sU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2bcp11tMBUmlh3sU p{margin:0;}#mermaid-svg-2bcp11tMBUmlh3sU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2bcp11tMBUmlh3sU .cluster-label text{fill:#333;}#mermaid-svg-2bcp11tMBUmlh3sU .cluster-label span{color:#333;}#mermaid-svg-2bcp11tMBUmlh3sU .cluster-label span p{background-color:transparent;}#mermaid-svg-2bcp11tMBUmlh3sU .label text,#mermaid-svg-2bcp11tMBUmlh3sU span{fill:#333;color:#333;}#mermaid-svg-2bcp11tMBUmlh3sU .node rect,#mermaid-svg-2bcp11tMBUmlh3sU .node circle,#mermaid-svg-2bcp11tMBUmlh3sU .node ellipse,#mermaid-svg-2bcp11tMBUmlh3sU .node polygon,#mermaid-svg-2bcp11tMBUmlh3sU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2bcp11tMBUmlh3sU .rough-node .label text,#mermaid-svg-2bcp11tMBUmlh3sU .node .label text,#mermaid-svg-2bcp11tMBUmlh3sU .image-shape .label,#mermaid-svg-2bcp11tMBUmlh3sU .icon-shape .label{text-anchor:middle;}#mermaid-svg-2bcp11tMBUmlh3sU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2bcp11tMBUmlh3sU .rough-node .label,#mermaid-svg-2bcp11tMBUmlh3sU .node .label,#mermaid-svg-2bcp11tMBUmlh3sU .image-shape .label,#mermaid-svg-2bcp11tMBUmlh3sU .icon-shape .label{text-align:center;}#mermaid-svg-2bcp11tMBUmlh3sU .node.clickable{cursor:pointer;}#mermaid-svg-2bcp11tMBUmlh3sU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2bcp11tMBUmlh3sU .arrowheadPath{fill:#333333;}#mermaid-svg-2bcp11tMBUmlh3sU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2bcp11tMBUmlh3sU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2bcp11tMBUmlh3sU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2bcp11tMBUmlh3sU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2bcp11tMBUmlh3sU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2bcp11tMBUmlh3sU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2bcp11tMBUmlh3sU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2bcp11tMBUmlh3sU .cluster text{fill:#333;}#mermaid-svg-2bcp11tMBUmlh3sU .cluster span{color:#333;}#mermaid-svg-2bcp11tMBUmlh3sU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2bcp11tMBUmlh3sU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2bcp11tMBUmlh3sU rect.text{fill:none;stroke-width:0;}#mermaid-svg-2bcp11tMBUmlh3sU .icon-shape,#mermaid-svg-2bcp11tMBUmlh3sU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2bcp11tMBUmlh3sU .icon-shape p,#mermaid-svg-2bcp11tMBUmlh3sU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2bcp11tMBUmlh3sU .icon-shape .label rect,#mermaid-svg-2bcp11tMBUmlh3sU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2bcp11tMBUmlh3sU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2bcp11tMBUmlh3sU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2bcp11tMBUmlh3sU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建属性方式
对象字面量

obj.name = 'Alice'
Object.defineProperty

{ value: 'Alice' }
writable: true

enumerable: true

configurable: true
writable: false

enumerable: false

configurable: false
默认全部可操作
默认全部锁定

对比表

创建方式 writable enumerable configurable
字面量 / 赋值 true true true
defineProperty(省略标志时) false false false

12.2.4 Object.defineProperties --- 批量定义

当需要一次定义多个属性时,使用 Object.defineProperties(obj, descriptors)

javascript 复制代码
const user = {};

Object.defineProperties(user, {
  name: {
    value: 'Alice',
    writable: true,
    enumerable: true,
    configurable: true
  },
  age: {
    value: 25,
    writable: true,
    enumerable: true,
    configurable: true
  },
  _password: {
    value: '123456',
    writable: true,
    enumerable: false,  // 隐藏
    configurable: false  // 锁定
  },
  fullName: {
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    set(value) {
      const parts = value.split(' ');
      this.firstName = parts[0] || '';
      this.lastName = parts[1] || '';
    },
    enumerable: true,
    configurable: true
  }
});

console.log(user.name);      // "Alice"
console.log(user.age);       // 25
console.log(user._password); // "123456"(可访问但不可枚举)
console.log(Object.keys(user)); // ['name', 'age', 'fullName']

12.2.5 Object.defineProperty 修改已有属性

当用 defineProperty 修改一个已存在的属性 时,被省略的标志保持不变 (不使用默认 false)。

javascript 复制代码
const user = {
  name: 'Alice',
  age: 25
};

// 只修改 writable,其他标志保持不变
Object.defineProperty(user, 'name', {
  writable: false
});

const desc = Object.getOwnPropertyDescriptor(user, 'name');
console.log(desc);
// {
//   value: 'Alice',
//   writable: false,     ← 被修改
//   enumerable: true,    ← 保持不变
//   configurable: true   ← 保持不变
// }

// user.name = 'Bob'; // TypeError(已变为只读)

12.2.6 属性的删除:delete 的条件

delete 操作符能否删除一个属性,取决于 configurable 标志:

javascript 复制代码
const obj = {};

// configurable: true
Object.defineProperty(obj, 'a', {
  value: 1,
  configurable: true
});

// configurable: false
Object.defineProperty(obj, 'b', {
  value: 2,
  configurable: false
});

console.log(delete obj.a); // true(删除成功)
console.log(obj.a);         // undefined

console.log(delete obj.b); // false(非严格模式下静默失败)
// 'use strict' 模式下:TypeError

console.log(obj.b);         // 2(仍然存在)

12.3 getter 与 setter

12.3.1 什么是访问器属性?

到目前为止我们接触的都是数据属性 ------直接存储一个值。JavaScript 还支持另一种属性:访问器属性 (Accessor Property),它不存储值,而是通过 getset 函数来动态计算和拦截读写操作。

属性类型 描述符字段 特点
数据属性 valuewritable 直接存储值
访问器属性 getset 读写时触发函数调用

#mermaid-svg-F01qQPbGoQyFmjR6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F01qQPbGoQyFmjR6 .error-icon{fill:#552222;}#mermaid-svg-F01qQPbGoQyFmjR6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F01qQPbGoQyFmjR6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F01qQPbGoQyFmjR6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F01qQPbGoQyFmjR6 .marker.cross{stroke:#333333;}#mermaid-svg-F01qQPbGoQyFmjR6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F01qQPbGoQyFmjR6 p{margin:0;}#mermaid-svg-F01qQPbGoQyFmjR6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 .cluster-label text{fill:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 .cluster-label span{color:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 .cluster-label span p{background-color:transparent;}#mermaid-svg-F01qQPbGoQyFmjR6 .label text,#mermaid-svg-F01qQPbGoQyFmjR6 span{fill:#333;color:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 .node rect,#mermaid-svg-F01qQPbGoQyFmjR6 .node circle,#mermaid-svg-F01qQPbGoQyFmjR6 .node ellipse,#mermaid-svg-F01qQPbGoQyFmjR6 .node polygon,#mermaid-svg-F01qQPbGoQyFmjR6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F01qQPbGoQyFmjR6 .rough-node .label text,#mermaid-svg-F01qQPbGoQyFmjR6 .node .label text,#mermaid-svg-F01qQPbGoQyFmjR6 .image-shape .label,#mermaid-svg-F01qQPbGoQyFmjR6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-F01qQPbGoQyFmjR6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F01qQPbGoQyFmjR6 .rough-node .label,#mermaid-svg-F01qQPbGoQyFmjR6 .node .label,#mermaid-svg-F01qQPbGoQyFmjR6 .image-shape .label,#mermaid-svg-F01qQPbGoQyFmjR6 .icon-shape .label{text-align:center;}#mermaid-svg-F01qQPbGoQyFmjR6 .node.clickable{cursor:pointer;}#mermaid-svg-F01qQPbGoQyFmjR6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F01qQPbGoQyFmjR6 .arrowheadPath{fill:#333333;}#mermaid-svg-F01qQPbGoQyFmjR6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F01qQPbGoQyFmjR6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F01qQPbGoQyFmjR6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F01qQPbGoQyFmjR6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F01qQPbGoQyFmjR6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F01qQPbGoQyFmjR6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F01qQPbGoQyFmjR6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F01qQPbGoQyFmjR6 .cluster text{fill:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 .cluster span{color:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F01qQPbGoQyFmjR6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F01qQPbGoQyFmjR6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-F01qQPbGoQyFmjR6 .icon-shape,#mermaid-svg-F01qQPbGoQyFmjR6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F01qQPbGoQyFmjR6 .icon-shape p,#mermaid-svg-F01qQPbGoQyFmjR6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F01qQPbGoQyFmjR6 .icon-shape .label rect,#mermaid-svg-F01qQPbGoQyFmjR6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F01qQPbGoQyFmjR6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F01qQPbGoQyFmjR6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F01qQPbGoQyFmjR6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据属性
访问器属性
obj.prop = 42
属性类型?
直接写入 value
调用 set(val)
在 set 中

校验/转换/存储
内部 this._prop = val
#mermaid-svg-4ksBElAJIYHHY9tn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4ksBElAJIYHHY9tn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4ksBElAJIYHHY9tn .error-icon{fill:#552222;}#mermaid-svg-4ksBElAJIYHHY9tn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4ksBElAJIYHHY9tn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4ksBElAJIYHHY9tn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4ksBElAJIYHHY9tn .marker.cross{stroke:#333333;}#mermaid-svg-4ksBElAJIYHHY9tn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4ksBElAJIYHHY9tn p{margin:0;}#mermaid-svg-4ksBElAJIYHHY9tn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4ksBElAJIYHHY9tn .cluster-label text{fill:#333;}#mermaid-svg-4ksBElAJIYHHY9tn .cluster-label span{color:#333;}#mermaid-svg-4ksBElAJIYHHY9tn .cluster-label span p{background-color:transparent;}#mermaid-svg-4ksBElAJIYHHY9tn .label text,#mermaid-svg-4ksBElAJIYHHY9tn span{fill:#333;color:#333;}#mermaid-svg-4ksBElAJIYHHY9tn .node rect,#mermaid-svg-4ksBElAJIYHHY9tn .node circle,#mermaid-svg-4ksBElAJIYHHY9tn .node ellipse,#mermaid-svg-4ksBElAJIYHHY9tn .node polygon,#mermaid-svg-4ksBElAJIYHHY9tn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4ksBElAJIYHHY9tn .rough-node .label text,#mermaid-svg-4ksBElAJIYHHY9tn .node .label text,#mermaid-svg-4ksBElAJIYHHY9tn .image-shape .label,#mermaid-svg-4ksBElAJIYHHY9tn .icon-shape .label{text-anchor:middle;}#mermaid-svg-4ksBElAJIYHHY9tn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4ksBElAJIYHHY9tn .rough-node .label,#mermaid-svg-4ksBElAJIYHHY9tn .node .label,#mermaid-svg-4ksBElAJIYHHY9tn .image-shape .label,#mermaid-svg-4ksBElAJIYHHY9tn .icon-shape .label{text-align:center;}#mermaid-svg-4ksBElAJIYHHY9tn .node.clickable{cursor:pointer;}#mermaid-svg-4ksBElAJIYHHY9tn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4ksBElAJIYHHY9tn .arrowheadPath{fill:#333333;}#mermaid-svg-4ksBElAJIYHHY9tn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4ksBElAJIYHHY9tn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4ksBElAJIYHHY9tn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4ksBElAJIYHHY9tn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4ksBElAJIYHHY9tn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4ksBElAJIYHHY9tn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4ksBElAJIYHHY9tn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4ksBElAJIYHHY9tn .cluster text{fill:#333;}#mermaid-svg-4ksBElAJIYHHY9tn .cluster span{color:#333;}#mermaid-svg-4ksBElAJIYHHY9tn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4ksBElAJIYHHY9tn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4ksBElAJIYHHY9tn rect.text{fill:none;stroke-width:0;}#mermaid-svg-4ksBElAJIYHHY9tn .icon-shape,#mermaid-svg-4ksBElAJIYHHY9tn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4ksBElAJIYHHY9tn .icon-shape p,#mermaid-svg-4ksBElAJIYHHY9tn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4ksBElAJIYHHY9tn .icon-shape .label rect,#mermaid-svg-4ksBElAJIYHHY9tn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4ksBElAJIYHHY9tn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4ksBElAJIYHHY9tn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4ksBElAJIYHHY9tn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据属性
访问器属性
读取 obj.prop
属性类型?
直接返回 value
调用 get()
在 get 中

计算/变换/返回
return this._prop

12.3.2 在对象字面量中定义 getter/setter

最简洁的方式是在对象字面量中用 get / set 关键字:

javascript 复制代码
const user = {
  firstName: 'Alice',
  lastName: 'Smith',

  // getter:读取 fullName 时自动调用
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  // setter:赋值 fullName 时自动调用
  set fullName(value) {
    const parts = value.split(' ');
    this.firstName = parts[0] || '';
    this.lastName = parts[1] || '';
  }
};

// 读取(触发 get)
console.log(user.fullName); // "Alice Smith"

// 写入(触发 set)
user.fullName = 'Bob Johnson';
console.log(user.firstName); // "Bob"
console.log(user.lastName);  // "Johnson"
console.log(user.fullName); // "Bob Johnson"

12.3.3 getter/setter 与数据属性的关系

getter/setter 本身不存储值 ,它们通常需要一个"后备存储"属性(通常以下划线 _ 前缀命名)来保存实际数据。

javascript 复制代码
const product = {
  _price: 0,  // 后备存储

  get price() {
    return this._price;
  },

  set price(value) {
    if (value < 0) {
      console.warn('价格不能为负数,已设为 0');
      this._price = 0;
    } else {
      this._price = value;
    }
  }
};

product.price = 100;   // 触发 set,_price = 100
console.log(product.price); // 100(触发 get)

product.price = -50;   // 触发 set,警告,_price = 0
console.log(product.price); // 0

为什么需要下划线前缀? 因为如果 getset 使用的属性名与自身相同,就会形成无限递归。_price 只是一种命名约定,JavaScript 并不强制。

12.3.4 使用 Object.defineProperty 定义访问器属性

javascript 复制代码
const user = {
  _name: 'Alice'
};

Object.defineProperty(user, 'name', {
  get() {
    return this._name.toUpperCase();
  },
  set(value) {
    if (typeof value !== 'string' || value.trim() === '') {
      throw new Error('name 不能为空');
    }
    this._name = value.trim();
  },
  enumerable: true,
  configurable: true
});

console.log(user.name); // "ALICE"

user.name = ' Bob ';
console.log(user.name); // "BOB"
console.log(user._name); // "Bob"

// 传入空字符串
// user.name = '';  // Error: name 不能为空

12.3.5 只有 getter(只读计算属性)

如果只定义 get 而不定义 set,该属性就是只读的

javascript 复制代码
const circle = {
  radius: 5,

  // 只读计算属性
  get area() {
    return Math.PI * this.radius ** 2;
  },

  get circumference() {
    return 2 * Math.PI * this.radius;
  }
};

console.log(circle.area);        // 78.53981633974483
console.log(circle.circumference); // 31.41592653589793

// 修改 radius,计算属性自动更新
circle.radius = 10;
console.log(circle.area);        // 314.1592653589793

// 只读:赋值无效
circle.area = 100;
console.log(circle.area); // 314.1592653589793(未改变)

12.3.6 class 中的 getter/setter

在 ES6 的 class 语法中,getter/setter 的定义方式完全相同:

javascript 复制代码
class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  // getter
  get celsius() {
    return this._celsius;
  }

  // setter:带校验
  set celsius(value) {
    if (value < -273.15) {
      throw new Error('温度不能低于绝对零度(-273.15°C)');
    }
    this._celsius = value;
  }

  // 只读计算属性
  get fahrenheit() {
    return this._celsius * 9 / 5 + 32;
  }

  get kelvin() {
    return this._celsius + 273.15;
  }
}

const temp = new Temperature(100);
console.log(temp.celsius);    // 100
console.log(temp.fahrenheit); // 212
console.log(temp.kelvin);     // 373.15

temp.celsius = 0;
console.log(temp.fahrenheit); // 32

// 触发校验
// temp.celsius = -300; // Error: 温度不能低于绝对零度(-273.15°C)

12.3.7 访问器属性的描述符

使用 Object.getOwnPropertyDescriptor 查看访问器属性时,描述符的结构与数据属性不同:

javascript 复制代码
const obj = {
  _val: 42,
  get val() { return this._val; },
  set val(v) { this._val = v; }
};

const desc = Object.getOwnPropertyDescriptor(obj, 'val');
console.log(desc);
// {
//   get: [Function: get val],
//   set: [Function: set val],
//   enumerable: true,
//   configurable: true
// }
// 注意:没有 value 和 writable 字段!
描述符字段 数据属性 访问器属性
value
writable
get
set
enumerable
configurable

12.4 属性的遍历与复制

12.4.1 各种遍历方法的枚举性差异

不同的遍历方法对 enumerable 标志的处理不同:

javascript 复制代码
const obj = {};

// 可枚举属性
Object.defineProperty(obj, 'visible', {
  value: 1, enumerable: true, configurable: true
});

// 不可枚举属性
Object.defineProperty(obj, 'hidden', {
  value: 2, enumerable: false, configurable: true
});

// Symbol 属性
const sym = Symbol('sym');
obj[sym] = 3;

console.log(Object.keys(obj));           // ['visible']
console.log(Object.values(obj));         // [1]
console.log(Object.entries(obj));        // [['visible', 1]]
console.log(Object.getOwnPropertyNames(obj)); // ['visible', 'hidden']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(sym)]
console.log(Reflect.ownKeys(obj));       // ['visible', 'hidden', Symbol(sym)]
方法 字符串自有属性 不可枚举属性 Symbol 属性 原型链属性
Object.keys()
Object.values()
Object.entries()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Reflect.ownKeys()
for...in

注意for...in 是唯一会遍历原型链 上属性的方法。这也是使用 for...in 时需要配合 hasOwnProperty 检查的原因。

12.4.2 Object.assign 与可枚举性

Object.assign 和展开运算符(...只复制可枚举的自有属性

javascript 复制代码
const source = {};

Object.defineProperties(source, {
  visible: {
    value: 'hello',
    enumerable: true,
    writable: true,
    configurable: true
  },
  hidden: {
    value: 'secret',
    enumerable: false,
    writable: true,
    configurable: true
  }
});

// Object.assign 只复制可枚举属性
const target1 = {};
Object.assign(target1, source);
console.log(target1); // { visible: 'hello' }
console.log(target1.hidden); // undefined

// 展开运算符同样只复制可枚举属性
const target2 = { ...source };
console.log(target2); // { visible: 'hello' }

如果需要完整复制(包括不可枚举属性),需要借助 Object.getOwnPropertyDescriptors

javascript 复制代码
const target3 = {};
Object.defineProperties(target3, Object.getOwnPropertyDescriptors(source));
console.log(target3.visible); // "hello"
console.log(target3.hidden);  // "secret"

12.4.3 对象的冻结方法

JavaScript 提供了三个层级递进的"冻结"方法,它们都基于属性描述符:

javascript 复制代码
const obj = {
  a: 1,
  b: 2
};

// Level 1: Object.preventExtensions --- 禁止添加新属性
Object.preventExtensions(obj);
// obj.c = 3; // 静默失败(非严格模式)
console.log(Object.isExtensible(obj)); // false

// Level 2: Object.seal --- 禁止增删,但允许修改现有属性值
const sealed = { x: 10 };
Object.seal(sealed);
// 等价于:所有属性的 configurable 设为 false + preventExtensions
console.log(Object.isSealed(sealed)); // true
sealed.x = 20; // ✅ 可以修改
// delete sealed.x;  // ❌ 不可删除
// sealed.y = 30;   // ❌ 不可添加

// Level 3: Object.freeze --- 完全冻结,禁止增删改
const frozen = { x: 10 };
Object.freeze(frozen);
// 等价于:所有属性的 configurable 和 writable 都设为 false + preventExtensions
console.log(Object.isFrozen(frozen)); // true
// frozen.x = 20;  // ❌ 不可修改
// delete frozen.x; // ❌ 不可删除
// frozen.y = 30;   // ❌ 不可添加
方法 添加属性 删除属性 修改值 修改描述符
普通对象
preventExtensions
seal
freeze

浅冻结的局限Object.freeze浅层的,嵌套对象不会被冻结。

javascript 复制代码
const obj = Object.freeze({
  data: { value: 1 }
});

obj.data = {};      // 失败
obj.data.value = 99; // 成功!嵌套对象未被冻结
console.log(obj.data.value); // 99

实现深冻结

javascript 复制代码
/**
 * 深度冻结对象及其所有嵌套属性
 * @param {Object} obj - 要冻结的对象
 * @returns {Object} 冻结后的对象
 */
function deepFreeze(obj) {
  // 先冻结自身
  Object.freeze(obj);

  // 获取所有自有属性
  const keys = Object.getOwnPropertyNames(obj);
  for (const key of keys) {
    const value = obj[key];
    if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
      deepFreeze(value);
    }
  }

  return obj;
}

const config = deepFreeze({
  name: 'app',
  settings: {
    theme: 'dark',
    options: {
      maxItems: 100
    }
  }
});

// config.name = 'other';          // 失败
// config.settings.theme = 'light'; // 失败
// config.settings.options.maxItems = 200; // 失败
console.log(config.settings.options.maxItems); // 100

12.5 实战

12.5.1 实战1:手写 Object.create

Object.create(proto, propertiesObject) 创建一个新对象,并将其原型链接到 proto。我们可以利用 defineProperty 来实现它。

javascript 复制代码
/**
 * 手写 Object.create
 * @param {Object|null} proto - 原型对象
 * @param {Object} [propertiesObject] - 属性描述符集合
 * @returns {Object} 新创建的对象
 */
function myCreate(proto, propertiesObject) {
  // 1. 参数校验
  if (proto !== null && typeof proto !== 'object') {
    throw new TypeError('Object prototype may only be an Object or null');
  }

  // 2. 创建一个原型为 proto 的空对象
  // 使用构造函数的方式链接原型
  function F() {}
  F.prototype = proto;
  const obj = new F();

  // 3. 如果传入了属性描述符,批量定义属性
  if (propertiesObject !== undefined) {
    Object.defineProperties(obj, propertiesObject);
  }

  return obj;
}

// ===== 测试 =====

// 测试1:创建以指定对象为原型的实例
const animal = {
  type: 'unknown',
  describe() {
    return `I am a ${this.type}`;
  }
};

const dog = myCreate(animal);
console.log(dog.describe()); // "I am a unknown"
console.log(Object.getPrototypeOf(dog) === animal); // true

// 测试2:带属性描述符
const cat = myCreate(animal, {
  type: {
    value: 'cat',
    writable: true,
    enumerable: true,
    configurable: true
  }
});
console.log(cat.describe()); // "I am a cat"

// 测试3:以 null 为原型(无原型的对象)
const pure = myCreate(null);
console.log(Object.getPrototypeOf(pure)); // null
console.log(pure.toString); // undefined(没有继承 Object.prototype)

// 测试4:与原生 Object.create 对比
const native = Object.create(animal, {
  type: { value: 'cat', writable: true, enumerable: true, configurable: true }
});
console.log(native.describe()); // "I am a cat"
console.log(cat.describe() === native.describe()); // true

12.5.2 实战2:实现数据响应式(Vue2 原理)

Vue2 的响应式系统核心就是利用 Object.defineProperty,在 get 时收集依赖,在 set 时触发更新。

javascript 复制代码
/**
 * 简易数据响应式系统(Vue2 核心原理)
 */

// 依赖收集器
let activeEffect = null;

/**
 * 依赖管理类:每个响应式属性对应一个 Dep 实例
 */
class Dep {
  constructor() {
    this.subscribers = new Set(); // 使用 Set 避免重复收集
  }

  // 收集依赖:将当前的 activeEffect 添加到订阅列表
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  // 通知更新:触发所有订阅的 effect
  notify() {
    this.subscribers.forEach(effect => effect());
  }
}

/**
 * 将对象转换为响应式对象
 * @param {Object} obj - 普通对象
 * @returns {Object} 响应式对象
 */
function reactive(obj) {
  const keys = Object.keys(obj);

  for (const key of keys) {
    let value = obj[key];
    const dep = new Dep();

    Object.defineProperty(obj, key, {
      get() {
        // 读取时收集依赖
        dep.depend();
        return value;
      },
      set(newValue) {
        if (newValue === value) return; // 值未变化,跳过
        value = newValue;
        // 写入时通知更新
        dep.notify();
      },
      enumerable: true,
      configurable: true
    });
  }

  return obj;
}

/**
 * 副作用函数注册器
 * @param {Function} effect - 副作用函数
 */
function watchEffect(effect) {
  activeEffect = effect;
  effect();        // 立即执行一次(触发 get,收集依赖)
  activeEffect = null;
}

// ===== 测试 =====

const state = reactive({
  count: 0,
  name: 'Alice'
});

let displayCount = '';
let displayName = '';

// 注册副作用:当 count 变化时自动更新
watchEffect(() => {
  displayCount = `Count: ${state.count}`;
  console.log(`[effect] ${displayCount}`);
});
// 输出:[effect] Count: 0

// 注册另一个副作用
watchEffect(() => {
  displayName = `Hello, ${state.name}!`;
  console.log(`[effect] ${displayName}`);
});
// 输出:[effect] Hello, Alice!

// 修改数据 → 自动触发副作用
state.count = 1;
// 输出:[effect] Count: 1

state.count = 10;
// 输出:[effect] Count: 10

state.name = 'Bob';
// 输出:[effect] Hello, Bob!

#mermaid-svg-MnAxgbEXBHJm7Saf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MnAxgbEXBHJm7Saf .error-icon{fill:#552222;}#mermaid-svg-MnAxgbEXBHJm7Saf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MnAxgbEXBHJm7Saf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MnAxgbEXBHJm7Saf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MnAxgbEXBHJm7Saf .marker.cross{stroke:#333333;}#mermaid-svg-MnAxgbEXBHJm7Saf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MnAxgbEXBHJm7Saf p{margin:0;}#mermaid-svg-MnAxgbEXBHJm7Saf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf .cluster-label text{fill:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf .cluster-label span{color:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf .cluster-label span p{background-color:transparent;}#mermaid-svg-MnAxgbEXBHJm7Saf .label text,#mermaid-svg-MnAxgbEXBHJm7Saf span{fill:#333;color:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf .node rect,#mermaid-svg-MnAxgbEXBHJm7Saf .node circle,#mermaid-svg-MnAxgbEXBHJm7Saf .node ellipse,#mermaid-svg-MnAxgbEXBHJm7Saf .node polygon,#mermaid-svg-MnAxgbEXBHJm7Saf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MnAxgbEXBHJm7Saf .rough-node .label text,#mermaid-svg-MnAxgbEXBHJm7Saf .node .label text,#mermaid-svg-MnAxgbEXBHJm7Saf .image-shape .label,#mermaid-svg-MnAxgbEXBHJm7Saf .icon-shape .label{text-anchor:middle;}#mermaid-svg-MnAxgbEXBHJm7Saf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MnAxgbEXBHJm7Saf .rough-node .label,#mermaid-svg-MnAxgbEXBHJm7Saf .node .label,#mermaid-svg-MnAxgbEXBHJm7Saf .image-shape .label,#mermaid-svg-MnAxgbEXBHJm7Saf .icon-shape .label{text-align:center;}#mermaid-svg-MnAxgbEXBHJm7Saf .node.clickable{cursor:pointer;}#mermaid-svg-MnAxgbEXBHJm7Saf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MnAxgbEXBHJm7Saf .arrowheadPath{fill:#333333;}#mermaid-svg-MnAxgbEXBHJm7Saf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MnAxgbEXBHJm7Saf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MnAxgbEXBHJm7Saf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnAxgbEXBHJm7Saf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MnAxgbEXBHJm7Saf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnAxgbEXBHJm7Saf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MnAxgbEXBHJm7Saf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MnAxgbEXBHJm7Saf .cluster text{fill:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf .cluster span{color:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MnAxgbEXBHJm7Saf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MnAxgbEXBHJm7Saf rect.text{fill:none;stroke-width:0;}#mermaid-svg-MnAxgbEXBHJm7Saf .icon-shape,#mermaid-svg-MnAxgbEXBHJm7Saf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnAxgbEXBHJm7Saf .icon-shape p,#mermaid-svg-MnAxgbEXBHJm7Saf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MnAxgbEXBHJm7Saf .icon-shape .label rect,#mermaid-svg-MnAxgbEXBHJm7Saf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnAxgbEXBHJm7Saf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MnAxgbEXBHJm7Saf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MnAxgbEXBHJm7Saf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} reactive(obj)
遍历每个属性
创建 Dep 实例
Object.defineProperty
get: dep.depend() 收集依赖
set: dep.notify() 触发更新
watchEffect 注册的函数

被添加到 subscribers
遍历 subscribers

重新执行 effect

12.5.3 实战3:实现可观察的属性代理

结合 getter/setter 和 Symbol,实现一个更完善的属性代理方案:

javascript 复制代码
const OBSERVE_KEY = Symbol('observe');

/**
 * 可观察对象类
 * 支持属性读写监听和变更通知
 */
class Observable {
  constructor(data = {}) {
    // 内部数据存储
    this._data = {};
    // 监听器集合
    this._listeners = {};

    // 将传入数据代理为可观察属性
    for (const key of Object.keys(data)) {
      this._defineReactive(key, data[key]);
    }
  }

  /**
   * 定义响应式属性
   * @param {string} key - 属性名
   * @param {*} value - 初始值
   */
  _defineReactive(key, value) {
    this._data[key] = value;
    this._listeners[key] = new Set();

    Object.defineProperty(this, key, {
      get() {
        return this._data[key];
      },
      set(newValue) {
        const oldValue = this._data[key];
        if (newValue === oldValue) return;
        this._data[key] = newValue;
        // 通知所有监听器
        this._listeners[key].forEach(fn => fn(newValue, oldValue));
      },
      enumerable: true,
      configurable: true
    });
  }

  /**
   * 监听属性变化
   * @param {string} key - 要监听的属性名
   * @param {Function} callback - 回调函数 (newValue, oldValue) => void
   * @returns {Function} 取消监听的函数
   */
  watch(key, callback) {
    if (!this._listeners[key]) {
      this._listeners[key] = new Set();
    }
    this._listeners[key].add(callback);

    // 返回取消监听函数
    return () => {
      this._listeners[key].delete(callback);
    };
  }

  /**
   * 添加新的响应式属性
   * @param {string} key - 属性名
   * @param {*} value - 初始值
   */
  addProperty(key, value) {
    this._defineReactive(key, value);
  }
}

// ===== 测试 =====

const person = new Observable({
  name: 'Alice',
  age: 25
});

// 监听 name 变化
const unwatchName = person.watch('name', (newVal, oldVal) => {
  console.log(`name 变化:${oldVal} → ${newVal}`);
});

// 监听 age 变化
person.watch('age', (newVal, oldVal) => {
  console.log(`age 变化:${oldVal} → ${newVal}`);
});

// 修改属性 → 触发监听
person.name = 'Bob';
// 输出:name 变化:Alice → Bob

person.age = 30;
// 输出:age 变化:25 → 30

// 添加新属性并监听
person.addProperty('city', '北京');
person.watch('city', (newVal, oldVal) => {
  console.log(`city 变化:${oldVal} → ${newVal}`);
});
person.city = '上海';
// 输出:city 变化:北京 → 上海

// 取消 name 的监听
unwatchName();
person.name = 'Charlie';
// 无输出(监听已取消)

console.log(person.name);  // "Charlie"
console.log(person.age);   // 30
console.log(person.city);  // "上海"

12.5.4 实战4:实现常量管理器

利用 writable: falseconfigurable: false,实现一个安全的常量管理工具:

javascript 复制代码
/**
 * 常量管理器
 * 利用属性描述符确保常量不可被修改、删除或重新定义
 */
class Constants {
  constructor() {
    this._prefix = '@@CONST_';
  }

  /**
   * 定义一个常量
   * @param {string} name - 常量名
   * @param {*} value - 常量值
   */
  define(name, value) {
    if (Object.prototype.hasOwnProperty.call(this, name)) {
      throw new Error(`常量 "${name}" 已存在,不可重复定义`);
    }

    Object.defineProperty(this, name, {
      value: value,
      writable: false,      // 不可修改
      enumerable: true,      // 可遍历
      configurable: false    // 不可删除、不可重配置
    });
  }

  /**
   * 批量定义常量
   * @param {Object} constants - 常量键值对
   */
  defineAll(constants) {
    for (const [name, value] of Object.entries(constants)) {
      this.define(name, value);
    }
  }

  /**
   * 检查常量是否存在
   * @param {string} name - 常量名
   * @returns {boolean}
   */
  has(name) {
    return Object.prototype.hasOwnProperty.call(this, name);
  }

  /**
   * 获取所有常量(排除方法)
   * @returns {Object}
   */
  toObject() {
    const result = {};
    const keys = Object.getOwnPropertyNames(this)
      .filter(k => !k.startsWith('_') && typeof this[k] !== 'function');

    for (const key of keys) {
      result[key] = this[key];
    }
    return result;
  }
}

// ===== 测试 =====

const config = new Constants();

// 定义单个常量
config.define('MAX_CONNECTIONS', 100);
config.define('API_BASE_URL', 'https://api.example.com');
config.define('TIMEOUT', 5000);

// 批量定义
config.defineAll({
  VERSION: '1.0.0',
  ENV: 'production'
});

// 读取常量
console.log(config.MAX_CONNECTIONS); // 100
console.log(config.API_BASE_URL);     // "https://api.example.com"
console.log(config.VERSION);          // "1.0.0"

// 尝试修改 → 失败
// config.MAX_CONNECTIONS = 200; // TypeError(严格模式)
config.MAX_CONNECTIONS = 200; // 静默失败
console.log(config.MAX_CONNECTIONS); // 100(未改变)

// 尝试删除 → 失败
// delete config.API_BASE_URL; // TypeError(严格模式)

// 重复定义 → 抛出错误
// config.define('MAX_CONNECTIONS', 300); // Error: 常量 "MAX_CONNECTIONS" 已存在

// 获取所有常量
console.log(config.toObject());
// { MAX_CONNECTIONS: 100, API_BASE_URL: '...', TIMEOUT: 5000, VERSION: '1.0.0', ENV: 'production' }

12.5.5 实战5:防篡改的对象克隆

javascript 复制代码
/**
 * 完整克隆对象,保留属性描述符和不可枚举属性
 * @param {Object} source - 源对象
 * @returns {Object} 克隆后的对象
 */
function cloneWithDescriptors(source) {
  if (source === null || typeof source !== 'object') {
    return source;
  }

  // 创建新对象
  const clone = {};

  // 获取所有自有属性的描述符(包括不可枚举、Symbol)
  const descriptors = Object.getOwnPropertyDescriptors(source);

  // 使用 defineProperties 完整复制描述符
  Object.defineProperties(clone, descriptors);

  return clone;
}

// ===== 测试 =====

const original = {};

// 添加各种类型的属性
Object.defineProperties(original, {
  normal: { value: 1, enumerable: true, writable: true, configurable: true },
  readonly: { value: 2, enumerable: true, writable: false, configurable: true },
  hidden: { value: 3, enumerable: false, writable: true, configurable: true },
  frozen: { value: 4, enumerable: true, writable: false, configurable: false }
});

const sym = Symbol('custom');
Object.defineProperty(original, sym, {
  value: 5, enumerable: true, writable: true, configurable: true
});

// 克隆
const copy = cloneWithDescriptors(original);

// 对比描述符
const originalDesc = Object.getOwnPropertyDescriptors(original);
const copyDesc = Object.getOwnPropertyDescriptors(copy);
console.log(JSON.stringify(originalDesc) === JSON.stringify(copyDesc)); // true

// 不可枚举属性也被复制
console.log(copy.hidden); // 3
console.log(Object.keys(copy)); // ['normal', 'readonly', 'frozen']

// Symbol 属性也被复制
console.log(copy[sym]); // 5

// 只读属性保持只读
// 'use strict' 下:copy.readonly = 99; // TypeError

12.6 defineProperty 的限制与 Proxy 的对比

12.6.1 defineProperty 的三大局限

Object.defineProperty 虽然强大,但存在一些固有限制,这也是 Vue3 改用 Proxy 的原因:

局限 说明 影响
无法监听新增属性 只能劫持已存在的属性 后续添加的属性不是响应式的
无法监听删除属性 delete obj.prop 不会触发 set Vue2 需要 $delete 辅助方法
无法监听数组索引变化 arr[0] = x 不触发 set(部分引擎) Vue2 需要重写数组方法
javascript 复制代码
// 局限1:无法监听新增属性
const obj = reactive({ a: 1 });
obj.b = 2; // b 不是响应式的!

// Vue2 的解决方案:Vue.set(obj, 'b', 2)

// 局限2:无法监听删除属性
delete obj.a; // 不触发任何通知
// Vue2 的解决方案:Vue.delete(obj, 'a')

// 局限3:数组索引和 length
const arr = reactive([1, 2, 3]);
arr[0] = 99;  // 不触发(某些引擎中 defineProperty 对数组索引无效)
arr.length = 0; // 不触发

12.6.2 Proxy vs defineProperty

javascript 复制代码
// === Proxy 实现的响应式 ===

function reactiveProxy(target) {
  const deps = new Map(); // key -> Set of effects

  function track(key) {
    if (activeEffect) {
      if (!deps.has(key)) deps.set(key, new Set());
      deps.get(key).add(activeEffect);
    }
  }

  function trigger(key) {
    if (deps.has(key)) {
      deps.get(key).forEach(effect => effect());
    }
  }

  return new Proxy(target, {
    get(obj, key, receiver) {
      track(key);
      const result = Reflect.get(obj, key, receiver);
      // 嵌套对象也做响应式处理
      if (result !== null && typeof result === 'object') {
        return reactiveProxy(result);
      }
      return result;
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver);
      trigger(key);
      return result;
    },
    deleteProperty(obj, key) {
      const result = Reflect.deleteProperty(obj, key);
      trigger(key);
      return result;
    }
  });
}

// Proxy 没有上述局限
const state = reactiveProxy({ a: 1 });
state.b = 2;    // ✅ 新增属性也能监听
delete state.a;  // ✅ 删除属性也能监听

const arr = reactiveProxy([1, 2, 3]);
arr[0] = 99;     // ✅ 数组索引变化也能监听
arr.push(4);     // ✅ 数组方法也能监听
特性 Object.defineProperty Proxy
监听新增属性
监听删除属性
监听数组索引 ❌(需重写方法)
监听 length
嵌套对象监听 初始化时递归 访问时懒代理
兼容性 ES5(IE9+) ES6(不支持 IE)
性能 初始化遍历所有属性 按需代理
Vue 版本 Vue2 Vue3

本章小结

知识点 要点
属性描述符 { value, writable, enumerable, configurable }
writable false 时属性只读,赋值静默失败或报错
enumerable false 时属性被隐藏,不出现在 keys/for...in
configurable false 时属性不可删除、不可重配置(writable 可单向收紧)
Object.defineProperty 定义/修改单个属性的描述符;新建属性时省略标志默认 false
Object.defineProperties 批量定义多个属性
数据属性 vs 访问器属性 数据属性有 value/writable,访问器属性有 get/set,二者互斥
getter/setter 读写时自动触发函数,可做校验、计算、依赖追踪
for...in 唯一遍历原型链且只含可枚举属性的方法
Object.assign / 展开运算符 只复制可枚举的自有属性
preventExtensions / seal / freeze 三个层级的对象保护,都是浅层操作
defineProperty 的局限 无法监听增删属性和数组索引 → Proxy 是更好的方案

一句话总结 :JavaScript 的属性不只是键值对,每个属性都携带 writableenumerableconfigurable 三个隐藏标志,通过 Object.defineProperty 可以精确控制属性的行为;getter/setter 让属性变成可拦截的"计算通道",是实现数据校验和响应式系统的基石。


下篇预告第13章:原始类型的方法 --- 我们将探讨一个有趣的问题:原始类型(number、string、boolean)明明不是对象,为什么能调用 .toFixed().toUpperCase() 等方法?背后涉及的"装箱"机制将揭开这个谜底。


作者 :xhmico | CSDN 博客https://blog.csdn.net/xhmico

参考资源The Modern JavaScript Tutorial | MDN Web Docs | ECMAScript 规范

相关推荐
gis开发之家2 小时前
《Vue3 从入门到大神29篇》单元测试与 E2E 测试 —— 保障 Vue3 项目的质量
前端·javascript·vue.js·单元测试·前端框架·vue3
柒和远方2 小时前
从等待到流式:LLM 流式输出的原理、协议与工程实践
javascript·llm
全栈项目管理程序猿3 小时前
Cesium-1.143 中文版 API
javascript·cesium
喜欢打篮球的普通人3 小时前
LLVM后端指令选择
android·java·javascript
Cobyte4 小时前
24.Vue Vapor 组件事件 emit 的实现
前端·javascript·vue.js
Flynt4 小时前
TypeScript 5.8 让我少踩了一个大坑,顺手再聊聊 --erasableSyntaxOnly
前端·javascript·typescript
默_笙4 小时前
👍 手写一个 MCP 文件读取服务:原来 AI 能"读"我的文件,全靠这个协议
前端·javascript
大流星4 小时前
LangChainJS之Chain链(四)
javascript·langchain
weedsfly4 小时前
前端开发中的装饰器模式——给函数和组件“加壳”的艺术
前端·javascript·面试