记一次错误 深拷贝 key值全部小写
深度克隆中键名转换的两种方式差异分析
问题背景
在实现一个深度克隆并将所有键名转换为小写的函数时,发现两行看似相似的代码会产生完全不同的结果:
// 方式1:使用原始值(错误方式)
newObj[key.toLowerCase()] = obj[key];// 方式2:使用递归处理后的值(正确方式)
newObj[key.toLowerCase()] = newObj[key];
核心差异分析
方式1的问题:使用原始值 obj[key]
function deepClone(obj) {if (typeof obj !== "object" || obj === null) {return obj;}const newObj = Array.isArray(obj) ? [] : {};for (let key in obj) {if (obj.hasOwnProperty(key)) {newObj[key] = deepClone(obj[key]); // 递归处理,存储到原键名if (key.toLowerCase() !== key) {newObj[key.toLowerCase()] = obj[key]; // ❌ 使用原始值覆盖delete newObj[key];}}}return newObj;
}
执行过程分析:
当处理对象 {A: "1", D: {E: "3", F: "22"}}
时:
-
处理键
A
:newObj['A'] = deepClone("1")
→newObj['A'] = "1"
key.toLowerCase() !== key
为 truenewObj['a'] = obj['A']
→newObj['a'] = "1"
(原始值)delete newObj['A']
-
处理键
D
:newObj['D'] = deepClone({E: "3", F: "22"})
- 递归调用返回
{E: "3", F: "22"}
(内层键名未转换!) newObj['d'] = obj['D']
→newObj['d'] = {E: "3", F: "22"}
(原始对象)delete newObj['D']
问题根源:使用 obj[key]
会直接获取原始对象,绕过了递归处理的结果。
方式2的正确实现:使用递归结果 newObj[key]
function deepClone(obj) {if (typeof obj !== "object" || obj === null) {return obj;}const newObj = Array.isArray(obj) ? [] : {};for (let key in obj) {if (obj.hasOwnProperty(key)) {newObj[key] = deepClone(obj[key]); // 递归处理,存储到原键名if (key.toLowerCase() !== key) {newObj[key.toLowerCase()] = newObj[key]; // ✅ 使用递归处理后的值delete newObj[key];}}}return newObj;
}
执行过程分析:
当处理同样的对象时:
-
处理键
A
:newObj['A'] = deepClone("1")
→newObj['A'] = "1"
newObj['a'] = newObj['A']
→newObj['a'] = "1"
(递归结果)delete newObj['A']
-
处理键
D
:newObj['D'] = deepClone({E: "3", F: "22"})
- 递归调用返回
{e: "3", f: "22"}
(内层键名已转换!) newObj['d'] = newObj['D']
→newObj['d'] = {e: "3", f: "22"}
(递归结果)delete newObj['D']
代码示例对比
测试代码
const testObj = {A: "1",B: {C: "2",D: {E: "3",F: "22"}}
};// 错误方式的结果
const wrongResult = deepCloneWrong(testObj);
console.log("错误方式:", JSON.stringify(wrongResult, null, 2));
// 输出:
// {
// "a": "1",
// "b": {
// "C": "2", // ❌ 保持大写
// "D": { // ❌ 保持大写
// "E": "3", // ❌ 保持大写
// "F": "22" // ❌ 保持大写
// }
// }
// }// 正确方式的结果
const correctResult = deepCloneCorrect(testObj);
console.log("正确方式:", JSON.stringify(correctResult, null, 2));
// 输出:
// {
// "a": "1",
// "b": {
// "c": "2", // ✅ 转换为小写
// "d": { // ✅ 转换为小写
// "e": "3", // ✅ 转换为小写
// "f": "22" // ✅ 转换为小写
// }
// }
// }
性能影响分析
操作步骤对比
方式1(错误但低效):
- 递归处理 → 创建新对象
- 存储到原键名
- 复制原始值到小写键名
- 删除原键名
方式2(正确且高效):
- 递归处理 → 创建新对象
- 存储到原键名
- 移动递归结果到小写键名
- 删除原键名
最优方案(推荐)
function deepCloneOptimal(obj) {if (typeof obj !== "object" || obj === null) {return obj;}const newObj = Array.isArray(obj) ? [] : {};for (let key in obj) {if (obj.hasOwnProperty(key)) {// 直接使用小写键名,避免额外操作const clonedValue = deepCloneOptimal(obj[key]);const newKey = key.toLowerCase();newObj[newKey] = clonedValue;}}return newObj;
}
性能优势:
- 减少属性操作次数(从3次减少到1次)
- 避免
delete
操作(相对昂贵) - 内存使用更高效
关键知识点总结
1. 递归与引用的关系
obj[key]
:指向原始对象的引用,未经过递归处理newObj[key]
:指向递归处理后的新对象
2. 深度优先处理原则
在深度克隆中,必须确保:
- 先处理子对象(递归调用)
- 再处理当前层级的转换
- 使用处理后的结果,而非原始引用
3. JavaScript 对象属性操作成本
操作类型按成本从低到高:
- 直接赋值:
obj.key = value
(最快) - 属性复制:
obj.newKey = obj.oldKey
- 属性删除:
delete obj.key
(最慢,可能触发对象重构)
断点调试
条件断点 27行 key==‘D’
打不上26行