自定义元素构造函数中this为何指向被定义节点?能否纯JS实现?
问题解答
1. 构造函数中this为何指向自定义元素节点?
这是浏览器原生自定义元素机制的特性:
当你用customElements.define把类和标签绑定后,浏览器解析到<custom-article>标签时,不会像普通类那样通过new创建全新对象,而是直接把DOM树上的这个节点作为你定义的ArticleElement类的实例。
调用构造函数时,浏览器会自动把这个DOM节点绑定为构造函数里的this。你调用super()其实是执行父类HTMLElement的构造函数,而父类构造也是基于这个DOM节点运行的,所以最终this就是当前的自定义元素节点。
2. 纯JavaScript实现类似customElements.define的功能
你之前的尝试问题在于直接调用类构造函数——类构造必须通过new执行,或者用Reflect.construct指定this上下文。下面是两种可行的实现方式:
方案一:用Reflect.construct执行构造函数
function defineCustomElement(tagName, CustomElementClass) { // 处理页面已存在的对应标签元素 const elements = document.getElementsByTagName(tagName); Array.from(elements).forEach(element => { // 让元素继承自定义类的原型 Object.setPrototypeOf(element, CustomElementClass.prototype); // 通过Reflect.construct执行构造函数,将当前元素作为this Reflect.construct(CustomElementClass, [], Object.getPrototypeOf(element).constructor); }); // 监听DOM变化,处理后续动态创建的元素 const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.tagName?.toLowerCase() === tagName.toLowerCase()) { Object.setPrototypeOf(node, CustomElementClass.prototype); Reflect.construct(CustomElementClass, [], Object.getPrototypeOf(node).constructor); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } // 使用示例(注意修复原构造函数的内容覆盖问题) class ArticleElement extends HTMLElement { constructor() { super(); // 先保存原内容,避免赋值innerHTML时被覆盖 const originalContent = this.innerHTML; this.innerHTML = ` <article> <h2>${this.getAttribute("title")}</h2> <p>${originalContent}</p> </article>`; } } defineCustomElement("custom-article", ArticleElement);
方案二:抽离初始化逻辑到独立方法(更简洁)
如果不想用Reflect.construct,可以把构造逻辑抽成单独方法,绑定原型后调用:
function defineCustomElement(tagName, CustomElementClass) { // 给类添加初始化方法(如果未定义) if (!CustomElementClass.prototype.init) { CustomElementClass.prototype.init = function() { const originalContent = this.innerHTML; this.innerHTML = ` <article> <h2>${this.getAttribute("title")}</h2> <p>${originalContent}</p> </article>`; }; } // 处理已有元素 const elements = document.getElementsByTagName(tagName); Array.from(elements).forEach(element => { Object.setPrototypeOf(element, CustomElementClass.prototype); element.init(); }); // 监听动态创建的元素 const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.tagName?.toLowerCase() === tagName.toLowerCase()) { Object.setPrototypeOf(node, CustomElementClass.prototype); node.init(); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } // 使用示例 class ArticleElement extends HTMLElement { // 无需构造函数,直接用init方法处理初始化 } defineCustomElement("custom-article", ArticleElement);
注意事项
- 原生
customElements.define还包含元素升级、whenDefined等细节逻辑,上面的实现覆盖了核心的已有元素处理和动态元素监听,可根据需求扩展 - 原构造函数存在一个小问题:直接赋值
innerHTML会覆盖原内容,导致${this.innerHTML}拿到空字符串,需要先保存原内容再赋值
内容的提问来源于stack exchange,提问作者Petar Todorov
相关产品推荐
相关产品推荐

