在浏览器事件模型中,事件回调接收到的参数通常是一个原生 Event 对象。jQuery 并没有直接把这个对象传给开发者,而是通过 jQuery.event.fix 进行一次包装。包装的核心并不是复制原生对象的所有字段,而是依赖一个固定字符串列表 jQuery.event.props。这个列表中的每一项,都对应一个被认为值得保留的跨浏览器通用属性。看到这个机制,很多人会误以为 jQuery 只是简单浅拷贝所有属性,实际上它是在刻意做减法,只同步白名单里的属性。这样做既维持了 API 的一致性,也规避了直接访问原生事件对象时可能遇到的异常。

白名单的定义与默认属性集合
在 jQuery 源码中,jQuery.event.props 并不是一开始就以数组形式存在,而是由一段空格分隔的字符串通过 split(" ") 生成数组。早期版本中的定义大致如下:
jQuery.event = {
props: "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
fixHooks: {}
};
这份默认名单里没有 clientX、pageY 这样的坐标属性,也没有 dataTransfer 或 touches,因为这些属性的存在与否依赖具体事件类型。通用列表只挑选那些几乎所有事件对象都支持的基础字段,例如 altKey 表示是否按下 Alt 键,timeStamp 表示事件创建时间戳,target 表示事件源元素。拷贝完成后,jQuery 还会对 target 做额外修正,因为在老版本 IE 中对应属性叫 srcElement。如果事件目标是一个文本节点,还会把它提升到父元素节点,保证开发者拿到的是元素而不是文本节点。
为什么源码里使用字符串而不是直接维护一个数组字面量?字符串写法更紧凑,也方便阅读和按空格维护。同时,jQuery 需要兼容老版本 IE,字符串在旧解析器下更稳定,而且通过 split(" ") 可以快速得到数组,性能开销可以忽略不计。这种设计也说明 jQuery 对内部数据结构的取舍非常务实:不是追求类型上的理想化,而是优先考虑兼容性和可维护性。
fix 流程如何消费白名单
事件进入 jQuery.event.fix 后,首先判断事件是否已经携带 jQuery.expando 标记。如果已经处理过就直接返回,避免重复包装。然后根据事件类型查找 fixHooks。例如 mouseenter 和 keydown 这类需要补充专有属性的类型,会有各自的 fixHook。fixHook 可以声明自己的 props,最终与全局 props 合并。合并逻辑是先取全局列表,再用 concat 追加特定类型的专有属性,然后倒序遍历数组,逐项把原始事件对象上的同名属性值赋给新的 jQuery.Event 实例。
fix: function(originalEvent) {
if (originalEvent[jQuery.expando]) {
return originalEvent;
}
var type = originalEvent.type,
fixHook = this.fixHooks[type] || {},
copy = fixHook.props ? this.props.concat(fixHook.props) : this.props,
event = new jQuery.Event(originalEvent),
i = copy.length,
prop;
while (i--) {
prop = copy[i];
event[prop] = originalEvent[prop];
}
if (!event.target) {
event.target = originalEvent.srcElement || document;
}
if (event.target.nodeType === 3) {
event.target = event.target.parentNode;
}
return fixHook.filter ? fixHook.filter(event, originalEvent) : event;
}
这种倒序遍历并不是功能上的必须,而是一种微小性能优化,因为递减判断比每次比较数组长度稍省操作。真正重要的是赋值过程是浅拷贝。对于对象类型,比如 target 和 relatedTarget,复制的是引用而不是创建新元素,所以后续访问 event.target 和 originalEvent.target 指向同一个 DOM 节点。浅拷贝不会破坏 DOM 关系,还减少了内存开销。对于字符串、数字、布尔值等基本类型,则是值拷贝,后续修改不会反向影响原始事件对象。
在属性复制完成后,代码还会统一修正 target。如果原生事件对象上没有 target,就回退到 srcElement,再不行就使用 document。这一层兜底逻辑正是为了抹平不同浏览器的事件对象差异,让开发者不必在业务代码里反复判断 target 是否存在。
为什么必须用白名单而不是全量遍历
原生事件对象在不同浏览器中差异非常大。Chrome、Firefox 中的 MouseEvent 继承自 UIEvent,带有许多只读属性;而老版本 IE 中的事件对象是宿主对象,某些属性直接访问会抛异常。如果用 for...in 遍历所有可枚举属性,不仅会触发未知 getter,还可能把浏览器内部使用的方法或循环引用一起复制,造成内存泄漏或性能下降。白名单把访问控制在十几个已验证属性上,是兼顾兼容与性能的折中方案。只要白名单中某个属性在原对象上不存在,赋值结果就是 undefined,不会引发异常。
与此同时,白名单还承担了标准化职责。开发者通过 jQuery 绑定事件时,回调中的 event.which、event.target 被保证存在,event.type 也已经被修正。但白名单不是万能,像 mousewheel 事件的 wheelDelta 和 detail 在旧版 Chrome 与 Firefox 中含义完全不同,需要 fixHooks 继续处理。白名单主要负责基础属性同步,类型级差异交给 fixHooks 的 props 和 filter 二次兜底。例如鼠标事件需要补充坐标属性,jQuery 内部会通过 mouseHooks 提供 button、clientX、pageY 等字段,再与全局列表合并。
mouseHooks: {
props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" ")
}
从这段源码可以看出,坐标类和按键类属性并没有进入全局白名单,而是按类型提供。这样做避免了不必要的属性复制:键盘事件根本不需要 clientX,鼠标事件也不一定需要 keyCode。把属性分门别类,既能减少每次事件包装时的复制次数,也让职责划分更清晰。
扩展白名单的两种方式与注意事项
当业务中用到自定义事件或原生属性未在默认列表时,可以通过修改 jQuery.event.props 数组进行全局扩展。例如文件拖拽事件中要读取 dataTransfer 对象,可以在加载 jQuery 后、绑定事件前执行:
jQuery.event.props.push("dataTransfer");
这种扩展是全局的,会影响所有事件类型。如果属性访问存在风险,可能把异常引入未使用该属性的事件。更稳妥的做法是针对具体事件类型定义 fixHook,只在对应事件里补充属性,并可以在 filter 中做条件判断和格式转换。
jQuery.event.fixHooks.drop = {
props: ["dataTransfer"],
filter: function(event, originalEvent) {
if (originalEvent.dataTransfer) {
event.dataTransfer = originalEvent.dataTransfer;
}
return event;
}
};
扩展时需要注意,若只把属性名放入 props,jQuery 只会执行浅拷贝。部分属性可能原本是只读的,或需要从原始事件上读取后再转换格式,这时候应该配合 filter 函数。filter 在属性复制完成后执行,可以同时访问原始事件和新的 jQuery.Event 对象,适合做修正和兜底。比如拖拽事件中 dataTransfer 可能不存在,在 filter 里判断并补充降级方案,比直接 push 更安全。另外,修改 jQuery.event.props 是运行期行为,不会改变源码,但要保证在首次事件触发前完成,否则已经包装过的事件不会重新按新列表处理。
整体来看,jQuery.event.props 的白名单机制是一种典型的兼容性设计:用固定属性列表换取跨浏览器一致性和性能,同时通过 fixHooks 保留按类型扩展的空间。它不是复制所有可能用到的字段,而是只复制基础、安全、通用的部分,把特殊逻辑交给类型钩子。理解这一点,能够帮助开发者在遇到事件属性缺失或自行扩展事件系统时,更清楚地判断问题出在列表范围还是后续处理流程。
jQuery.event.props原生事件属性白名单机制修改时间:2026-10-03 21:48:09