JavaScript 为我们提供了许多几何属性,可以用来读取元素宽、高等信息,这些信息通常用来移动和定位元素。
一个简单的例子
举一个简单的例子:
<div id="example">
...Text...
</div>
<style>
#example {
width: 300px;
height: 200px;
border: 25px solid #e8c48f;
overflow: auto;
}
</style>
元素 #example
包含边框、padding,而且还能滚动。没有包含 margin 原因,是因为它不属于元素的一部分。
元素的呈现效果如下:
注意:滚动条!
为了覆盖全面、复杂的情况,演示元素包含滚动条。虽然设置了
width
(content width) 为300px
,但因为存在滚动条,占据了一部分的空间(假设滚动条16px
宽),因此 content width 最终的渲染尺寸变为 300px - 16px,也就是284px
了。
再提醒一遍,滚动条占据的是内容区(content area,包括 content width 和 content height)的空间,与 >padding
无关,因此上面图示里显示的滚动条虽然靠近 padding 显示,但实际占据的是内容区(即 content width)的空间!
几何属性
下图列出了一个元素所具备的所有几何属性。
需要注意的是,这个属性的属性值都是以像素(px)为单位计算的。
下面,我们从最外面的属性开始讲起。
offsetParnt,offsetLeft/Top
这些属性很少用,但因为是“最外部”属性,所以从它开始讲。
offsetParent
用来获取最近的祖先元素,浏览器依据这个元素计算坐标,进行页面渲染。
这些祖先元素是符合下列条件之一的元素:
定位元素(
position
属性值为absolute
、relative
、fixed
或sticky
)<td>
、<th>
或<table>
,还有<body>
offsetLeft
/offsetTop
表示当前元素相对 offsetParent
左上角的偏移量,分别表示水平偏移和垂直偏移。
下例中,<div>
的 offsetParent
是 <main>
,offsetLeft
/offsetTop
表示距离 <main>
左上角的偏移量:
译者注:这里说的“距离左上角”是指距离祖先元素的 border 内边缘,或者说 padding 外边缘的距离。
<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
:
隐藏元素(元素
display: none
了或者没在文档里)<body>
和<html>
position: fixed
元素
offsetWidth/Height
下面将目光转向元素本身。
这两个属性获取的是元素的“外部”尺寸,就是说,它们的值是包括元素边框的。
上图元素中:
offsetWidth = 390
:也就是内部宽度(300px
)加两边 padding(2 * 20px
) 和两边的 border(2 * 25px
)得到的结果offsetHeight = 290
:外部高度(同理可得)
注意:隐藏元素返回的几何属性值为 0 或者 null
几何属性只针对显示元素的尺寸计算的。
如果元素(或者其任意祖先元素)display: none
了,或者不在文档中,那么得到的几何属性值就为 0
(例外是 offsetParent
属性返回的是 null
)。
举个例子,我们手动创建了一个元素,但没有插入到文档中,那么这个元素的 offsetParent
值就为 null
,offsetWidth
、offsetHeight
就为 0
。
利用这个特性,我们可以写出一个简陋版的检查元素是否隐藏的工具函数:
function isHidden(elem) {
return !elem.offsetWidth && !elem.offsetHeight;
}
当然对于文档里无内容的空元素(比如一个空的<div>
)该函数也返回true
,所以说是“简陋”的。
clientTop/Left
再往里面就是边框了。
测量它们可以用 clientTop
和 clientLeft
。
下面例子里:
clientLeft = 25
:左部边框宽。clientTop = 25
:顶部边框宽。
但准确地讲,并不是边框,而是指内侧(padding box,不包含滚动条的外部边沿)距离外侧(border-box 外部边沿)的相对位置。
那么区别是什么?
当文档是从右到左(操作系统是阿拉伯或希伯来语言)时,它变得显而易见了。 这时滚动条不在右侧,而是在左侧。发现 clientLeft
的值是包含了滚动条宽度的。
下图情况下,clientLeft
不是 25
,而是加上滚动条后的 41
(25 + 16
):
clientWidth/Height
这两个属性,用于获取元素内容区宽高。
这个内容的宽度包含 padding
,但不包含滚动条。
看上面图片,我们先来计算 clientHeight
的值:其实很好算,因为没有水平滚动条,所以它的值就是边框内的元素高度:CSS height 200px
加上下 padding(2 * 20px
),共 240px
。
现在计算 clientWidth
:这里的内容区宽度其实并不是 300px
,而是 284px
,因为还有 16px
宽的滚动条占据着内容区一部分宽度。所以拿 284px
加上左右 padding,共 324px
。
如果没有 padding 的话,clientWidth/clientHeight
的值就是指内容区的宽高,总之就是边框区域内、滚动条里的那块区域。
所以,没有 padding 的话,通过 clientWidth/clientHeight
得到的是内容区域的尺寸了。
scrollTop/Height
clientHeight
计算的只是元素可见区域的高度。而
scrollHeight
计算的就是元素完整的高度了(即包含元素没有露出来的那部分内容高度)。
上面图片里:
scrollHeight = 723px
:即元素完整的内容区高度(包括没在视线之内的其他未滚出来的部分)。scrollWidth = 324px
:内部宽度,因为没有水平滚动条,所以其值等于clientWidth
,否则就是元素的内容区宽度。
我们可以使用这些属性,将元素的当前宽/高设置成完整的宽/高,像这样:
// 将元素的当前宽/高设置成完整的宽/高
element.style.height = `${element.scrollHeight}px`;
scrollLeft/scrollTop
属性 scrollLeft
/scrollTop
是元素隐藏的、滚动出去部分的宽度/高度。
下图展示了,一个 block,拥有垂直滚动条的元素的 scrollHeight
和 scrollTop
的情况:
在下面的图片中,我们可以看到具有垂直滚动条的块的 scrollHeight 和 scrollTop 值情况。
换句话说,scrollTop
是说元素“滚动了多少”。
scrollLeft
/scrollTop
可被修改大多数几何属性是只读的,但是
scrollLeft
/scrollTop
可被修改,会引发浏览器依据设置值滚动元素。如果我们为元素添加了一个点击事件,在事件处理函数里写上
elem.scrollTop += 10
,那么就会发生每点击一次元素,元素就向下滚动10px
距离的情况发生。将元素的
scrollTop
值设置为0
或Infinity
会对应地将元素滚动到顶部或底部。
不要使用从 getComputedStyle 方法里获得的宽高
我们刚才介绍了 DOM 元素里的几何属性,它们常用来获取元素宽高,计算运动距离。
不过在《操作样式和类名》一章里,我们知道也可以用 getComputedStyle
方法获得元素的 CSS width 和 height。
所以为什么不能像下面这样获得元素的宽呢?
let elem = document.body;
alert( getComputedStyle(elem).width ); // 打印元素的 CSS 宽度
但是为什么我们还要用几何属性呢?两个原因:
首先,因为 CSS width/height 的计算,还依赖于另一个属性:
box-sizing
。一旦box-sizing
的属性值改变了,接下来可能就会干扰我们的 JavaScript 代码了。然后,得到的 CSS
width
/height
属性值可能是auto
,拿行内元素举例子:
<span id="elem"></span>
<script>
alert( getComputedStyle(elem).width ); // auto
</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 而言,通常是在 12px
到 20px
之间的值。如果滚动条不占据空间,就返回 0
。
三、将球放到球场中间
这是初始的样子:
球场中间的坐标是多少?
计算出来之后,再把球放到中间就可以了。
元素是通过 JavaScript 移动的,而不是 CSS。
这个代码对任意大小的球(
10
,20
,30
像素)和球场里都能正常工作。
PS. 当然,可以使用 CSS 完成居中,但在这里我们仅要求使用 JavaScript。此外,之后我们还会遇到其他必须使用 JavaScript 时的主题和更复杂情况。在这里,我们先做一个“热身”。
四、区别:CSS width 和 clientWidth
getComputedStyle(elem).width
和 elem.clientWidth
的区别有哪些?
至少列出 3 点,多的话更好。
答案
一、
解决方案是:
let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight;
二、
为了得到滚动条宽度,我们先创建一个带有滚动条的元素,但是没有 border 和 padding。
元素的 offsetWidth 减去 clientWidth 的差值就是滚动条的宽度了。
// 创建一个显示滚动条的 div
let div = document.createElement('div');
div.style.overflowY = 'scroll';
div.style.width = '50px';
div.style.height = '50px';
// 放置到文档中, 否则尺寸的话就是 0
document.body.append(div);
let scrollWidth = div.offsetWidth - div.clientWidth;
div.remove();
alert(scrollWidth);
三、
球是 position: absolute
的。表示它的 left
/top
坐标是相对于最近的定位祖先元素计算的,也就是 #field
(因为有样式 poisition: relative
)。
坐标是从球场内部左上角开始计算的:
获取内部宽/高的话,使用的是 clientWidth
/clientHeight
。因此球场的中心点在 (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';
看起来是这样的:
因此,我们还需要将移动:水平方向向左移动球一半的宽度,垂直方向向上移动球一半的高度:
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>
是无效的:
<img src="ball.png" id="ball">
当浏览器无法获取图片宽/高(从标签特性或 CSS 中),那么在图片加载完成之前,都会认为是 0
。
真实场景下,浏览器会在第一次加载图片后就缓存它,那么在下一次加载的时候,会立即得到尺寸。
但是第一次加载的时候,ball.offsetWidth
是 0
。这是导致计算出错误的坐标值。
我们可以通过为 <img>
添加 width/height
来修复这个 bug。
<img src="ball.png" width="40" height="40" id="ball">
或者在 CSS 中指定:
#ball {
width: 40px;
height: 40px;
}
四、
区别:
clientWidth
是数值,而getComputedStyle(elem).width
返回的是一个携带px
后缀的字符串。对行内元素来说,
getComputedStyle
的返回值可能是像"auto"
这样的非数值结果。clientWidth
是元素内部的内容区域 + padding 区域,而 CSS width(在标准盒子模型下)是指不含 padding 的内容区区域。如果存在滚动条,浏览器会认为滚动条也占据空间,在一些浏览器中得到的 CSS width 是减去滚动条空间之后的值(因为它不再适用于内容了),但另一些浏览器则会忽略滚动条。而
clientWidth
属性值总是一致的:如果有滚动条的话,返回的始终是减去滚动条之后的值。
(完)