0

0

如何利用JavaScript的Object.observe监听对象变化,以及它被废弃后的替代方案有哪些?

紅蓮之龍

紅蓮之龍

发布时间:2025-09-21 21:52:01

|

791人浏览过

|

来源于php中文网

原创

object.observe因设计复杂、性能问题及proxy的出现被废弃,现主要通过proxy实现对象监听,也可用object.defineproperty或响应式框架替代。

如何利用javascript的object.observe监听对象变化,以及它被废弃后的替代方案有哪些?

Object.observe
曾是 JavaScript 中一个非常有前景的提案,它允许开发者直接监听对象属性的变化。然而,这个特性最终在 ES2016 规范制定过程中被废弃了。现在,要实现类似的对象变化监听功能,我们主要依赖 ES6 引入的
Proxy
对象,或者通过自定义
setter/getter
函数 (
Object.defineProperty
) 来实现,更复杂的场景则会借助各种响应式编程库或框架。

解决方案

Object.observe
的初衷是提供一种高效、原生的方式来追踪 JavaScript 对象的修改,包括属性的添加、删除、修改,甚至是属性描述符的变化。它通过一个回调函数来接收这些变化通知。但由于其设计上的复杂性、性能考量以及与新兴的
Proxy
机制相比缺乏灵活性,最终被标准委员会放弃了。

要替代

Object.observe
的功能,我们现在主要有以下几种策略:

1. 使用 ES6 Proxy

立即学习Java免费学习笔记(深入)”;

Proxy
是目前最强大、最通用的解决方案。它允许你为目标对象创建一个代理,然后拦截对该对象的所有操作,比如属性的读取 (
get
)、设置 (
set
)、删除 (
deleteProperty
) 等。通过在这些拦截器(称为“陷阱”或
trap
)中加入自定义逻辑,我们就能实现对对象变化的监听。

function createReactiveObject(obj, callback) {
  return new Proxy(obj, {
    set(target, property, value, receiver) {
      const oldValue = target[property];
      // 避免不必要的触发,如果新旧值相同(且不是NaN)
      if (oldValue === value) {
        return true;
      }

      const result = Reflect.set(target, property, value, receiver);
      // 只有成功设置了属性才触发回调
      if (result) {
        callback({
          type: 'update',
          property: property,
          oldValue: oldValue,
          newValue: value,
          target: target
        });
      }
      return result;
    },
    deleteProperty(target, property) {
      if (Reflect.has(target, property)) {
        const oldValue = target[property];
        const result = Reflect.deleteProperty(target, property);
        if (result) {
          callback({
            type: 'delete',
            property: property,
            oldValue: oldValue,
            target: target
          });
        }
        return result;
      }
      return false; // 属性不存在,删除失败
    },
    // 也可以拦截属性的添加,但通常 set 已经涵盖了首次赋值
    // get(target, property, receiver) { ... }
  });
}

let data = { a: 1, b: 'hello' };
let reactiveData = createReactiveObject(data, (change) => {
  console.log('对象发生变化:', change);
});

reactiveData.a = 2; // 输出: 对象发生变化: { type: 'update', property: 'a', oldValue: 1, newValue: 2, target: { a: 2, b: 'hello' } }
reactiveData.c = 3; // 输出: 对象发生变化: { type: 'update', property: 'c', oldValue: undefined, newValue: 3, target: { a: 2, b: 'hello', c: 3 } }
delete reactiveData.b; // 输出: 对象发生变化: { type: 'delete', property: 'b', oldValue: 'hello', target: { a: 2, c: 3 } }

Proxy
的强大之处在于它能拦截几乎所有的对象操作,这让我们可以构建出非常灵活和强大的响应式系统,Vue 3 的响应式核心就是基于
Proxy
实现的。

2. 使用 Object.defineProperty 定义 Setter/Getter

这是在

Proxy
出现之前,实现对象属性监听的常见方法。通过
Object.defineProperty
,我们可以为对象的特定属性定义自定义的
getter
setter
函数。当属性被读取或修改时,这些函数会被调用。

function defineReactiveProperty(obj, key, val, callback) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // console.log(`属性 ${key} 被读取`);
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      const oldValue = val;
      val = newVal;
      callback({
        type: 'update',
        property: key,
        oldValue: oldValue,
        newValue: newVal,
        target: obj
      });
      // console.log(`属性 ${key} 被修改为 ${newVal}`);
    }
  });
}

let oldData = { x: 10, y: 'world' };
defineReactiveProperty(oldData, 'x', oldData.x, (change) => {
  console.log('对象属性变化:', change);
});
defineReactiveProperty(oldData, 'y', oldData.y, (change) => {
  console.log('对象属性变化:', change);
});

oldData.x = 20; // 输出: 对象属性变化: { type: 'update', property: 'x', oldValue: 10, newValue: 20, target: { x: 20, y: 'world' } }
oldData.y = 'hello world'; // 输出: 对象属性变化: { type: 'update', property: 'y', oldValue: 'world', newValue: 'hello world', target: { x: 20, y: 'hello world' } }
oldData.z = 30; // 无法监听,因为 z 不是通过 defineReactiveProperty 定义的

这种方法在 Vue 2.x 中被广泛使用,但它有明显的局限性:无法直接监听对象属性的添加和删除,也无法监听数组索引的变化(需要对数组方法进行额外封装)。

3. 响应式库/框架

如果你正在使用 Vue、React (结合状态管理库如 Redux/MobX)、Angular 等现代前端框架,它们通常已经内置了强大的响应式系统。这些系统在底层会利用

Proxy
Object.defineProperty
来实现对数据的监听和视图的自动更新,你通常不需要手动去实现这些细节。例如,Vue 3 的
reactive
函数就是基于
Proxy
实现的,而 MobX 也是通过
Proxy
defineProperty
来创建可观察对象的。


Object.observe
究竟为什么被废弃?深入探讨其设计缺陷与社区考量

说实话,

Object.observe
的废弃,在当时社区里还是引起了一波讨论的。我个人觉得,这背后并非单一原因,而是多方面因素交织的结果,既有技术层面的考量,也有标准制定哲学上的取舍。

首先,性能与实现复杂度是绕不开的话题。尽管

Object.observe
旨在提供原生、高效的监听机制,但要在 JavaScript 引擎层面实现对所有对象、所有类型变化的细粒度追踪,并保证性能开销可控,这本身就是一项巨大的挑战。想象一下,一个对象可能被多个观察者监听,每次变化都需要遍历这些观察者并触发回调,这在大型应用中可能会形成不小的性能瓶颈。更重要的是,它的异步通知机制,虽然避免了同步修改导致的无限循环问题,但也引入了新的复杂性,比如如何处理短时间内多次修改的批处理、如何保证通知的顺序等,这些都需要引擎进行复杂的调度和优化。

Unscreen
Unscreen

AI智能视频背景移除工具

下载

其次,粒度与实用性的平衡

Object.observe
提供了非常细致的变化类型(
add
update
delete
splice
setPrototype
reconfigure
),这在理论上很美好,但在实际开发中,开发者往往只需要知道“某个东西变了”,而不需要知道它具体是“被添加了”还是“被修改了”。这种过度的粒度,反而增加了开发者处理回调逻辑的负担,使得 API 显得有些笨重。

再者,与新兴的

Proxy
机制的重叠与冲突。当
Proxy
作为 ES6 的一部分被提出并逐渐成熟时,
Object.observe
的地位就变得尴尬了。
Proxy
提供了一种更底层、更通用的元编程能力,它能拦截对象的所有基本操作,而不仅仅是属性变化。这意味着,开发者可以基于
Proxy
自己构建出各种各样的“观察”机制,其灵活性远超
Object.observe
。标准委员会可能认为,提供一个更基础、更强大的原语 (
Proxy
),让开发者在此基础上构建上层应用,比提供一个特定场景 (
Object.observe
) 的高层 API 更符合 JavaScript 的设计哲学。
Proxy
给了开发者更多的控制权和可能性,而
Object.observe
则显得过于“意见化”和受限。

最后,浏览器厂商的采纳意愿也是一个关键因素。虽然 Chrome 率先实现了

Object.observe
,但其他主要浏览器(如 Firefox 和 Safari)并没有积极跟进。在没有广泛共识和采纳的情况下,一个特性很难成为真正的 Web 标准。TC39(ECMAScript 标准委员会)在权衡利弊后,最终决定将其从规范中移除,将精力集中在
Proxy
等更具前瞻性和通用性的特性上。

所以,

Object.observe
的废弃,可以说是一个“生不逢时”的例子。它试图解决一个真切的需求,但在技术演进的浪潮中,被更通用、更灵活的
Proxy
所取代,同时其自身的设计复杂性也成为了负担。


使用ES6 Proxy实现深度监听与复杂对象变化的最佳实践

ES6 的

Proxy
对象无疑是现代 JavaScript 中处理对象变化监听的利器,尤其是在需要深度监听复杂对象时,它的能力简直是开挂。不过,要用好它,特别是实现“深度监听”,还是有些门道的。

基本原理回顾

Proxy
的核心在于它是一个占位符,你对代理对象执行的任何操作(读取属性、设置属性、调用方法等)都会被拦截,并转发到你定义的
handler
对象中的对应“陷阱”方法。

const handler = {
  get(target, property, receiver) {
    console.log(`Getting property "${String(property)}"`);
    return Reflect.get(target, property, receiver);
  },
  set(target, property, value, receiver) {
    console.log(`Setting property "${String(property)}" to "${value}"`);
    return Reflect.set(target, property, value, receiver);
  }
};

let myObject = { a: 1 };
let proxyObject = new Proxy(myObject, handler);

proxyObject.a; // 输出: Getting property "a"
proxyObject.a = 2; // 输出: Setting property "a" to "2"

实现深度监听的挑战与策略

Proxy
默认只能监听代理对象本身的直接属性操作。当对象内部嵌套了其他对象或数组时,直接修改这些嵌套对象的属性,
proxyObject
set
陷阱是不会被触发的。

let nestedData = {
  info: { name: 'Alice', age: 30 },
  tags: ['js', 'dev']
};
let reactiveNestedData = new Proxy(nestedData, handler);

reactiveNestedData.info.name = 'Bob'; // handler的set不会被触发!
// 因为我们修改的是 info 对象内部的 name 属性,而不是 reactiveNestedData 对象的直接属性

要实现深度监听,核心策略是:

get
陷阱中,当访问到嵌套的对象或数组时,也将其包装成
Proxy
这样,无论你访问多深层次的属性,都会经过
Proxy
的层层拦截。

function createDeepReactive(obj, callback) {
  const isObject = (val) => val && typeof val === 'object';

  return new Proxy(obj, {
    get(target, property, receiver) {
      const res = Reflect.get(target, property, receiver);
      // 如果获取到的值是对象(且不是null),就递归地将其也包装成 Proxy
      if (isObject(res)) {
        return createDeepReactive(res, callback);
      }
      return res;
    },
    set(target, property, value, receiver) {
      const oldValue = Reflect.get(target, property, receiver);
      // 避免重复设置相同的值
      if (oldValue === value) {
        return true;
      }

      // 如果新值是对象,也需要包装成 Proxy
      const newValue = isObject(value) ? createDeepReactive(value, callback) : value;

      const result = Reflect.set(target, property, newValue, receiver);
      if (result) {
        callback({
          type: 'update',
          property: property,
          oldValue: oldValue,
          newValue: newValue,
          target: target
        });
      }
      return result;
    },
    deleteProperty(target, property) {
      if (Reflect.has(target, property)) {
        const oldValue = Reflect.get(target, property);
        const result = Reflect.deleteProperty(target, property);
        if (result) {
          callback({
            type: 'delete',
            property: property,
            oldValue: oldValue,
            target: target
          });
        }
        return result;
      }
      return false;
    }
  });
}

let deepData = {
  user: {
    name: 'Charlie',
    address: {
      city: 'New York',
      zip: '10001'
    }
  },
  items: [
    { id: 1, name: 'Laptop' },
    { id: 2, name: 'Mouse' }
  ]
};

let reactiveDeepData = createDeepReactive(deepData, (change) => {
  console.log('深度对象变化:', change);
});

reactiveDeepData.user.name = 'David'; // 触发回调
// 输出: 深度对象变化: { type: 'update', property: 'name', oldValue: 'Charlie', newValue: 'David', target: { name: 'David', address: { city: 'New York', zip: '10001' } } }

reactiveDeepData.user.address.city = 'Los Angeles'; // 触发回调
// 输出: 深度对象变化: { type: 'update', property: 'city', oldValue: 'New York', newValue: 'Los Angeles', target: { city: 'Los Angeles', zip: '10001' } }

reactiveDeepData.items.push({ id: 3, name: 'Keyboard' }); // 触发回调 (数组的push本质是修改length属性和添加新索引属性)
// 输出: 深度对象变化: { type: 'update', property: '2', oldValue: undefined, newValue: { id: 3, name: 'Keyboard' }, target: [ { id: 1, name: 'Laptop' }, { id: 2, name: 'Mouse' }, { id: 3, name: 'Keyboard' } ] }
// 输出: 深度对象变化: { type: 'update', property: 'length', oldValue: 2, newValue: 3, target: [ { id: 1, name: 'Laptop' }, { id: 2, name: 'Mouse' }, { id: 3, name: 'Keyboard' } ] }

// 替换整个对象
reactiveDeepData.user = { name: 'Eve', address: { city: 'London', zip: 'SW1A 0AA' } }; // 触发回调
// 输出: 深度对象变化: { type: 'update', property: 'user', oldValue: { name: 'David', address: { city: 'Los Angeles', zip: '10001' } }, newValue: Proxy { ... }, target: { user: Proxy { ... }, items: Proxy { ... } } }

最佳实践与注意事项:

  1. 递归代理的性能考量: 深度监听意味着当数据结构非常庞大且嵌套很深时,每次
    get
    操作都可能创建一个新的
    Proxy
    ,这会带来一定的性能开销和内存占用。在实际应用中,你可能需要权衡是否所有数据都需要深度响应,或者只对特定关键数据进行深度代理。
  2. 避免循环引用: 递归创建
    Proxy
    时要小心循环引用的情况,虽然
    Proxy
    本身通常能处理,但在某些极端场景下可能会导致栈溢出或无限循环。
  3. Reflect
    的使用:
    总是推荐使用
    Reflect
    对象来执行默认的对象操作(如
    Reflect.get
    ,
    Reflect.set
    ,
    Reflect.deleteProperty
    )。这不仅代码更清晰,也避免了
    this
    指向问题,并确保了操作的正确性。
  4. 数组操作的特殊性: 数组是特殊的对象。像
    push
    ,
    pop
    ,
    splice
    等方法会修改数组的
    length
    属性和/或特定索引的属性。我们的
    set
    陷阱可以捕捉到这些变化。但如果你想监听数组的每一个方法调用,可能需要在
    get
    陷阱中对数组方法进行包装,或者直接拦截
    apply
    陷阱(如果你的
    Proxy
    是一个函数)。不过,通常
    set
    陷阱足以覆盖大部分需求。
  5. 原始值的替换:
    Proxy
    只能代理对象。如果你直接替换一个属性为原始值(如
    reactiveDeepData.user = null
    ),那
    user
    将不再是
    Proxy
    。但下次你再将
    user
    设置为一个对象时,它又会被重新代理。
  6. this
    上下文问题:
    Proxy
    在拦截方法调用时,
    this
    的指向可能会成为一个坑。如果方法内部使用了
    this
    ,并且
    this
    期望指向原始对象,那么在
    get
    陷阱中返回方法时,需要使用
    Reflect.apply
    bind
    来确保
    this
    的正确性。不过,对于简单的属性监听,这通常不是问题。
  7. 应用场景:
    Proxy
    在状态管理库(如 Vue 3 的响应式系统)、ORM 框架、数据校验、日志记录、访问控制等领域都有非常强大的应用。理解并掌握它,能让你在这些领域如鱼得水。

除了Proxy,还有哪些不那么“现代”但依然实用的对象变化追踪策略?

虽然

Proxy
是目前最强大、最推荐的解决方案,但并非所有场景都适合或者需要
Proxy
。在一些老旧项目、对浏览器兼容性有更高要求、或者仅仅是需要追踪少量属性变化的场景下,一些“不那么现代”但依然实用的策略仍然有其价值。

1.

Object.defineProperty
的 Getter/Setter 机制

这个我们前面已经提到了,它

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
chrome什么意思
chrome什么意思

chrome是浏览器的意思,由Google开发的网络浏览器,它在2008年首次发布,并迅速成为全球最受欢迎的浏览器之一。本专题为大家提供chrome相关的文章、下载、课程内容,供大家免费下载体验。

1057

2023.08.11

chrome无法加载插件怎么办
chrome无法加载插件怎么办

chrome无法加载插件可以通过检查插件是否已正确安装、禁用和启用插件、清除插件缓存、更新浏览器和插件、检查网络连接和尝试在隐身模式下加载插件方法解决。更多关于chrome相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

838

2023.11.06

es6新特性
es6新特性

es6新特性有:1、块级作用域变量;2、箭头函数;3、模板字符串;4、解构赋值;5、默认参数;6、 扩展运算符;7、 类和继承;8、Promise。本专题为大家提供es6新特性的相关的文章、下载、课程内容,供大家免费下载体验。

106

2023.07.17

es6新特性有哪些
es6新特性有哪些

es6的新特性有:1、块级作用域;2、箭头函数;3、解构赋值;4、默认参数;5、扩展运算符;6、模板字符串;7、类和模块;8、迭代器和生成器;9、Promise对象;10、模块化导入和导出等等。本专题为大家提供es6新特性的相关的文章、下载、课程内容,供大家免费下载体验。

197

2023.08.04

JavaScript ES6新特性
JavaScript ES6新特性

ES6是JavaScript的根本性升级,引入let/const实现块级作用域、箭头函数解决this绑定问题、解构赋值与模板字符串简化数据处理、对象简写与模块化提升代码可读性与组织性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

231

2025.12.24

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

254

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

1089

2024.03.01

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

548

2023.12.01

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 10.5万人学习

MongoDB 教程
MongoDB 教程

共17课时 | 3.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号