JavaScript 实现数组变化监测的原理与代码实现

Ada
Ada
发布时间:2026-10-16| 8人浏览过
原创

在 JavaScript 中直接修改 Array.prototype 存在风险,因此通常采用在原数组和原生原型之间插入一个自定义原型对象的方式来实现数组变化监测。该方法通过重写 push、splice 等改变原数组的七种方法,在执行原生逻辑前后拦截数据变动,从而实现类似 Vue 2.x 的数据劫持效果。

核心思路:原型链插入与劫持

Object.defineProperty 仅能监测对象属性的读写,无法直接监测数组索引的变化。要监测数组变化,核心在于捕获所有会改变原数组的操作入口。

常用修改原数组的方法包括:push、pop、shift、unshift、reverse、sort 和 splice。我们不应直接污染全局原型,而是利用原型链特性,创建一个新对象继承自 Array.prototype,并将其作为目标数组的原型。

// 创建继承自 Array.prototype 的新原型
let newPrototype = Object.create(Array.prototype);

// 定义需要劫持的方法列表
const methodsToPatch = [
  'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'
];

// 遍历并重新定义这些方法
methodsToPatch.forEach(method => {
  // 缓存原生方法
  const originalMethod = Array.prototype[method];
  
  newPrototype[method] = function(...args) {
    console.log(`检测到调用: ${method}`);
    // 执行原生逻辑,保持 this 指向当前数组
    return originalMethod.call(this, ...args);
  };
});

// 将自定义原型赋予数组
const arr = [];
arr.__proto__ = newPrototype;

// 测试
arr.push(1); // 控制台打印: 检测到调用: push

通过这种方式,当对数组执行上述操作时,会先命中 newPrototype 上的方法,从而触发我们的监听逻辑,随后再交由原生方法处理实际的数据变更。

第1步:处理嵌套数组的递归监测

如果数组中包含子数组(如 [[1, 2]]),子数组也需要绑定新的原型才能被监测。我们需要一个递归函数 observeArr 来遍历数组项:

function observeArr(arr) {
  if (!Array.isArray(arr)) return;
  
  // 1. 绑定原型
  arr.__proto__ = newPrototype;
  
  // 2. 遍历每一项,如果是数组则递归处理
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      observeArr(arr[i]);
    }
  }
}

第2步:动态添加元素的自动监测

仅靠初始遍历是不够的,使用 push、unshift 或 splice 新增的元素如果是数组,也必须立即被赋予原型。这需要在劫持的方法内部进行判断和处理。

不同方法新增元素的位置不同:push 和 unshift 的新增项即为传入的参数;splice 的新增项从第三个参数开始。

methodsToPatch.forEach(method => {
  const originalMethod = Array.prototype[method];
  
  newPrototype[method] = function(...args) {
    let insertedItems;
    
    switch (method) {
      case 'push':
      case 'unshift':
        insertedItems = args;
        break;
      case 'splice':
        // splice(start, deleteCount, ...items)
        insertedItems = args.slice(2);
        break;
      default:
        insertedItems = [];
    }
    
    // 对新插入的数组项进行观测
    if (insertedItems.length > 0) {
      insertedItems.forEach(item => {
        if (Array.isArray(item)) {
          observeArr(item);
        }
      });
    }
    
    return originalMethod.call(this, ...args);
  };
});

综合方案:结合对象监测 (Vue 2.x 原理)

在实际应用(如 Vue 2)中,通常需要同时监测对象和数组。整合后的逻辑如下:

注意:以下代码为逻辑演示,生产环境中建议使用模块化封装。图片展示了如何在浏览器控制台查看数组的所有属性和方法,辅助理解原型结构。

/**
 * 完整的 observe 模块逻辑
 */
let newPrototype = Object.create(Array.prototype);
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'];

// 1. 劫持数组方法
methodsToPatch.forEach(method => {
  const originalMethod = Array.prototype[method];
  newPrototype[method] = function(...args) {
    // 获取新增项
    let inserted;
    if (['push', 'unshift'].includes(method)) inserted = args;
    else if (method === 'splice') inserted = args.slice(2);
    
    // 对新增项进行处理
    if (inserted) {
      inserted.forEach(item => {
        if (typeof item === 'object' && item !== null) {
          if (Array.isArray(item)) observeArr(item);
          else observeObj(item);
        }
      });
    }
    return originalMethod.call(this, ...args);
  };
});

// 2. 递归观测数组
function observeArr(arr) {
  if (!Array.isArray(arr)) return;
  arr.__proto__ = newPrototype;
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    if (item instanceof Object) {
      if (Array.isArray(item)) observeArr(item);
      else observeObj(item);
    }
  }
}

// 3. 观测对象 (简化版 DefineProperty)
function observeObj(obj) {
  if (typeof obj !== 'object' || obj == null) return;
  if (Array.isArray(obj)) { observeArr(obj); return; }
  
  const keys = Object.keys(obj);
  keys.forEach(key => {
    let value = obj[key];
    // 如果值是对象/数组,递归观测
    if (typeof value === 'object' && value != null) {
      if (Array.isArray(value)) observeArr(value);
      else observeObj(value);
    }
    
    Object.defineProperty(obj, key, {
      get() {
        console.log(`读取属性: ${key}, 值: ${value}`);
        return value;
      },
      set(newVal) {
        console.log(`设置属性: ${key}, 新值: ${newVal}`);
        if (typeof newVal === 'object' && newVal != null) {
          if (Array.isArray(newVal)) observeArr(newVal);
          else observeObj(newVal);
        }
        value = newVal;
      }
    });
  });
}

// 测试示例
const data = { a: 1, b: [{ c: 2 }] };
observeObj(data);
data.a = 99;       // 触发对象 setter
data.b[0].push(3); // 触发数组方法劫持

局限性与总结

基于原型链劫持的方案虽然有效,但存在两个主要缺陷:

  1. 只能拦截已知方法:只有通过 push、splice 等方法改变数组时才能被监测。直接通过索引赋值(arr[0] = 1)或直接修改 length 属性,无法触发劫持逻辑。这也是 Vue 2 中 $set API 存在的原因。
  2. 性能开销:深度递归观测嵌套对象和数组会带来一定的内存和计算成本。

现代前端框架(如 Vue 3)已全面转向 Proxy,从根本上解决了无法监测索引变化和性能损耗问题。但在理解底层数据响应式原理时,掌握原型链劫持法依然具有重要价值。

以上就是 JavaScript 如何实现数组变化监测的详细内容,更多关于 JS 数据劫持与原型链技术的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。