本文详解JavaScript中通过原型链劫持监测数组变化的实现原理。核心思路是在数组实例与Array.prototype之间插入新原型,重写push、splice等方法以拦截操作。文章还探讨了嵌套数组的递归处理及直接修改length属性的局限性,并指出这是Vue早期版本的已知缺陷,新版Proxy已解决此问题。
前言
此前在介绍Object.defineProperty时曾提及,该方法仅能监测对象属性的变化,无法直接监测数组内容的变更。
本文将深入解析实现数组变化监测的具体方案。
核心逻辑在于识别那些会修改原数组的方法,并对这些方法进行劫持处理。
上述观点是本文的关键,建议反复阅读并理解后再继续后续内容。
常见的会改变原数组的方法包括push、pop、shift、unshift、reverse、sort和splice。
换句话说,这些方法构成了修改数组数据的入口点。
实现方案:在数组实例与数组原型之间插入一个新的原型对象。
直接修改Array.prototype存在极高的风险,可能影响其他代码库。
另一种思路是复制现有的数组原型,但原型上的方法通常不可枚举,导致无法直接复制。
因此,采用在数组实例和Array.prototype之间插入新原型的方式,构建新的原型链:数组实例 => 新原型 => Array.prototype。只需在新原型上定义同名方法即可实现劫持。
首先通过伪代码理解这一机制:
// 伪代码
let arr = [];
arr.__proto__ = newPrototype;
newPrototype.__proto__ = Array.prototype;
// 然后可以在新原型上添加同名的方法就可以了
newPrototype.push = xxx;
转换为实际代码时,核心使用了Object.create方法来创建新原型。
// Object.create返回一个新对象,而来新对象的__proto__就是传进去的参数。
let newPrototype = Object.create(Array.prototype);
// 然后可以在新原型上添加同名的方法就可以了
newPrototype.push = xxx;
// 需要监测的数组,绑定新的原型就可以了
let arr = [];
arr.__proto__ = newPrototype;
以push方法为例,演示如何劫持该方法。
即在新原型上重新定义push方法,内部调用原始push逻辑,同时可以执行额外的监控操作。
let newPrototype = Object.create(Array.prototype);
// 在新原型上添加同名push
newPrototype.push = function(...args) {
// 语义化this
let curArr = this;
console.log("使用了push");
// 最后还是会执行原始的push
return Array.prototype.push.call(curArr, ...args);
};
// 需要监测的数组,绑定新的原型就可以了
let arr = [];
arr.__proto__ = newPrototype;
// 执行push的时候,就会打印了
arr.push(1);
其他方法的实现逻辑类似,读者可尝试编写其他方法的劫持代码。
对其他数组方法进行统一劫持。
由于各方法逻辑一致,可通过遍历方法列表来批量实现。
let newPrototype = Object.create(Array.prototype);
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"];
methods.forEach(method => {
newPrototype[method] = function(...args) {
console.log(`使用了${method}`);
return Array.prototype[method].call(this, ...args);
};
});
// 需要监测的数组,绑定新的原型就可以了
let arr = [];
arr.__proto__ = newPrototype;
// 执行的时候,就会打印了
arr.push(1);
arr.pop();
如果数组中包含数组项,同样需要对这些嵌套数组进行监测。
当数组元素中可能包含子数组时,需遍历数组每一项,若发现是数组,则使其指向新的原型。
这里需要用到递归处理。
let newPrototype = Object.create(Array.prototype);
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"];
methods.forEach(method => {
newPrototype[method] = function(...args) {
console.log(`使用了${method}`);
return Array.prototype[method].call(this, ...args);
};
});
function observeArr(arr) {
// 既是条件限制,也是递归的终止条件
if (!Array.isArray(arr)) {
return;
}
// 整个数组指向新的原型
arr.__proto__ = newPrototype;
// 数组的每项,如果是数组,也指向新的原型。
arr.forEach(observeArr);
}
// 需要监测的数组,绑定新的原型就可以了
let arr = [[1, 2, 3]];
observeArr(arr);
// 执行的时候,就会打印了
arr[0].push(1);
arr[1].pop();
对于数组新添加的元素,如果其本身是数组,也需要指向新的原型以确保持续监测。
能够添加元素的方法包括push、unshift和splice。
需定位新添加的元素,若为数组则同样指向新的原型。
let newPrototype = Object.create(Array.prototype);
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"];
methods.forEach(method => {
newPrototype[method] = function(...args) {
console.log(`使用了${method}`);
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
default:
break;
}
inserted && observeArr(inserted);
return Array.prototype[method].call(this, ...args);
};
});
function observeArr(arr) {
// 即是条件限制,也是递归的终止条件
if (!Array.isArray(arr)) {
return;
}
// 整个数组指向新的原型
arr.__proto__ = newPrototype;
// 数组的每项,如果是数组,也指向新的原型。
arr.forEach(observeArr);
}
// 这里可以导出去,方便别的文件使用
export default observeArr;
// 需要监测的数组,绑定新的原型就可以了
let arr = [];
observeArr(arr);
let addItem = [1, 2, 3];
arr.push(addItem);
// 执行的时候,就会打印了
addItem.push(1);
addItem.pop();
综合使用Object.defineProperty监测对象和数组。
既然已具备监测对象和数组的方法,将两者结合即可实现监测数组内对象或对象内数组的变化。
可将监测数组和对象的逻辑封装为独立文件,便于后续复用。
为便于直接运行示例,此处将代码合并展示。
/**
* observeArr的部分
**/
// 生成新的原型
let newPrototype = Object.create(Array.prototype);
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"];
// 在新原型上面添加以上方法,实现劫持
methods.forEach(method => {
newPrototype[method] = function(...args) {
console.log(`使用了${method}`);
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
default:
break;
}
inserted && observeArr(inserted);
return Array.prototype[method].call(this, ...args);
};
});
function observeArr(arr) {
// 新加!!!是对象的话,需要用对象
if (Object.prototype.toString.call(arr) === "[object Object]") {
observeObj(arr);
return;
}
if (Array.isArray(arr)) {
// 整个数组指向新的原型
arr.__proto__ = newPrototype;
// 数组的每项,如果是数组,也指向新的原型。
arr.forEach(observeArr);
}
// 不是对象或者数组的,什么都不做
}
/**
* observeObj的部分
**/
function observeObj(obj) {
// 加上参数限制,必须是对象才有劫持,也是递归的终止条件
if (typeof obj !== "object" || obj == null) {
return;
}
// 新加!!!数组交给数组处理
if (Array.isArray(obj)) {
observeArr(obj);
return;
}
// 是对象的话 才开始递归
for (let key in obj) {
// 直接使用 obj.hasOwnProperty会提示不规范
if (Object.prototype.hasOwnProperty.call(obj, key)) {
observeKey(obj, key);
// 这里劫持该属性的属性值,如果不是对象直接返回,不影响
observeObj(obj[key]);
}
}
return obj;
}
function observeKey(obj, key) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log("读取属性", value);
return value;
},
set(newValue) {
console.log("设置属性", newValue);
value = newValue;
}
});
}
/**
* demo试试
**/
let data = { a: 1, b: [1, 2, { c: 2 }] };
observeObj(data);
data.a = 2;
data.b.push([2, 3]);
let arr = [{ a: "数组里的对象" }, 3, 4];
observeArr(arr);
arr[0].a = 3;
缺陷分析
数组并非只能通过方法修改,例如直接设置length属性或删除元素,或直接通过索引赋值(如arr[0]=xxx)改变数组内容。
只有使用push、pop、shift、unshift、reverse、sort、splice这些方法时,才能检测到数组变化。
这也是Vue.js早期版本的缺陷,不过新版Vue使用Proxy已彻底解决此问题。
因此在使用Vue时,建议尽量使用上述方法操作数组。
附注:查看数组的所有属性和方法
在控制台输入dir([])即可看到数组所有的属性和方法。
具体用法可参考MDN文档,点击侧边栏查看对应方法的详细说明。

总结
至此,关于JavaScript如何监测数组变化的文章介绍完毕。更多相关JS监测数组变化内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!