0

0

javascript数组如何实现观察者模式

畫卷琴夢

畫卷琴夢

发布时间:2025-08-12 15:20:02

|

485人浏览过

|

来源于php中文网

原创

javascript数组本身不支持观察者模式,要实现需通过封装或proxy拦截操作并通知订阅者。1. 使用自定义类可封装数组,重写push、pop、splice等方法,在操作后调用\_notify通知订阅者;2. 直接索引赋值无法用setter捕获,需借助es6 proxy的set陷阱实现;3. proxy通过get和set陷阱统一拦截数组读写操作,能全面监控方法调用和索引修改,自动触发回调;4. 深层对象属性变化仍需递归观察,proxy虽无法完全解决但大幅简化实现;5. 需注意性能开销与内存泄漏风险,确保提供unsubscribe机制。该方案有效解耦数据变化与响应逻辑,适用于ui更新、数据同步等场景。

javascript数组如何实现观察者模式

JavaScript数组本身并没有内置的观察者模式,这意味着当你直接修改一个数组(比如

push
pop
splice
,或者直接通过索引赋值
arr[0] = 'newValue'
)时,它不会自动通知其他部分。要实现观察者模式,你需要围绕数组构建一个额外的层,通常是一个封装器或代理,来拦截这些修改操作,并在发生变化时主动通知订阅者。

javascript数组如何实现观察者模式

解决方案

要让JavaScript数组拥有观察能力,核心思路是创建一个中间层,这个层负责管理数组的实际操作以及通知机制。最直接的方法是创建一个自定义的类或对象来封装数组,并暴露订阅/取消订阅的方法。

这个封装器需要:

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

javascript数组如何实现观察者模式
  1. 持有实际的数组实例
  2. 维护一个订阅者列表(通常是回调函数的集合)。
  3. 提供
    subscribe
    方法
    ,允许外部代码注册回调函数。
  4. 提供
    unsubscribe
    方法
    ,允许外部代码移除回调函数。
  5. 重写或拦截数组的关键修改方法(如
    push
    ,
    pop
    ,
    splice
    ,
    shift
    ,
    unshift
    ,
    sort
    ,
    reverse
    ),在调用原始数组方法后,遍历订阅者列表并执行回调,传递变化的详细信息。
  6. 处理直接通过索引赋值的情况,这通常需要更高级的机制,比如使用ES6的
    Proxy

举个例子,一个基础的封装器可能长这样:

class ObservableArray {
    constructor(initialArray = []) {
        this._array = initialArray;
        this._subscribers = [];
    }

    subscribe(callback) {
        this._subscribers.push(callback);
        // 返回一个取消订阅的函数,方便管理
        return () => this.unsubscribe(callback);
    }

    unsubscribe(callback) {
        this._subscribers = this._subscribers.filter(sub => sub !== callback);
    }

    _notify(changeType, payload) {
        this._subscribers.forEach(callback => callback(changeType, payload));
    }

    // 拦截 push
    push(...items) {
        const oldLength = this._array.length;
        const result = this._array.push(...items);
        this._notify('push', { items, newLength: this._array.length, oldLength });
        return result;
    }

    // 拦截 pop
    pop() {
        const item = this._array.pop();
        this._notify('pop', { item, newLength: this._array.length });
        return item;
    }

    // 拦截 splice
    splice(start, deleteCount, ...items) {
        const removed = this._array.splice(start, deleteCount, ...items);
        this._notify('splice', { start, deleteCount, items, removed, newLength: this._array.length });
        return removed;
    }

    // 访问器,确保外部不能直接修改 _array
    get array() {
        return [...this._array]; // 返回副本,防止直接修改
    }

    // 尝试处理直接索引赋值,但这很复杂,通常需要Proxy
    set(index, value) {
        if (index >= 0 && index < this._array.length) {
            const oldValue = this._array[index];
            this._array[index] = value;
            this._notify('set', { index, oldValue, newValue: value });
            return true;
        } else if (index === this._array.length) { // 类似push
            this._array[index] = value;
            this._notify('push', { items: [value], newLength: this._array.length, oldLength: this._array.length - 1 });
            return true;
        }
        return false;
    }
}

// 实际使用
// const myObservableArray = new ObservableArray([1, 2, 3]);
// myObservableArray.subscribe((type, payload) => {
//     console.log(`Array changed: ${type}`, payload, 'Current array:', myObservableArray.array);
// });
// myObservableArray.push(4); // 会触发通知
// myObservableArray.set(0, 100); // 会触发通知

这种手动拦截的方式虽然可行,但对于所有数组方法和直接索引赋值的处理会变得非常冗长和容易出错。这也是为什么ES6的

Proxy
在处理这类问题时显得尤为强大。

javascript数组如何实现观察者模式

为什么你需要观察一个JavaScript数组?

说实话,我个人在项目里遇到需要观察数组变动的情况,大多都和UI渲染、数据同步以及某些业务逻辑的自动化触发有关。这不仅仅是为了“酷”,而是实实在在解决了许多痛点。

一个很典型的场景就是前端框架中的数据绑定。想象一下,你有一个用户列表数组,当用户添加、删除或者修改了列表中的某一项时,你希望页面能立刻更新,而不需要手动去重新渲染整个列表。观察者模式在这里就发挥了关键作用:数组变动时,它会通知订阅者(比如UI组件),然后组件根据变化的数据进行局部更新。这大大提高了开发效率,也让代码逻辑更清晰。

再比如,在一些复杂的数据处理流程中,你可能需要根据数组的状态变化来触发后续的计算或API请求。比如,一个购物车商品列表,每当商品数量或种类发生变化时,你可能需要重新计算总价,或者自动保存到用户的会话中。如果能直接“监听”数组,这些操作就能做到自动化和解耦,而不是在每次修改数组的地方都手动调用一次更新函数,那样代码会变得非常冗余且难以维护。它本质上是在解决“数据变化如何驱动行为变化”的问题,让数据和行为之间的耦合度降到最低。

观察数组变动时会遇到哪些常见挑战?

在尝试让数组变得“可观察”时,你很快就会发现一些让人头疼的地方,这不像观察一个普通对象那么直接。

动态WEB网站中的PHP和MySQL:直观的QuickPro指南第2版
动态WEB网站中的PHP和MySQL:直观的QuickPro指南第2版

动态WEB网站中的PHP和MySQL详细反映实际程序的需求,仔细地探讨外部数据的验证(例如信用卡卡号的格式)、用户登录以及如何使用模板建立网页的标准外观。动态WEB网站中的PHP和MySQL的内容不仅仅是这些。书中还提到如何串联JavaScript与PHP让用户操作时更快、更方便。还有正确处理用户输入错误的方法,让网站看起来更专业。另外还引入大量来自PEAR外挂函数库的强大功能,对常用的、强大的包

下载

首先,也是最让人困惑的,是JavaScript数组的“原生”修改方式太多样了

push
,
pop
,
splice
,
shift
,
unshift
,
sort
,
reverse
这些方法都会直接修改原数组。如果你只是简单地封装一个数组,而没有对这些方法进行拦截或重写,那么它们对数组的修改将是“静默”的,你的观察者根本不会知道发生了什么。更要命的是,直接通过索引赋值,比如
myArray[0] = 'newValue'
,这种操作是无法通过传统setter/getter(
Object.defineProperty
)来捕获的,因为数组的索引被视为属性,但它们不是通过标准的setter机制触发的。这导致了早期很多MVVM框架在处理数组响应式时,不得不提供额外的
$set
Vue.set
方法来弥补这个缺陷。

其次,是深层观察的复杂性。如果你的数组里存储的不是基本类型(字符串、数字),而是对象,那么当这些对象内部的属性发生变化时,数组本身并没有改变。比如

[{id: 1, name: 'A'}]
变成
[{id: 1, name: 'B'}]
,数组的引用没变,长度没变,但内容变了。要观察到这种变化,你需要对数组中的每个元素也进行深度观察,这无疑增加了实现的复杂度和性能开销。你得递归地为每个新加入的或被修改的元素也添加观察机制。

再来,是性能考量和内存泄漏风险。如果你有大量的数组实例需要被观察,或者一个数组被大量的订阅者监听,那么每次数组变动时的通知开销可能会变得很大。特别是当通知逻辑本身比较复杂时,这会影响应用的响应速度。同时,如果订阅者没有正确地取消订阅,就可能导致内存泄漏,因为被订阅的对象会一直持有对订阅者回调函数的引用,阻止垃圾回收。这要求你在设计订阅/取消订阅机制时要非常严谨,提供便捷的取消订阅方式(比如返回一个取消函数)。

这些挑战都促使开发者去寻找更优雅、更底层的解决方案,而ES6的

Proxy
正是为此而生。

Proxy对象如何简化数组观察?

说实话,当我第一次深入了解ES6的

Proxy
对象时,我感觉它简直就是为解决数组观察这种“老大难”问题量身定制的。它提供了一种前所未有的能力,让你可以在操作对象(包括数组)时,拦截几乎所有的底层操作,而不仅仅是属性的读写。

Proxy
的工作原理是创建一个目标对象的代理,所有对代理对象的操作都会先经过你定义的“陷阱”(traps),然后你可以在这些陷阱里执行自定义逻辑,再决定是否将操作转发给目标对象。对于数组观察,这简直是天赐良机。

最关键的几个陷阱是:

  1. set(target, property, value, receiver)
    :这个陷阱能捕获到对数组元素的所有直接赋值操作,无论是
    arr[0] = 'newValue'
    还是
    arr.length = 0
    ,甚至是你给数组添加新属性(虽然数组通常不这么用)。这意味着你不再需要担心那些“静默”的索引赋值了。
  2. get(target, property, receiver)
    :这个陷阱可以捕获对数组属性的读取操作。这在拦截数组方法时特别有用。当有人调用
    arr.push()
    时,实际上是先
    get
    push
    这个方法,然后调用它。我们可以在
    get
    陷阱中返回一个“包装过”的
    push
    方法,这个包装方法在调用原始
    push
    之前或之后触发通知。

通过

Proxy
,你可以用一种相对统一且优雅的方式来处理数组的所有修改操作。

这是一个简化版的

Proxy
实现数组观察的例子:

function createObservableArray(initialArray = []) {
    const subscribers = [];
    const array = initialArray;

    const notify = (changeType, payload) => {
        subscribers.forEach(callback => callback(changeType, payload));
    };

    const handler = {
        set(target, property, value, receiver) {
            const oldValue = target[property];
            const result = Reflect.set(target, property, value, receiver); // 执行原始赋值

            // 仅在实际值发生变化时通知,或处理新元素添加
            if (property !== 'length' && oldValue !== value) {
                notify('set', { index: property, oldValue, newValue: value });
            } else if (property === 'length' && oldValue !== value) {
                // 长度变化可能意味着元素被移除或添加
                if (value < oldValue) { // 长度变短,可能是pop或splice导致
                    notify('remove', { oldLength: oldValue, newLength: value });
                } else { // 长度变长
                    notify('add', { oldLength: oldValue, newLength: value });
                }
            }
            return result;
        },

        get(target, property, receiver) {
            // 拦截数组的修改方法
            if (['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].includes(property)) {
                return function(...args) {
                    const oldArray = [...target]; // 记录修改前的状态
                    const result = Reflect.apply(target[property], target, args); // 调用原始方法
                    const newArray = target; // 修改后的数组

                    // 根据不同的方法类型发送通知
                    switch (property) {
                        case 'push':
                            notify('push', { added: args, newArray, oldArray });
                            break;
                        case 'pop':
                            notify('pop', { removed: result, newArray, oldArray });
                            break;
                        case 'shift':
                            notify('shift', { removed: result, newArray, oldArray });
                            break;
                        case 'unshift':
                            notify('unshift', { added: args, newArray, oldArray });
                            break;
                        case 'splice':
                            notify('splice', { args, removed: result, newArray, oldArray });
                            break;
                        case 'sort':
                        case 'reverse':
                            notify(property, { newArray, oldArray }); // 排序或反转
                            break;
                    }
                    return result;
                };
            }
            // 对于其他属性(如length,或普通元素访问),直接返回
            return Reflect.get(target, property, receiver);
        }
    };

    const proxy = new Proxy(array, handler);

    // 暴露订阅和取消订阅的方法
    proxy.subscribe = (callback) => {
        subscribers.push(callback);
        return () => proxy.unsubscribe(callback);
    };
    proxy.unsubscribe = (callback) => {
        subscribers.splice(subscribers.indexOf(callback), 1);
    };

    return proxy;
}

// 实际使用
// const myProxyArray = createObservableArray([10, 20, 30]);
// myProxyArray.subscribe((type, payload) => {
//     console.log(`Proxy Array changed: ${type}`, payload, 'Current array:', myProxyArray);
// });
// myProxyArray.push(40); // 触发 push 通知
// myProxyArray[0] = 100; // 触发 set 通知
// myProxyArray.pop(); // 触发 pop 通知
// myProxyArray.splice(0, 1); // 触发 splice 通知
// console.log(myProxyArray[0]); // 不会触发通知,只是读取

Proxy
的优势在于它的拦截能力非常全面,代码量相对更少,而且更接近原生行为。但它也有局限性,比如它不能被polyfill,在一些老旧的浏览器环境(如IE)中是无法使用的。同时,它只是解决了数组本身的修改通知,如果数组里包含的是对象,而你还需要观察这些对象的深层变化,那依然需要结合其他机制(比如递归地为每个对象也创建
Proxy
)。不过,对于大多数场景下的数组变动观察,
Proxy
无疑是一个强大且优雅的解决方案。

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
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新特性的相关的文章、下载、课程内容,供大家免费下载体验。

195

2023.08.04

JavaScript ES6新特性
JavaScript ES6新特性

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

222

2025.12.24

sort排序函数用法
sort排序函数用法

sort排序函数的用法:1、对列表进行排序,默认情况下,sort函数按升序排序,因此最终输出的结果是按从小到大的顺序排列的;2、对元组进行排序,默认情况下,sort函数按元素的大小进行排序,因此最终输出的结果是按从小到大的顺序排列的;3、对字典进行排序,由于字典是无序的,因此排序后的结果仍然是原来的字典,使用一个lambda表达式作为key参数的值,用于指定排序的依据。

395

2023.09.04

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

319

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

212

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1502

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

624

2023.11.24

C++ 设计模式与软件架构
C++ 设计模式与软件架构

本专题深入讲解 C++ 中的常见设计模式与架构优化,包括单例模式、工厂模式、观察者模式、策略模式、命令模式等,结合实际案例展示如何在 C++ 项目中应用这些模式提升代码可维护性与扩展性。通过案例分析,帮助开发者掌握 如何运用设计模式构建高质量的软件架构,提升系统的灵活性与可扩展性。

8

2026.01.30

热门下载

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

精品课程

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

共42课时 | 7.4万人学习

Vue3.x 工具篇--十天技能课堂
Vue3.x 工具篇--十天技能课堂

共26课时 | 1.5万人学习

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

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