作为前端工程师,在日常的开发过程中经常会涉及到获取元素尺寸及位置的操作,而 DOM 为我们提供了好几种属性及方法,每种属性及方法获取到的结果都不太一样,因此,搞清楚它们之间的区别是能够正确使用的前提。
这篇文章就来温习一下这些属性及方法。
假设有如下 HTML 结构:
<div id="container">
<div id="target">
<pre>
width: 300px;
height: 150px;
padding: 10px;
margin: 15px;
border: solid 1px black;
background-color: deepskyblue;
color: white;
font-weight: bold;
overflow: auto;
</pre>
</div>
</div>
<pre> 标签中显示的是 id 为 #target 的 div 元素的 CSS 代码,方便阅读。
对应的 CSS 代码如下:
html, body {
margin: 0;
padding: 0;
}
#container {
margin: 20px;
padding: 30px;
}
#target {
width: 300px;
height: 150px;
padding: 10px;
margin: 15px;
border: solid 1px black;
background-color: deepskyblue;
color: white;
font-weight: bold;
overflow: auto;
}
pre {
margin: 0;
background-color: orange;
}
记住代码中的 width、height、padding、margin 和 border,这些值对于后文的获取结果非常重要。
得到的效果如下:

注意边缘的白色区域,这是 #container 元素的样式造成的,后文会用到。
接下来,我们将使用这个示例来演示各种属性与方法之间的区别。
clientWidth
获取元素的 clientWidth:
let target = document.getElementById("target");
console.log(target.clientWidth); // 303
得到的值为 303,前文中我们设置的 width 为 300px,border 为 1px,两者加起来的和应该是 302 才对,为什么多了 1 呢?
如果我们去掉 #target 元素的 overflow:

得到的值将是 320。对比前后的效果可以发现,前后 17 的差值其实是滚动条的宽度,和 border 无关。
因此,元素的 clientWidth 是其 width 与左右 padding 的和。
值得一提的是,pre 元素在 Firefox 中的高度比在 Chrome 中小:

两者相差 30px。
clientHeight
获取元素的 clientHeight:
let target = document.getElementById("target");
console.log(target.clientHeight); // 170
得到的值是 170。
因此,元素的 clientHeight 是其 height 与上下 padding 的和。
clientLeft
获取元素的 clientLeft:
let target = document.getElementById("target");
console.log(target.clientLeft); // 1
得到的值是 1,正好是左边 border 的宽度。
因此,元素的 clientLeft 是其 border-left 的宽度。
clientTop
获取元素的 clientTop:
let target = document.getElementById("target");
console.log(target.clientTop); // 1
得到的值是 1,正好是上边 border 的宽度(前文中我们设置的 border 是针对所有方向的)。
因此,元素的 clientLeft 是其 border-top 的宽度。
offsetWidth
获取元素的 offsetWidth:
let target = document.getElementById("target");
console.log(target.offsetWidth); // 322
得到的值是 322,正好是其 width、左右 padding 与 border 的和。
因此,元素的 offsetWidth 是其 width、左右 padding 与 border 宽度的和。
offsetHeight
获取元素的 offsetHeight:
let target = document.getElementById("target");
console.log(target.offsetHeight); // 172
得到的值是 172,正好是其 width、上下 padding 与 border 的和。
因此,元素的 offsetWidth 是其 width、上下 padding 与 border 的和。
offsetLeft
获取元素的 offsetLeft:
let target = document.getElementById("target");
console.log(target.offsetLeft); // 65
得到的值是 65,正好是 #target 元素自身的 margin-left 与 #container 元素的 margin-left 和 padding-left 的和。
由此可以得出,此时的 #target 元素是相对 body 元素的进行偏移的,我们可以通过获取元素的 offsetParent 来查看其相对偏移对象:
console.log(target.offsetParent); // <body></body>
得到的结果是 body 元素。
这只是元素默认情况下的偏移量,即相对 body 元素的偏移量。
当元素的某个(或多个)祖先元素的 position 不为 static 或 transform 属性不为 none 或 perspective 属性不为 none 时,则获取到的偏移量是相对于最近的符合前述条件的祖先元素的偏移量。
例如,如果我们将 #container 元素的 perspective 设为 10px:
#container {
margin: 20px;
padding: 30px;
perspective: 10px
}
再次获取元素的 offsetLeft:
console.log(target.offsetLeft); // 45
得到的值是 45,正好是其自身的 margin-left 与父元素 #container 的 padding-left 的和。
查看其相对偏移对象:
console.log(target.offsetParent); // <div id="container"></div>
得到的结果是 #container 元素。
因此,元素的 offsetLeft 有两种情况:
- 默认情况下,是其
border-left到body元素的border-left之间所经过的所有margin与padding之和; - 如果其某个(或多个)祖先元素的
position不为static或transform属性不为none或perspective属性不为none,则是其border-left到最近的满足前述条件的祖先元素的border-left之间所经过的所有margin与padding之和。
如果有兄弟元素,则需加上兄弟元素的占位。
offsetTop
获取元素的 offsetTop:
let target = document.getElementById("target");
console.log(target.offsetHeight); // 65
得到的值是 65。
元素的 offsetTop 值和 offsetLeft 一样,也有上述的两种情况,除此之外,它还会受到 margin 的影响。
当发生 margin 塌陷时,计算元素的 offsetTop 时所采用的 margin 会以发生塌陷的两个 margin 中最大的值(不确定是 margin-top 还是 margin-bottom,根据实际情况分析)为准。
例如,如果我们去掉 #container 元素的 padding:
#container {
margin: 20px;
/*padding: 30px;*/
}
再次获取元素的 offsetTop:
console.log(target.offsetHeight); // 20
得到的值是 20,因为 #target 元素和 #container 元素发生了 margin 塌陷,而两者之间最大的 margin 是 #container 元素的 20px(#target 元素的 margin 为 15px),所以得到的值是 20。
如果此时我们再将 #container 元素的 perspective 设为 10px:
#container {
margin: 20px;
/*padding: 30px;*/
perspective: 10px;
}
再次获取元素的 offsetTop:
console.log(target.offsetHeight); // 0
得到的值是 0,这也是因为发生了 margin 塌陷的原因。
因此,元素的 offsetTop 有三种情况:
- 默认情况下,是其
border-top到body元素的border-top之间所经过的所有margin与padding之和; - 如果其某个(或多个)祖先元素的
position不为static或transform属性不为none或perspective属性不为none,则是其border-top到最近的满足前述条件的祖先元素的border-top之间所经过的所有margin与padding之和。 - 如果发生了
margin塌陷,则以两者之间最大的margin值为准。
如果有兄弟元素,则需加上兄弟元素的占位。
scrollWidth
我们先修改一下 pre 元素的 CSS 代码,增加它的宽度:
pre {
margin: 0 20px;
background-color: orange;
width: 500px;
border: solid 2px black;
padding: 10px;
}
然后获取元素的 scrollWidth:
let target = document.getElementById("target");
console.log(target.scrollWidth) // 554
得到的值是 554,在边距上,仅包含了 #target 元素的 padding-left 和 pre 元素的 margin-left,查看一下效果:

可以看到,#target 元素的 padding-right 被忽略了,Firefox 上的表现也是一样的。
因此,元素的 scrollWidth 在计算边距时,仅包含左侧经过的 margin 和 padding。
scrollHeight
我们先修改一下 pre 元素的 CSS 代码,增加它的高度:
pre {
margin: 20px 0;
background-color: orange;
height: 500px;
border: solid 2px black;
padding: 10px;
}
然后获取元素的 scrollHeight:
let target = document.getElementById("target");
console.log(target.scrollHeight) // 584
得到的值是 584,在边距上,包含了 #target 元素的上下 padding,查看一下效果:

Firefox 上的表现略有不同,得到的值是 574,在边距上,仅包含了 #target 元素的 padding-top。
查看一下 Firefox 上的效果:

可以明显看到底部边距比顶部边距窄,两者对 pre 的 margin 处理是一致的,都包含在内。
因此,元素的 scrollHeight 在计算边距时,Chrome 会包含上下 padding,而 Firefox 仅包含 padding-top。
scrollLeft
表现同 scrollWidth。
scrollTop
表现同 scrollHeight。
getClientRects 与 getBoundingClientRect
两者都可以用于获取元素相对于视口的位置信息和自身尺寸信息,区别在于 getClientRects 返回的是 DOMRectList,而 getBoundingClientRect 返回的是 DOMRect,后者使用更方便一些。
例如:
console.log(target.getClientRects());
console.log(target.getBoundingClientRect());
得到的结果如下:

可以看到,两者在内容上是一样的,不过 getClientRects 兼容性差一些,通常我们都选择使用 getBoundingClientRect。
结语
获取元素的尺寸及位置在开发中是高频操作,也是比较容易出错的地方,温习一下,总是好的。
