Skip to content

读取元素的尺寸(只读)与读取元素的位置 (只读)(Day13 2/3) ​

本页对应 Day13_DOM,共 6 段视频,约 1 小时 38 分。

学习目标 ​

  • 能使用 offsetWidth/offsetHeight、clientWidth/clientHeight、getBoundingClientRect() 读取元素尺寸,并说明各自是否包含边框、滚动条及其适用场景。
  • 能使用 offsetLeft/offsetTop 与 getBoundingClientRect() 读取元素位置,并准确区分二者分别参照定位祖先元素与视口。
  • 能独立实现图片延迟加载:监听页面滚动,判断图片是否进入视口,并通过自定义属性把图片地址写入 src。
  • 能读写元素的 scrollTop/scrollLeft 控制元素内部内容滚动,并说明其可读可写、最小值为 0、需设置非默认 overflow 等条件。
  • 能通过 document.documentElement.scrollTop 读写页面滚动距离,并实现按钮控制页面上下滚动。
  • 能分析无缝滚动的实现条件,使用定时器修改 scrollLeft 使内容持续滚动,并说明到达边界后如何设计循环方案。

知识点 ​

为什么需要读取元素尺寸 ​

写页面时,很多判断不能靠“看起来差不多”。比如做图片延迟加载、判断元素是否进入视口、做滚动动画,都要先拿到元素的真实尺寸。这里的关键是:读取尺寸和读取样式不是一回事。

转写稿里先演示了用计算样式读宽高:

js
getComputedStyle(box).width
getComputedStyle(box).height

如果 CSS 里写的是 width: 600px,读到的往往是字符串 "600px",不是数字;高度没写时可能读到 auto 的计算结果,比如 277。它读到的是内容宽高,不是“总宽总高”。要算总宽总高,还得自己加内边距、边框,既麻烦又容易漏。所以需要专门的一组只读属性来读元素尺寸。

元素的三组尺寸 ​

笔记里给了三组属性,先保留原代码,注意其中 offfsetWidth、offfsetHeight、offsetHeihgt 是拼写错误,正确是 offsetWidth、offsetHeight:

js
元素对象.offfsetWidth / 元素对象.offfsetHeight	获取元素的总宽总高
元素对象.clientWidth / 元素对象.clientHeight    获取元素宽高,内容+内边距
元素对象.scrollWidth / 元素对象.scrollHeight	获取元素宽高,client加上溢出的部分

怎么理解?

  • offsetWidth / offsetHeight:元素占位的总宽高,通常包含内容、内边距、边框,不含外边距。
  • clientWidth / clientHeight:内容区加内边距,不含边框。
  • scrollWidth / scrollHeight:在 client 的基础上,把溢出、需要滚动才能看到的内容也算进去。

怎么用?先问自己要的是“元素占多大”“可视内容区多大”还是“内容总共多大”。比如判断内容有没有溢出,就看 scrollHeight 是否大于 clientHeight。易错点是:这些属性读出来是数字,不带 px,可以直接计算;而 getComputedStyle 读宽度常带单位,不能直接拿来加减。

getBoundingClientRect 读尺寸 ​

笔记还给了:

js
元素对象.getBoundingClientRect() 返回对象,对象包含元素的位置和尺寸信心,对象有如下属性:
元素对象.getBoundingClientRect().width	 同offsetWidth
元素对象.getBoundingClientRect().height  同offsetHeihgt

这里的“信心”是同音字,应为“信息”;offsetHeihgt 应为 offsetHeight。这个方法返回一个对象,里面既有尺寸也有位置信息。本节只先记尺寸:width 同 offsetWidth,height 同 offsetHeight。易错点是不要把它当成能改尺寸的方法,它只是读取;也不要和后面的位置属性混在一起,本节先掌握尺寸。

获取视口尺寸 ​

笔记代码:

js
// 会包括滚动条本身的宽度
window.innerWidth
window.innerHeight

// 不会包括滚动条本身的宽度
document.documentElenment.clientWidth
document.documentElenment.clientHeight

documentElenment 是拼写错误,正确是 document.documentElement。为什么需要它?因为 window.innerWidth/innerHeight 能读视口,但会把滚动条本身也算进去;document.documentElement.clientWidth/clientHeight 不算滚动条。转写稿演示:页面没滚动条时两者看起来一样,比如宽 889、高 667;后来加一个高 div 让页面出现纵向滚动条,宽度差了约 17px,这 17px 就是滚动条宽度。大多数浏览器默认滚动条宽度约 17px。

怎么想?元素一般不会跑到滚动条上面,所以做布局判断时,不算滚动条的视口宽高更贴近元素可用区域。怎么做?优先用:

js
document.documentElement.clientWidth
document.documentElement.clientHeight

易错点有三个:第一,document.documentElement 不要拼错;第二,根元素用 clientWidth/clientHeight 读视口属于特殊用法,别拿普通元素的规则硬套;第三,视口尺寸是只读的,代码不能随便改用户屏幕大小,转写稿也强调“不能太离谱”,不要想着用代码改视口。

调试与总结 ​

调试时先分清对象:读普通元素尺寸用元素对象;读视口尺寸用 window 或 document.documentElement。再分清口径:总宽高看 offset,内容加内边距看 client,内容溢出看 scroll。最后记住本节主题是“只读”:这些属性用来读,不用来改。真正要改元素尺寸,还是回到 CSS 或行内样式;而视口尺寸不是页面代码能随意修改的东西。

为什么需要读取元素位置 ​

做页面交互时,经常要知道“这个元素现在在哪儿”。比如做定位提示、拖拽、滚动加载、判断元素是否进入视口,都离不开位置信息。靠肉眼估、靠 CSS 里写的值猜都不行:元素可能被父级内边距、边框、外边距影响,页面还可能滚动。因此需要一组只读属性/方法,直接从浏览器布局结果里读位置。本节要分清三套东西:offsetLeft / offsetTop、clientLeft / clientTop、getBoundingClientRect()。它们名字都像“位置”,但参照物完全不同。

offsetLeft / offsetTop:相对第一个定位祖先或页面 ​

为什么需要:想知道元素在文档布局中的横向、纵向偏移,尤其是相对某个定位父容器时,offsetLeft / offsetTop 很直接。怎么想:它的规则和定位的“包含块”很像——找第一个定位的祖先元素,以它的左上角为原点;如果祖先都没有定位,就参照页面。怎么做:

html
<div id="book">内容</div>
<script>
  const box = document.querySelector('#book');
  console.log(box.offsetLeft, box.offsetTop);
</script>

转写稿里的调试很关键:一开始父元素没有定位,读取到类似 30 和 188,这是一直算到页面边界的结果;给父元素加上 position: relative 后,值变成 20 和 178,说明参照物变成了这个定位父元素,而且是从父元素的内边距区域开始算,不是从边框开始。易错点:offsetLeft 不是相对视口;祖先没定位时参照页面,不是参照窗口;页面滚动后它通常不因滚动而改变,因为元素和页面是一起滚的。

clientLeft / clientTop:读的是边框宽度 ​

为什么需要:有时计算内部可用区域,需要知道元素左边框、上边框有多宽。怎么想:别被名字骗了,clientLeft / clientTop 不是元素位置。怎么做:

css
#book {
  border-left: 10px dashed #999;
  border-top: 2px solid #999;
}
js
console.log(box.clientLeft); // 左边框宽度,示例中为 10
console.log(box.clientTop);  // 上边框宽度,示例中为 2

转写稿演示:没有边框时读到 0;给左边框设 10px 后 clientLeft 变成 10。易错点:它不表示“离左边多远”;无边框就是 0;右边框、下边框不在这两个属性里。

getBoundingClientRect:相对视口的位置 ​

为什么需要:判断元素是否出现在用户眼前、做滚动加载时,必须知道元素相对“视口”的位置。怎么想:这个方法返回一个对象,里面既有尺寸也有位置;本节关注 left、top、x、y、right、bottom。left/top 是元素左上角相对视口左上角的坐标,x 同 left,y 同 top;right 是元素右边的 x 坐标,bottom 是元素底部的 y 坐标。怎么做:

js
const rect = box.getBoundingClientRect();
console.log(rect.left, rect.top);
console.log(rect.x, rect.y);
console.log(rect.right, rect.bottom);

转写稿用画图演示:黑色当视口,红色/灰色当长页面。页面滚动后,元素在页面上的位置不变,但它在视口里的 top 会变小,甚至变成 0 或负数。易错点:right 不是“距离视口右边多远”,bottom 不是“距离视口底部多远”,它们仍是元素右边、底部的坐标;参照物永远是视口左上角,和祖先定不定位无关。

调试对比与易错点汇总 ​

把父元素定位去掉,页面不滚动时,offsetLeft/offsetTop 和 getBoundingClientRect().left/top 可能看起来一样;一旦页面滚动,二者就分开了:前者仍按页面/定位祖先算,后者按视口算。易错点再强调:offsetLeft/Top 找第一个定位祖先,没有就参照页面;clientLeft/Top 是边框宽度;getBoundingClientRect() 是相对视口,且 right/bottom 是坐标不是距离。读位置属性是只读操作,别试图用它们直接改布局。

元素内容滚动的前提:先让元素“能滚” ​

为什么需要:页面整体能在视口里滚动,元素内部的内容也可能比元素本身高或宽,这时就要读取“内容在元素里滚了多远”,或者用代码控制它滚到某个位置。
怎么想:元素内部内容溢出后,默认 overflow 是 visible,内容会直接溢出显示,这时不能形成“内容在元素内部滚动”的场景。转写稿中先给外层元素设宽高、内边距、边框,再给 overflow: auto,才出现滚动条;换成 hidden 也能滚,只是看不到滚动条。
怎么做:给元素设置固定宽高,并设置 overflow 为 auto、hidden 等非 visible 的值。
易错点:overflow: visible 是默认值,内容溢出就露出来,不能滚;hidden 虽然看不见滚动条,但内容仍可被程序滚动。

读取元素内容滚动:scrollTop / scrollLeft ​

为什么需要:要知道内容当前滚到哪,才能做按钮控制、滚动监听、图片延迟加载、无缝滚动等效果。
怎么想:转写稿中用画图方式推导:元素像一个小视口,内容比它高;滚动条往下拖,内容往上走,scrollTop 从 0 开始变大;滚动条往右拖,内容往左走,scrollLeft 从 0 开始变大。两者最小值都是 0,到边界后浏览器会限制,不会无限增大。
怎么做:直接读取元素对象的这两个属性:

scrollLeft		内容在元素中向左滚动的距离
scrollTop		内容在元素中向上滚动的距离

注意: 需要设置元素 overflow 的值不是 visible.

调试时可在元素的 scroll 事件里输出 box.scrollTop、box.scrollLeft,拖动滚动条观察数值变化。
易错点:方向别记反:scrollTop 变大表示内容向上滚;scrollLeft 变大表示内容向左滚。滚动条位置和内容移动方向是相反的。

写入元素内容滚动的位置:可读也可写 ​

为什么需要:除了用户拖滚动条,程序也要能控制内容滚动,例如点按钮让内容左移、右移、上移、下移。
怎么想:既然 scrollTop、scrollLeft 可读可写,就可以在事件里修改它们。转写稿中用按钮演示:点左箭头让 box.scrollLeft += 20,点右箭头用 -=;上下同理改 scrollTop。
怎么做:先获取可滚动元素,再在按钮点击事件中修改:

js
box.scrollLeft += 20;
box.scrollTop -= 20;

易错点:到边界后继续加或减不会生效,浏览器会自动限制;代码控制滚动也会触发 scroll 事件,不要以为只有用户拖滚动条才触发。

监听元素内容滚动:scroll 事件 ​

为什么需要:内容一滚,往往要重新判断位置、加载内容或更新界面,所以需要监听滚动过程。
怎么想:转写稿中把 scroll 事件监听给外层可滚动元素,而不是只监听 window;元素内容滚动时事件会不停触发。
怎么做:给元素绑定 onscroll 或 addEventListener('scroll', ...),在回调里读取 scrollTop、scrollLeft。
易错点:监听对象要选对:元素内部滚动监听元素;整个页面滚动监听 window。不要把两者混在一起。

读写整个页面在视口中滚动的位置 ​

为什么需要:整个页面也会在视口中滚动,需要知道页面滚了多少,或用代码控制页面滚动。
怎么想:转写稿强调 window 不是 DOM 元素,没有 scrollTop;页面在视口中滚动,要用根元素 document.documentElement 来读写。这是特殊用法,记住即可。
怎么做:读取和写入页面滚动位置:

js
document.documentElement.scrollLeft
document.documentElement.scrollTop

页面滚动监听用 window.onscroll,按钮控制可用 document.documentElement.scrollTop += 20。
易错点:不要写 window.scrollTop;页面滚动时,元素在“页面上的位置”不变,但它在“视口上的位置”会变,这是转写稿中用画图反复推导的区别。

调试过程与易错点汇总 ​

转写稿的调试顺序是:先让元素出现滚动条,再监听 scroll 输出 scrollTop/scrollLeft,观察拖动时从 0 增大;然后改用按钮写入属性,验证代码控制也能滚,并且到边界自动停;最后把同样思路映射到页面,用 document.documentElement.scrollTop 输出页面滚动距离。无缝滚动案例的思路也是让 scrollLeft 不停变大,但滚到头会停,需要自己思考如何接上,转写稿把它留作练习。核心易错点:overflow: visible 不能滚;scrollTop/scrollLeft 最小为 0;方向别反;元素滚动和页面滚动监听对象不同;页面滚动用根元素是特殊用法。

先说明:标题与转写稿不一致 ​

本节笔记标题写的是“元素节点的添加/删除/替换/克隆”,但转写稿实际讲的是元素尺寸、元素位置、图片延迟加载、元素/页面滚动距离与无缝滚动,并未出现节点增删替换克隆的 API。为避免编造,下面按转写稿真实内容整理;转写稿中没有图片,因此不插入图片。

读取元素尺寸:计算样式与 client 系列 ​

为什么需要:做布局、拖拽、懒加载时,经常要先知道元素或视口有多大。
怎么想:getComputedStyle 能读计算样式,但读到的宽高通常只是内容宽高;要总宽高还得加内边距、边框等。视口尺寸可用 window.innerWidth/innerHeight,也可用根元素 document.documentElement.clientWidth/clientHeight。
怎么做:

js
const box = document.querySelector('#box');
const cs = getComputedStyle(box);
console.log(cs.width, cs.height); // 常见为内容宽高,height 可能是 auto 计算值

console.log(window.innerWidth, window.innerHeight);
console.log(
  document.documentElement.clientWidth,
  document.documentElement.clientHeight
);

转写稿演示:页面出现纵向滚动条后,两种视口宽度差约 17px,因为 window.innerWidth 包含滚动条本身,而 documentElement.clientWidth 不包含。
易错点:不要把计算样式宽高直接当总宽高;documentElement.clientWidth/clientHeight 获取视口属于特殊用法,记住即可;修改尺寸仍应写行内样式或改样式,不能靠读尺寸属性反向修改。

读取元素位置:offset、clientLeft/Top 与 getBoundingClientRect ​

为什么需要:定位、吸顶、懒加载都要判断“元素在谁坐标系里的哪里”。
怎么想:offsetLeft/offsetTop 参照第一个定位祖先元素;若没有定位祖先,则参照页面。clientLeft/clientTop 不是位置,而是左边框、上边框宽度。getBoundingClientRect() 的 left/top 是元素相对视口的坐标;right/bottom 是元素右边、底边的视口坐标,不是“距离视口右边/底边多少”。
怎么做:

js
const p = document.querySelector('#book');
console.log(p.offsetLeft, p.offsetTop);
console.log(p.clientLeft, p.clientTop); // 边框宽度
const rect = p.getBoundingClientRect();
console.log(rect.left, rect.top, rect.right, rect.bottom);
console.log(rect.x, rect.y); // x 等同 left,y 等同 top

易错点:给父元素加 position: relative 后,offsetLeft/offsetTop 会改为相对该父元素;页面滚动后,getBoundingClientRect().top 会变化,而元素在页面文档中的位置不变。

图片延迟加载:用滚动事件判断进入视口 ​

为什么需要:页面图片很多时,一次性下载全部图片会占用带宽、拖慢加载;用户没看到的图片可能根本不需要下载。
怎么想:图片先不写 src,把真实地址放在 data-src;监听页面滚动,判断图片是否进入视口。判断条件可用 getBoundingClientRect().top < 视口高度。
怎么做:

html
<img class="lazy" data-src="./img/01.jpg" alt="示例图一">
<img class="lazy" data-src="./img/02.jpg" alt="示例图二">
js
const imgs = document.querySelectorAll('.lazy');

function loadImages() {
  const viewH = document.documentElement.clientHeight;
  imgs.forEach(img => {
    if (img.getBoundingClientRect().top < viewH && !img.src) {
      img.src = img.dataset.src;
    }
  });
}

loadImages(); // 首屏先加载一次
window.onscroll = loadImages;

易错点:只在 scroll 里执行会漏掉首屏,所以要先调用一次;已加载图片不应反复处理,转写稿把它留作优化作业:加载过的不要再遍历或不要再设 src。

读写元素与页面的滚动距离 ​

为什么需要:自定义滚动、返回顶部、轮播无缝滚动都要读或改滚动距离。
怎么想:元素内容滚动看 scrollTop/scrollLeft,它们可读可写;前提是元素 overflow 不能是默认的 visible,auto、hidden 等才可滚。页面滚动则用 document.documentElement.scrollTop/scrollLeft。
怎么做:

js
const box = document.querySelector('#box');
box.onscroll = function () {
  console.log(box.scrollTop, box.scrollLeft);
};

box.scrollTop += 20;      // 内容向上滚
box.scrollLeft += 20;     // 内容向左滚

window.onscroll = function () {
  console.log(document.documentElement.scrollTop);
};
document.documentElement.scrollTop += 20; // 页面往下滚

易错点:scrollTop/scrollLeft 最小为 0,到边界后继续加也不生效,浏览器会自动限制;元素滚动必须设置非 visible 的 overflow。

无缝滚动的起点与作业 ​

为什么需要:横向图片列表滚到头会停,视觉上不连续。
怎么想:定时器不断增大容器的 scrollLeft,滚到末尾前做处理,让用户感觉“又接上了”。
怎么做:

js
const box = document.querySelector('#box');
setInterval(() => {
  box.scrollLeft += 1;
}, 10);

转写稿只写到“到头会停”,并把“如何实现真正无缝”留作作业。合理方向是:判断是否到达临界值,再调整 scrollLeft 或复制内容;但具体实现转写稿未给完整代码,不能虚构为标准答案。
易错点:只加 scrollLeft 一定会停;不要死记代码形式,要分析“什么时候到达边界、什么时候重置或换内容”。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

常见错误 ​

  1. 把 getComputedStyle().width 当成元素总宽

    • 现象:读到的宽度只有 600,和实际占位对不上,算布局时总差边框、内边距。
    • 原因:计算样式里的 width 只是内容宽,不是盒子的总宽;offsetWidth 才是总宽。
    • 修法:要总宽高用 offsetWidth/offsetHeight;要内容+内边距用 clientWidth/clientHeight;要内容实际宽高用 scrollWidth/scrollHeight。先明确“要哪个尺寸”,再选 API。
  2. 用 offsetLeft/offsetTop 当视口坐标

    • 现象:给父级加上 position: relative 后,同一个元素的 offsetLeft/offsetTop 突然变了;页面滚动后它又不变。
    • 原因:offsetLeft/offsetTop 参照的是第一个定位祖先,没有定位祖先才参照页面,不是视口。
    • 修法:要视口坐标用 getBoundingClientRect().left/top;要页面坐标用 offsetLeft/offsetTop,或用 rect.left + 页面滚动距离 换算。
  3. 把 rect.right / rect.bottom 当成“距右 / 距底的距离”

    • 现象:页面一滚动,bottom 变小,和“距离底部”的直觉相反。
    • 原因:right/bottom 是元素右边、底边在视口中的坐标,不是到视口右/底边的距离。
    • 修法:距右 = innerWidth - rect.right,距底 = innerHeight - rect.bottom;坐标和距离要分清。
  4. 滚动事件绑错对象,读到的 scrollTop 一直是 0

    • 现象:元素内部滚动时监听 window,或页面滚动时读元素 scrollTop,值始终为 0。
    • 原因:滚动源和读取对象不匹配。
    • 修法:元素内滚动绑元素、读 element.scrollTop/scrollLeft;整个页面滚动绑 window,读 document.documentElement.scrollTop/scrollLeft。
  5. 给 scrollTop 赋值不生效

    • 现象:代码里写了 box.scrollTop = 100,页面没有任何滚动。
    • 原因:元素 overflow 仍是默认的 visible,没有形成可滚动容器。
    • 修法:给元素设置 overflow: auto / hidden / scroll(只要不是 visible);同时注意 scrollTop 有边界,浏览器会自动限制,不会滚出内容区。
  6. 图片延迟加载首屏空白,滚动一下才出现

    • 现象:刷新页面后第一屏图片不加载,必须滚动才显示。
    • 原因:加载判断只写在 scroll 事件里,初始状态没有触发滚动事件。
    • 修法:把“检测并加载”封装成函数,先主动执行一次处理首屏,再绑定 scroll。
  7. 延迟加载每次滚动都全量遍历图片

    • 现象:图片多时滚动卡顿,已经加载过的图片仍被反复判断。
    • 原因:没有区分“已加载”和“未加载”,每次 scroll 都遍历整个集合。
    • 修法:只遍历未加载的图片,加载后加标记或从待处理集合中移除;滚动处理里避免无意义重复计算。
  8. 无缝滚动只靠 scrollLeft 一直加

    • 现象:滚到最右边就停住,再怎么加也不动。
    • 原因:scrollLeft 有最大值,浏览器会自动限制。
    • 修法:在到达边界前做重置、复制内容或改用位移方案,核心是处理“边界”而不是无限递增。

练习 ​

  1. 准备一个带边框、内边距、固定宽高且内容溢出的元素,分别读取 offsetWidth/offsetHeight、clientWidth/clientHeight、scrollWidth/scrollHeight,写出三组值的差值来源;再修改 overflow 观察哪些值发生变化。

  2. 页面上放一个普通元素,分别记录页面未滚动和滚动后的 offsetTop 与 getBoundingClientRect().top,解释两者变化差异;再给其父级加 position: relative,观察 offsetTop 的变化并说明原因。

  3. 实现一个元素内滚动控制:两个按钮分别让内容向上、向下滚动固定距离,要求滚到顶部/底部时不报错、不越界;用 scrollTop 完成读写,并说明为什么赋值到边界后继续加没有效果。

  4. 实现图片延迟加载:首屏图片立即加载,页面滚动时只加载进入视口的图片;要求已加载图片不被重复处理,并说明你如何标记或过滤已加载项。

  5. 实现横向无缝滚动:图片排成一行,用定时器让 scrollLeft 增加,到达右边界后仍要“看起来无限滚动”。写出你的边界处理方案,并说明为什么不能只让 scrollLeft 无限递增。

原视频对照 ​

#视频时长
1读取元素尺寸0210:42
2读取元素位置23:07
3图片延迟加载案例24:44
4读写元素中内容滚动的距离18:18
5读写页面滚动的距离14:21
6无缝滚动案例07:38

小结 ​

  1. 尺寸读取先分清三套值:offset 是总盒,client 是内容+内边距,scroll 是内容实际占位;选错 API 比算错公式更常见。

  2. 位置有两套坐标系:offsetLeft/offsetTop 是定位祖先或页面坐标,getBoundingClientRect() 是视口坐标;页面滚动只影响后者。

  3. right/bottom 是坐标,不是距离;需要“距右/距底”时必须自己用视口尺寸换算。

  4. 滚动读写要成对:元素内滚动绑元素读元素,页面滚动绑 window 读 documentElement;scrollTop/scrollLeft 可读可写,但有边界。

  5. 元素可滚动的前提是 overflow 不是 visible;页面滚动不要依赖 body.scrollTop。

  6. 延迟加载的核心是“进入视口再赋 src”,首屏要主动执行一次检测,滚动过程中要避免全量重复遍历。

  7. 无缝滚动的难点不在“让值变大”,而在边界处理;浏览器会自动限制 scrollLeft/scrollTop,不能假设可以无限递增。

  8. 尺寸、位置、视口尺寸基本都是只读的;代码不能修改视口大小,能读能写的只有滚动位置这类状态。