HTML 文档和元素的宽度、位置
量页面时要先分清三套尺子:屏幕是整块显示器,视口是浏览器里能看见的那一块,文档是整张网页(往往比视口高,所以会出滚动条)。元素自己还有内容盒、边框盒和可滚动尺寸,不能拿同一个属性到处套。
窗口、屏幕与页面滚动
这些属性都挂在 window 上,用来量浏览器窗口和屏幕,而不是某一个 DOM 节点。
window.innerWidth // 视口宽度(含滚动条占位)。Android、iOS 都能读到
window.innerHeight // 视口高度。Android、iOS 都能读到
window.outerWidth // 整个浏览器窗口宽度(含标签栏、地址栏等)。Android 能读到,iOS 上常不可靠
window.outerHeight // 整个浏览器窗口高度。Android 能读到,iOS 上常不可靠
window.screen.height // 屏幕高度
window.screen.width // 屏幕宽度
window.screen.availHeight // 屏幕可用高度
window.screen.availWidth // 屏幕可用宽度
// screen.width / height:屏幕宽高(如 1920 × 1080,受缩放、设备像素比影响,单位是 CSS 像素)
// screen.availWidth / availHeight:可用宽高,即分辨率减去任务栏、Mac Dock 等系统 UI
window.screenTop // 浏览器窗口相对屏幕顶部的距离
window.screenLeft // 浏览器窗口相对屏幕左边的距离
window.scrollX // 页面水平滚动距离(像素,只读)
window.scrollY // 页面垂直滚动距离(像素,只读)
window.pageXOffset // window.scrollX 的别名
window.pageYOffset // window.scrollY 的别名
注意:innerWidth、innerHeight 量的是视口,不含浏览器外壳。outerWidth、outerHeight 含外壳。移动端尤其是 iOS 上,后一对经常和 innerWidth、innerHeight 相同,或根本没有参考价值。
元素的 client、offset、scroll
以 document.body 为例,同一套属性也可以用在普通元素上。
document.body.clientWidth // 内容 + padding(不含 border、滚动条)
document.body.clientHeight
document.body.clientLeft // 左边框宽度(有时含垂直滚动条占位)
document.body.clientTop // 上边框宽度
document.body.offsetWidth // 内容 + padding + border(可见边框盒,含滚动条)
document.body.offsetHeight
document.body.offsetTop // 相对 offsetParent 上边缘的距离
document.body.offsetLeft // 相对 offsetParent 左边缘的距离
document.body.scrollWidth // 可滚动内容的宽度(溢出部分也算进去)
document.body.scrollHeight // 可滚动内容的高度
document.body.scrollTop // 内容向上滚了多少
document.body.scrollLeft // 内容向右滚了多少
document.documentElement.scrollWidth // 整张文档可滚动内容的宽度(小于视口时,有的浏览器仍报成视口宽)
document.documentElement.scrollHeight // 整张文档可滚动内容的高度
document.documentElement.scrollTop // 标准模式下,文档垂直滚动通常读这里
document.documentElement.scrollLeft // 标准模式下,文档水平滚动通常读这里
offset
offsetWidth、offsetHeight 是边框盒:内容 + padding + border。
offsetLeft、offsetTop 相对的是 offsetParent(最近的定位祖先,或 td、table、body),不是任意父元素。把祖先链上的偏移累加,才能得到相对文档的坐标。
历史差异(旧引擎,现行浏览器大体按相对 offsetParent 边框盒来算):
- IE6、IE7:
offsetLeft≈offsetParent的padding-left+ 当前元素的margin-left - IE8 及以上:
offsetLeft≈offsetParent的margin-left+ 当前元素的margin-left - 旧版 Firefox:还会再加
offsetParent的padding-left
client
clientWidth、clientHeight 是内容 + padding,不含 border。
- 没有滚动条时,接近内容区设定的宽高。
- 出现滚动条时,滚动条会吃掉一部分客户区,读到的值是「原宽高减去滚动条」。
clientLeft、clientTop读取的是该元素自己的左边框、上边框宽度,不是「子级到父级内容区的距离」。
scroll
scrollWidth、scrollHeight 是客户区再加上溢出、需要滚才能看见的那部分。内容撑不满时,有的浏览器仍会把它报成不小于视口;取文档尺寸时要用 Math.max(scrollWidth, clientWidth)。
scrollTop、scrollLeft 是已经滚过的距离,可以赋值,赋值后页面会立刻滚到对应位置。
document.body.scrollTop 和 document.documentElement.scrollTop 往往只有一个非 0。要文档的真实垂直滚动,可以相加:
document.body.scrollTop + document.documentElement.scrollTop
现行也可以直接用 document.scrollingElement(标准模式指向 documentElement,混杂模式指向 body),或读 window.scrollY。
鼠标事件里的坐标
clientX、clientY 属于事件对象,不是元素属性。
event.clientX // 点击点相对视口左侧
event.clientY // 点击点相对视口顶部
event.clientX + document.documentElement.scrollLeft // 相对文档左侧的水平距离
相对文档的位置,现行事件里还有 pageX、pageY,等价于「视口坐标 + 页面滚动」。
文档渲染模式
文档有混杂模式(quirks)和标准模式。Gecko 还有「准标准模式」,和标准模式几乎一样,差别主要在表格单元格里的图片布局;它的模式字符串仍是 CSS1Compat。
document.compatMode 表示当前用哪套:
BackCompat:标准兼容关闭(混杂模式)。CSS1Compat:标准兼容开启(标准模式或准标准模式)。
两种模式的主要区别:
- 怎么触发:没有 DOCTYPE,或 DOCTYPE 不完整、过旧,进入混杂模式(
BackCompat)。写了完整 DOCTYPE(如<!DOCTYPE html>)进入标准模式(CSS1Compat)。 width、height算哪一盒:混杂模式(尤其是旧 IE)把padding、border算进width、height,表现接近box-sizing: border-box。标准模式按内容盒算,padding和border再往外加,即默认的content-box。- 视口和滚动记在谁身上:混杂模式量客户区、读
scrollTop要用document.body。标准模式要用document.documentElement(<html>)。这也是上面要判断compatMode的原因。 - 准标准模式:字符串仍是
CSS1Compat,盒模型和滚动根节点已按标准来;和标准模式的差别主要在表格单元格里图片是否按基线对齐(容易多出一条缝)。
document.documentElement 对应 <html>,document.body 对应 <body>。量「浏览器客户区」时:
BackCompat:用document.body.clientWidth、clientHeightCSS1Compat:用document.documentElement.clientWidth、clientHeight
有 <!DOCTYPE html> 的页面几乎都是 CSS1Compat。
浏览器窗口可视区域
浏览器窗口里能看见的那一块网页面积叫做 viewport(视口)。
- IE8 及更早没有
window.innerWidth、window.innerHeight - 标准模式:
document.documentElement.clientWidth、clientHeight(不含滚动条) - 混杂模式:
document.body.clientWidth、clientHeight
function getViewport() {
if (document.compatMode == "BackCompat") {
return {
width: document.body.clientWidth,
height: document.body.clientHeight
}
} else {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
}
}
下面这段同时取出客户区宽高、文档可滚动宽高、滚动位置,兼容混杂模式和标准模式(含当时的 IE、Firefox、Safari、Opera、Chrome):
if (document.compatMode == "BackCompat") {
cWidth = document.body.clientWidth;
cHeight = document.body.clientHeight;
sWidth = document.body.scrollWidth;
sHeight = document.body.scrollHeight;
sLeft = document.body.scrollLeft;
sTop = document.body.scrollTop;
} else { // document.compatMode == "CSS1Compat"
cWidth = document.documentElement.clientWidth;
cHeight = document.documentElement.clientHeight;
sWidth = document.documentElement.scrollWidth;
sHeight = document.documentElement.scrollHeight;
sLeft = document.documentElement.scrollLeft == 0 ? document.body.scrollLeft : document.documentElement.scrollLeft;
sTop = document.documentElement.scrollTop == 0 ? document.body.scrollTop : document.documentElement.scrollTop;
}
注意:
- 必须等页面加载完成、
document生成后再跑,否则会报错。 - 多数情况
document.documentElement.clientWidth是对的;IE6 混杂模式要用document.body.clientWidth,所以要判断compatMode。 clientWidth、clientHeight只读,不能赋值。window.innerWidth含滚动条占位,documentElement.clientWidth不含。移动端地址栏伸缩、软键盘还可以再看window.visualViewport。
网页的大小
网页的全部面积是它的文档尺寸,通常由内容和 CSS 决定。
内容能在视口里一次性显示完(没有滚动条)时,文档尺寸和视口相等。显示不完时,滚动视口才能看到其余部分。
function getPagearea() {
if (document.compatMode == "BackCompat") {
return {
width: Math.max(document.body.scrollWidth, document.body.clientWidth),
height: Math.max(document.body.scrollHeight, document.body.clientHeight)
}
} else {
return {
width: Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth),
height: Math.max(document.documentElement.scrollHeight, document.documentElement.clientHeight)
}
}
}
理论上没有滚动条时 clientWidth 应等于 scrollWidth。实际上各浏览器处理不一致,两个值未必相等,所以取较大者。
网页元素的位置
获取网页元素的绝对位置
这里的绝对位置,指元素左上角相对整张网页左上角的坐标。每个元素的 offsetLeft、offsetTop 只相对自己的 offsetParent,把这条链累加起来就是文档坐标:
function getElementLeft(element) {
var actualLeft = element.offsetLeft
var current = element.offsetParent
while (current !== null) {
actualLeft += current.offsetLeft
current = current.offsetParent
}
return actualLeft
}
function getElementTop(element) {
var actualTop = element.offsetTop
var current = element.offsetParent
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
return actualTop
}
transform、zoom 等会让这条链和视觉位置对不上,这种布局更适合用下面的 getBoundingClientRect()。
获取网页元素的相对位置
相对位置指元素左上角相对视口左上角的坐标:绝对坐标减去页面滚动距离。垂直滚动读 scrollTop,水平滚动读 scrollLeft。
function getElementViewLeft(element) {
var actualLeft = element.offsetLeft
var current = element.offsetParent
while (current !== null) {
actualLeft += current.offsetLeft
current = current.offsetParent
}
var elementScrollLeft = document.compatMode == "BackCompat" ? document.body.scrollLeft : document.documentElement.scrollLeft
return actualLeft - elementScrollLeft
}
function getElementViewTop(element) {
var actualTop = element.offsetTop
var current = element.offsetParent
while (current !== null) {
actualTop += current.offsetTop
current = current.offsetParent
}
var elementScrollTop = document.compatMode == "BackCompat" ? document.body.scrollTop : document.documentElement.scrollTop
return actualTop - elementScrollTop
}
scrollTop、scrollLeft 可写,赋值后页面立即滚动,从而改变元素相对视口的位置。element.scrollIntoView() 作用类似,可以把元素滚进视口(常见是靠向视口顶部)。
获取元素位置的快速方法
getBoundingClientRect() 一次给出相对视口的盒子,返回对象至少包含 left、right、top、bottom(现行还有 width、height、x、y)。left、top 是左上角,right、bottom 是右下角,都相对视口左上角。
相对视口:
var X = this.getBoundingClientRect().left
var Y = this.getBoundingClientRect().top
相对文档(视口坐标 + 滚动距离):
var X = this.getBoundingClientRect().left + document.documentElement.scrollLeft + document.body.scrollLeft
var Y = this.getBoundingClientRect().top + document.documentElement.scrollTop + document.body.scrollTop
现行浏览器都支持该方法。早期不支持的是 Firefox 2.x 等旧引擎;IE、Firefox 3+、Opera 9.5+ 当时已经可用。不要再按「Safari、Chrome 不支持」来写代码。
结语
先确定量的是屏幕、视口还是文档,再选 client、offset 还是 scroll。位置要么累加 offsetParent,要么用 getBoundingClientRect() 直接拿视口坐标。模式判断只为兼容混杂模式;现在有 DOCTYPE 的页面,优先信 documentElement 和 window.scrollY。