tabIndex是前端开发里一个容易被忽视的属性,它直接决定了元素能否通过Tab键获得焦点,进而影响键盘可访问性。原生的tabIndex在浏览器之间存在不少默认行为差异:有的元素天然可聚焦,有的元素必须显式设置tabIndex后才可聚焦,而当属性缺失时,不同浏览器对el.tabIndex的返回值也可能不一致。jQuery为了让开发者拿到统一、可预期的结果,在其prop方法内部针对tabIndex做了一层专门的默认行为修正。下面我们就来剖析这层修正的具体实现和设计考虑。

一、原生tabIndex属性与浏览器默认行为差异
按照HTML规范,tabIndex的取值有三类语义:负值表示元素可聚焦但不能通过键盘Tab键访问;零值表示元素可聚焦且按文档顺序参与Tab导航;正值表示可聚焦且拥有更高的导航优先级,数值越小越先被访问。这个语义本身并不复杂,麻烦出在默认值上。
规范定义了一批默认可聚焦的元素,常见的有<a>(带href属性)、<button>、<input>、<select>、<textarea>以及<iframe>等。这些元素即使不写tabIndex属性,在DOM属性层面也表现为tabIndex为0。而像<div>、<span>这类普通元素,如果不显式设置tabIndex,则不可聚焦。早期的IE和标准浏览器在这一点上的实现并不统一,IE6、IE7时代甚至会给div这样的元素返回0,导致开发者无法通过返回值判断元素是否真的可聚焦。
另一个差异点在于返回类型。即便属性写在HTML上,某些老浏览器通过属性接口读到的可能是字符串,而通过属性接口读到的才是数值。这种类型层面的不一致,会让依赖严格比较的代码在部分浏览器中悄悄失效。jQuery要做的,就是把这一堆乱象抹平。
二、attr与prop两条链路的本质区别
要理解jQuery对tabIndex的修正,首先要分清attr和prop两条链路。attr操作的是HTML标签上的属性,对应DOM节点的attributes集合,读写的是文档层面书写的内容;prop操作的是DOM对象自身的属性,读写的是运行时JavaScript对象上的字段。对于tabIndex来说,两者经常给出不同的结果。
典型例子是给一个<div>设置tabindex="0"后再读取:
var div = document.createElement('div');
div.tabIndex = 0;
div.getAttribute('tabindex'); // 部分浏览器返回 null(未写入HTML)
div.tabIndex; // 返回 0
$(div).attr('tabindex'); // 依赖attributes,结果不稳定
$(div).prop('tabIndex'); // 稳定返回数字 0可以看到,通过prop读取tabIndex拿到的是数值,而且经过jQuery的修正后,所有浏览器表现一致。反过来,通过attr读取则可能返回字符串"0"或者undefined,取决于属性是否真正落到了attributes集合里。这也是社区反复强调的一条经验:读写tabIndex请使用prop,不要使用attr。
三、jQuery源码中的propFix与tabIndex修正实现
jQuery在属性模块中维护了一个propFix映射表,用于把驼峰命名统一到标准属性名,比如class对应className、for对应htmlFor、tabindex对应tabIndex。这解决了开发者写$(el).prop('tabindex')时的小写书写问题。jQuery 1.6.x时期的实现大致如下:
// jQuery内部:属性名修正
jQuery.propFix = {
tabindex: "tabIndex",
readonly: "readOnly",
"for": "htmlFor",
class: "className",
maxlength: "maxLength",
cellspacing: "cellSpacing",
rowspan: "rowSpan",
colspan: "colSpan",
tabindex: "tabIndex",
usemap: "useMap",
frameborder: "frameBorder"
};仅有名字映射还不够,关键是取值的修正。jQuery在prop的取值钩子里对tabIndex做了特殊判断:先检查元素是否有显式的tabindex属性,有则直接解析为整数返回;没有的话,再根据元素类型判断它是否属于默认可聚焦的集合,属于则返回0,不属于则把结果归一化处理,避免返回不稳定的值。其核心思路可以用下面的等价代码表达:
// 等价于jQuery内部对tabIndex的取值修正逻辑
function getTabIndex(elem) {
var attributeNode = elem.getAttributeNode("tabindex");
// 显式设置了tabindex属性,按规范解析为整数
if (attributeNode) {
return parseInt(attributeNode.value, 10);
}
// 未显式设置时,判断是否属于默认可聚焦元素
// 如a[href]、button、input、select、textarea等
if (isDefaultFocusable(elem)) {
return 0;
}
// 普通元素不可聚焦,统一返回一个明确的值
return elem.tabIndex; // 现代浏览器统一为 -1 或 0
}这段逻辑的价值在于:无论浏览器原生实现如何,开发者通过jQuery.fn.prop('tabIndex')拿到的始终是一个数值,且语义符合HTML规范。设置方向同理,jQuery会把传入的值转成数字再赋给DOM属性,避免字符串和数字混用带来的比较陷阱。
值得一提的是,这种修正并不是硬编码在prop的主流程里,而是通过钩子机制挂载的。jQuery把浏览器差异性的处理都收敛到钩子层,主流程保持干净,这也是它兼容性工程值得学习的地方。
四、实践建议:正确读写tabIndex的姿势
在日常开发中,处理tabIndex有三条建议。第一,统一使用prop读写,$(el).prop('tabIndex', -1)比attr更可靠,尤其是在动态创建的元素上。第二,判断元素是否可聚焦时,不要只依赖tabIndex的返回值,还要结合元素类型,因为div设置tabIndex="-1"后可聚焦但不可Tab导航,这是两种不同的状态。第三,构建自定义控件时,遵循无障碍实践的惯例:容器设为-1便于脚本聚焦,子项设为0参与正常导航。
// 可聚焦但不进入Tab序列,常用于模态框容器
$('#modal').prop('tabIndex', -1).trigger('focus');
// 让自定义列表项参与键盘导航
$('li.menu-item').prop('tabIndex', 0);此外,如果项目已经使用现代框架,Vue和React对tabIndex的处理同样走的是DOM属性这条链路,与jQuery的prop思路一致。理解了jQuery这层默认行为修正,也就理解了为什么各框架都不约而同地选择绕开attributes这条路。浏览器兼容的历史包袱最终由库来承担,而开发者只需要面对一套统一的语义,这正是jQuery这类基础设施库的核心价值所在。