一、前言
microdiff 是一款对比两个对象(object or array)属性的库,相比较其它同类型的库,有以下特点:
- 包体积小于 1 kb
- 执行效率高
- 零依赖
- 支持 TypeScript
基本的使用方式如下:
import diff from "microdiff";
const obj1 = {
originalProperty: true,
};
const obj2 = {
originalProperty: true,
newProperty: "new",
};
console.log(diff(obj1, obj2));
// [{type: "CREATE", path: ["newProperty"], value: "new"}]
二、遍历对象属性
既然要对比两个对象的属性,那么必然需要先遍历对象的属性,JavaScript 提供了应用于各种场景的遍历方法:
- for...in
- Object.keys()
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Reflect.ownKeys()
要想完全掌握上述方法,可以从对象属性的两个维度着手,第一个维度就是属性类型:
const obj = {
foo: 'foo',
bar: 20,
}
Object.prototype.bzz = 'bzz';
console.log(obj.bzz); // bzz
obj[Symbol('far')] = 'far'; // [Symbol(far)]: 'far'
由于 JavScript 的原型链机制以及 ES6 新增的 Symbol 基本数据类型,使得属性按照类型可以分为:
- 自身属性
- 继承属性
- Symbol 属性
第二个维度就是属性的描述符:属性除了自身的 value 之外,还有三个特殊的特性,统称为属性描述符。
Object.getOwnPropertyDescriptors(obj)
{
foo: {
value: 'foo',
writable: true,
enumerable: true,
configurable: true
},
bar: { value: 20, writable: true, enumerable: true, configurable: true },
[Symbol(far)]: {
value: 'far',
writable: true,
enumerable: true,
configurable: true
}
}
通过 Object.getOwnPropertyDescriptors 方法可以获取到对象所有属性的所有描述符,根据 enumerable 属性描述符可以把属性划分为:
- 不可枚举属性
- 可枚举属性
从上图中可以看出只有 for...in 是会遍历出原型链属性的,所以早期的 JavaScript 代码中,为了避免这种情况的出现,需要这样处理:
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
// 过滤原型链上的属性
console.log(i);
}
}
三、实现原理
两个对象属性的差异化情况分为三种:
interface Difference {
type: "CREATE" | "REMOVE" | "CHANGE";
path: (string | number)[];
value?: any;
}
microdiff 采用 for...in 来遍历对象属性,所以它不适用于不可枚举属性和 Symbol 属性的场景。
属性的新增和删除是比较容易判断的:
- 当属性仅存于 obj,则是属性的删除
- 当属性仅存于 newObj,则是属性的新增
// 部分代码省略
// 属性删除
for (const key in obj) {
const objKey = obj[key];
const path = isObjArray ? +key : key;
if (!(key in newObj)) {
diffs.push({
type: "REMOVE",
path: [path],
});
continue;
}
}
...
// 属性新增
const isNewObjArray = Array.isArray(newObj);
for (const key in newObj) {
if (!(key in obj)) {
diffs.push({
type: "CREATE",
path: [isNewObjArray ? +key : key],
value: newObj[key],
});
}
}
属性更新的判断会稍微复杂点,对于子属性值为对象的情况下,会采用递归的方式再次 diff 处理,同时也考虑了一些特殊的数据类型和循环引用的问题:
const newObjKey = newObj[key];
const areObjects =
typeof objKey === "object" && typeof newObjKey === "object";
if (
objKey &&
newObjKey &&
areObjects &&
// 特殊类型的过滤
!richTypes[Object.getPrototypeOf(objKey).constructor.name] &&
// 循环引用的处理
(options.cyclesFix ? !_stack.includes(objKey) : true)
) {
// 递归处理
const nestedDiffs = diff(
objKey,
newObjKey,
options,
options.cyclesFix ? _stack.concat([objKey]) : []
);
diffs.push.apply(
diffs,
nestedDiffs.map((difference) => {
difference.path.unshift(path);
return difference;
})
);
} else if (
objKey !== newObjKey &&
!(
areObjects &&
(isNaN(objKey)
// 考虑到 Number 和 String 包装类型
? objKey + "" === newObjKey + ""
: +objKey === +newObjKey)
)
) {
diffs.push({
path: [path],
type: "CHANGE",
value: newObjKey,
});
}
如果忽略成本问题,「可以采用 WeakSet 才检查循环引用,这样性能会更优」。
四、总结
本文重点内容如下:
- 理解不同属性遍历方法的差异性,根据不同应用场景,选择合适的方法
- 递归遍历对象属性时,注意检查循环引用
最后,「如果本文对您有帮助,欢迎点赞、收藏、分享」。