














我主要想实现 js 对象 某一个属性 寻找它的父节点
想要实现“寻找一个属性的父节点”,我们需要先厘清 JavaScript 中两种不同的“父子”关系。
在 JS 中,有两种非常容易混淆的“父级”概念:
protoA 是 a 的父节点。const a = { b: { c: 1 } },此时对象 b 是属性 c 的物理父节点。针对这两种截然不同的场景,实现“寻找父节点”的方法如下:
如果你想知道一个属性到底是从哪一层原型链上继承过来的,我们可以写一个辅助函数,顺着 Object.getPrototypeOf() 一层一层往上爬,直到找到拥有该属性的那个原型对象。
function findPrototypeParent(obj, propName) {
let current = obj;
while (current !== null) {
// 检查当前这一层对象自身是否含有这个属性
if (Object.prototype.hasOwnProperty.call(current, propName)) {
return current; // 找到了!返回这个父节点原型
}
// 没找到,继续往上一层原型爬
current = Object.getPrototypeOf(current);
}
return null; // 整个原型链都没找到
}
// === 测试你的设想 ===
const protoA = { customProp: "我是定制属性" };
const a = Object.create(protoA); // a 继承 protoA
// 1. 查找 a 自身的属性
console.log(findPrototypeParent(a, 'customProp') === protoA); // true (成功找到父节点 protoA)
// 2. 查找原生方法
console.log(findPrototypeParent(a, 'hasOwnProperty') === Object.prototype); // true (成功找到老祖宗)
如果你问的是:有一个深层嵌套的对象(像树状数据、JSON),怎么知道某个属性的上一级对象是谁?
const company = {
name: "科技公司",
department: {
name: "研发部",
leader: {
name: "张三",
age: 35
}
}
};
⚠️ 核心痛点:在 JavaScript 中,底层的引用是单向的。
leader对象只知道自己有name: "张三",它绝对不知道自己的上级叫department。
如果你想在不知道完整路径的情况下,在整个大对象里肉眼搜索某个属性并拿到它的父节点,就需要用到递归深度搜索(DFS):
function findNestedParent(root, targetKey) {
// 如果当前不是对象,直接返回 null
if (typeof root !== 'object' || root === null) return null;
// 如果当前对象直接拥有这个属性,那么当前对象就是父节点
if (Object.prototype.hasOwnProperty.call(root, targetKey)) {
return root;
}
// 否则,递归遍历当前对象的所有子属性
for (let key in root) {
if (Object.prototype.hasOwnProperty.call(root, key)) {
const result = findNestedParent(root[key], targetKey);
if (result) return result; // 如果在子树里找到了,层层返回
}
}
return null; // 没找到
}
// === 测试嵌套查找 ===
const parent = findNestedParent(company, "age");
console.log(parent); // 输出: { name: '张三', age: 35 } (成功拿到了 age 的直接父节点)
如果你的项目频繁需要寻找嵌套对象的“父节点”,最优雅的做法通常是在创建/初始化数据的时候,手动给子节点挂载一个隐藏的父级引用:
// 手动建立双向绑定
company.department.leader.__parent__ = company.department;
company.department.__parent__ = company;
// 这样你随时可以通过属性直接找爸爸了:
console.log(company.department.leader.__parent__); // 直接拿到 department
你目前面对的数据结构更偏向于哪一种呢?是原型链的探究,还是在处理类似树状(Tree)的业务数据(比如多级菜单、组织架构)?
company.department.leader.parent = company.department;
这个是我要的 但是我需要 parent 不是显示的 是隐藏的,比如我要将这个leader对象 作为 params 给到一个post api 就不显示 parent
太赞了,你的直觉非常敏锐,而且直接切中了前端开发在处理树状数据时最经典的一个痛点:循环引用与数据污染。
如果你直接用 leader.__parent__ = company.department,当你把 leader 传给 JSON.stringify() 或 POST API 时,不仅会把 __parent__ 传给后端,甚至还会因为 department 里面又有 leader,leader 里面又有 __parent__,从而导致浏览器直接抛出爆栈错误:TypeError: Converting circular structure to JSON(无法序列化循环结构)。
要做到“平时能用属性找爸爸,但发请求、打印时又完全隐形”,JavaScript 提供了两种非常完美的“隐形斗篷”技术。
Object.defineProperty(经典、推荐)这是 JS 最正统的做法。在 JS 中,对象的属性其实有一个隐藏的开关叫 enumerable(可枚举性)。如果把这个开关关掉,这个属性就会变成“隐形”的:
JSON.stringify() 读不到它。for...in 循环、Object.keys() 遍历不到它。console.log 打印时它会变灰或者不显示。obj.__parent__ 点出来。const company = {
department: {
name: "研发部",
leader: { name: "张三", age: 35 }
}
};
// 核心:用 defineProperty 来绑定“隐形”的父节点
Object.defineProperty(company.department.leader, '__parent__', {
value: company.department, // 指向父节点
enumerable: false, // ❌ 不可枚举(隐形的关键!)
writable: true,
configurable: true
});
// === 测试效果 ===
// 1. 你可以正常用它找父节点:
console.log(company.department.leader.__parent__.name); // 输出: "研发部"
// 2. 模拟 POST API 发送数据(序列化):
const apiParams = JSON.stringify(company.department.leader);
console.log(apiParams);
// 输出: {"name":"张三","age":35} <-- 干净漂亮!__parent__ 自动消失了!
Symbol 作为属性名(现代、优雅)在 ES6 中,引入了 Symbol 类型。用 Symbol 作为对象的键名,天然就具备极强的隐蔽性。它同样不会被 JSON.stringify() 和普通的 for...in 遍历到。
// 1. 定义一个全局唯一的“隐形标记”
const ParentSymbol = Symbol('parent');
const company = {
department: {
name: "研发部",
leader: { name: "张三", age: 35 }
}
};
// 2. 挂载父节点
company.department.leader[ParentSymbol] = company.department;
// === 测试效果 ===
// 1. 通过 Symbol 找父节点:
console.log(company.department.leader[ParentSymbol].name); // 输出: "研发部"
// 2. 模拟 POST API 发送数据:
console.log(JSON.stringify(company.department.leader));
// 输出: {"name":"张三","age":35} <-- Symbol 属性自动被忽略了!
在实际业务中,手动一个个去写 Object.defineProperty 太麻烦了。既然你想要树状结构,我们可以写一个通用的初始化函数,只要把你的原始数据扔进去,它就会自动递归把所有子节点都绑上隐形的 __parent__:
function buildTreeRelations(obj, parent = null) {
if (typeof obj !== 'object' || obj === null) return;
// 如果有父节点,绑定隐形的 __parent__
if (parent) {
Object.defineProperty(obj, '__parent__', {
value: parent,
enumerable: false, // 隐形
configurable: true,
writable: true
});
}
// 递归遍历子属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
buildTreeRelations(obj[key], obj);
}
}
}
// === 业务使用 ===
const myData = {
name: "集团",
sub: {
name: "子公司",
dept: { name: "财务部" }
}
};
// 一键注入隐形父节点
buildTreeRelations(myData);
// 现在任何一层都可以直接找爸爸了:
const target = myData.sub.dept;
console.log(target.__parent__.name); // "子公司"
console.log(target.__parent__.__parent__.name); // "集团"
// 发送给后端,依然是绝对纯净的数据:
// axios.post('/api', target) -> 实际发送的就是 { name: "财务部" }
这两种方案里,方案一(Object.defineProperty)是最符合你直觉的,因为你依然可以用小写字符串 .__parent__ 去访问它,代码可读性最高。
你觉得写成 buildTreeRelations 这种自动化函数,能完美适配你现在的业务场景吗?
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。