结论:__proto__不是理解Custom Elements激活逻辑的关键路径,浏览器通过customElements.define()建立类注册与DOM映射关系,实例化由upgrade触发而非__proto__改写,手动操作__proto__无效且危险。

直接说结论:__proto__ 不是理解 Custom Elements 激活逻辑的关键路径,也不该被用于干预或“剖析”其原型挂载过程。浏览器内部通过 customElements.define() 建立的是规范化的类注册与 DOM 实例映射关系,而非依赖手动操作 __proto__ 链来实现升级或激活。
Custom Elements 的实例化不靠 __proto__ 动态改写
自定义元素(如 <my-button>)在 DOM 中的实例,其原型链在构造时就已由浏览器严格确定:
- 静态 HTML 中的
<my-button>节点,初始被解析为HTMLUnknownElement,其__proto__指向HTMLUnknownElement.prototype; - 调用
customElements.define('my-button', MyButton)后,不会自动修改已有节点的__proto__; - 真正让节点“活起来”的,是
customElements.upgrade(node)—— 它不是重写__proto__,而是触发浏览器内部机制:销毁旧实例、用新类重新构造、执行constructor和connectedCallback。
__proto__ 查看有用,但不能用于控制生命周期
你可以在控制台观察原型链,验证注册是否生效:
const el = document.querySelector('my-button');
console.log(el.__proto__ === HTMLUnknownElement.prototype); // true(未 upgrade 前)
customElements.upgrade(el);
console.log(el.__proto__ === MyButton.prototype); // true(upgrade 后)
但这只是结果呈现,不是实现手段。试图手动赋值 el.__proto__ = MyButton.prototype:
立即学习“前端免费学习笔记(深入)”;
- 在现代浏览器中会静默失败或抛出 TypeError(
__proto__是只读 accessor); - 即使在旧环境能写入,也不会触发
constructor、connectedCallback等生命周期,组件行为完全缺失; - 破坏 Shadow DOM 初始化时机、事件绑定、属性监听等关键机制。
真正决定“激活”的是注册时机与 upgrade 调用
能否让自定义标签正常工作,取决于两个硬性条件:
-
注册必须早于 HTML 解析:脚本需在
<my-button>标签被 parser 读取前执行define(),否则节点永远停留在HTMLUnknownElement状态; -
已存在节点必须显式 upgrade:SSR 渲染、CMS 输出或动态 innerHTML 插入的节点,需在 define 后主动遍历并调用
customElements.upgrade(); - 新插入节点(如
document.createElement('my-button').append())会自动走完整生命周期,无需 upgrade —— 这是浏览器原生保障,与__proto__无关。
想深入原理,应关注类继承与 define 机制
Custom Elements 的核心契约是:
- 你的类必须 显式继承
HTMLElement(或内置扩展类),确保原型链终点是HTMLElement.prototype; -
customElements.define()将类名与构造函数绑定到全局注册表,浏览器据此在创建/升级时调用new MyButton(); - 所有生命周期回调(
connectedCallback、attributeChangedCallback)均由浏览器按规范调用,不依赖开发者干预原型链。
所以,与其调试 __proto__,不如检查:类是否正确继承、define 是否提前执行、是否遗漏 upgrade 已有节点、connectedCallback 中是否做了重复初始化。


















