前端重新學習(17)DOM 擴展

DOM的兩個主要擴展

  • Selectors API(選擇符 API)
  • HTML5
  • Element Traversal (元素遍歷)規範
  • 專有的DOM擴展  

雖然 DOM爲與 XML及 HTML文檔交互制定了一系列核心 API,但仍然有幾個規範對標準的 DOM 進行了擴展。這些擴展中有很多原來是瀏覽器專有的,但後來成爲了事實標準,於是其他瀏覽器也都提 供了相同的實現。介紹的三個這方面的規範如下。

  •  Selectors API,定義了兩個方法,讓開發人員能夠基於 CSS選擇符從 DOM中取得元素,這兩個 方法是 querySelector()和 querySelectorAll()。
  •  Element Traversal,爲 DOM元素定義了額外的屬性,讓開發人員能夠更方便地從一個元素跳到 另一個元素。之所以會出現這個擴展,是因爲瀏覽器處理 DOM元素間空白符的方式不一樣。
  •  HTML5,爲標準的 DOM定義了很多擴展功能。其中包括在 innerHTML 屬性這樣的事實標準基 礎上提供的標準定義,以及爲管理焦點、設置字符集、滾動頁面而規定的擴展 API。

雖然目前 DOM擴展的數量還不多,但隨着 Web 技術的發展,相信一定還會涌現出更多擴展來。很 多瀏覽器都在試驗專有的擴展,而這些擴展一旦獲得認可,就能成爲“僞”標準,甚至會被收錄到規範 的更新版本中。 
 
 

 

選擇符 API 

衆多 JavaScript庫中常用的一項功能,就是根據 CSS選擇符選擇與某個模式匹配的 DOM元素。 實際上,jQuery(www.jquery.com)的核心就是通過 CSS選擇符查詢 DOM文檔取得元素的引用,從而 拋開了 getElementById()和 getElementsByTagName()。 

Selectors API(www.w3.org/TR/selectors-api/)是由 W3C 發起制定的一個標準,致力於讓瀏覽器原 生支持 CSS查詢。所有實現這一功能的 JavaScript庫都會寫一個基礎的 CSS解析器,然後再使用已有的 DOM 方法查詢文檔並找到匹配的節點。儘管庫開發人員在不知疲倦地改進這一過程的性能,但到頭來 都只能通過運行 JavaScript代碼來完成查詢操作。而把這個功能變成原生 API之後,解析和樹查詢操作 可以在瀏覽器內部通過編譯後的代碼來完成,極大地改善了性能。 Selectors API Level 1的核心是兩個方法:querySelector()和 querySelectorAll()。在兼容的瀏 覽器中,可以通過 Document 及 Element 類型的實例調用它們。目前已完全支持 Selectors API Level 1 的瀏覽器有IE 8+、Firefox 3.5+、Safari 3.1+、Chrome和Opera 10+。

querySelector()方法 

querySelector()方法接收一個 CSS選擇符,返回與該模式匹配的第一個元素,如果沒有找到匹 配的元素,返回 null。請看下面的例子。 

//取得 body 元素
 var body = document.querySelector("body"); 
 
//取得 ID 爲"myDiv"的元素
 var myDiv = document.querySelector("#myDiv"); 
 
//取得類爲"selected"的第一個元素
 var selected = document.querySelector(".selected"); 
 
//取得類爲"button"的第一個圖像元素
 var img = document.body.querySelector("img.button"); 

通過 Document 類型調用 querySelector()方法時,會在文檔元素的範圍內查找匹配的元素。而 通過 Element 類型調用 querySelector()方法時,只會在該元素後代元素的範圍內查找匹配的元素。
 CSS選擇符可以簡單也可以複雜,視情況而定。如果傳入了不被支持的選擇符,querySelector() 會拋出錯誤。

querySelectorAll()方法 

querySelectorAll()方法接收的參數與 querySelector()方法一樣,都是一個 CSS選擇符,但 返回的是所有匹配的元素而不僅僅是一個元素。這個方法返回的是一個 NodeList 的實例。 

具體來說,返回的值實際上是帶有所有屬性和方法的 NodeList,而其底層實現則類似於一組元素 的快照,而非不斷對文檔進行搜索的動態查詢。這樣實現可以避免使用 NodeList 對象通常會引起的大 多數性能問題。 只要傳給 querySelectorAll()方法的 CSS選擇符有效,該方法都會返回一個 NodeList 對象, 而不管找到多少匹配的元素。如果沒有找到匹配的元素,NodeList 就是空的。 與 querySelector()類似,能夠調用 querySelectorAll()方法的類型包括 Document、 DocumentFragment 和 Element。下面是幾個例子。

 
 

//取得某<div>中的所有<em>元素(類似於 getElementsByTagName("em"))
 var ems = document.getElementById("myDiv").querySelectorAll("em"); 
 
//取得類爲"selected"的所有元素
 var selecteds = document.querySelectorAll(".selected"); 
 
//取得所有<p>元素中的所有<strong>元素
 var strongs = document.querySelectorAll("p strong"); 

要取得返回的 NodeList 中的每一個元素,可以使用 item()方法,也可以使用方括號語法,比如

var i, len, strong;
 for (i=0, len=strongs.length; i < len; i++){
      strong = strongs[i];
   //或者 strongs.item(i)
     strong.className = "important";
 }

同樣與 querySelector()類似,如果傳入了瀏覽器不支持的選擇符或者選擇符中有語法錯誤, querySelectorAll()會拋出錯誤。 

matchesSelector()方法 

Selectors API Level 2規範爲 Element 類型新增了一個方法 matchesSelector()。這個方法接收 一個參數,即 CSS選擇符,如果調用元素與該選擇符匹配,返回 true;否則,返回 false。看例子。 

if (document.body.matchesSelector("body.page1")){
      //true
 } 
 

在取得某個元素引用的情況下,使用這個方法能夠方便地檢測它是否會被 querySelector()或 querySelectorAll()方法返回。 

截至 2011年年中,還沒有瀏覽器支持 matchesSelector()方法;不過,也有一些實驗性的實現。 IE 9+通過msMatchesSelector()支持該方法,Firefox 3.6+通過mozMatchesSelector()支持該方法, Safari 5+和 Chrome通過 webkitMatchesSelector()支持該方法。因此,如果你想使用這個方法, 好是編寫一個包裝函數。 

function matchesSelector(element, selector){ 
     if (element.matchesSelector){ 
        return element.matchesSelector(selector);
     } else if (element.msMatchesSelector){
         return element.msMatchesSelector(selector);
     } else if (element.mozMatchesSelector){
         return element.mozMatchesSelector(selector);
     } else if (element.webkitMatchesSelector){
         return element.webkitMatchesSelector(selector);
     } else {
         throw new Error("Not supported.");
     }
 } 
 
if (matchesSelector(document.body, "body.page1")){
     //執行操作
 } 
 

元素遍歷 

對於元素間的空格,IE9及之前版本不會返回文本節點,而其他所有瀏覽器都會返回文本節點。這樣, 就導致了在使用 childNodes 和 firstChild 等屬性時的行爲不一致。爲了彌補這一差異,而同時又保 持DOM規範不變,Element Traversal規範(www.w3.org/TR/ElementTraversal/)新定義了一組屬性。 

  •      Element Traversal API爲 DOM元素添加了以下 5個屬性。 
  •  childElementCount:返回子元素(不包括文本節點和註釋)的個數。
  •  firstElementChild:指向第一個子元素;firstChild 的元素版。
  •  lastElementChild:指向後一個子元素;lastChild 的元素版。
  •  previousElementSibling:指向前一個同輩元素;previousSibling 的元素版。
  •  nextElementSibling:指向後一個同輩元素;nextSibling 的元素版。

支持的瀏覽器爲 DOM元素添加了這些屬性,利用這些元素不必擔心空白文本節點,從而可以更方便地查找 DOM元素了。

下面來看一個例子。過去,要跨瀏覽器遍歷某元素的所有子元素,需要像下面這樣寫代碼。 

var i,
    len,
     child = element.firstChild; while(child != element.lastChild){
     if (child.nodeType == 1){
       //檢查是不是元素
        processChild(child);
     }
     child = child.nextSibling;
 } 
 ///而使用 Element Traversal新增的元素,代碼會更簡潔。 
 
 var i,
      len,
     child = element.firstElementChild;
 while(child != element.lastElementChild){
     processChild(child);   //已知其是元素 
     child = child.nextElementSibling; 
} 
 

支持 Element Traversal規範的瀏覽器有 IE 9+、Firefox 3.5+、Safari 4+、Chrome和 Opera 10+

HTML5 

對於傳統 HTML 而言,HTML5是一個叛逆。所有之前的版本對 JavaScript 接口的描述都不過三言 兩語,主要篇幅都用於定義標記,與 JavaScript相關的內容一概交由 DOM規範去定義。

而 HTML5規範則圍繞如何使用新增標記定義了大量 JavaScript API。其中一些 API與 DOM重疊, 定義了瀏覽器應該支持的 DOM擴展。 

因爲 HTML5涉及的面非常廣,本節只討論與 DOM節點相關的內容。HTML5的 其他相關內容將在本書其他章節中穿插介紹。 

 與類相關的擴充 

HTML4在 Web 開發領域得到廣泛採用後導致了一個很大的變化,即 class 屬性用得越來越多,一 方面可以通過它爲元素添加樣式,另一方面還可以用它表示元素的語義。於是,自然就有很多 JavaScript 代碼會來操作 CSS類,比如動態修改類或者搜索文檔中具有給定類或給定的一組類的元素,等等。爲了 讓開發人員適應並增加對 class 屬性的新認識,HTML5新增了很多 API,致力於簡化 CSS類的用法。
 

1. getElementsByClassName()方法 

 

HTML5添加的 getElementsByClassName()方法是受人歡迎的一個方法,可以通過 document 對象及所有 HTML元素調用該方法。這個方法早出現在 JavaScript庫中,是通過既有的 DOM功能實 現的,而原生的實現具有極大的性能優勢。

getElementsByClassName()方法接收一個參數,即一個包含一或多個類名的字符串,返回帶有 指定類的所有元素的 NodeList。傳入多個類名時,類名的先後順序不重要。來看下面的例子。 

//取得所有類中包含"username"和"current"的元素,類名的先後順序無所謂
 var allCurrentUsernames = document.getElementsByClassName("username current"); 
 
//取得 ID爲"myDiv"的元素中帶有類名"selected"的所有元素
 var selected = document.getElementById("myDiv").getElementsByClassName("selected"); 

調用這個方法時,只有位於調用元素子樹中的元素纔會返回。在 document 對象上調用 getElementsByClassName()始終會返回與類名匹配的所有元素,在元素上調用該方法就只會返回後 代元素中匹配的元素。

使用這個方法可以更方便地爲帶有某些類的元素添加事件處理程序,從而不必再侷限於使用ID或標 簽名。不過別忘了,因爲返回的對象是 NodeList,所以使用這個方法與使用 getElementsByTagName() 以及其他返回 NodeList 的 DOM方法都具有同樣的性能問題。 支持 getElementsByClassName()方法的瀏覽器有 IE 9+、Firefox 3+、Safari 3.1+、Chrome和 Opera 9.5+。 

2. classList 屬性 

在操作類名時,需要通過 className 屬性添加、刪除和替換類名。因爲 className 中是一個字 符串,所以即使只修改字符串一部分,也必須每次都設置整個字符串的值。比如,以下面的 HTML 代 碼爲例。 

<div class="bd user disabled">...</div> 

這個<div>元素一共有三個類名。要從中刪除一個類名,需要把這三個類名拆開,刪除不想要的那 個,然後再把其他類名拼成一個新字符串。請看下面的例子。 

//刪除"user"類 
 
//首先,取得類名字符串並拆分成數組
 var classNames = div.className.split(/\s+/); 
 
//找到要刪的類名
 var pos = -1,
      i,
     len;
 for (i=0, len=classNames.length; i < len; i++){
     if (classNames[i] == "user"){
         pos = i;
         break;
     }
 } 
 
//刪除類名
 classNames.splice(i,1); 
 
//把剩下的類名拼成字符串並重新設置
 div.className = classNames.join(" "); 

爲了從<div>元素的 class 屬性中刪除"user",以上這些代碼都是必需的。必須得通過類似的算 法替換類名並確認元素中是否包含該類名。添加類名可以通過拼接字符串完成,但必須要通過檢測確定 不會多次添加相同的類名。很多 JavaScript庫都實現了這個方法,以簡化這些操作。 

HTML5 新增了一種操作類名的方式,可以讓操作更簡單也更安全,那就是爲所有元素添加 classList 屬性。這個 classList 屬性是新集合類型 DOMTokenList 的實例。與其他 DOM集合類似,
DOMTokenList 有一個表示自己包含多少元素的 length 屬性,而要取得每個元素可以使用 item()方 法,也可以使用方括號語法。此外,這個新類型還定義如下方法。 

  •  add(value):將給定的字符串值添加到列表中。如果值已經存在,就不添加了。
  •  contains(value):表示列表中是否存在給定的值,如果存在則返回 true,否則返回 false。
  •  remove(value):從列表中刪除給定的字符串。
  •  toggle(value):如果列表中已經存在給定的值,刪除它;如果列表中沒有給定的值,添加它。

 這樣,前面那麼多行代碼用下面這一行代碼就可以代替了: 

div.classList.remove("user"); 

以上代碼能夠確保其他類名不受此次修改的影響。其他方法也能極大地減少類似基本操作的複雜 性,如下面的例子所示。 

//刪除"disabled"類
 div.classList.remove("disabled"); 
 
//添加"current"類
 div.classList.add("current"); 
 
//切換"user"類
 div.classList.toggle("user"); 
 
//確定元素中是否包含既定的類名
 if (div.classList.contains("bd") && !div.classList.contains("disabled")){
     //執行操作 
}
 
//迭代類名
 for (var i=0, len=div.classList.length; i < len; i++){
     doSomething(div.classList[i]);
 } 
 

有了 classList 屬性,除非你需要全部刪除所有類名,或者完全重寫元素的 class 屬性,否則也 就用不到 className 屬性了。 支持 classList 屬性的瀏覽器有 Firefox 3.6+和 Chrome。 

焦點管理 

HTML5也添加了輔助管理 DOM焦點的功能。首先就是 document.activeElement 屬性,這個 屬性始終會引用 DOM中當前獲得了焦點的元素。元素獲得焦點的方式有頁面加載、用戶輸入(通常是 通過按 Tab 鍵)和在代碼中調用 focus()方法。來看幾個例子。 

var button = document.getElementById("myButton");
 button.focus();
 alert(document.activeElement === button);   //true 

默認情況下,文檔剛剛加載完成時,document.activeElement 中保存的是 document.body 元 素的引用。文檔加載期間,document.activeElement 的值爲 null。 

另外就是新增了 document.hasFocus()方法,這個方法用於確定文檔是否獲得了焦點。 

var button = document.getElementById("myButton"); 
button.focus();
 alert(document.hasFocus());  //true 

通過檢測文檔是否獲得了焦點,可以知道用戶是不是正在與頁面交互。 查詢文檔獲知哪個元素獲得了焦點,以及確定文檔是否獲得了焦點,這兩個功能重要的用途是提 高 Web 應用的無障礙性。無障礙 Web 應用的一個主要標誌就是恰當的焦點管理,而確切地知道哪個元 素獲得了焦點是一個極大的進步,至少我們不用再像過去那樣靠猜測了。 實現了這兩個屬性的瀏覽器的包括 IE 4+、Firefox 3+、Safari 4+、Chrome和 Opera 8+。 

HTMLDocument的變化 

HTML5擴展了 HTMLDocument,增加了新的功能。與 HTML5中新增的其他 DOM擴展類似,這些 變化同樣基於那些已經得到很多瀏覽器完美支持的專有擴展。所以,儘管這些擴展被寫入標準的時間相 對不長,但很多瀏覽器很早就已經支持這些功能了。

1. readyState 屬性 

IE4早爲 document 對象引入了 readyState 屬性。然後,其他瀏覽器也都陸續添加這個屬性, 終 HTML5把這個屬性納入了標準當中。Document 的 readyState 屬性有兩個可能的值: 

  •  loading,正在加載文檔;
  •  complete,已經加載完文檔。 

使用 document.readyState 的恰當方式,就是通過它來實現一個指示文檔已經加載完成的指 示器。在這個屬性得到廣泛支持之前,要實現這樣一個指示器,必須藉助 onload 事件處理程序設置一 個標籤,表明文檔已經加載完畢。document.readyState 屬性的基本用法如下。 

if (document.readyState == "complete"){
      //執行操作
 } 

支持 readyState 屬性的瀏覽器有 IE4+、Firefox 3.6+、Safari、Chrome和 Opera 9+。 

2. 兼容模式 

自從 IE6開始區分渲染頁面的模式是標準的還是混雜的,檢測頁面的兼容模式就成爲瀏覽器的必要 功能。IE爲此給 document 添加了一個名爲 compatMode 的屬性,這個屬性就是爲了告訴開發人員瀏 覽器採用了哪種渲染模式。就像下面例子中所展示的那樣,在標準模式下,document.compatMode 的 值等於"CSS1Compat",而在混雜模式下,document.compatMode 的值等於"BackCompat"。 

if (document.compatMode == "CSS1Compat"){
     alert("Standards mode");
 } else {
     alert("Quirks mode");
 } 
 


 後來,陸續實現這個屬性的瀏覽器有 Firefox、Safari 3.1+、Opera 和 Chrome。終,HTML5 也把 這個屬性納入標準,對其實現做出了明確規定。 

3. head 屬性 

作爲對 document.body 引用文檔的<body>元素的補充,HTML5新增了 document.head 屬性, 引用文檔的<head>元素。要引用文檔的<head>元素,可以結合使用這個屬性和另一種後備方法。 

var head = document.head || document.getElementsByTagName("head")[0]; 

如果可用,就使用 document.head,否則仍然使用 getElementsByTagName()方法。 實現 document.head 屬性的瀏覽器包括 Chrome和 Safari 5。  

字符集屬性 

HTML5新增了幾個與文檔字符集有關的屬性。其中,charset 屬性表示文檔中實際使用的字符集, 也可以用來指定新字符集。默認情況下,這個屬性的值爲"UTF-16",但可以通過<meta>元素、響應頭 部或直接設置 charset 屬性修改這個值。來看一個例子。 
 

alert(document.charset); //"UTF-16"
 document.charset = "UTF-8"; 
 

另一個屬性是 defaultCharset,表示根據默認瀏覽器及操作系統的設置,當前文檔默認的字符集 應該是什麼。如果文檔沒有使用默認的字符集,那 charset 和 defaultCharset 屬性的值可能會不一 樣,例如: 

if (document.charset != document.defaultCharset){
      alert("Custom character set being used.");
 } 

通過這兩個屬性可以得到文檔使用的字符編碼的具體信息,也能對字符編碼進行準確地控制。運行 適當的情況下,可以保證用戶正常查看頁面或使用應用。 

支持 document.charset 屬性的瀏覽器有 IE、Firefox、Safari、Opera 和 Chrome。支持 document.defaultCharset 屬性的瀏覽器有 IE、Safari和 Chrome。 

自定義數據屬性 

HTML5規定可以爲元素添加非標準的屬性,但要添加前綴 data-,目的是爲元素提供與渲染無關的 信息,或者提供語義信息。這些屬性可以任意添加、隨便命名,只要以 data-開頭即可。來看一個例子。
 

<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div> 


 添加了自定義屬性之後,可以通過元素的 dataset 屬性來訪問自定義屬性的值。dataset 屬性的 值是 DOMStringMap 的一個實例,也就是一個名值對兒的映射。在這個映射中,每個 data-name 形式 的屬性都會有一個對應的屬性,只不過屬性名沒有 data-前綴(比如,自定義屬性是 data-myname, 那映射中對應的屬性就是 myname)。還是看一個例子吧。 

//本例中使用的方法僅用於演示 
 
var div = document.getElementById("myDiv"); 
 
//取得自定義屬性的值
 var appId = div.dataset.appId; var myName = div.dataset.myname; 
 
//設置值
 div.dataset.appId = 23456; div.dataset.myname = "Michael"; 
 
//有沒有"myname"值呢?
 if (div.dataset.myname){ 
  alert("Hello, " + div.dataset.myname);
 } 
 

如果需要給元素添加一些不可見的數據以便進行其他處理,那就要用到自定義數據屬性。在跟蹤鏈 接或混搭應用中,通過自定義數據屬性能方便地知道點擊來自頁面中的哪個部分。 在編寫本書時,支持自定義數據屬性的瀏覽器有 Firefox 6+和 Chrome。 

插入標記 

雖然 DOM爲操作節點提供了細緻入微的控制手段,但在需要給文檔插入大量新 HTML標記的情況 下,通過 DOM操作仍然非常麻煩,因爲不僅要創建一系列 DOM節點,而且還要小心地按照正確的順 序把它們連接起來。相對而言,使用插入標記的技術,直接插入 HTML 字符串不僅更簡單,速度也更 快。以下與插入標記相關的 DOM擴展已經納入了 HTML5規範。 

1. innerHTML 屬性 

在讀模式下,innerHTML 屬性返回與調用元素的所有子節點(包括元素、註釋和文本節點)對應 的 HTML標記。在寫模式下,innerHTML 會根據指定的值創建新的 DOM樹,然後用這個 DOM樹完全 替換調用元素原先的所有子節點。下面是一個例子。

 

<div id="content">
      <p>This is a <strong>paragraph</strong>with a list following it.</p>
     <ul>
         <li>Item 1</li>
         <li>Item 2</li>
         <li>Item 3</li>
     </ul>
 </div> 

對於上面的<div>元素來說,它的 innerHTML 屬性會返回如下字符串

<p>This is a <strong>paragraph</strong> with a list following it.</p>
  <ul>
     <li>Item 1</li>
     <li>Item 2</li>
     <li>Item 3</li>
 </ul> 
 

但是,不同瀏覽器返回的文本格式會有所不同。IE和 Opera會將所有標籤轉換爲大寫形式,而 Safari、 Chrome和 Firefox則會原原本本地按照原先文檔中(或指定這些標籤時)的格式返回 HTML,包括空格 和縮進。不要指望所有瀏覽器返回的 innerHTML 值完全相同。 在寫模式下,innerHTML 的值會被解析爲 DOM子樹,替換調用元素原來的所有子節點。因爲它的 值被認爲是 HTML,所以其中的所有標籤都會按照瀏覽器處理 HTML 的標準方式轉換爲元素(同樣, 這裏的轉換結果也因瀏覽器而異)。如果設置的值僅是文本而沒有 HTML標籤,那麼結果就是設置純文 本,如下所示。 
 

div.innerHTML = "Hello world!"; 

爲 innerHTML 設置的包含 HTML的字符串值與解析後 innerHTML 的值大不相同。來看下面的 例子。 

div.innerHTML = "Hello & welcome, <b>\"reader\"!</b>"; 

以上操作得到的結果如下

<div id="content">Hello &amp; welcome, <b>&quot;reader&quot;!</b></div> 

設置了 innerHTML 之後,可以像訪問文檔中的其他節點一樣訪問新創建的節點。 

爲innerHTML設置HTML字符串後,瀏覽器會將這個字符串解析爲相應的DOM 樹。因此設置了 innerHTML 之後,再從中讀取 HTML字符串,會得到與設置時不一 樣的結果。原因在於返回的字符串是根據原始 HTML字符串創建的 DOM樹經過序列 化之後的結果。 

使用 innerHTML 屬性也有一些限制。比如,在大多數瀏覽器中,通過 innerHTML 插入<script> 元素並不會執行其中的腳本。IE8 及更早版本是唯一能在這種情況下執行腳本的瀏覽器,但必須滿足一 些條件。一是必須爲<script>元素指定 defer 屬性,二是<script>元素必須位於(微軟所謂的)“有 作用域的元素”(scoped element)之後。<script>元素被認爲是“無作用域的元素”(NoScope element), 也就是在頁面中看不到的元素,與<style>元素或註釋類似。如果通過 innerHTML 插入的字符串開頭 就是一個“無作用域的元素”,那麼 IE會在解析這個字符串前先刪除該元素。換句話說,以下代碼達不 到目的:

div.innerHTML = "<script defer>alert('hi');<\/script>"; //無效 

此時,innerHTML 字符串一開始(而且整個)就是一個“無作用域的元素”,所以這個字符串會變 成空字符串。如果想插入這段腳本,必須在前面添加一個“有作用域的元素”,可以是一個文本節點, 也可以是一個沒有結束標籤的元素如<input>。例如,下面這幾行代碼都可以正常執行: 

div.innerHTML = "_<script defer>alert('hi');<\/script>";
 div.innerHTML = "<div>&nbsp;</div><script defer>alert('hi');<\/script>";
 div.innerHTML = "<input type=\"hidden\"><script defer>alert('hi');<\/script>"; 

第一行代碼會在<script>元素前插入一個文本節點。事後,爲了不影響頁面顯示,你可能需要移 除這個文本節點。第二行代碼採用的方法類似,只不過使用的是一個包含非換行空格的<div>元素。如 果僅僅插入一個空的<div>元素,還是不行;必須要包含一點兒內容,瀏覽器纔會創建文本節點。同樣, 爲了不影響頁面佈局,恐怕還得移除這個節點。第三行代碼使用的是一個隱藏的<input>域,也能達到 相同的效果。不過,由於隱藏的<input>域不影響頁面佈局,因此這種方式在大多數情況下都是首選。

大多數瀏覽器都支持以直觀的方式通過 innerHTML 插入<style>元素,例如

div.innerHTML = "<style type=\"text/css\">body {background-color: red; }</style>"; 

但在 IE8 及更早版本中,<style>也是一個“沒有作用域的元素”,因此必須像下面這樣給它前置 一個“有作用域的元素”: 

div.innerHTML = "_<style type=\"text/css\">body {background-color: red; }</style>"; 
div.removeChild(div.firstChild); 

並不是所有元素都支持 innerHTML 屬性。不支持 innerHTML 的元素有:<col>、<colgroup>、 <frameset>、<head>、<html>、<style>、<table>、<tbody>、<thead>、<tfoot>和<tr>。此 外,在 IE8及更早版本中,<title>元素也沒有 innerHTML 屬性。 

Firefox對在內容類型爲application/xhtml+xml的XHTML文檔中設置innerHTML 有嚴格的限制。在 XHTML 文檔中使用 innerHTML 時,XHTML代碼必須完全符合 要求。如果代碼格式不正確,設置 innerHTML 將會靜默地失敗。 

無論什麼時候,只要使用 innerHTML 從外部插入 HTML,都應該首先以可靠的方式處理 HTML。 IE8爲此提供了 window.toStaticHTML()方法,這個方法接收一個參數,即一個 HTML字符串;返回 一個經過無害處理後的版本——從源 HTML 中刪除所有腳本節點和事件處理程序屬性。下面就是一個 例子: 

var text = "<a href=\"#\" οnclick=\"alert('hi')\">Click Me</a>";
 var sanitized = window.toStaticHTML(text);
       //Internet Explorer 8 only 
alert(sanitized);   //"<a href=\"#\">Click Me</a>" 

這個例子將一個 HTML 鏈接字符串傳給了 toStaticHTML()方法,得到的無害版本中去掉了 onclick 屬性。雖然目前只有 IE8原生支持這個方法,但我們還是建議讀者在通過 innerHTML 插入代 碼之前,儘可能先手工檢查一下其中的文本內容。 

2. outerHTML 屬性 

在讀模式下,outerHTML 返回調用它的元素及所有子節點的 HTML標籤。在寫模式下,outerHTML 會根據指定的 HTML字符串創建新的 DOM子樹,然後用這個 DOM子樹完全替換調用元素。下面是一 個例子。 
 

<div id="content">
     <p>This is a <strong>paragraph</strong> with a list following it.</p>
     <ul>
         <li>Item 1</li>
         <li>Item 2</li>
         <li>Item 3</li>
     </ul>
 </div> 

如果在<div>元素上調用 outerHTML,會返回與上面相同的代碼,包括<div>本身。不過,由於瀏 覽器解析和解釋 HTML 標記的不同,結果也可能會有所不同。(這裏的不同與使用 innerHTML 屬性時 存在的差異性質是一樣的。) 使用 outerHTML 屬性以下面這種方式設置值: 

div.outerHTML = "<p>This is a paragraph.</p>"; 

這行代碼完成的操作與下面這些 DOM腳本代碼一樣: 

var p = document.createElement("p");
 p.appendChild(document.createTextNode("This is a paragraph."));
 div.parentNode.replaceChild(p, div); 

結果,就是新創建的<p>元素會取代 DOM樹中的<div>元素。 支持 outerHTML 屬性的瀏覽器有 IE4+、Safari 4+、Chrome和 Opera 8+。Firefox 7及之前版本都不 支持 outerHTML 屬性。 

3. insertAdjacentHTML()方法 

插入標記的後一個新增方式是insertAdjacentHTML()方法。這個方法早也是在IE中出現的, 它接收兩個參數:插入位置和要插入的 HTML文本。第一個參數必須是下列值之一: 

  •     "beforebegin",在當前元素之前插入一個緊鄰的同輩元素;
  •  "afterbegin",在當前元素之下插入一個新的子元素或在第一個子元素之前再插入新的子元素;
  •     "beforeend",在當前元素之下插入一個新的子元素或在後一個子元素之後再插入新的子元素;
  •   "afterend",在當前元素之後插入一個緊鄰的同輩元素。 

注意,這些值都必須是小寫形式。第二個參數是一個 HTML字符串(與 innerHTML 和 outerHTML 的值相同),如果瀏覽器無法解析該字符串,就會拋出錯誤。以下是這個方法的基本用法示例。 


 
 
//作爲前一個同輩元素插入
 element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>"); 
 
//作爲第一個子元素插入
 element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>"); 
 
//作爲最後一個子元素插入
 element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>"); 
 
//作爲後一個同輩元素插入
 element.insertAdjacentHTML("afterend", "<p>Hello world!</p>");

支持 insertAdjacentHTML()方法的瀏覽器有 IE、Firefox 8+、Safari、Opera和 Chrome。 

4. 內存與性能問題 

使用本節介紹的方法替換子節點可能會導致瀏覽器的內存佔用問題,尤其是在 IE 中,問題更加明 顯。在刪除帶有事件處理程序或引用了其他 JavaScript 對象子樹時,就有可能導致內存佔用問題。假設 某個元素有一個事件處理程序(或者引用了一個 JavaScript對象作爲屬性),在使用前述某個屬性將該元 素從文檔樹中刪除後,元素與事件處理程序(或 JavaScript 對象)之間的綁定關係在內存中並沒有一併 刪除。如果這種情況頻繁出現,頁面佔用的內存數量就會明顯增加。因此,在使用 innerHTML、 outerHTML 屬性和 insertAdjacentHTML()方法時,好先手工刪除要被替換的元素的所有事件處理 程序和 JavaScript對象屬性(第 13章將進一步討論事件處理程序)。 不過,使用這幾個屬性——特別是使用 innerHTML,仍然還是可以爲我們提供很多便利的。一般 來說,在插入大量新 HTML標記時,使用 innerHTML 屬性與通過多次 DOM操作先創建節點再指定它 們之間的關係相比,效率要高得多。這是因爲在設置 innerHTML 或 outerHTML 時,就會創建一個HTML 解析器。這個解析器是在瀏覽器級別的代碼(通常是 C++編寫的)基礎上運行的,因此比執行 JavaScript 快得多。不可避免地,創建和銷燬 HTML解析器也會帶來性能損失,所以好能夠將設置 innerHTML 或 outerHTML 的次數控制在合理的範圍內。例如,下列代碼使用 innerHTML 創建了很多列表項: 

for (var i=0, len=values.length; i < len; i++){
      ul.innerHTML += "<li>" + values[i] + "</li>"; //要避免這種頻繁操作!!
 } 
 

這種每次循環都設置一次 innerHTML 的做法效率很低。而且,每次循環還要從 innerHTML 中讀 取一次信息,就意味着每次循環要訪問兩次 innerHTML。好的做法是單獨構建字符串,然後再一次 性地將結果字符串賦值給 innerHTML,像下面這樣: 
 

var itemsHtml = ""; 
for (var i=0, len=values.length; i < len; i++){
     itemsHtml += "<li>" + values[i] + "</li>";
 }
 ul.innerHTML = itemsHtml; 

這個例子的效率要高得多,因爲它只對 innerHTML 執行了一次賦值操作。 

scrollIntoView()方法

如何滾動頁面也是 DOM規範沒有解決的一個問題。爲了解決這個問題,瀏覽器實現了一些方法, 以方便開發人員更好地控制頁面滾動。在各種專有方法中,HTML5終選擇了 scrollIntoView()作 爲標準方法。 scrollIntoView()可以在所有 HTML 元素上調用,通過滾動瀏覽器窗口或某個容器元素,調用 元素就可以出現在視口中。如果給這個方法傳入 true 作爲參數,或者不傳入任何參數,那麼窗口滾動 之後會讓調用元素的頂部與視口頂部儘可能平齊。如果傳入 false 作爲參數,調用元素會儘可能全部 出現在視口中,(可能的話,調用元素的底部會與視口頂部平齊。)不過頂部不一定平齊,例如: 

 
//讓元素可見
 document.forms[0].scrollIntoView(); 


 當頁面發生變化時,一般會用這個方法來吸引用戶的注意力。實際上,爲某個元素設置焦點也會導 致瀏覽器滾動並顯示出獲得焦點的元素。 支持 scrollIntoView()方法的瀏覽器有 IE、Firefox、Safari和 Opera。 

專有擴展

雖然所有瀏覽器開發商都知曉堅持標準的重要性,但在發現某項功能缺失時,這些開發商都會一如 既往地向 DOM中添加專有擴展,以彌補功能上的不足。表面上看,這種各行其事的做法似乎不太好, 但實際上專有擴展爲 Web 開發領域提供了很多重要的功能,這些功能終都在 HTML5規範中得到了標 準化。

即便如此,仍然還有大量專有的 DOM擴展沒有成爲標準。但這並不是說它們將來不會被寫進標準, 而只是說在編寫本書的時候,它們還是專有功能,而且只得到了少數瀏覽器的支持。 

 文檔模式 

IE8引入了一個新的概念叫“文檔模式”(document mode)。頁面的文檔模式決定了可以使用什麼功 能。換句話說,文檔模式決定了你可以使用哪個級別的 CSS,可以在 JavaScript中使用哪些 API,以及 如何對待文檔類型(doctype)。到了 IE9,總共有以下 4種文檔模式。

  •      IE5:以混雜模式渲染頁面(IE5的默認模式就是混雜模式)。IE8 及更高版本中的新功能都無法 使用。
  •  IE7:以 IE7標準模式渲染頁面。IE8及更高版本中的新功能都無法使用。
  •  IE8:以 IE8標準模式渲染頁面。IE8中的新功能都可以使用,因此可以使用 Selectors API、更多 CSS2級選擇符和某些 CSS3功能,還有一些 HTML5的功能。不過 IE9中的新功能無法使用。
  •  IE9:以 IE9標準模式渲染頁面。IE9中的新功能都可以使用,比如 ECMAScript 5、完整的 CSS3 以及更多 HTML5功能。這個文檔模式是高級的模式。 

要理解 IE8及更高版本的工作原理,必須理解文檔模式。 要強制瀏覽器以某種模式渲染頁面,可以使用 HTTP頭部信息 X-UA-Compatible,或通過等價的 <meta>標籤來設置: 


 <meta http-equiv="X-UA-Compatible" content="IE=IEVersion"> 

注意,這裏 IE 的版本(IEVersion)有以下一些不同的值,而且這些值並不一定與上述 4 種文檔 模式對應

  •       Edge:始終以新的文檔模式來渲染頁面。忽略文檔類型聲明。對於 IE8,始終保持以 IE8 標 準模式渲染頁面。對於 IE9,則以 IE9標準模式渲染頁面。
  •   EmulateIE9:如果有文檔類型聲明,則以 IE9標準模式渲染頁面,否則將文檔模式設置爲 IE5。
  •   EmulateIE8:如果有文檔類型聲明,則以 IE8標準模式渲染頁面,否則將文檔模式設置爲 IE5。
  •   EmulateIE7:如果有文檔類型聲明,則以 IE7標準模式渲染頁面,否則將文檔模式設置爲 IE5。
  •   9:強制以 IE9標準模式渲染頁面,忽略文檔類型聲明。
  •  8:強制以 IE8標準模式渲染頁面,忽略文檔類型聲明。
  •  7:強制以 IE7標準模式渲染頁面,忽略文檔類型聲明。
  •  5:強制將文檔模式設置爲 IE5,忽略文檔類型聲明。

比如,要想讓文檔模式像在 IE7中一樣,可以使用下面這行代碼: 

<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"> 

如果不打算考慮文檔類型聲明,而直接使用 IE7標準模式,那麼可以使用下面這行代碼: 


 <meta http-equiv="X-UA-Compatible" content="IE=7"> 

沒有規定說必須在頁面中設置 X-UA-Compatible。默認情況下,瀏覽器會通過文檔類型聲明來確 定是使用佳的可用文檔模式,還是使用混雜模式。 通過 document.documentMode 屬性可以知道給定頁面使用的是什麼文檔模式。這個屬性是 IE8 中新增的,它會返回使用的文檔模式的版本號(在 IE9中,可能返回的版本號爲 5、7、8、9): 
 
var mode = document.documentMode; 
 知道頁面採用的是什麼文檔模式,有助於理解頁面的行爲方式。無論在什麼文檔模式下,都可以訪 問這個屬性。 

children屬性 

由於 IE9之前的版本與其他瀏覽器在處理文本節點中的空白符時有差異,因此就出現了 children 屬性。這個屬性是 HTMLCollection 的實例,只包含元素中同樣還是元素的子節點。除此之外, children 屬性與 childNodes 沒有什麼區別,即在元素只包含元素子節點時,這兩個屬性的值相同。 下面是訪問 children 屬性的示例代碼: 
 

var childCount = element.children.length;
 var firstChild = element.children[0]; 

支持 children 屬性的瀏覽器有 IE5、Firefox 3.5、Safari 2(但有 bug)、Safari 3(完全支持)、Opera8 和 Chrome(所有版本)。IE8及更早版本的 children 屬性中也會包含註釋節點,但 IE9之後的版本則 只返回元素節點。 .

 contains()方法 

在實際開發中,經常需要知道某個節點是不是另一個節點的後代。IE爲此率先引入了 contains() 方法,以便不通過在 DOM文檔樹中查找即可獲得這個信息。調用 contains()方法的應該是祖先節點, 也就是搜索開始的節點,這個方法接收一個參數,即要檢測的後代節點。如果被檢測的節點是後代節點, 該方法返回 true;否則,返回 false。以下是一個例子: 

alert(document.documentElement.contains(document.body));    //true 

這個例子測試了<body>元素是不是<html>元素的後代,在格式正確的 HTML頁面中,以上代碼返 回 true。支持 contains()方法的瀏覽器有 IE、Firefox 9+、Safari、Opera和 Chrome。 使用 DOM Level 3 compareDocumentPosition()也能夠確定節點間的關係。支持這個方法的瀏 覽器有 IE9+、Firefox、Safari、Opera 9.5+和 Chrome。如前所述,這個方法用於確定兩個節點間的關係, 返回一個表示該關係的位掩碼( bitmask)。下表列出了這個位掩碼的值。 

爲模仿 contains()方法,應該關注的是掩碼 16。可以對 compareDocumentPosition()的結果 執行按位與,以確定參考節點(調用 compareDocumentPosition()方法的當前節點)是否包含給定 的節點(傳入的節點)。來看下面的例子: 

var result = document.documentElement.compareDocumentPosition(document.body);
 alert(!!(result & 16)); 

執行上面的代碼後,結果會變成 20(表示“居後”的 4加上表示“被包含”的 16)。對掩碼 16執 行按位操作會返回一個非零數值,而兩個邏輯非操作符會將該數值轉換成布爾值。 使用一些瀏覽器及能力檢測,就可以寫出如下所示的一個通用的 contains 函數: 

function contains(refNode, otherNode){
      if (typeof refNode.contains == "function" &&
             (!client.engine.webkit || client.engine.webkit >= 522)){
         return refNode.contains(otherNode);
     } else if (typeof refNode.compareDocumentPosition == "function"){
         return !!(refNode.compareDocumentPosition(otherNode) & 16);
     } else {
         var node = otherNode.parentNode;
         do {
             if (node === refNode){
                 return true;
             } else {
                 node = node.parentNode;
             }
         } while (node !== null);
   return false;
     }
 } 

這個函數組合使用了三種方式來確定一個節點是不是另一個節點的後代。函數的第一個參數是參考 節點,第二個參數是要檢查的節點。在函數體內,首先檢測 refNode 中是否存在 contains()方法(能 力檢測)。這一部分代碼還檢查了當前瀏覽器所用的 WebKit 版本號。如果方法存在而且不是 WebKit (!client.engine.webkit),則繼續執行代碼。

否則,如果瀏覽器是 WebKit 且至少是 Safari 3(WebKit 版本號爲 522 或更高),那麼也可以繼續執行代碼。在 WebKit 版本號小於 522 的 Safari 瀏覽器中, contains()方法不能正常使用。 接下來檢查是否存在 compareDocumentPosition()方法,而函數的後一步則是自 otherNode 開始向上遍歷 DOM結構,以遞歸方式取得 parentNode,並檢查其是否與 refNode 相等。在文檔樹的 頂端,parentNode 的值等於 null,於是循環結束。這是針對舊版本 Safari設計的一個後備策略。 

插入文本 

前面介紹過,IE原來專有的插入標記的屬性 innerHTML 和 outerHTML 已經被 HTML5納入規範。 但另外兩個插入文本的專有屬性則沒有這麼好的運氣。這兩個沒有被 HTML5看中的屬性是 innerText 和 outerText。

1. innerText 屬性 

通過 innertText 屬性可以操作元素中包含的所有文本內容,包括子文檔樹中的文本。在通過 innerText 讀取值時,它會按照由淺入深的順序,將子文檔樹中的所有文本拼接起來。在通過 innerText 寫入值時,結果會刪除元素的所有子節點,插入包含相應文本值的文本節點。來看下面這 個 HTML代碼示例。 

<div id="content">
     <p>This is a <strong>paragraph</strong> with a list following it.</p>
     <ul>
         <li>Item 1</li>
         <li>Item 2</li>
         <li>Item 3</li>
     </ul>
 </div> 
 

對於這個例子中的<div>元素而言,其 innerText 屬性會返回下列字符串

This is a paragraph with a list following it. 
 Item 1
 Item 2
 Item 3 
 

由於不同瀏覽器處理空白符的方式不同,因此輸出的文本可能會也可能不會包含原始 HTML 代碼 中的縮進。 使用 innerText 屬性設置這個<div>元素的內容,則只需一行代碼: 

div.innerText = "Hello world!"; 

執行這行代碼後,頁面的 HTML代碼就會變成如下所示。 

<div id="content">Hello world!</div> 

設置innerText屬性移除了先前存在的所有子節點,完全改變了DOM子樹。此外,設置innerText 屬性的同時,也對文本中存在的 HTML 語法字符(小於號、大於號、引號及和號)進行了編碼。再看 一個例子。 
 

div.innerText = "Hello & welcome, <b>\"reader\"!</b>"; 

運行以上代碼之後,會得到如下所示的結果。 

<div id="content">Hello &amp; welcome, &lt;b&gt;&quot;reader&quot;!&lt;/b&gt;</div> 

設置 innerText 永遠只會生成當前節點的一個子文本節點,而爲了確保只生成一個子文本節點, 就必須要對文本進行 HTML編碼。利用這一點,可以通過 innerText 屬性過濾掉 HTML標籤。方法是 將 innerText 設置爲等於 innerText,這樣就可以去掉所有 HTML標籤,比如: 
 

div.innerText = div.innerText; 

執行這行代碼後,就用原來的文本內容替換了容器元素中的所有內容(包括子節點,因而也就去掉 了 HTML標籤)。 支持 innerText 屬性的瀏覽器包括 IE4+、Safari 3+、Opera 8+和 Chrome。Firefox 雖然不支持 innerText,但支持作用類似的 textContent 屬性。textContent 是 DOM Level 3規定的一個屬性,其 他支持 textContent 屬性的瀏覽器還有 IE9+、Safari 3+、Opera 10+和 Chrome。爲了確保跨瀏覽器兼 容,有必要編寫一個類似於下面的函數來檢測可以使用哪個屬性。 
 

function getInnerText(element){
     return (typeof element.textContent == "string") ?
         element.textContent : element.innerText;
 } 
 
function setInnerText(element, text){
     if (typeof element.textContent == "string"){
         element.textContent = text;
     } else {
         element.innerText = text;
     }
 } 
 

這兩個函數都接收一個元素作爲參數,然後檢查這個元素是不是有 textContent 屬性。如果有, 那麼 typeof element.textContent 應該是"string";如果沒有,那麼這兩個函數就會改爲使用 innerText。可以像下面這樣調用這兩個函數。 

setInnerText(div, "Hello world!");
 alert(getInnerText(div));    //"Hello world!" 

使用這兩個函數可以確保在不同的瀏覽器中使用正確的屬性。 

實際上,innerText 與 textContent 返回的內容並不完全一樣。比如, innerText 會忽略行內的樣式和腳本,而 textContent 則會像返回其他文本一樣返 回行內的樣式和腳本代碼。避免跨瀏覽器兼容問題的最佳途徑,就是從不包含行內樣 式或行內腳本的 DOM子樹副本或 DOM片段中讀取文本。 

 outerText 屬性 

除了作用範圍擴大到了包含調用它的節點之外,outerText 與 innerText 基本上沒有多大區別。 在讀取文本值時,outerText 與 innerText 的結果完全一樣。但在寫模式下,outerText 就完全不 同了:outerText 不只是替換調用它的元素的子節點,而是會替換整個元素(包括子節點)。比如: 
 

div.outerText = "Hello world!"; 

這行代碼實際上相當於如下兩行代碼: 

var text = document.createTextNode("Hello world!"); 
div.parentNode.replaceChild(text, div); 

本質上,新的文本節點會完全取代調用 outerText 的元素。此後,該元素就從文檔中被刪除,無 法訪問。 支持 outerText 屬性的瀏覽器有 IE4+、Safari 3+、Opera 8+和 Chrome。由於這個屬性會導致調用 它的元素不存在,因此並不常用。我們也建議讀者儘可能不要使用這個屬性。 

滾動


 如前所述,HTML5 之前的規範並沒有就與頁面滾動相關的 API 做出任何規定。但 HTML5 在將 scrollIntoView()納入規範之後,仍然還有其他幾個專有方法可以在不同的瀏覽器中使用。下面列出 的幾個方法都是對 HTMLElement 類型的擴展,因此在所有元素中都可以調用。 

  •     scrollIntoViewIfNeeded(alignCenter):只在當前元素在視口中不可見的情況下,才滾 動瀏覽器窗口或容器元素,終讓它可見。如果當前元素在視口中可見,這個方法什麼也不做。 如果將可選的 alignCenter 參數設置爲 true,則表示儘量將元素顯示在視口中部(垂直方向)。 Safari和 Chrome實現了這個方法。
  •  scrollByLines(lineCount):將元素的內容滾動指定的行高,lineCount 值可以是正值, 也可以是負值。Safari和 Chrome實現了這個方法。
  •  scrollByPages(pageCount):將元素的內容滾動指定的頁面高度,具體高度由元素的高度決 定。Safari和 Chrome實現了這個方法。 

希望大家要注意的是,scrollIntoView()和 scrollIntoViewIfNeeded()的作用對象是元素的 容器,而 scrollByLines()和 scrollByPages()影響的則是元素自身。下面還是來看幾個示例吧。 

//將頁面主體滾動 5 行
 document.body.scrollByLines(5); 
//在當前元素不可見的時候,讓它進入瀏覽器的視口
 document.images[0].scrollIntoViewIfNeeded(); 
 
//將頁面主體往回滾動 1 頁
 document.body.scrollByPages(-1); 

由於 scrollIntoView()是唯一一個所有瀏覽器都支持的方法,因此還是這個方法常用。 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章