JavaScript 为我们提供了许多几何属性,可以用来读取元素宽、高等信息,这些信息通常用来移动和定位元素。

一个简单的例子

举一个简单的例子:

  1. <div id="example">
  2. ...Text...
  3. </div>
  4. <style>
  5. #example {
  6. width: 300px;
  7. height: 200px;
  8. border: 25px solid #e8c48f;
  9. overflow: auto;
  10. }
  11. </style>

元素 #example 包含边框、padding,而且还能滚动。没有包含 margin 原因,是因为它不属于元素的一部分。

元素的呈现效果如下:
metric-css.svg

可以在 sanbox 中查看演示

注意:滚动条!

为了覆盖全面、复杂的情况,演示元素包含滚动条。虽然设置了 width(content width) 为 300px,但因为存在滚动条,占据了一部分的空间(假设滚动条 16px 宽),因此 content width 最终的渲染尺寸变为 300px - 16px,也就是 284px 了。
再提醒一遍,滚动条占据的是内容区(content area,包括 content width 和 content height)的空间,与 > padding 无关,因此上面图示里显示的滚动条虽然靠近 padding 显示,但实际占据的是内容区(即 content width)的空间!

几何属性

下图列出了一个元素所具备的所有几何属性。

metric-all.svg

需要注意的是,这个属性的属性值都是以像素(px)为单位计算的。

下面,我们从最外面的属性开始讲起。

offsetParnt,offsetLeft/Top

这些属性很少用,但因为是“最外部”属性,所以从它开始讲。

offsetParent 用来获取最近的祖先元素,浏览器依据这个元素计算坐标,进行页面渲染。

这些祖先元素是符合下列条件之一的元素:

  1. 定位元素(position 属性值为 absoluterelativefixedsticky

  2. <td><th><table>,还有

  3. <body>

offsetLeft/offsetTop 表示当前元素相对 offsetParent 左上角的偏移量,分别表示水平偏移和垂直偏移。

下例中,<div>offsetParent<main>offsetLeft/offsetTop 表示距离 <main> 左上角的偏移量:

译者注:这里说的“距离左上角”是指距离祖先元素的 border 内边缘,或者说 padding 外边缘的距离。

  1. <main style="position: relative" id="main">
  2. <article>
  3. <div id="example" style="position: absolute; left: 180px; top: 180px;">
  4. ...
  5. </div>
  6. </article>
  7. </main>
  8. <script>
  9. alert(example.offsetParent.id); // main
  10. alert(example.offsetLeft); // 180 (注意:是数值,不是字符串 "180px")
  11. alert(example.offsetTop); // 180
  12. </script>

metric-offset-parent.svg

下面三种情况,元素的 offsetParent 的值为 null

  1. 隐藏元素(元素 display: none 了或者没在文档里)

  2. <body><html>

  3. position: fixed 元素

offsetWidth/Height

下面将目光转向元素本身。

这两个属性获取的是元素的“外部”尺寸,就是说,它们的值是包括元素边框的。

metric-offset-width-height.svg

上图元素中:

  • offsetWidth = 390:也就是内部宽度(300px)加两边 padding(2 * 20px) 和两边的 border(2 * 25px)得到的结果

  • offsetHeight = 290:外部高度(同理可得)

注意:隐藏元素返回的几何属性值为 0 或者 null

几何属性只针对显示元素的尺寸计算的。

如果元素(或者其任意祖先元素)display: none 了,或者不在文档中,那么得到的几何属性值就为 0(例外是 offsetParent 属性返回的是 null)。

举个例子,我们手动创建了一个元素,但没有插入到文档中,那么这个元素的 offsetParent 值就为 nulloffsetWidthoffsetHeight 就为 0

利用这个特性,我们可以写出一个简陋版的检查元素是否隐藏的工具函数:

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


当然对于文档里无内容的空元素(比如一个空的 <div>)该函数也返回 true,所以说是“简陋”的。

clientTop/Left

再往里面就是边框了。

测量它们可以用 clientTopclientLeft

下面例子里:

  • clientLeft = 25:左部边框宽。

  • clientTop = 25:顶部边框宽。

元素尺寸与滚动 - 图5

但准确地讲,并不是边框,而是指内侧(padding box,不包含滚动条的外部边沿)距离外侧(border-box 外部边沿)的相对位置。

那么区别是什么?

当文档是从右到左(操作系统是阿拉伯或希伯来语言)时,它变得显而易见了。 这时滚动条不在右侧,而是在左侧。发现 clientLeft 的值是包含了滚动条宽度的。

下图情况下,clientLeft 不是 25,而是加上滚动条后的 4125 + 16):

元素尺寸与滚动 - 图6

clientWidth/Height

这两个属性,用于获取元素内容区宽高。

这个内容的宽度包含 padding,但不包含滚动条。

元素尺寸与滚动 - 图7

看上面图片,我们先来计算 clientHeight 的值:其实很好算,因为没有水平滚动条,所以它的值就是边框内的元素高度:CSS height 200px 加上下 padding(2 * 20px),共 240px

现在计算 clientWidth:这里的内容区宽度其实并不是 300px,而是 284px,因为还有 16px 宽的滚动条占据着内容区一部分宽度。所以拿 284px 加上左右 padding,共 324px

如果没有 padding 的话,clientWidth/clientHeight 的值就是指内容区的宽高,总之就是边框区域内、滚动条里的那块区域。

元素尺寸与滚动 - 图8

所以,没有 padding 的话,通过 clientWidth/clientHeight 得到的是内容区域的尺寸了。

scrollTop/Height

  • clientHeight 计算的只是元素可见区域的高度。

  • scrollHeight 计算的就是元素完整的高度了(即包含元素没有露出来的那部分内容高度)。

元素尺寸与滚动 - 图9

上面图片里:

  • scrollHeight = 723px:即元素完整的内容区高度(包括没在视线之内的其他未滚出来的部分)。

  • scrollWidth = 324px:内部宽度,因为没有水平滚动条,所以其值等于 clientWidth,否则就是元素的内容区宽度。

我们可以使用这些属性,将元素的当前宽/高设置成完整的宽/高,像这样:

  1. // 将元素的当前宽/高设置成完整的宽/高
  2. element.style.height = `${element.scrollHeight}px`;

scrollLeft/scrollTop

属性 scrollLeft/scrollTop 是元素隐藏的、滚动出去部分的宽度/高度。

下图展示了,一个 block,拥有垂直滚动条的元素的 scrollHeightscrollTop 的情况:

在下面的图片中,我们可以看到具有垂直滚动条的块的 scrollHeight 和 scrollTop 值情况。

元素尺寸与滚动 - 图10

换句话说,scrollTop 是说元素“滚动了多少”。

元素尺寸与滚动 - 图11scrollLeft/scrollTop 可被修改

大多数几何属性是只读的,但是 scrollLeft/scrollTop 可被修改,会引发浏览器依据设置值滚动元素。

如果我们为元素添加了一个点击事件,在事件处理函数里写上 elem.scrollTop += 10,那么就会发生每点击一次元素,元素就向下滚动 10px 距离的情况发生。

将元素的 scrollTop 值设置为 0Infinity 会对应地将元素滚动到顶部或底部。

不要使用从 getComputedStyle 方法里获得的宽高

我们刚才介绍了 DOM 元素里的几何属性,它们常用来获取元素宽高,计算运动距离。

不过在《操作样式和类名》一章里,我们知道也可以用 getComputedStyle 方法获得元素的 CSS width 和 height。

所以为什么不能像下面这样获得元素的宽呢?

  1. let elem = document.body;
  2. alert( getComputedStyle(elem).width ); // 打印元素的 CSS 宽度

但是为什么我们还要用几何属性呢?两个原因:

  1. 首先,因为 CSS width/height 的计算,还依赖于另一个属性:box-sizing。一旦 box-sizing 的属性值改变了,接下来可能就会干扰我们的 JavaScript 代码了。

  2. 然后,得到的 CSS width/height 属性值可能是 auto,拿行内元素举例子:

  1. <span id="elem"></span>
  2. <script>
  3. alert( getComputedStyle(elem).width ); // auto
  4. </script>

在 CSS 的观点来看,width: auto 的属性值是再正常不过了,但是在 JavaScript 中,我们希望得到精确的用 px 计算出来的数值,这样的数值可以用于计算,这样看的话,CSS width 就变得没用了。

还有一个原因:滚动条。有时,在没有滚动条的情况下代码工作正常,而当滚动条出现时,代码就开始有问题了,因为滚动条在某些浏览器中,是要占据内容区尺寸的。所以我们可以使用的真实元素宽度,可能是小于 CSS width 的。

clientWidth/clientHeight 属性会将滚动条尺寸排除在外。但是 getComputedStyle(elem).width 的情况就不同了。一些浏览器(例如 Chrome)返回的是真实的内部可用宽度,可另一些浏览器(例如 FireFox)得到的是 CSS width(才不管滚动条呢)。这种跨浏览器的差异使我们不使用 getComputedStyle 方法获取元素宽高的原因。

请注意,我们描述的不同性,仅对在使用 getComputedStyle 方法读取宽、高的时候,用 getComputedStyle 方法获得的其他的属性是没问题的。

请注意,所描述的差异仅仅是用 JavaScript 读取 getComputedStyle(…).width 的值,在视觉上一切都是正确的。

总结

元素具备下列的几何属性:

  • offsetParent:获得最近的定位祖先元素或 <td><th><table><body>

  • offsetLeft/offsetTop:相对于 offsetParent 左上角、X/Y 轴上偏移量。

  • offsetWidth/offsetHeight:获得元素的外部尺寸,也就是 border box 区域。

  • clientLeft/clientTop:内部距离外部的距离。对于从左到右排版的操作系统,这两个值就等于对应方向上的边框宽度;对于从右到左排版的操作系统,clientLeft 也包含了左侧滚动条的宽度。

  • clientWidth/clientHeight:padding box 宽高,不包含滚动条。

  • scrollWidth/scrollHeight:元素的完整宽/高,包含 padding,但是不包含滚动条。

  • scrollLeft/ScrollTop:其实就是指水平/垂直滚动条的滚动距离,从左上角算起。

上面这些属性,除了 scrollLeft/scrollTop,都是是只读的,scrollLeft/scrollTop 的值可被修改。

练习题

问题

一、距离底部的距离是多少?

elem.scrollTop 表示元素顶部滚出去的距离。那么怎样获得“scrollBottom”呢——也就是说元素滚动时,距离底部的距离?

写一个对任意元素 elem 都能正常工作的代码。

PS. 请检查您的代码:如果没有滚动条或元素完全向下滚动,则它应返回 0

二、滚动条的宽度是多少?

书写代码返回标准滚动条的宽度。

对 Windows 而言,通常是在 12px20px 之间的值。如果滚动条不占据空间,就返回 0

三、将球放到球场中间

这是初始的样子:

元素尺寸与滚动 - 图12

球场中间的坐标是多少?

计算出来之后,再把球放到中间就可以了。

元素尺寸与滚动 - 图13

  • 元素是通过 JavaScript 移动的,而不是 CSS。

  • 这个代码对任意大小的球(102030 像素)和球场里都能正常工作。

PS. 当然,可以使用 CSS 完成居中,但在这里我们仅要求使用 JavaScript。此外,之后我们还会遇到其他必须使用 JavaScript 时的主题和更复杂情况。在这里,我们先做一个“热身”。

四、区别:CSS width 和 clientWidth

getComputedStyle(elem).widthelem.clientWidth 的区别有哪些?

至少列出 3 点,多的话更好。

答案

一、

解决方案是:

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

二、

为了得到滚动条宽度,我们先创建一个带有滚动条的元素,但是没有 border 和 padding。

元素的 offsetWidth 减去 clientWidth 的差值就是滚动条的宽度了。

  1. // 创建一个显示滚动条的 div
  2. let div = document.createElement('div');
  3. div.style.overflowY = 'scroll';
  4. div.style.width = '50px';
  5. div.style.height = '50px';
  6. // 放置到文档中, 否则尺寸的话就是 0
  7. document.body.append(div);
  8. let scrollWidth = div.offsetWidth - div.clientWidth;
  9. div.remove();
  10. alert(scrollWidth);

三、

球是 position: absolute 的。表示它的 left/top 坐标是相对于最近的定位祖先元素计算的,也就是 #field(因为有样式 poisition: relative)。

坐标是从球场内部左上角开始计算的:

元素尺寸与滚动 - 图14

获取内部宽/高的话,使用的是 clientWidth/clientHeight。因此球场的中心点在 (clientWidth/2, clientHeight/2) 这个位置。

但是如果直接将值赋给 ball.style.left/top 是不对的,这样的话,我们是将球的左上角定位在球场中间:

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

看起来是这样的:

元素尺寸与滚动 - 图15

因此,我们还需要将移动:水平方向向左移动球一半的宽度,垂直方向向上移动球一半的高度:

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

注意:有陷阱!

这对于没有宽/高的 <img> 是无效的:

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

当浏览器无法获取图片宽/高(从标签特性或 CSS 中),那么在图片加载完成之前,都会认为是 0

真实场景下,浏览器会在第一次加载图片后就缓存它,那么在下一次加载的时候,会立即得到尺寸。

但是第一次加载的时候,ball.offsetWidth0。这是导致计算出错误的坐标值。

我们可以通过为 <img> 添加 width/height 来修复这个 bug。

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

或者在 CSS 中指定:

  1. #ball {
  2. width: 40px;
  3. height: 40px;
  4. }

四、

区别:

  1. clientWidth 是数值,而 getComputedStyle(elem).width 返回的是一个携带 px 后缀的字符串。

  2. 对行内元素来说,getComputedStyle 的返回值可能是像 "auto" 这样的非数值结果。

  3. clientWidth 是元素内部的内容区域 + padding 区域,而 CSS width(在标准盒子模型下)是指不含 padding 的内容区区域。

  4. 如果存在滚动条,浏览器会认为滚动条也占据空间,在一些浏览器中得到的 CSS width 是减去滚动条空间之后的值(因为它不再适用于内容了),但另一些浏览器则会忽略滚动条。而 clientWidth 属性值总是一致的:如果有滚动条的话,返回的始终是减去滚动条之后的值。

(完)