JavaScript递归处理对象属性描述符时TypeError问题排查
递归获取对象属性描述符时遇到Object.assign的undefined问题
我正在编写一个递归函数,用于实现递归获取JavaScript对象的Object.getOwnPropertyDescriptors并扁平化的功能。
以下是我的代码:
let master = []; function something (obj,str) { if (Object(obj)===obj) { pobj = Object.getOwnPropertyDescriptors(obj) for (let kk in pobj) { maststr = str; //console.log(obj); //console.log("line 8"); //console.log(kk); if (master.includes(kk)||kk==="constructor"||kk==="globalThis"||kk==="global") { //circular reference // ||typeof obj[kk]==="function" return; } else { maststr = str + "." + kk ; console.log("line - 1st"); console.log(maststr); console.log(kk); console.log(pobj[kk]); pobj[kk]["name"] = maststr; master.push(pobj[kk]); console.log("line - 2nd"); console.log(maststr); if (maststr==="globalThis.Function.prototype.arguments"||maststr==="globalThis.Function.prototype.caller" ||maststr==="globalThis.Function.prototype.callee") continue; something(obj[kk], maststr); } } } else { //master.push(obj); return; } } something(globalThis,"globalThis"); console.log(master);
代码运行一切正常,直到处理到Object.assign属性时,该属性意外变为undefined,并抛出如下错误:
/Users/user/Documents/playground/playgroundtests_js/E16_2.js:19 pobj[kk]["name"] = maststr; ^ TypeError: Cannot set properties of undefined (setting 'name') at something (/Users/user/Documents/playground/playgroundtests_js/E16_2.js:19:21) at something (/Users/user/Documents/playground/playgroundtests_js/E16_2.js:25:4) at Object.<anonymous> (/Users/user/Documents/playground/playgroundtests_js/E16_2.js:38:1) at Module._compile (node:internal/modules/cjs/loader:1191:14) at Object.Module._extensions..js (node:internal/modules/cjs/loader:1245:10) at Module.load (node:internal/modules/cjs/loader:1069:32) at Function.Module._load (node:internal/modules/cjs/loader:904:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:22:47
在抛出错误前打印该属性,结果确实是undefined,但在Node REPL中测试Object.getOwnPropertyDescriptors(Object.assign)却能正常返回对象。
请问出现此问题的原因是什么,该如何解决?
问题原因
核心问题有三个:
- 全局变量被递归覆盖:你代码里的
pobj、maststr没有用let/const声明,属于全局变量。递归调用时,新的赋值会直接覆盖外层循环的变量,导致外层循环里的pobj[kk]变成undefined。 - 循环引用判断逻辑完全错误:
master数组存的是属性描述符对象,但你用master.includes(kk)判断(kk是字符串属性名),永远不会匹配,根本无法阻止循环引用,还会误判跳过正常属性。 - 错误使用
return中断循环:遇到constructor等属性时直接return,会中断整个遍历循环,导致后续属性都无法处理。
解决方法
1. 修复全局变量问题
给pobj、maststr加上let/const声明,变成函数内局部变量,避免递归时被覆盖。
2. 重构循环引用判断
用Set存储已处理的对象本身(不是属性描述符),这样才能准确判断循环引用,而且Set.has()的效率远高于数组includes()。
3. 替换return为continue
遇到不需要处理的属性时,用continue跳过当前项即可,不要中断整个循环。
4. 增加防御性判断
在操作属性描述符前,先判断是否为undefined,避免报错。
完整修复后的代码
let master = []; // 用Set存储已处理的对象,防止循环引用 let processedObjects = new Set(); function something(obj, str) { // 非对象或已处理过,直接返回 if (Object(obj) !== obj || processedObjects.has(obj)) { return; } processedObjects.add(obj); const pobj = Object.getOwnPropertyDescriptors(obj); for (const kk in pobj) { // 跳过不需要处理的属性 if (kk === "constructor" || kk === "globalThis" || kk === "global") { continue; } const maststr = str + "." + kk; const descriptor = pobj[kk]; // 防御性判断,避免undefined if (!descriptor) { console.warn(`无属性描述符:${maststr}`); continue; } // 给描述符添加路径名称 descriptor.name = maststr; master.push(descriptor); // 跳过特殊属性,不递归 if (maststr === "globalThis.Function.prototype.arguments" || maststr === "globalThis.Function.prototype.caller" || maststr === "globalThis.Function.prototype.callee") { continue; } // 只递归处理数据属性的value(访问器属性无value,无需处理) if ("value" in descriptor) { something(descriptor.value, maststr); } } } something(globalThis, "globalThis"); console.log(master);
额外说明
- 区分数据属性和访问器属性:访问器属性只有
get/set,没有value,不需要递归处理其值。 - 使用
Set跟踪已处理对象,能高效避免循环引用导致的死递归。
内容的提问来源于stack exchange,提问作者forgodsakehold
相关产品推荐
相关产品推荐

