首页/文章/八股文

Object.defineProperty 是否可以监听拦截数组变化

2025-02-14
1243 字约 5 分钟
...

关键词:Object.defineProperty 监听数组变化

  1. 基本原理与部分可行性

    • Object.defineProperty可以用于监听和拦截数组的某些变化,但不是原生地对所有数组操作都能很好地监听。

    • 数组在 JavaScript 中是特殊的对象,其索引可以看作是对象属性。理论上,我们可以使用Object.defineProperty为数组的每个索引(属性)定义属性描述符,以此来尝试监听数组元素的读取和设置操作。

    • 例如,对于一个简单的数组元素设置操作,可以这样定义:

      let arr = [1, 2, 3];
      Object.defineProperty(arr, "0", {
        get: function () {
          console.log("读取索引为0的元素");
          return arr[0];
        },
        set: function (value) {
          console.log("设置索引为0的元素");
          arr[0] = value;
        },
      });
      • 当通过arr[0]读取或设置元素时,相应的get和set函数会被触发,从而实现对这个特定索引元素的变化监听。
  2. 局限性

    • 无法自动监听所有元素:这种方式需要为每个要监听的索引单独使用Object.defineProperty进行定义。如果数组长度是动态变化的,或者要监听整个数组,这种逐个定义的方式就非常繁琐且不实用。例如,对于一个有很多元素的数组或者长度会不断变化的数组,几乎不可能预先为每个可能的索引都定义属性描述符。
    • 无法直接监听数组方法:它不能直接监听数组的方法(如push、pop、shift、unshift、splice等)引起的变化。这些方法会改变数组的状态,但不会触发通过Object.defineProperty为数组元素定义的get和set操作。比如,当使用push方法添加元素到数组时,不会自动触发之前为数组元素定义的set操作来监听这个新元素的添加。
  3. 解决方案 - 重写数组方法实现全面监听

以下是使用Object.defineProperty来实现监听数组部分常见操作(如修改元素、添加元素、删除元素等)的基本思路和示例代码:

3.1. 整体思路

要使用Object.defineProperty监听数组,主要思路是对数组的原型方法进行重定义,在这些重定义的方法内部,通过Object.defineProperty来设置属性描述符,使得在执行这些操作时能够触发自定义的监听函数,从而实现对数组变化的监听。

3.2. 具体步骤及示例代码

(1)创建一个继承自原生数组的新类

首先,创建一个新的类,让它继承自原生数组,以便后续可以在这个新类上添加自定义的监听逻辑。

function ObservableArray() {
  // 调用原生数组构造函数,确保可以像正常数组一样使用
  Array.apply(this, arguments);
}
ObservableArray.prototype = Object.create(Array.prototype);
ObservableArray.prototype.constructor = ObservableArray;

(2)重定义数组的部分原型方法

接下来,重定义数组的一些常见操作的原型方法,比如push、pop、shift、unshift、splice等,在这些重定义的方法内部添加监听逻辑。

以push方法为例:

ObservableArray.prototype.push = function () {
  // 保存当前数组长度,用于后续判断添加了几个元素
  var previousLength = this.length;

  // 调用原生数组的push方法,执行实际的添加操作
  var result = Array.prototype.push.apply(this, arguments);

  // 遍历新添加的元素,为每个元素设置属性描述符以实现监听
  for (var i = previousLength; i < this.length; i++) {
    (function (index) {
      Object.defineProperty(this, index, {
        enumerable: true,
        configurable: true,
        get: function () {
          console.log("正在读取索引为" + index + "的元素");
          return this[index];
        },
        set: function (value) {
          console.log("正在设置索引为" + index + "的元素为" + value);
          this[index] = value;
        },
      });
    }).call(this, i);
  }

  console.log("执行了push操作,添加了" + (this.length - previousLength) + "个元素");

  return result;
};

在上述push方法的重定义中:

  • 首先调用原生数组的push方法来执行实际的添加元素操作,并保存添加前的数组长度。
  • 然后,通过循环为新添加的每个元素使用Object.defineProperty设置属性描述符。在get方法中,当读取该元素时会打印相应信息;在set方法中,当设置该元素时也会打印相应信息。
  • 最后,打印出执行push操作添加的元素个数。

类似地,可以重定义其他如pop、shift、unshift、splice等方法,以下是pop方法的重定义示例:

ObservableArray.prototype.pop = function () {
  var result = Array.prototype.pop.apply(this, arguments);

  if (this.length >= 0) {
    Object.defineProperty(this, this.length, {
      enumerable: true,
      configurable: true,
      get: function () {
        console.log("正在读取最后一个元素");
        return this[this.length];
      },
      set: function (value) {
        console.log("正在设置最后一个元素为" + value);
        this[this.length] = value;
      },
    });
  }

  console.log("执行了pop操作");

  return result;
};

3.3. 使用示例

创建ObservableArray的实例并进行操作来测试监听效果:

var myArray = new ObservableArray(1, 2, 3);

myArray.push(4, 5);
var poppedElement = myArray.pop();
myArray[0] = 10;

在上述示例中:

  • 首先创建了一个ObservableArray实例myArray并初始化为[1, 2, 3]。
  • 然后执行myArray.push(4, 5),此时会触发重定义的push方法,添加元素的同时会为新添加的元素设置监听逻辑,并且会打印出相关操作信息。
  • 接着执行myArray.pop(),触发重定义的pop方法,执行弹出操作并设置对最后一个元素的监听逻辑,同时打印出相关操作信息。
  • 最后执行myArray[0] = 10,由于之前为数组元素设置了监听逻辑(在push方法中对新添加元素设置了监听),所以会触发相应的set逻辑,打印出相关信息。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2026-08-20
Embedding 向量模型:从语义表示到相似度计算
前言 大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个 高维向量 ,再用几何方法比较。这一步就是 Embedding。 本篇基于我本地 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。 一、为什么需要 Embedding 传统关键词检索是「字面匹配」: Embedding 做的是「语义匹...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试
八股文2026-08-19
深入理解 JavaScript 原型与原型链:从关系图到核心逻辑 - heshanwan - 博客园
在 JavaScript 世界里, 原型(Prototype)和原型链(Prototype Chain) 是理解对象继承、属性查找机制的基石。很多开发者初学时对它们 “又爱又恨”,这篇文章将结合经典关系图,用通俗易懂的方式拆解原型与原型链的核心逻辑,帮你彻底掌握这套机制! &nbsp; 一、先搞懂几个核心概念 在分析关系图前,先明确 JavaScript 中与原型相关的关键概念,避免后续混淆: 1\. 函数对象与普通对象 函数对象 :由...
面试
八股文2026-08-17
JavaScript 闭包
摘要 :闭包(Closure)是 函数与其词法环境(Lexical Environment)的组合 ——一个函数可以"记住"并访问它被创建时所处的外部作用域中的变量,即使它在该作用域之外被执行。一句话记忆: 函数打包了出生地的变量,走到哪都能用 。核心要点:① 内部函数引用外部变量即形成闭包;② 闭包实现变量私有化/数据封装/柯里化;③ 潜在风险是内存泄漏和循环引用;④ 避免泄漏需手动解除引用和及时移除监听器。 一、什么是闭包 1.1...
面试
八股文2026-08-17
this 指向与 call / apply / bind
第 1 段(10min)回顾与预习:为什么必须搞懂 this 1.1 先接昨日(闭包)的线 昨天你学了 闭包 。闭包解决的是"函数记住它定义时所在作用域里的变量"。 闭包回答的问题是: “这个函数能访问到哪些变量?” → 看定义位置(词法作用域)。 而今天的主角 回答的是一个完全不同的问题: “这个函数执行时,站在谁的角度说话?” → 看 调用位置 (谁调用的、怎么调用的)。 一句话概括两者的分工: 变量看 定义处 , 看 调用处 。这...
面试
八股文2026-08-17
事件循环与宏任务 / 微任务
第 1 段(10min)回顾与预习:接住昨天的线 1.1 先接昨日(原型链)的线 昨天你学了 对象怎么找属性 (沿原型链上溯)。今天换一个维度: 代码怎么排队执行 。 把前三天的知识串成一句话,你已经有了 JS 的三张地图: 1.2 先凭直觉回答 4 个小问题(不用对,先写下答案) 学完第 2、3、4 段再回来对照。第 1 段只做一件事:记住画面—— JS 单线程,但靠"排队 + 轮转"假装多线程 。 第 2 段(15min)知识块①:...
面试

评论

请登录后发表评论

去登录
加载评论中...