Appearance
尺寸与位置 ✨
要点速览
- 分类:只读度量(
client/offset/scroll)与可读写度量(scrollTop/scrollLeft/style)。 clientWidth/Height:padding + content,不含border/scrollbar/margin。offsetWidth/Height:border + padding + content,通常包含滚动条厚度。scrollWidth/Height:滚动容器的内容总尺寸(含padding),与是否出现滚动条无关。- 坐标系:
client相对视口,page相对文档,screen相对屏幕,offset相对事件源。 - 最稳测量:使用
getBoundingClientRect()获取视口中的边界盒,再结合滚动偏移换算为页面坐标。
概念总览
在浏览器中操作元素的移动与碰撞检测,离不开对尺寸与位置的精准理解。不同属性面向不同的盒模型边界与坐标系。如果没有区分清楚,将导致数值不一致、计算错误与兼容性问题。
本文系统梳理元素尺寸属性与事件坐标属性,并给出常见坑与实践准则。
在 JavaScript 中操作 DOM 节点使其运动的时候,常常会涉及到各种宽高以及位置坐标等概念,如果不能很好地理解这些属性所代表的意义,就会在书写代码时遇到不小的问题。而由于这些属性概念较多,加上浏览器之间实现方式不同,常常会造成概念混淆。
本章,我们就一起来总结一下使用 JavaScript 操作 DOM 时,尺寸和宽高相关的属性。
主要分为以下两部分:
- DOM 对象相关尺寸和位置属性
- 只读属性
clientWidth和clientHeight属性offsetWidth和offsetHeight属性clientTop和clientLeft属性offsetLeft和offsetTop属性scrollHeight和scrollWidth属性
- 可读可写属性
scrollTop和scrollLeft属性domObj.style.xxx属性
- 只读属性
- event 事件对象相关尺寸和位置属性
clientX和clientY属性screenX和screenY属性offsetX和offsetY属性pageX和pageY属性
DOM 对象相关尺寸和位置属性
盒模型与度量
元素的盒模型由外到内依次为:margin → border → padding → content。
- 大多数 DOM 尺寸属性不包含
margin。 client*面向可视内容盒(padding + content)。offset*面向边框盒(border + padding + content)。scroll*面向滚动内容总面积(含padding,不含border)。
在 DOM 对象所提供的尺寸和位置相关属性中,可以分为只读属性和可读可写属性。
只读属性
所谓的只读属性指的是 DOM 节点的固有属性,该属性只能通过 JavaScript 去获取而不能通过 JavaScript 去设置,而且获取的值是只有数字并不带单位的( px、em 等 )
常见的只读属性有:
clientWidth和clientHeight属性offsetWidth和offsetHeight属性clientTop和clientLeft属性offsetLeft和offsetTop属性scrollHeight和scrollWidth属性
下面我们来一组一组进行介绍。
clientWidth 与 clientHeight
该属性指的是元素的可视部分宽度和高度,即 padding + content,例如:
html
<div id="container" class="container"></div>css
.container {
width: 200px;
height: 200px;
background-color: red;
border: 1px solid;
overflow: auto;
padding: 10px;
margin: 20px;
}js
let container = document.getElementById("container");
console.log("clientWidth:", container.clientWidth); // 220
console.log("clientHeight:", container.clientHeight); // 220说明与注意
- 返回的是可视内容盒尺寸(
padding + content)。 - 当垂直滚动条出现时,
clientWidth会扣除滚动条占用的宽度(受系统样式影响)。 - 不包含
margin与border,若需要请配合getComputedStyle()。
offsetWidth 与 offsetHeight
这一对属性指的是元素的 border+padding+content 的宽度和高度。例如:
js
let container = document.getElementById("container");
console.log("offsetWidth:", container.offsetWidth); // 222
console.log("offsetWidth:", container.offsetWidth); // 222说明与注意
offset*面向边框盒尺寸(border + padding + content)。- 通常包含滚动条厚度;覆盖式滚动条系统可能表现不同。
可以看到该属性和 clientWidth 以及 clientHeight 相比,多了设定的边框 border 的宽度和高度。
clientTop 与 clientLeft
这一对属性是用来读取元素的 border 的宽度和高度的。例如:
js
let container = document.getElementById("container");
console.log("clientTop:", container.clientTop); // 1
console.log("clientLeft:", container.clientLeft); // 1说明与注意
- 代表上/左边框的像素厚度。
- 历史实现中可能受滚动条与书写方向影响;现代浏览器趋于一致。
offsetLeft 与 offsetTop
首先需要介绍一下 offsetParent 属性,该属性是获取当前元素的离自己最近的并且定了位的祖先元素,该祖先元素就是当前元素的 offsetParent。
如果从该元素向上寻找,找不到这样一个祖先元素,那么当前元素的 offsetParent 就是 body 元素。
offsetLeft 和 offsetTop 指的是当前元素,相对于其 offsetParent 左边距离和上边距离,即当前元素的 border 到包含它的 offsetParent 的 border 的距离。
下面我们来具体举例说明:
js
let container = document.getElementById("container");
console.log(container.offsetParent); // body可以看到,我们直接访问 container 盒子的 offsetParent 属性,因为不存在定了位的祖先元素,所以显示出来的是 body 元素。
接下来我们对上面的代码进行改造:
html
<div id="container" class="container">
<div id="item" class="item"></div>
</div>css
.container {
width: 200px;
height: 200px;
background-color: red;
border: 1px solid;
overflow: auto;
padding: 10px;
margin: 20px;
position: relative;
}
.item {
width: 50px;
height: 50px;
background-color: blue;
position: absolute;
/* 到container盒子的距离不受padding影响 */
left: 100px;
top: 100px;
}当前效果如下:

接下来我们来获取 item 盒子的 offsetLeft 以及 offsetTop 属性值。
js
let container = document.getElementById("container");
let item = document.getElementById("item");
console.log(item.offsetParent); // container 盒子 dom 对象
console.log(item.offsetLeft); // 100
console.log(item.offsetTop); // 100说明与注意
offset*是元素边框盒到offsetParent内边缘的距离。- 使用
transform位移不会改变offset*,测量位移请用getBoundingClientRect()。
与 getBoundingClientRect() 的关系
offset* 适合与定位上下文(最近定位祖先)做相对测量;若元素应用了 CSS 变换或处于复杂布局(滚动/缩放)中,推荐以 getBoundingClientRect() 作为统一测量基准,并按需换算为页面坐标或相对父元素坐标。
接下来我们不对 item 子元素进行定位,而是使用 margin 的方式来设置子盒子的位置,如下:
css
.item {
width: 50px;
height: 50px;
background-color: blue;
margin: 50px;
}然后再次获取 item 盒子的 offsetLeft 以及 offsetTop 属性值,如下:
js
let container = document.getElementById("container");
let item = document.getElementById("item");
console.log(item.offsetParent); // container 盒子 dom 对象
console.log(item.offsetLeft); // 60
console.log(item.offsetTop); // 60可以看到,打印出来的是 60,因为我们设置的 margin 的值为 50,但是其定了位的父元素还设置了 10 像素的 padding,所以加起来就是 60。
scrollWidth 与 scrollHeight
顾名思义,这两个属性指的是当元素内部的内容超出其宽度和高度的时候,元素内部内容的实际宽度和高度。
如果当前元素的内容没有超过其高度或者宽度,那么返回的就是元素的可视部分宽度和高度,即和 clientWidth 和 clientHeight 属性值相同。
html
<div id="container" class="container">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Nulla repellat porro
atque culpa rem sunt sed! Voluptates vel incidunt accusamus reiciendis aut,
adipisci ut. Hic, impedit officia.Quis, animi beatae. Facere dolorum quasi
laborum, rem facilis illum necessitatibus sint doloribus beatae exercitationem
sapiente! Quod vel cupiditate quam libero, delectus natus.
</div>css
.container {
width: 200px;
height: 200px;
background-color: red;
border: 1px solid;
overflow: auto;
padding: 10px;
margin: 20px;
}上面的代码中,我们为 container 盒子加入了一些文字,使其能够产生滚动效果,接下来访问 scrollHeight 和 scrollWidth 属性,如下:
js
let container = document.getElementById("container");
console.log("scrollWidth:", container.scrollWidth); // scrollWidth: 220
console.log("scrollHeight", container.scrollHeight); // scrollHeight 372如果 container 盒子不具备滚动的条件,那么返回的值和 clientWidth 和 clientHeight 属性值是相同的。
html
<div id="container" class="container"></div>js
let container = document.getElementById("container");
console.log("scrollWidth:", container.scrollWidth); // scrollWidth: 220
console.log("scrollHeight", container.scrollHeight); // scrollHeight 220说明与注意
- 内容未溢出时,
scroll*与client*相等;溢出后scroll*更大。 scroll*包含padding,不包含border;内联元素换行会影响数值。
可读可写属性
所谓的可读可写属性指的是不仅能通过 JavaScript 获取该属性的值,还能够通过 JavaScript 为该属性赋值。
scrollTop 与 scrollLeft
这对属性是可读写的,指的是当元素其中的内容超出其宽高的时候,元素被卷起的高度和宽度。
html
<div id="container" class="container">
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Nulla repellat porro
atque culpa rem sunt sed! Voluptates vel incidunt accusamus reiciendis aut,
adipisci ut. Hic, impedit officia.Quis, animi beatae. Facere dolorum quasi
laborum, rem facilis illum necessitatibus sint doloribus beatae exercitationem
sapiente! Quod vel cupiditate quam libero, delectus natus.
</div>css
.container {
width: 200px;
height: 200px;
background-color: red;
border: 1px solid;
overflow: auto;
padding: 10px;
margin: 20px;
}js
let container = document.getElementById("container");
container.onscroll = function () {
console.log("scrollTop:", container.scrollTop);
console.log("scrollLeft", container.scrollLeft);
};页面滚动值兼容
页面层面的滚动偏移在不同模式与历史浏览器中存在差异。推荐优先读取 window.pageXOffset/pageYOffset,并在需要时与 document.documentElement/document.body 组合判断:
js
const scrollY =
window.pageYOffset ??
document.documentElement.scrollTop ??
document.body.scrollTop ??
0;
const scrollX =
window.pageXOffset ??
document.documentElement.scrollLeft ??
document.body.scrollLeft ??
0;在上面的代码中,我们的 container 盒子内容超出了容器的高度,我们为该盒子绑定了 scroll 事件,滚动的时候打印出 scrollTop 和 scrollLeft,即元素被卷起的高度和宽度。
效果如下:

该属性因为是可读可写属性,所以可以通过赋值来让内容自动滚动到某个位置。例如很多网站右下角都有回到顶部的按钮,背后对应的 JavaScript 代码就是通过该属性来实现的。
js
const container = document.getElementById("container");
container.scrollTop = 0; // 滚动到顶部结合边界盒进行滚动定位
当需要将元素滚动到视口特定位置时,可结合 getBoundingClientRect() 的 top/left 与容器的滚动值进行计算,从而实现“滚动至元素顶端/居中”等功能。
element.style.xxx 属性
对于一个 DOM 元素来讲,它的 style 属性返回的也是一个对象,并且这个对象中的任意一个属性是可读写的。例如 domObj.style.top、domObj.style.wdith 等,在读的时候,它们返回的值常常是带有单位的(如 px )。
但是,对于这种方式,它只能够获取到该元素的行内样式,而并不能获取到该元素最终计算好的样式。如果想要获取计算好的样式,需要使用 obj.currentstyle( IE )和 getComputedStyle( IE 之外的浏览器 )
另一方面,由于 domObj.style.xxx 属性能够被赋值,所以 JavaScript 控制 DOM 元素运动的原理就是通过不断修改这些属性的值而达到其位置改变的,需要注意的是,给这些属性赋值的时候需要带单位的要带上单位,否则不生效。
event 事件对象相关尺寸和位置属性
对于元素的运动的操作,通常还会涉及到事件里面的 event 对象,而 event 对象也存在很多位置属性,且由于浏览器兼容性问题会导致这些属性间相互混淆,这里也来进行一个总结。
clientX 与 clientY
这对属性指的是事件发生时,鼠标点击位置相对于浏览器(可视区)的坐标,即浏览器左上角坐标的( 0 , 0 ),该属性以浏览器左上角坐标为原点,计算鼠标点击位置距离其左上角的位置,
不管浏览器窗口大小如何变化,都不会影响点击位置的坐标。
html
<div id="container" class="container"></div>css
* {
margin: 0;
padding: 0;
}
.container {
width: 200px;
height: 200px;
background-color: red;
border: 1px solid;
overflow: auto;
padding: 10px;
margin: 20px;
}js
let container = document.getElementById("container");
container.onclick = function (ev) {
let evt = ev || event;
console.log(evt.clientX + ":" + evt.clientY);
};在上面的代码中,我们为 container 盒子绑定了点击事件,获取该盒子的 clientX 以及 clientY 的值,接下来我们来进行点击测试:

我们分别点击该盒子的最左上角和最右下角,打印出来的值分别是(20 , 20)和 (241 , 241),可以看出这确实是以浏览器左上角坐标的( 0 , 0 )为原点来进行计算的。
screenX 与 screenY
screenX 和 screenY 是事件发生时鼠标点击位置相对于设备屏幕的坐标,以设备屏幕的左上角为原点,事件发生时鼠标点击的地方即为该点的 screenX 和 screenY 值。例如:
js
let container = document.getElementById("container");
container.onclick = function (ev) {
let evt = ev || event;
console.log(evt.screenX + ":" + evt.screenY);
};
我们将浏览器缩小,同样是点击 container 盒子的最左上角,打印出来的是(368 , 365),因为这是以屏幕最左上角为原点的。
offsetX 与 offsetY
这一对属性是指事件发生时,鼠标点击位置相对于该事件源的位置,即点击该 DOM 元素,以该 DOM 左上角为原点来计算鼠标点击位置的坐标。例如:
js
let container = document.getElementById("container");
container.onclick = function (ev) {
let evt = ev || event;
console.log("offsetX:", evt.offsetX);
console.log("offsetY:", evt.offsetY);
};同样点击 container 元素的最左上角,打印出( 0, 0 )
注意
在复杂嵌套、SVG 或 Canvas 等场景下,事件源的变化会影响 offsetX/offsetY。若需要稳定结果,建议使用 getBoundingClientRect() 与 client/page 坐标进行换算。
统一换算公式
统一相对坐标计算:相对X = clientX - rect.left,相对Y = clientY - rect.top;其中 rect 来自 getBoundingClientRect()。
pageX 与 pageY
顾名思义,该属性是指事件发生时,鼠标点击位置相对于页面的位置,通常浏览器窗口没有出现滚动条时,该属性和 clientX 及 clientY 是等价的。
例如:
js
let container = document.getElementById("container");
container.onclick = function (ev) {
let evt = ev || event;
console.log("pageX:", evt.pageX);
console.log("pageY:", evt.pageY);
console.log("clientX:", evt.clientX);
console.log("clientY:", evt.clientY);
};此时点击 container 盒子,得到的 pageX、pageY 的值和 clientX、clientY 完全相同。
但是当浏览器出现纵向滚动条的时候,pageY 通常会大于 clientY,因为页面还存在被卷起来的部分的高度(横向滚动条同理)。
例如:
css
...
body{
height: 5000px;
}
...我们为 body 添加一个 height 为 500px,使其能够产生滚动效果,此时两组属性的区别就出来了。如下:

属性对照表
| 类型 | 属性 | 包含内容 | 参考系/相对对象 |
|---|---|---|---|
| 尺寸(只读) | clientWidth/Height | padding + content | 元素自身 |
| 尺寸(只读) | offsetWidth/Height | border + padding + content | 元素自身 |
| 尺寸(只读) | scrollWidth/Height | 滚动内容总尺寸(含 padding) | 元素自身 |
| 边框厚度 | clientTop/Left | 上/左边框厚度 | 元素自身 |
| 位置(只读) | offsetLeft/Top | 到 offsetParent 内边缘的距离 | 最近定位祖先或文档根 |
| 滚动(读写) | scrollTop/Left | 已滚动距离 | 元素自身/页面 |
| 坐标(事件) | clientX/Y | 视口坐标 | 浏览器视口 |
| 坐标(事件) | pageX/Y | 文档坐标 | 整个页面 |
| 坐标(事件) | screenX/Y | 屏幕坐标 | 设备屏幕 |
| 坐标(事件) | offsetX/Y | 事件源坐标 | 事件目标元素 |
最稳测量:边界盒与坐标换算
在存在变换、滚动与嵌套布局的场景,优先使用 getBoundingClientRect():
js
function getViewportBox(el) {
return el.getBoundingClientRect();
}
function getPageBox(el) {
const rect = el.getBoundingClientRect();
const scrollY = window.pageYOffset ?? document.documentElement.scrollTop ?? 0;
const scrollX =
window.pageXOffset ?? document.documentElement.scrollLeft ?? 0;
return {
top: rect.top + scrollY,
left: rect.left + scrollX,
right: rect.right + scrollX,
bottom: rect.bottom + scrollY,
width: rect.width,
height: rect.height,
};
}getBoundingClientRect()
用于获取元素在视口坐标系中的边界盒(DOMRect)。返回的对象包含:x/y/left/top/right/bottom/width/height。
js
const el = document.getElementById("container");
const rect = el.getBoundingClientRect();
console.log(rect.left, rect.top, rect.width, rect.height);用途与特性
- 参考系:相对浏览器视口左上角;随页面滚动而变化。
- 包含项:边框盒尺寸(包含
border与padding,不包含margin)。 - 精度:可能为小数,适合精确测量与碰撞检测。
- 变换:受 CSS
transform影响,适合测量经过位移/缩放后的真实占位。
多行/复杂元素
对于跨多行的内联元素(如换行文本),可使用 getClientRects() 获得多个矩形片段:
js
const ranges = el.getClientRects();
for (const r of ranges) {
console.log(r.left, r.top, r.right, r.bottom);
}事件坐标到元素内坐标的换算可通过 client 坐标与边界盒进行:
js
const box = document.getElementById("container");
box.addEventListener("click", (evt) => {
const rect = box.getBoundingClientRect();
const relX = evt.clientX - rect.left;
const relY = evt.clientY - rect.top;
console.log(relX, relY);
});常见坑与最佳实践
- 不要指望任何属性返回
margin,需用getComputedStyle()自行计算。 - 使用
transform位移不会改变offset*与client*,测量请用getBoundingClientRect()。 - 叠加滚动容器时,优先基于
page坐标或逐层换算,避免混用不同参考系。 - 覆盖式滚动条(macOS)可能让
clientWidth与offsetWidth的差值为 0,逻辑需要兼容。 - 事件坐标需要结合滚动与容器边界统一换算,保证一致性。
