滚动位置的获取看起来是一个再简单不过的需求,一行代码就能搞定,但在jQuery源码中,针对scrollTop和scrollLeft的处理却专门写了很长一段兼容逻辑。这些代码存在的意义,正是为了抹平不同浏览器、不同文档模式下的行为差异。本文带你逐行剖析jQuery.css以及配套的cssHooks中关于滚动位置的处理细节,弄清楚jQuery到底在兼容什么。

一、问题的起点:为什么滚动位置不能直接取
现代浏览器中,获取一个元素的滚动位置非常直观,直接访问DOM属性即可:
var top = el.scrollTop; var left = el.scrollLeft;
但对于window对象,情况就不一样了。window本身不是Element节点,没有scrollTop这个直接属性(在部分浏览器中window.scrollY可用,在另一些浏览器中则依赖document.documentElement或document.body)。如果要支持“对window调用scrollTop也能正确返回页面滚动值”这样的统一接口,就必须做浏览器分支判断。这正是jQuery在cssHooks中注册scrollTop和scrollLeft钩子的根本动机:让开发者无论传入的是DOM元素还是window对象,API行为保持完全一致。
另一个历史性难点是老版本IE。在IE8及更早版本的怪异模式(quirks mode)下,页面滚动位置需要从document.body上取,而在标准模式下则要从document.documentElement上取。Chroma、Firefox等浏览器在这方面的实现也经历过各自的演进。如果没有统一的封装,开发者就得自己写一堆特性检测代码,非常容易出错。
二、cssHooks机制:jQuery.css如何被拦截
要理解jQuery.css在滚动位置上的处理,先要知道cssHooks的设计。jQuery.css是所有样式读取的统一入口,其内部流程大致是:先检查cssHooks中是否存在针对该属性名前缀的钩子,如果有就优先调用钩子函数,钩子返回结果则直接使用;否则走标准的getComputedStyle流程。源码简化后如下:
css: function( elem, name, extra, styles ) {
var val, num, hooks,
origName = jQuery.camelCase( name );
styles = styles || getStyles( elem );
// 如果注册了针对该属性的cssHook,优先调用
if ( (hooks = cssHooks[ origName ]) ) {
val = hooks.get( elem, true, extra );
}
// 否则走通用的计算样式读取
if ( val === undefined ) {
val = curCSS( elem, name, styles );
}
// extra参数控制是否转成数字或附带单位
if ( extra === "" && val != null ) {
return String(val);
}
return val;
}而针对滚动位置,jQuery在初始化阶段注册了这样一个钩子(jQuery 1.x至2.x的核心写法):
jQuery.each([ "top", "left" ], function( i, prop ) {
jQuery.cssHooks[ prop ] = {
get: function( elem, computed, extra ) {
if ( computed ) {
// 拿到元素的ownerDocument,再取其defaultView
var win = getWindow( elem );
// 如果目标就是window本身,直接取页面滚动值
if ( win ) {
win.scrollTo( prop === "top" ? win.pageXOffset : 0,
prop === "top" ? 0 : win.pageYOffset );
}
return win[ "page" + prop ] ||
// 兼容老IE:标准模式取documentElement,怪异模式取body
jQuery.support.boxModel && document.documentElement[ prop ] ||
document.body[ prop ];
}
return elem[ prop ];
}
};
});注意,这里的钩子key实际上是scrollTop与scrollLeft,jQuery在注册时通过each遍历top和left来组合出两个钩子。核心逻辑分三步:第一步通过getWindow判断目标是否关联了一个window视图;第二步如果目标是window,就利用pageXOffset或scrollTo系列属性取页面滚动值;第三步兜底处理老IE,用documentElement和body双路径取值。这样无论调用者是$(window).scrollTop()还是$(el).scrollTop(),最终都收敛到统一的get流程。
三、getWindow与文档判断:兼容代码的关键细节
getWindow是这段兼容逻辑里非常精巧的一环,它的作用是判断传入的对象背后是否真的存在一个浏览器视图。源码大致如下:
function getWindow( elem ) {
return jQuery.isWindow( elem ) ?
elem :
elem.nodeType === 9 ?
elem.defaultView || elem.parentWindow :
false;
}这段代码分三种情况:如果elem本身就是window(通过isWindow判断,通常是elem != null && elem == elem.window这样的技巧),直接返回它;如果elem是document节点(nodeType为9),则取其defaultView,老IE下退化为parentWindow;如果既不是window也不是document,返回false,表示走普通元素的取值路径。defaultView这个属性是W3C标准,parentWindow是IE私有,两者用或运算串联,就完成了跨浏览器。
为什么nodeType === 9这个判断这么重要?因为页面上有很多场景会拿到document对象而不是元素对象,比如$(document).scrollTop()这种写法,开发者期望的是获取整个页面的滚动位置。此时如果直接访问document.scrollTop,得到的是undefined。有了这段判断,jQuery会把document转换成其所属的window,再走window分支,API语义就完全符合直觉了。
四、boxModel判断与老IE的怪异模式
前面钩子代码中出现的jQuery.support.boxModel,是jQuery 1.x时代一个著名的特性检测项,用来判断当前文档是否处于标准盒模型模式。它通过创建一个带padding和border的div并比较其offsetWidth来动态检测。对于滚动位置的读取来说,boxModel的值直接决定了应该从哪个节点取scroll值:
if ( jQuery.support.boxModel ) {
// 标准模式:滚动值挂在documentElement上
scrollTop = document.documentElement.scrollTop;
} else {
// 怪异模式:滚动值挂在body上
scrollTop = document.body.scrollTop;
}这段兼容存在的背景是IE6、IE7时代的怪异模式:当页面缺少DOCTYPE声明时,IE会进入quirks模式,此时documentElement的scrollTop恒为0,真正的滚动值反而存放在body上。其他浏览器的表现也不完全统一。因此在没有统一标准之前,开发者必须写这种双重取值。到了IE8+以及现代浏览器全面支持标准模式后,documentElement.scrollTop已经成为可靠路径,boxModel检测也就逐渐被jQuery废弃了。jQuery 3.x中,这段代码已经被简化了很多,因为需要兼容的浏览器范围大幅缩小。
这也给我们一个提示:如果在自己的项目里只面向现代浏览器,完全不需要这么多兼容分支,直接用标准属性即可;但如果要维护老系统、兼容老IE,jQuery当年的这些处理思路依然值得参考。
五、原生JavaScript下的现代兼容封装
理解了jQuery源码的兼容思路之后,我们可以用原生JavaScript写一个轻量版的统一取值函数,不依赖任何库就能覆盖绝大多数场景:
function getScroll(target, prop) {
// prop为top或left
var isWindow = target != null && target === target.window;
var doc = target != null && target.nodeType === 9 ? target : target.ownerDocument;
if (isWindow || (doc && target === doc)) {
// window或document:取页面滚动
var win = isWindow ? target : (doc.defaultView || doc.parentWindow);
if (win) {
return prop === "top"
? (win.pageYOffset != null ? win.pageYOffset
: (doc.documentElement.scrollTop || doc.body.scrollTop))
: (win.pageXOffset != null ? win.pageXOffset
: (doc.documentElement.scrollLeft || doc.body.scrollLeft));
}
}
// 普通元素
return target[prop === "top" ? "scrollTop" : "scrollLeft"] || 0;
}这个封装遵循了jQuery相同的思路:先区分window、document和普通元素三种对象,window优先用pageXOffset和pageYOffset,老环境退回到documentElement或body,普通元素直接读自身属性。值得一提的是,如果需要同时支持隐藏元素的滚动位置读取,还要注意一个细节:display为none的元素在某些浏览器中scrollTop始终为0,这一点在jQuery的offset相关方法中也有额外的处理逻辑。
六、实际项目中的应用建议
通过这次源码剖析,可以总结出几条实用经验。第一,滚动位置读取一定要考虑目标对象类型,不要假设传入的一定是Element,封装API时应该像jQuery一样显式处理window和document。第二,涉及老浏览器兼容时,documentElement与body的双路径取值是标准做法,特性检测优于浏览器嗅探。第三,学习这类源码的价值不在于背诵代码本身,而在于理解每个分支背后的浏览器历史:quirks模式、IE私有属性、W3C标准的渐进统一,这些脉络清楚了,遇到任何兼容问题都能迅速定位根因。
如今大多数项目已不再需要如此深度的兼容,但回看jQuery.css中这段几十行的钩子代码,它把浏览器碎片化时代的一地鸡毛封装成了两个再简单不过的API调用,这正是优秀类库设计的体现,也值得每一位前端工程师借鉴。
jQuery源码jQuery.cssscrollTop修改时间:2026-09-05 09:33:36