本來嘛,既然東西是IE發明的,應該一切都以IE為標準才是,不明白非得讓w3c那幫官僚認可才行。後知後覺的w3c唯一做的好事是第一次瀏覽器大戰後,讓世界分裂成兩大陣營進行冷戰。那個時間,蘋果被微軟打得快破產,為了避免壟斷嫌疑,微軟資助了蘋果,其中一個條件是讓Mac也用上IE,不過像safari,opera等小眾瀏覽器,早在第一次瀏覽器大戰中,就實現了許多網景與IE的方法。剩下的就是Firefox,以前是沒有什麼人理睬w3c這個傻冒的,也就Firefox誕生後開始大量實現(也可能在暗中制定)許多w3c草案的方法。當然不能說w3c全部都不好,如xpath,xquery等東西,還是很有用,當然也很小眾。IE即使沒有它們還能活得很好。此後,IE還有一系列與布局有關的方法,如offset家族,scroll家族,client家族與getClientRects() 、getBoundingClientRect()。這些方法非常有用,以致於Firefox這樣頑固的瀏覽器最後還是實現了它們。特別是getBoundingClientRect方法,有了它可以速度取得元素節點的座標與尺寸。最後w3c終於坐不住了,把它們與一些處理樣式的方法統一起來,稱之為CSSOM(Cascading Style Sheets Object Model,層疊樣式表物件模型)。
按草案的提交順序,分為兩部分。第一部分,處理樣式表的方法與屬性。這方面w3c做得非常失敗了,在IE下好像就只有document.styleSheets用得了,不過這方法極可能本來就是IE的。聽說IE9全部支援,不過xp不支援IE9也是白搭的!第二部分,處理布局的方法與屬性。基本上是IE發明的東西,當然還有一些Firefox的東西。現在重點看一下第二部分的內容:
首先是一個叫AbstractView的介面,是所有視圖的基礎。在FF中我們真的能找到這東西。
alert(window.AbstractView); //[object AbstractView]
然後是Media介面, 我找不到它存在的證明。
再然後是ScreenView介面, 它在FF等標準瀏覽器叫做document.defaultView。不過實際上它被綁定了比w3c草案更多的屬性:
var aa = document.getElementById("aa"); var s = "" for(var name in document.defaultView) if(! /firebug/i.test(name)) s += name+" : "+document.defaultView[name]+"
" aa.innerHTML = s;
<br /><!doctype html><br /><html lang="en"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta content="IE=6" http-equiv="X-UA-Compatible"/></p><p> <title>CSSOM by 司徒正美</title></p><p> <meta http-equiv="content-type" content="text/html;charset=UTF-8" /><br /> <script type="text/javascript"><br /> window.onload = function(){<br /> var aa = document.getElementById("aa");<br /> var s = ""<br /> for(var name in document.defaultView)<br /> if(! /firebug/i.test(name))<br /> s += name+" : "+document.defaultView[name]+"<br>"<br /> aa.innerHTML = s;</p><p> }</p><p> </script><br /> </head><br /> <body><br /> <p>請在標準瀏覽器下運行:</p></p><p> <div id="aa"></p><p> </div><br /> </body><br /></html><br />
運行代碼
alert(window.innerWidth) alert(window.innerHeight) alert(window.outerWidth) alert(window.outerHeight) alert(window.pageXOffset) alert(window.pageYOffset) alert(window.screenX) alert(window.screenY) alert(window.scroll) alert(window.scrollTo) alert(window.scrollBy)
Screen介面,用於提供與使用者顯示器相關的資訊:
alert(screen.availWidth) alert(screen.availHeight) alert(screen.width) alert(screen.height) alert(screen.colorDepth) alert(screen.pixelDepth)
剩下的就不細節了,基本上是IE那一套東西還有對滑鼠事件對象的屬性進行標準化。它提供了許多計算基準來計算offsetLeft等的值。現在重點看看offsetParent。記得John Resig在《Pro.JavaScript.Techniques》這本嚴重缺乏檢測的書中提供了兩個計算元素座標值的函數來為害人間:
//page 144 // Find the X (Horizontal, Left) position of an element function pageX(elem) { // See if we're at the root element, or not return elem.offsetParent ? // If we can still go up, add the current offset and recurse upwards elem.offsetLeft + pageX( elem.offsetParent ) : // Otherwise, just get the current offset elem.offsetLeft; } // Find the Y (Vertical, Top) position of an element function pageY(elem) { // See if we're at the root element, or not return elem.offsetParent ? // If we can still go up, add the current offset and recurse upw elem.offsetTop + pageY( elem.offsetParent ) : // Otherwise, just get the current offset elem.offsetTop; }
或是整合成如下類似版本:
var getOffseft = function( el ) { var x = 0; var y = 0; do { x += el.offsetLeft; y += el.offsetTop; } while ((el = el.offsetParent)); return {x:x,y:y}; }
但實際來說,IE與標準瀏覽器早期的版本對這個offsetParent都有些bug,如果要計算位移量,還要利用offsetLeft與offsetTop,而它們也有bug,因此上面的方法計算的結果與實際出入很大。在jQuery中,John Resig重新設計了這個函數,並且添加一系列輔助方法來擷取瀏覽器相應的特徵:
jQuery.fn.offset = function( options ) { var elem = this[0]; if ( options ) { return this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); } if ( !elem || !elem.ownerDocument ) { return null; } //特殊處理body if ( elem === elem.ownerDocument.body ) { return jQuery.offset.bodyOffset( elem ); } //獲得關於offset相關的瀏覽器特徵 jQuery.offset.initialize(); var offsetParent = elem.offsetParent, prevOffsetParent = elem, doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement, body = doc.body, defaultView = doc.defaultView, prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, top = elem.offsetTop, left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { //HTML,BODY,以及不具備CSS盒子模型的元素及display為fixed的元素沒有offsetParent屬性 if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { break; } computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle; top -= elem.scrollTop; left -= elem.scrollLeft; if ( elem === offsetParent ) { top += elem.offsetTop; left += elem.offsetLeft; //offset應該返回的是border-box,但在一些表格元素卻沒有計算它們的border值,需要自行添加 //在IE下表格元素的display為table,table-row與table-cell if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } prevOffsetParent = offsetParent, offsetParent = elem.offsetParent; } //修正safari的錯誤 if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } prevComputedStyle = computedStyle; } //最後加上body的位移量 if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) { top += body.offsetTop; left += body.offsetLeft; } //使用固定定位,可能出現捲軸,我們要獲得取大的滾動距離 if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { top += Math.max( docElem.scrollTop, body.scrollTop ); left += Math.max( docElem.scrollLeft, body.scrollLeft ); } return { top: top, left: left }; };
如上所示,這種通過累加方式獲得元素位置的方法非常不得人心,況且我還沒有把它的輔助函數寫出來呢!如果不是藉助於架構,沒有人願意寫這麼一大堆的函數來擷取這兩個值。但是,getBoundingClientRect的出現與運用讓情況大為改觀,樂得John Resig專門寫了篇文章大加讚賞此方法。他的部落格中間貼了一幅圖,很直觀地顯示新方法在篇幅上的精簡,更別提在效能上的改善了!下面是我的實現:
//by 司徒正美 coords = function( el ) {//取得元素(左上方)的座標 if ( !el || !el.ownerDocument ) { return null; } var pos = { x:0, y:0 }, owner = el.ownerDocument; if(el.tagName === "BODY"){ pos.x = el.offsetTop; pos.y = body.offsetLeft; //http://hkom.blog1.fc2.com/?mode=m&no=750 body的位移量是不包含margin的 if(parseFloat(dom.getStyle(el,"margin-top"))!== el.offsetTop){ pos.x += parseFloat( dom.getStyle(el, "margin-top") ) || 0; pos.y += parseFloat( dom.getStyle(el, "margin-left")) || 0; } return pos; }else if(owner.getBoxObjectFor){//Firefox return owner.getBoxObjectFor(el); }else if (el.getBoundingClientRect) { //如果支援getBoundingClientRect //我們可以通過getBoundingClientRect來獲得元素相對於client的rect. //http://msdn.microsoft.com/en-us/library/ms536433.aspx var box = el.getBoundingClientRect(), root = owner.documentElement, body = owner.getElementsByTagName("body")[0], clientTop = root.clientTop || body.clientTop || 0, clientLeft = root.clientLeft || body.clientLeft || 0; // 加上document的scroll的部分尺寸到left,top中。 // IE中會自動加上2px的border,這裡是去掉document的邊框大小。 // http://msdn.microsoft.com/en-us/library/ms533564(VS.85).aspx pos.x = box.left + (self.pageYOffset || dom.feature.w3cBox && root.scrollTop || body.scrollTop ) - clientTop, pos.y = box.top + (self.pageXOffset || dom.feature.w3cBox && root.scrollLeft || body.scrollLeft) - clientLeft; } return pos; }
這隻是CSSOM方法的運用的一個特例,在精確獲得元素的樣式時,我們會更頻繁地運用到它們。在做特效時,那幾大家族的出現次數就更頻繁。感謝微軟帶給我們這些好東西。但如果你想弄清楚offsetWidth,clientWidth等的準確含義時,我建議還是請到Firefox官網查看,微軟現在連自己的東西都不能作主,這真可悲。
相關連結:
- http://www.alanamos.de/articles/firefox_2+3_offset_attributes.html
- http://www.w3.org/TR/cssom-view/
- http://www.gtalbot.org/BrowserBugsSection/MSIE8Bugs/CSSOM-offsetParent-prop.html
- http://www.java2s.com/Code/JavaScript/HTML/UsingtheoffsetParentProperty.htm