對於每一位Web開發的同學而言,在CSS的世界當中,每一個元素都是一個盒子,都有描述盒子大小和位置的相關屬性。比如CSS的盒模型相關的屬性和position 相關屬性。不過今天我們學習和聊的不是CSS的世界,而是來學習和聊JavaScript中怎麼獲取元素尺寸和位置。
在JavaScript中也有很多屬性允許我們讀取有關元素的width 、height 和其他幾何特性的信息。對於元素的位置獲取和控制,在JavaScript中與CSS有所不同,移動或定位元素時,經常需要它們來正確地計算坐標。
在這篇教程中,將學習JavaScript如何獲取HTML元素的確切位置和大小,並瞭解它們的工作原理。
HTML中的佈局(位置)
在大多數情況下,元素的位置取決於其自身的CSS屬性,但在很大程度上取決於其父元素的CSS屬性。這裡所指的屬性主要是padding 、margin 和border 。
比如下面這個簡單示例,示例中名為example 的div 元素的盒模型視圖可以很好地顯示這些屬性如何影響佈局:
#example {
width: 300px;
height: 200px;
border: 25px solid #E8C48F;
padding: 20px;
overflow: auto;
}
盒模型的視覺圖如下所示:

通過瀏覽器開發者工具可以很輕易的看到元素#example 的盒模型視圖。視圖中清晰的表示了padding 、margin 和border 的值是如何表示的。而且在視圖中,可以看到每個CSS屬性,以及其對應的值。
#example 元素它有border 、padding 和滾動條(因為我們顯式設置了元素的width 和height ,並且將overflow 設置為auto )。元素沒有margin ,因為不是元素本身的一部分,也沒有為它提供特殊屬性。
如果將#example 元素上的各個屬性繪製到圖形,看起來像下面這樣:

上面的圖片展示了元素有滾動條的情形,這也是最複雜的情況。有些瀏覽器(不是所有)通過從內容中獲取空間來為它保留空間。
因此,如果沒有滾動條,內容(content )的width 將是300px ,但是如果滾動條的寬度是16px (不同的設備和瀏覽器的寬度不同,如下圖所示),那麼內容的width 將是300 - 16 = 284px 。在具體使用的過程中,我們應該要考慮到這一點。這也就是為什麼要舉一個帶有滾動條的示例。如果沒有滾動條,那麼事情就會變得簡單點。

有關於瀏覽器滾動條的特性,可以點擊這裡進行瞭解。
上面的示例,由於我們的元素屬於正常的文檔流,從瀏覽器開發者工具中截出的元素盒模型視圖,只看到了元素尺寸相關的屬性,但並沒有看到元素有關於位置的尺寸。如果我們在元素中添加position 相關的屬性,那麼就可以看到。比如下面這個示例:
#container {
padding: 24px;
margin: 24px;
border: 50px #ccc solid;
left: 10px;
top: 200px;
position: absolute;
}
元素#container 的盒模型視圖如下:

幾何結構
元素提供的width 、height 和其他幾何結構的屬性,其值總是數字。它們被假定為以像素為單位。總體情況如下圖所示:

其實它有很多屬性,我們很難把它們都放在一張圖中,我在網上找了一張描述相對全一點的圖:

雖然這圖上的屬性,在CSS的世界中並沒有看過,但它們的值很簡單,也很容易理解。JavaScript就是通過這些屬性來獲取元素的位置或元素的尺寸。
讓我們從元素的外部開始探索它們。
offsetParent,offsetleft和offsetTop
offsetParent 、offsetLeft 和offsetTop 三個屬性是“最外層”的幾何結構的屬性,因此我們從這幾個屬性著手開始學習。
**offsetParent** :返回一個指向最近的(closest ,指包含層級上的最近)包含該元素的定位元素。如果沒有定位的元素,則offsetParent 為最近的table 元素對象或根元素(標準模式下為<html> 元素,怪異模式下為<body> 元素)。當元素的style.display 設置為none 或position 為fixed 時,offsetParent 返回null 。
在大多數實際情況下,可以使用offsetParent 獲取最近的CSS位置(CSS-Positioned)的祖先。而其中offsetleft 和offsetTop 提供相對於左上角的x 和y 坐標。
比如下面這個示例,內部的<div> 元素具有<main> 作為offsetParent ,而offsetLeft 和offsetTop 從左上角移動180px (即向右下角方向移動)。
<main style="position: relative" id="main">
<article>
<div id="example" style="position: absolute; left: 180px; top: 180px">...</div>
</article>
</main>
let exampleEl = document.getElementById('example')
console.log(exampleEl.offsetParent)

從console.log() 輸出的結果可以看到,元素#example 的offsetParent 是main 元素。通過offsetLeft 和offsetTop 輸出的值為180 。

特別注意,有幾種情況之下,offsetParent 返回的值為null :
- 不可見的元素(比如元素設置
display:none 或元素就不在document 中)
<body> 和<html> 元素
position:fixed 的元素
從上圖中大家對offsetLeft 和offsetTop 或許有所感知,但還是花一點篇幅來描述:
**offsetLeft** :當前元素左上角相對於offsetParent 節點的左邊界偏移的像素值。對塊級元素來說,offsetTop 、offsetLeft 、offsetWidth 及 offsetHeight 描述了元素相對於 offsetParent 的邊界框。然而,對於可被截斷到下一行的行內元素(如 span ),offsetTop 和 offsetLeft 描述的是第一個邊界框的位置(使用 Element.getClientRects() 來獲取其寬度和高度),而 offsetWidth 和 offsetHeight 描述的是邊界框的尺寸(使用 Element.getBoundingClientRect 來獲取其位置)。因此,使用 offsetLeft 、offsetTop 、offsetWidth 、offsetHeight 來對應 left 、top 、width 和 height 的一個盒子將不會是文本容器 span 的盒子邊界。
**offsetTop** :當前元素相對於其 offsetParent 元素的頂部的距離。
事實上,每個元素的父元素都有一個相似的偏移量值,所以我們的循環只是將它們加起來,直到沒有父元素為止。最終的結果是所有偏移量的總和,以幫助我們為元素獲取一個精確的位置。該位置考慮了margin 、padding 和top 、left 相關的值。但有一件事情是遺漏了的。
偏移量屬性沒有考慮的一個值是元素的border 。原因是,border 被認為是內部元素的左上角的一部分,但是它的大小對某些東西的位置有影響。為了測量border 的大小,我們使用clientLeft 和clientTop 來獲取。這兩個屬性我們後面會介紹。
offsetWidth和offsetHeight
上面我們看了元素外部影響元素的相關屬性。現在我們來看看元素自身相關的屬性。主要有offsetWidth 和offsetHeight :
offsetWidth :一個元素的佈局寬度。offsetWidth 是測量包含元素的邊框、水平線上的內邊距、豎直方向滾動條以及CSS設置的寬度的值。
offsetHeight :元素的像素高度,高度包含該元素的垂直內邊距和邊框,且是一個整數。通常,元素的offsetHeight 是一種元素CSS高度的衡量標準,包括元素的邊框、內邊距和元素的水平滾動條(如果存在且渲染的話),不包含:before 或:after 等偽類元素的高度。對於文檔的body對象,它包括代替元素的CSS高度線性總含量高。浮動元素的向下延伸內容高度是被忽略的。
offsetWidth 和offsetHeight 提供元素的“外部”的寬度和高度。換句話說,包括border 在內的全部大小。
為了更易於理解,還是用張圖來闡述:

在實際使用之中,不顯示的元素其幾何結構屬性的值為0 或null 。也就是說,幾何結構屬性只對可見的元素進行計算。
如果一個元素(或它的任何一個祖先元素)的display:none 或不在文檔中,那麼所有的幾何屬性都為0 或null 。那麼問題來了,我們應該怎麼去判斷呢?為瞭解決這個問題我們可以寫一個函數。比如:
function isHidden(elem) {
return !elem.offsetWidth && !elem.offsetHeight;
}
請注意,對於屏幕上的元素,isHidden 返回的值是true ,但是沒有任何大小,比如一個空的<div> 。
clientTop和clientLeft
前面提到過offsetLeft 和offsetTop 沒有考慮border 。為了測量border 的大小,我們使用clientLeft 和clientTop 來獲取。接下來我們來瞭解和學習clientTop 和clientLeft 。
比如上面的示例:
border-left 的寬度:clientLeft = 25
border-top 的寬度:clientTop = 25

但準確地說,它們不是border ,而是內部與外部的相對坐標。那麼有什麼區別呢?
比如這樣的一個示例,當文檔是從右到左排版(操作系統是阿拉伯語或希伯來語)時,也就是direction 為rtl ,它就可見了。然後滾動條不在右邊,而是在左邊,這個時候clientLeft 的值也包含了滾動條的寬度。
基於上面的示例,如果direction:rtl ,這個時候clientLeft 的值就變成了25 + 16 = 41 。

clientWidth 和 clientHeight
clientWidth 和clientHeight 屬性可以用來獲取元素邊框內區域的大小。它們包括了內容的寬度和padding ,但不包含滾動條寬度:

先來看上圖是有關於clientHeight 的值,因為它更容易。主要是沒有水平滾動條,所以它正好是邊框內區域的總和:height + padding-top + padding-bottom ,即:200 + 2 * 20 = 240px 。
再來看clientWidth 。這裡的內容寬度不是300px ,而是284px ,因為還有一個16px 寬的滾動條。所以clientWidth 的值是284 + 20 * 2 = 324px 。
如果沒有padding ,那麼clientWidth 和clientHeight 就是border 和滾動內的內容區域寬度和高度。

也就是說,當沒有padding 時,clientWidth 和clientHeight 可以用來獲取內容區域的width 和height 。
現在,使用offset* 和client* 屬性,我們已經考慮了padding 、margin 、border 、top 和left 相關的屬性。在99%的情況之下,這應該是我們需要考慮的全部,除非你的運用場景是剩下的那1%部分。
很多時候,元素有可能是位於帶有滾動條的容器之中。如果是這樣的話,為了確保能準確的獲取到元素的位置,還需要通過scrollLeft 和scrollTop 屬性來進行測量,並從總數中減去它們。
scrollLeft 和 scrollTop
scrollLeft 和scrollTop 屬性可以獲取或設置滾動元素隱藏部分的寬度和高度。
scrollLeft 可以讀取或設置元素滾動條到元素左邊的距離。如果這個元素的內容排列方向(direction ) 是rtl (right-to-left ) ,那麼滾動條會位於最右側(內容開始處),並且scrollLeft 值為0 。此時,當你從右到左拖動滾動條時,scrollLeft 會從0 變為負數。
scrollTop 可以獲取或設置一個元素的內容垂直滾動的像素數。 一個元素的 scrollTop 值是這個元素的頂部到它的最頂部可見內容(的頂部)的距離的度量。當一個元素的內容沒有產生垂直方向的滾動條,那麼它的 scrollTop 值為0 。
在下面的圖片中,我們可以看到在一個塊中垂直滾動條的scrollHeight 和scrollTop :

大多數幾何結構屬性都是只讀的,但是scrollLeft 和scrollTop 是可更改的。如果將scrollTop 設置為0 或Infinity 將會使元素分別滾動到瀏覽器的最頂端和最底端。
scrollWidth和scrollHeight
clientWidth 和clientHeight 僅負責元素的可見部分。而屬性scrollWidth 和scrollHeight 還會包括不可見(隱藏)的部分。
scrollWidth 返回該元素區域寬度和自身寬度中較大的一個,若自身寬度大於內容寬度(存在滾動條),那麼scrollWidth 將大於clientWidth 。需要注意的是,改屬性返回的是四捨五入後的整數值,如果需要小數,則需要使用getBoundingClientRect() 。
scrollHeight 返回該元素內容高度。包括被overflow 隱藏掉的部分,包含padding ,但不包含margin 。和scrollWidth 類似,如果需要小數,則需要使用getBoundingClientRect() 。
比如:

上圖中可以看出:
scrollHeight = 723 是指元素整個內容的高度,還包括隱藏的部分;scrollWidth = 324 是指元素整個內容的寬度,這裡沒有水平滾動條,所以它等於clientWidth 。
我們可以使用這些屬性將元素擴展到它的寬度和高度。
element.style.height = `${element.scrollHeight}px`;
除些之外,這兩個屬性最常見的使用場景就是:判斷元素是否滾動到底部,比如下面的代碼,如果返回的值為true ,表示滾動到底部,反之則不是:
ele.scrollHeight - ele.scrollTop === ele.clientHeight
不要從CSS中獲取寬度和高度
前面介紹的一些屬性都是有關於DOM元素的幾何結構屬性。在JavaScript中通常用來計算寬度、高度和距離。但在“樣式和類”一節中學習中我們知道,在JavaScript中可以使用getComputedStyle 來讀取CSS中的width 和height 。
既然如此,那麼為什麼不這樣來讀取元素的寬度呢?
let elem = document.body;
console.log( getComputedStyle(elem).width );
其實我們使用幾何結構屬性,其實是有原因所在的:
首先,CSS的width 和height 取決於另一個屬性:box-sizing 。在CSS中,這個屬性定義了元素的寬度和高度。換句話說,CSS可以改變元素盒模型的大小,而這種改變有可能會破壞JavaScript的幾何結構屬性取得的值。
其次,CSS的寬度和高度可以是auto ,例如一個內聯元素span 。
還有一個原因,就是滾動條。因為滾動條會佔用一些內容的空間。所以內容的實際寬度將會小於CSS設置的寬度。那麼在JavaScript中clientWidth 和clientHeight 時,就需要考慮到這一點。
但getComputedStyle(elem).width 有所不同。有些瀏覽器(比如Chrome瀏覽器)返回的是innerWidth 減去滾動條的寬度,另外有一些瀏覽器(比如Firefox),CSS的寬度會忽略滾動條的寬度。在這種瀏覽器中使用getComputedStyled 取出的寬度將沒什麼差異,也不需要依賴於前面介紹的幾何結構屬性。
注意:熟悉CSS的同學應該知道,元素的盒模型分為content-box 和border-box 之類。那麼在JavaScript中,上述的這些屬性也略有不同。有關於這方面的方同之處,可以閱讀前期整理的學習筆記《視口寬高、位置與滾動高度》。
小示例
基於上面的相關信息,如果我們要獲取元素的位置,就可以依賴於上述的相關屬性封裝一個函數,比如getPosition() :
function getPosition(el) {
var xPos = 0;
var yPos = 0;
while (el) {
if (el.tagName == "BODY") {
// deal with browser quirks with body/window/document and page scroll
var xScroll = el.scrollLeft || document.documentElement.scrollLeft;
var yScroll = el.scrollTop || document.documentElement.scrollTop;
xPos += (el.offsetLeft - xScroll + el.clientLeft);
yPos += (el.offsetTop - yScroll + el.clientTop);
} else {
// for all other non-BODY elements
xPos += (el.offsetLeft - el.scrollLeft + el.clientLeft);
yPos += (el.offsetTop - el.scrollTop + el.clientTop);
}
el = el.offsetParent;
}
return {
x: xPos,
y: yPos
};
}
有關於上述代碼的詳細闡述,可以點擊這裡。
總結
從編寫代碼的角度來說,找到元素的確切位置和獲取尺寸並不困難。但要搞清楚這些幾何屬性以及將這些屬性運用到實際的元素當中,還是有點棘手的。尤其是要考慮所有瀏覽器的情形以及瀏覽器怪異模式下。
JavaScript檢測元素有六個DOM的幾何結構屬性:offsetWidth 、offsetHeight 、clientWidth 、clientHeight 、scrollWidth 、scrollHeight 。再加上offsetTop 、offsetLeft 、scrollTop 、scrollLeft 、clientTop 和clientLeft 等方向距離的屬性。這樣一來,讓事情就變得複雜,對於像我這樣的初學者而言,極難理解,也易產生一些錯誤。正因這個原因,整理了一篇這樣的文章,因涉及的內容較多,有些零亂,加上是初學者,難免有不對之處,如果有不對之處,煩請各路大嬸拍正。
|