扩展 HTML 原生标签(上)

在 HTML 中有很多元素标签,如 DIV、 SPAN、INPUT、TABLE 等,元素标签也是组成 HTML 页面的核心内容。在 Javascript 中,每个元素标签都有自己的属性、方法和事件,通过这三个要素,可以通过 Javascript 操作 HTML,让使用者可以和页面进行交互。

虽然每个元素标签都有大量的属性、方法和事件,但有的时候依然不能满足我们的要求,有时,我经常会感慨,要是这个元素有这个属性就好了。有没有想过为标签添加一个自定义的属性?本文就来介绍如何为元素添加自定义的属性、方法和事件,甚至如何自定义一个元素标签。在 Vue、React 等 SPA 框架大行其道的今天,本文可以说是上一个冷门知识,不过总是一个解决问题的思路。

root.js 标签库中,扩展了一些原生标签,如 TABLE、BUTTON 等,有兴趣可自行了解。

第一节 找到标签类

我们都知道,通过原型prototype可以扩展 Javascript 原有的类或自己创建的类。例如:

这样我们就为 String 类型增加了一个新的方法toInt(),用于将整数字符串转成整数。

那么,要扩展原生的元素标签,是不是扩展 HTML 元素相应的类就可以了呢?答案是可以是可以,但是还不够。首先要知道每个标签对应的类是什么,打开浏览器,按 F12 打开开发工具,在控制台中输入HTML,注意全部大写。在下拉提示中,可以看到所有的 HTML 元素类,其中 HTMLElement是基类,不用管。例如HTMLAnchorElement对应的是 A 标签,HTMLDivElement对应的是 DIV 标签,所有的元素的类都以HTML开头。

我们可以通过原型为某个 HTML 标签增加一个属性或方法,例如:

这样,引用这段脚本的页面上所有的 A 标签都增加了color属性和jump方法。嗯,就是这么简单,但是事情还远远没有结束,比如color属性只能在 Javascript 中使用,而没有实际的效果,仅可用来保存一个值,和变量没有多大差别。下面,就依次介绍一下如何在原生标签上正确添加属性、方法和事件。

补充:使用原型prototype扩展原生标签或其他对象的属性只能使用基本数据类型初始化赋值,如字符串、变量、null 等,而不能使用引用类型赋值,如对象、数组等。如果变量确实是引用类型,可以在标签的逻辑过程中再赋值为引用类型。否则的话这个对象的所有实例都会使用同一个引用地址,比如同一个数组。

第二节 标签的属性

先介绍一下基础知识,还是从 A 标签开始。

属性href是 A 标签的一个原生属性,在 HTML 中,原生属性可以在 Javascript 中直接使用,像上面例子中this.href,也可以在 CSS 选择器中使用,如 a[href]。如果我们添加一个自定义属性就没那么幸运了,不能直接调用。

属性color是我们在这个 A 标签添加的一个自定义属性,我们不能直接通过this.color这种形式调用,Javascript 中操作自定义属性可以用两个方法:getAttribute()setAttribute()

利用上面两个方法,我们就可以操作自定义属性了。重点,我们可以为 HTML 标签添加任意的自定义属性,并且可以通过上面两个方法进行操作。特别说明一下自定义属性的命名问题,自定义属性名可以是任意字符,英文字母、数字、各种特殊符号甚至中文都可以。但是如果也想让 CSS 选择器支持,如a[color=blue],必须遵守三个规则:对于单字符属性,支持英文字母、下划线_和中文;对于多字符属性,支持以非数字开头的英文字母、数字、下划线_、中横线-和中文字符的任意组合;多字符属性如果以中横线-开头,第二个字符不能是数字。一般情况下,不建议使用中文和除中横线以外的其他特殊符号做为属性名,建议以英文单词作为属性名且多个英文单词之间使用中横线-隔开,就和 CSS 样式属性一样。

问题来了,我们可不可以像原生属性一样使用自定义属性,比如this.color,使用getAttribute()setAttribute()太麻烦了。上一节已经提到过可以通过原型来为原生标签增加属性,那么如何将这两个操作关联起来?好吧,来感谢一下 ES 6 的Object.defineProperty方法。

注意扩展的是HTMLAnchorElement.prototype而不是HTMLAnchorElement,然后我们就可以这样用了。

定义多个属性可以使用Object.defineProperties方法。下一个问题,如果我非要定义一个包含特殊符号的属性怎么办,比如 root.js 中的服务器端事件这样写onclick+="post:/api/...."(定义在标签上的事件本质上还是一个属性)。这种情况下定义方式是一样的,调用时可以按对象名引用:

还有一种场景是扩展原生的属性,为原生属性增加更多的功能。例如我们想为 INPUT 输入框的value属性增加切面方法,当获取值和设置值时执行特定的函数,比如去掉所有空格。

首先,我们尝试第一种方法:

在浏览器中运行会直接报错Uncaught RangeError: Maximum call stack size exceeded,因为this.value会指向属性自身,而不是我们想向的原来的 INPUT 的原生属性value。很显然, 通过Object.defineProperty定义的属性可以覆盖原生属性。

换第二种方法:

第二种方法看起来可以正常工作,好像是成功了。在稍微旧一点的浏览器中,设置值比如input.value = '123'并不能将值显示在文本框中。这种方式也不是作者建议的方式,因为除了这个属性以外,还有其他标签的更多属性如果需要重写的话还可能会产生其他问题。因为标签的原生属性除了设置值以外,还有可能有其他功能,INPUT 标签的 value 就要在文本框中显示值,并不简单的设置一下就可以了。所以,我们需要找到原生属性的gettersetter方法。

每个标签原生属性的gettersetter可以使用__lookupGetter____lookupSetter__方法找到。

不过这个方法已经被建议弃用了,新的方法如下:

那么我们要实现的扩展逻辑就可以是:

这是一个比较完美的解决方案了。再给一个扩展 SELECT 标签属性的例子,比如 SELECT 在多选模式下,我们需要获取所有选中项的索引,然而selectedIndex只能得到第一项的索引,我们可以增加一个selectedIndexes属性,代码如下:

至此扩展属性已经说完了,下一节我们开始说说如何为原生标签增加方法。


参考链接


微信公众号
码农老吴  |  星源工作室  |  开发月志  |  问题反馈
联系我们:wu@qross.io     手机/微信:18618171102
京 ICP 备 20027445 号
$(h1)!