扩展 HTML 原生标签(上)

扩展 HTML 原生标签(上)

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

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

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

第一节 找到标签类

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

String.prototype.toInt = function() {
    return parseInt(this.toString());
}

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

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

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

HTMLAnchorElement.prototype.color = 'blue';
HTMLAnchorElement.prototype.jump = function() {
    window.location.href = this.href;
}

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

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

第二节 标签的属性

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

<a href="/index.htm">Home</a>

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

<a href="/index.htm" color="blue">Home</a>

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

let a = document.querySelector('a');
console.log(a.color); //不正确
a.setAttribute('color', 'red');
console.log(a.getAttribute('color')); //正确

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

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

Object.defineProperty(HTMLAnchorElement.prototype, 'color', {
    get() {
        return this.getAttribute('color');
    },
    set(color) {
        this.setAttribute('color', color);
    }
});

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

let a = document.querySelector('a');
console.log(a.color); //正确
a.color = 'red';
console.log(a.getAttribute('color')); //也正确

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

a.onclick+ //错误
a['onclick+'] //正确,也比 getAttribute('onclick+') 省事不少

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

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

Object.defineProperty(HTMLInputElement.prototype, 'value', {
    get() {
        return this.value.toLowerCase();
    },
    set(value) {
        this.value = value.toUpperCase();
    }
});

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

换第二种方法:

Object.defineProperty(HTMLInputElement.prototype, 'value', {
    get() {
        return (this.getAttribute('value') ?? '').toLowerCase(); //this.getAttribute('value') 如果没有初始化 value 则会返回 null
    },
    set(value) {
        this.setAttribute('value', value.toUpperCase());
    }
});

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

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

let getter = HTMLInputElement.prototype.__lookupGetter__('value');
let setter = HTMLInputElement.prototype.__lookupSetter__('value');

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

let getter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').get;
let setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;

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

Object.defineProperties(HTMLInputElement.prototype, {
        'value': {
            get() {
                return getter.call(this).replaceAll(' ', ''); //去掉所有空格
            },
            set(value) {
                setter.call(this, String(value).replaceAll(' ', ''));  //去掉所有空格
            }
        }
    });

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

Object.defineProperties(HTMLSelectElement.prototype, {
    selectedIndexes: {
            get() {
                if (this.multiple) {
                    return [...this.selectedOptions].map(option => option.index);
                }
                else {
                    return [this.selectedIndex];
                }
            },
            set(indexes) {
                if (this.multiple) {
                    let before = this.selectedIndexes;
                    before.filter(index => !indexes.includes(index)).forEach(index => this.options[index].selected = false);
                    indexes.filter(index => !before.includes(index)).forEach(index => this.options[index].selected = true);
                }
                else {
                    this.selectedIndex = indexes[0] ?? -1;
                }
            }
        }
})

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

原文链接:http://www.qross.cn/blog/20210731

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值