TypeScript同步对象对比函数本地正常,部署后部分失效求助
问题分析与解决思路
你的核心问题在于深比较函数isEqual存在多处逻辑缺陷,加上生产环境可能存在的对象结构差异、React状态更新的引用问题,导致判断失效。以下是具体的排查和修复方案:
一、修复isEqual函数的核心缺陷
原函数存在以下致命问题:
- 未校验两个对象的键是否完全一致:仅遍历
oldObj的键,若newObj多了字段会被忽略 - 数组未先判断长度:两个数组长度不同时,依然会遍历比较,导致错误判断相等
- 使用
instanceof判断数组不可靠:跨iframe或特殊环境下Array instanceof Array可能为false - 未处理特殊类型(Date、RegExp、NaN):比如两个相同时间的Date对象会被误判为不等,NaN会被误判为不等
- 未显式处理null/undefined的边界情况:虽然逻辑上没问题,但显式处理更清晰
优化后的函数:
export function isEqual<T>(oldObj: T, newObj: T): boolean { // 引用相等直接返回true if (oldObj === newObj) return true; // 一方为null/undefined,另一方不为时返回false if (oldObj == null || newObj == null) return false; // 用toString判断类型,比instanceof更可靠 const oldType = Object.prototype.toString.call(oldObj); const newType = Object.prototype.toString.call(newObj); if (oldType !== newType) return false; // 处理Date类型:比较时间戳 if (oldType === '[object Date]') { return (oldObj as Date).getTime() === (newObj as Date).getTime(); } // 处理RegExp类型:比较字符串形式 if (oldType === '[object RegExp]') { return oldObj.toString() === newObj.toString(); } // 处理数组:先判断长度,再逐个比较元素 if (Array.isArray(oldObj) && Array.isArray(newObj)) { if (oldObj.length !== newObj.length) return false; for (let i = 0; i < oldObj.length; i++) { if (!isEqual(oldObj[i], newObj[i])) { return false; } } return true; } // 处理普通对象:校验键的数量和内容都一致 if (oldType === '[object Object]' && newType === '[object Object]') { const oldKeys = Object.keys(oldObj); const newKeys = Object.keys(newObj); if (oldKeys.length !== newKeys.length) return false; for (const key of oldKeys) { if (!newKeys.includes(key) || !isEqual(oldObj[key as keyof T], newObj[key as keyof T])) { return false; } } return true; } // 处理数字类型,包含NaN的特殊情况 if (typeof oldObj === 'number' && typeof newObj === 'number') { return isNaN(oldObj) && isNaN(newObj) ? true : oldObj === newObj; } // 其他原始类型直接比较 return oldObj === newObj; }
二、React使用的注意事项
- 保存原始基准数据:组件初始化时,必须对API返回的初始数据做深拷贝(比如
JSON.parse(JSON.stringify(initialData))),保存到state或ref中作为oldObj的基准值,避免后续操作修改原始数据导致比较失效。 - 确保状态更新生成新引用:更新表单数据时,必须生成新的对象/数组引用,不能直接修改原state。例如:
const handleChange = (key: keyof FormData, value: any) => { setFormData(prev => ({ ...prev, [key]: value })); };
- 正确配置useMemo依赖:确保
useMemo的依赖数组包含原始基准数据和当前表单数据,且基准数据不会在渲染时重新生成:
// 组件挂载时保存深拷贝的初始数据 const [initialFormData] = useState(() => JSON.parse(JSON.stringify(apiData))); const [formData, setFormData] = useState(initialFormData); const isFormChanged = useMemo(() => { return !isEqual(initialFormData, formData); }, [initialFormData, formData]);
三、生产环境排查步骤
- 打印对象结构对比:在生产环境添加日志,打印
initialFormData和formData的JSON.stringify结果,对比两者的差异,看是否存在预期外的字段类型变化(比如日期变成字符串、数字变成字符串)或额外字段。 - 校验API返回差异:对比本地和生产环境的API响应,确认返回的数据结构、字段类型完全一致,避免生产环境返回的数据有额外字段或类型转换。
- 测试特殊场景:针对失效的场景(比如修改嵌套对象后恢复、修改数组后恢复)单独测试,定位到具体的字段类型,看是否是特殊类型(Date、数组)的比较逻辑问题。
内容的提问来源于stack exchange,提问作者Àlex Park Viñas
相关产品推荐
相关产品推荐

