基础算法
数据库
Javascript
HTML 编程
SVG
附录
在 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 就要在文本框中显示值,并不简单的设置一下就可以了。所以,我们需要找到原生属性的getter
和setter
方法。
每个标签原生属性的getter
和setter
可以使用__lookupGetter__
和__lookupSetter__
方法找到。
不过这个方法已经被建议弃用了,新的方法如下:
那么我们要实现的扩展逻辑就可以是:
这是一个比较完美的解决方案了。再给一个扩展 SELECT 标签属性的例子,比如 SELECT 在多选模式下,我们需要获取所有选中项的索引,然而selectedIndex
只能得到第一项的索引,我们可以增加一个selectedIndexes
属性,代码如下:
至此扩展属性已经说完了,下一节我们开始说说如何为原生标签增加方法。
参考链接