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

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

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

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

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

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

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

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

3.1. 整体思路

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

3.2. 具体步骤及示例代码

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

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

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

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

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

push方法为例:

 1ObservableArray.prototype.push = function () {
 2  // 保存当前数组长度,用于后续判断添加了几个元素
 3  var previousLength = this.length;
 4
 5  // 调用原生数组的push方法,执行实际的添加操作
 6  var result = Array.prototype.push.apply(this, arguments);
 7
 8  // 遍历新添加的元素,为每个元素设置属性描述符以实现监听
 9  for (var i = previousLength; i < this.length; i++) {
10    (function (index) {
11      Object.defineProperty(this, index, {
12        enumerable: true,
13        configurable: true,
14        get: function () {
15          console.log("正在读取索引为" + index + "的元素");
16          return this[index];
17        },
18        set: function (value) {
19          console.log("正在设置索引为" + index + "的元素为" + value);
20          this[index] = value;
21        },
22      });
23    }).call(this, i);
24  }
25
26  console.log("执行了push操作,添加了" + (this.length - previousLength) + "个元素");
27
28  return result;
29};

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

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

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

 1ObservableArray.prototype.pop = function () {
 2  var result = Array.prototype.pop.apply(this, arguments);
 3
 4  if (this.length >= 0) {
 5    Object.defineProperty(this, this.length, {
 6      enumerable: true,
 7      configurable: true,
 8      get: function () {
 9        console.log("正在读取最后一个元素");
10        return this[this.length];
11      },
12      set: function (value) {
13        console.log("正在设置最后一个元素为" + value);
14        this[this.length] = value;
15      },
16    });
17  }
18
19  console.log("执行了pop操作");
20
21  return result;
22};

3.3. 使用示例

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

 1var myArray = new ObservableArray(1, 2, 3);
 2
 3myArray.push(4, 5);
 4var poppedElement = myArray.pop();
 5myArray[0] = 10;

在上述示例中:

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