2022年7月27日

元素大小和滚动

JavaScript 中有许多属性可让我们读取有关元素宽度、高度和其他几何特征的信息。

我们在 JavaScript 中移动或定位元素时,我们会经常需要它们。

示例元素

作为演示属性的示例元素,我们将使用下面给出的元素:

<div id="example">
  ...Text...
</div>
<style>
  #example {
    width: 300px;
    height: 200px;
    border: 25px solid #E8C48F;
    padding: 20px;
    overflow: auto;
  }
</style>

它有边框(border),内边距(padding)和滚动(scrolling)等全套功能。但没有外边距(margin),因为它们不是元素本身的一部分,并且它们没什么特殊的属性。

这个元素看起来就像这样:

你可以 在 sandbox 中打开这个文档

注意滚动条

上图演示了元素具有滚动条这种最复杂的情况。一些浏览器(并非全部)通过从内容(上面标记为 “content width”)中获取空间来为滚动条保留空间。

因此,如果没有滚动条,内容宽度将是 300 px,但是如果滚动条宽度是 16px(不同的设备和浏览器,滚动条的宽度可能有所不同),那么还剩下 300 - 16 = 284px,我们应该考虑到这一点。这就是为什么本章的例子总是假设有滚动条。如果没有滚动条,一些计算会更简单。

文本可能会溢出到 padding-bottom

在我们的插图中的 padding 中通常显示为空,但是如果元素中有很多文本,并且溢出了,那么浏览器会在 padding-bottom 处显示“溢出”文本,这是正常现象。

几何

这是带有几何属性的整体图片:

这些属性的值在技术上讲是数字,但这些数字其实是“像素(pixel)”,因此它们是像素测量值。

让我们从元素外部开始探索属性。

offsetParent,offsetLeft/Top

这些属性很少使用,但它们仍然是“最外面”的几何属性,所以我们将从它们开始。

offsetParent 是最接近的祖先(ancestor),在浏览器渲染期间,它被用于计算坐标。

最近的祖先为下列之一:

  1. CSS 定位的(positionabsoluterelativefixed),
  2. <td><th><table>
  3. <body>

属性 offsetLeft/offsetTop 提供相对于 offsetParent 左上角的 x/y 坐标。

在下面这个例子中,内部的 <div><main> 作为 offsetParent,并且 offsetLeft/offsetTop 让它从左上角位移(180):

<main style="position: relative" id="main">
  <article>
    <div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
  </article>
</main>
<script>
  alert(example.offsetParent.id); // main
  alert(example.offsetLeft); // 180(注意:这是一个数字,不是字符串 "180px")
  alert(example.offsetTop); // 180
</script>

有以下几种情况下,offsetParent 的值为 null

  1. 对于未显示的元素(display:none 或者不在文档中)。
  2. 对于 <body><html>
  3. 对于带有 position:fixed 的元素。

offsetWidth/Height

现在,让我们继续关注元素本身。

这两个属性是最简单的。它们提供了元素的“外部” width/height。或者,换句话说,它的完整大小(包括边框)。

对于我们的示例元素:

  • offsetWidth = 390 —— 外部宽度(width),可以计算为内部 CSS-width(300px)加上 padding(2 * 20px)和 border(2 * 25px)。
  • offsetHeight = 290 —— 外部高度(height)。
对于未显示的元素,几何属性为 0/null

仅针对显示的元素计算几何属性。

如果一个元素(或其任何祖先)具有 display:none 或不在文档中,则所有几何属性均为零(或 offsetParentnull)。

例如,当我们创建了一个元素,但尚未将其插入文档中,或者它(或它的祖先)具有 display:none 时,offsetParentnull,并且 offsetWidthoffsetHeight0

我们可以用它来检查一个元素是否被隐藏,像这样:

function isHidden(elem) {
  return !elem.offsetWidth && !elem.offsetHeight;
}

请注意,对于会展示在屏幕上,但大小为零的元素,它们的 isHidden 返回 true

clientTop/Left

在元素内部,我们有边框(border)。

为了测量它们,可以使用 clientTopclientLeft

在我们的例子中:

  • clientLeft = 25 —— 左边框宽度
  • clientTop = 25 —— 上边框宽度

……但准确地说 —— 这些属性不是边框的 width/height,而是内侧与外侧的相对坐标。

有什么区别?

当文档从右到左显示(操作系统为阿拉伯语或希伯来语)时,影响就显现出来了。此时滚动条不在右边,而是在左边,此时 clientLeft 则包含了滚动条的宽度。

在这种情况下,clientLeft 的值将不是 25,而是加上滚动条的宽度 25 + 16 = 41

这是希伯来语的例子:

clientWidth/Height

这些属性提供了元素边框内区域的大小。

它们包括了 “content width” 和 “padding”,但不包括滚动条宽度(scrollbar):

在上图中,我们首先考虑 clientHeight

这里没有水平滚动条,所以它恰好是 border 内的总和:CSS-height 200px 加上顶部和底部的 padding(2 * 20px),总计 240px

现在 clientWidth —— 这里的 “content width” 不是 300px,而是 284px,因为被滚动条占用了 16px。所以加起来就是 284px 加上左侧和右侧的 padding,总计 324px

如果这里没有 padding,那么 clientWidth/Height 代表的就是内容区域,即 border 和 scrollbar(如果有)内的区域。

因此,当没有 padding 时,我们可以使用 clientWidth/clientHeight 来获取内容区域的大小。

scrollWidth/Height

这些属性就像 clientWidth/clientHeight,但它们还包括滚动出(隐藏)的部分:

在上图中:

  • scrollHeight = 723 —— 是内容区域的完整内部高度,包括滚动出的部分。
  • scrollWidth = 324 —— 是完整的内部宽度,这里我们没有水平滚动,因此它等于 clientWidth

我们可以使用这些属性将元素展开(expand)到整个 width/height。

像这样:

// 将元素展开(expand)到完整的内容高度
element.style.height = `${element.scrollHeight}px`;

点击按钮展开元素:

text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text text

scrollLeft/scrollTop

属性 scrollLeft/scrollTop 是元素的隐藏、滚动部分的 width/height。

在下图中,我们可以看到带有垂直滚动块的 scrollHeightscrollTop

换句话说,scrollTop 就是“已经滚动了多少”。

scrollLeft/scrollTop 是可修改的

大多数几何属性是只读的,但是 scrollLeft/scrollTop 是可修改的,并且浏览器会滚动该元素。

如果你点击下面的元素,则会执行代码 elem.scrollTop += 10。这使得元素内容向下滚动 10px

Click
Me
1
2
3
4
5
6
7
8
9

scrollTop 设置为 0 或一个大的值,例如 1e9,将会使元素滚动到顶部/底部。

不要从 CSS 中获取 width/height

我们刚刚介绍了 DOM 元素的几何属性,它们可用于获得宽度、高度和计算距离。

但是,正如我们在 样式和类 一章所知道的那样,我们可以使用 getComputedStyle 来读取 CSS-width 和 height。

那为什么不像这样用 getComputedStyle 读取元素的 width 呢?

let elem = document.body;

alert( getComputedStyle(elem).width ); // 显示 elem 的 CSS width

为什么我们应该使用几何属性呢?这里有两个原因:

  1. 首先,CSS width/height 取决于另一个属性:box-sizing,它定义了“什么是” CSS 宽度和高度。出于 CSS 的目的而对 box-sizing 进行的更改可能会破坏此类 JavaScript 操作。

  2. 其次,CSS 的 width/height 可能是 auto,例如内联(inline)元素:

    <span id="elem">Hello!</span>
    
    <script>
      alert( getComputedStyle(elem).width ); // auto
    </script>

    从 CSS 的观点来看,width:auto 是完全正常的,但在 JavaScript 中,我们需要一个确切的 px 大小,以便我们在计算中使用它。因此,这里的 CSS 宽度没什么用。

还有另一个原因:滚动条。有时,在没有滚动条的情况下代码工作正常,当出现滚动条时,代码就出现了 bug,因为在某些浏览器中,滚动条会占用内容的空间。因此,可用于内容的实际宽度小于 CSS 宽度。而 clientWidth/clientHeight 则会考虑到这一点。

……但是,使用 getComputedStyle(elem).width 时,情况就不同了。某些浏览器(例如 Chrome)返回的是实际内部宽度减去滚动条宽度,而某些浏览器(例如 Firefox)返回的是 CSS 宽度(忽略了滚动条)。这种跨浏览器的差异是不使用 getComputedStyle 而依靠几何属性的原因。

如果你的浏览器保留了滚动条的空间(大多数 Windows 中的浏览器),那么你可以在下面测试它。

带有文本的元素具有 width:300px

在桌面 Windows 操作系统上,Firefox、Chrome、Edge 都为滚动条保留了空间。但 Firefox 显示的是 300px,而 Chrome 和 Edge 显示较少。这是因为 Firefox 返回 CSS 宽度,其他浏览器返回“真实”宽度。

请注意,所描述的差异只是关于从 JavaScript 读取的 getComputedStyle(...).width,而视觉上看,一切都是正确的。

总结

元素具有以下几何属性:

  • offsetParent —— 是最接近的 CSS 定位的祖先,或者是 tdthtablebody
  • offsetLeft/offsetTop —— 是相对于 offsetParent 的左上角边缘的坐标。
  • offsetWidth/offsetHeight —— 元素的“外部” width/height,边框(border)尺寸计算在内。
  • clientLeft/clientTop —— 从元素左上角外角到左上角内角的距离。对于从左到右显示内容的操作系统来说,它们始终是左侧/顶部 border 的宽度。而对于从右到左显示内容的操作系统来说,垂直滚动条在左边,所以 clientLeft 也包括滚动条的宽度。
  • clientWidth/clientHeight —— 内容的 width/height,包括 padding,但不包括滚动条(scrollbar)。
  • scrollWidth/scrollHeight —— 内容的 width/height,就像 clientWidth/clientHeight 一样,但还包括元素的滚动出的不可见的部分。
  • scrollLeft/scrollTop —— 从元素的左上角开始,滚动出元素的上半部分的 width/height。

除了 scrollLeft/scrollTop 外,所有属性都是只读的。如果我们修改 scrollLeft/scrollTop,浏览器会滚动对应的元素。

任务

重要程度: 5

elem.scrollTop 属性是从顶部滚动出来的部分的大小。如何获得底部滚动的大小(我们称其为 scrollBottom)?

编写适用于任意 elem 的代码。

P.S. 请检查你的代码:如果没有滚动,或元素底部已经完全滚动完成,那么它应该返回 0

解决方案:

let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight;

换句话说:(完全高度)减去(已滚出顶部的高度)减去(可见部分的高度)—— 得到的结果就是滚动出来的底部的部分。

重要程度: 3

编写代码,返回标准滚动条宽度。

对于 Windows,它通常在 12px20px 之间变化。如果浏览器没有为其保留任何空间(滚动条以半透明的形式处于文本上面,也是可能发生的),那么它可能是 0px

P.S. 该代码应适用于任何 HTML 文档,而不依赖于其内容。

为了获得滚动条的宽度,我们可以创建一个带有滚动条的元素,但是没有边框(border)和内边距(padding)。

然后,它的全宽度 offsetWidth 和内部内容宽度 clientWidth 之间的差值就是滚动条的宽度:

// 创建一个包含滚动条的 div
let div = document.createElement('div');

div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';

// 必须将其放入文档(document)中,否则其大小将为 0
document.body.append(div);
let scrollWidth = div.offsetWidth - div.clientWidth;

div.remove();

alert(scrollWidth);
重要程度: 5

源文件的效果如下:

区域(field)的中心坐标是多少?

计算它们,并将小球置于绿色的区域(field)中心:

  • 该元素应该通过 JavaScript 移动,而不是 CSS。
  • 该代码应该适用于任何大小的球(102030 像素)以及任意大小的区域(field),而不应该绑定到给定值。

P.S. 当然了,置于中心的操作通过 CSS 也可以完成,但是这里我们需要通过 JavaScript 完成。此外,当必须使用 JavaScript 时,我们可能会遇到其他话题以及更加复杂的情况,这里我们只是做一个“热身”。

打开一个任务沙箱。

球具有 position:absolute。这意味着它的 left/top 坐标是从最近的具有定位属性的元素开始测量的,这个元素即 #field(因为它有 position:relative)。

坐标从场(field)的左上角内侧开始:

内部的场(field)的 width/height 是 clientWidth/clientHeight。所以场(field)的中心坐标为 (clientWidth/2, clientHeight/2)

……但是,如果我们将 ball.style.left/top 设置为这种值,那么在中心的会是球的左上边缘,而不是整个球:

ball.style.left = Math.round(field.clientWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2) + 'px';

这是它将显示出来的效果:

为了使球的中心与场(field)的中心重合,我们应该把球向左移动球宽度的一半,并向上移动球高度的一半:

ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px';
ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px';

现在,球终于居中了。

注意:陷阱!

<img> 没有 width/height 时,代码将无法可靠地工作:

<img src="ball.png" id="ball">

当浏览器不知道图片的 width/height(通过标签 attribute 或 CSS)时,它会假定它们等于 0,直到图片加载完成。

因此,在图片加载完成之前,ball.offsetWidth 的值为 0。这会导致上面的代码中会有错误的坐标。

在第一次加载完成后,浏览器通常会缓存该图片,并在下一次加载时,浏览器会立即拥有该图片的大小。但是在第一次加载时,ball.offsetWidth 的值为 0

我们应该通过在 <img> 中添加 width/height 来解决这个问题:

<img src="ball.png" width="40" height="40" id="ball">

……或者在 CSS 中提供大小:

#ball {
  width: 40px;
  height: 40px;
}

使用沙箱打开解决方案。

重要程度: 5

getComputedStyle(elem).widthelem.clientWidth 之间有什么不同点?

指出至少三种不同点。当然越多越好。

不同点:

  1. clientWidth 值是数值,而 getComputedStyle(elem).width 返回一个以 px 作为后缀的字符串。
  2. getComputedStyle 可能会返回非数值的 width,例如内联(inline)元素的 "auto"
  3. clientWidth 是元素的内部内容区域加上 padding,而 CSS width(具有标准的 box-sizing)是内部内容区域,不包括 padding
  4. 如果有滚动条,并且浏览器为其保留了空间,那么某些浏览器会从 CSS width 中减去该空间(因为它不再可用于内容),而有些则不会这样做。clientWidth 属性总是相同的:如果为滚动条保留了空间,那么将减去滚动条的大小。
教程路线图