《JavaScript 闖關記》之 DOM(上)

DOM(文檔對象模型)是針對 HTML 和 XML 文檔的一個 API。DOM 描繪了一個層次化的節點樹,容許開發人員添加、移除和修改頁面的某一部分。javascript

節點層次

DOM 能夠將任何 HTML 或 XML 文檔描繪成一個由多層節點構成的結構。節點分爲幾種不一樣的類型,每種類型分別表示文檔中不一樣的信息及(或)標記。每一個節點都擁有各自的特色、數據和方法,另外也與其餘節點存在某種關係。節點之間的關係構成了層次,而全部頁面標記則表現爲一個以特定節點爲根節點的樹形結構。如下面的 HTML 爲例:html

Sample Page
    
    
        

Hello World!

複製代碼

能夠將這個簡單的 HTML 文檔表示爲一個層次結構,如圖下圖所示。java

在這個例子中,文檔元素是文檔的最外層元素,文檔中的其餘全部元素都包含在文檔元素中。每一個文檔只能有一個文檔元素。node

每一段標記均可以經過樹中的一個節點來表示:HTML 元素經過元素節點表示,特性(attribute)經過特性節點表示,文檔類型經過文檔類型節點表示,而註釋則經過註釋節點表示。總共有12種節點類型,這些類型都繼承自一個基類型。ios

Node 類型

DOM1 級定義了一個 Node 接口,該接口將由 DOM 中的全部節點類型實現。這個 Node 接口在 JavaScript 中是做爲 Node 類型實現的;除了 IE 以外,在其餘全部瀏覽器中均可以訪問到這個類型。JavaScript 中的全部節點類型都繼承自 Node 類型,所以全部節點類型都共享着相同的基本屬性和方法。git

每一個節點都有一個 nodeType 屬性,用於代表節點的類型。節點類型由在 Node 類型中定義的下列12個數值常量來表示,任何節點類型必居其一:github

  • Node.ELEMENT_NODE(1);
  • Node.ATTRIBUTE_NODE(2);
  • Node.TEXT_NODE(3);
  • Node.CDATA_SECTION_NODE(4);
  • Node.ENTITY_REFERENCE_NODE(5);
  • Node.ENTITY_NODE(6);
  • Node.PROCESSING_INSTRUCTION_NODE(7);
  • Node.COMMENT_NODE(8);
  • Node.DOCUMENT_NODE(9);
  • Node.DOCUMENT_TYPE_NODE(10);
  • Node.DOCUMENT_FRAGMENT_NODE(11);
  • Node.NOTATION_NODE(12)。

經過比較上面這些常量,能夠很容易地肯定節點的類型,例如:api

if (someNode.nodeType == Node.ELEMENT_NODE){   // 在IE中無效
    console.log("Node is an element.");
}複製代碼

這個例子比較了 someNode.nodeTypeNode.ELEMENT_NODE 常量。若是兩者相等,則意味着 someNode 確實是一個元素。然而,因爲 IE 沒有公開 Node 類型的構造函數,所以上面的代碼在 IE 中會致使錯誤。爲了確保跨瀏覽器兼容,最好仍是將 nodeType 屬性與數字值進行比較,以下所示:數組

if (someNode.nodeType == 1){    // 適用於全部瀏覽器
    console.log("Node is an element.");
}複製代碼

並非全部節點類型都受到 Web 瀏覽器的支持。開發人員最經常使用的就是元素和文本節點。瀏覽器

Node 屬性概述

Node 經常使用屬性主要有如下10個,接下來咱們會着重講解部分屬性。

  • nodeType:顯示節點的類型
  • nodeName:顯示節點的名稱
  • nodeValue:顯示節點的值
  • attributes:獲取一個屬性節點
  • firstChild:表示某一節點的第一個節點
  • lastChild:表示某一節點的最後一個子節點
  • childNodes:表示所在節點的全部子節點
  • parentNode:表示所在節點的父節點
  • nextSibling:緊挨着當前節點的下一個節點
  • previousSibling:緊挨着當前節點的上一個節點

nodeNamenodeValue 屬性

要了解節點的具體信息,可使用 nodeNamenodeValue 這兩個屬性。這兩個屬性的值徹底取決於節點的類型。在使用這兩個值之前,最好是像下面這樣先檢測一下節點的類型。

if (someNode.nodeType == 1){
    value = someNode.nodeName;    // nodeName的值是元素的標籤名
}複製代碼

在這個例子中,首先檢查節點類型,看它是否是一個元素。若是是,則取得並保存 nodeName 的值。對於元素節點,nodeName 中保存的始終都是元素的標籤名,而 nodeValue 的值則始終爲 null

節點關係

文檔中全部的節點之間都存在這樣或那樣的關係。節點間的各類關係能夠用傳統的家族關係來描述,至關於把文檔樹比喻成家譜。

每一個節點都有一個 childNodes 屬性,其中保存着一個 NodeList 對象。NodeList 是一種類數組對象,用於保存一組有序的節點,能夠經過位置來訪問這些節點。請注意,雖然能夠經過方括號語法來訪問 NodeList 的值,並且這個對象也有 length 屬性,但它並非 Array 的實例。NodeList 對象的獨特之處在於,它其實是基於 DOM 結構動態執行查詢的結果,所以 DOM 結構的變化可以自動反映在 NodeList 對象中。

下面的例子展現瞭如何訪問保存在 NodeList 中的節點——能夠經過方括號,也可使用 item() 方法。

var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;複製代碼

不管使用方括號仍是使用 item() 方法都沒有問題,但使用方括號語法看起來與訪問數組類似,所以頗受一些開發人員的青睞。另外,要注意 length 屬性表示的是訪問 NodeList 的那一刻,其中包含的節點數量。

每一個節點都有一個 parentNode 屬性,該屬性指向文檔樹中的父節點。包含在 childNodes 列表中的全部節點都具備相同的父節點,所以它們的 parentNode 屬性都指向同一個節點。此外,包含在 childNodes 列表中的每一個節點相互之間都是同胞節點。經過使用列表中每一個節點的 previousSiblingnextSibling 屬性,能夠訪問同一列表中的其餘節點。列表中第一個節點的 previousSibling 屬性值爲 null,而列表中最後一個節點的 nextSibling 屬性的值一樣也爲 null,以下面的例子所示:

if (someNode.nextSibling === null){
    console.log("Last node in the parent’s childNodes list.");
} else if (someNode.previousSibling === null){
    console.log("First node in the parent’s childNodes list.");
}複製代碼

固然,若是列表中只有一個節點,那麼該節點的 nextSiblingpreviousSibling 都爲 null

父節點與其第一個和最後一個子節點之間也存在特殊關係。父節點的 firstChildlastChild 屬性分別指向其 childNodes 列表中的第一個和最後一個節點。其中,someNode.firstChild 的值始終等於 someNode.childNodes[0],而 someNode.lastChild 的值始終等於 someNode.childNodes [someNode.childNodes.length-1]。在只有一個子節點的狀況下, firstChildlastChild 指向同一個節點。若是沒有子節點,那麼 firstChildlastChild 的值均爲 null。明確這些關係可以對咱們查找和訪問文檔結構中的節點提供極大的便利。下圖形象地展現了上述關係。

在反映這些關係的全部屬性當中,childNodes 屬性與其餘屬性相比更方便一些,由於只須使用簡單的關係指針,就能夠經過它訪問文檔樹中的任何節點。另外,hasChildNodes() 也是一個很是有用的方法,這個方法在節點包含一或多個子節點的狀況下返回 true;應該說,這是比查詢 childNodes 列表的 length 屬性更簡單的方法。

全部節點都有的最後一個屬性是 ownerDocument,該屬性指向表示整個文檔的文檔節點。這種關係表示的是任何節點都屬於它所在的文檔,任何節點都不能同時存在於兩個或更多個文檔中。經過這個屬性,咱們能夠沒必要在節點層次中經過層層回溯到達頂端,而是能夠直接訪問文檔節點。

操做節點

由於關係指針都是隻讀的,因此 DOM 提供了一些操做節點的方法。其中,最經常使用的方法是 appendChild(),用於向 childNodes 列表的末尾添加一個節點。添加節點後,childNodes 的新增節點、父節點及之前的最後一個子節點的關係指針都會相應地獲得更新。更新完成後,appendChild() 返回新增的節點。來看下面的例子:

var returnedNode = someNode.appendChild(newNode);
console.log(returnedNode == newNode);         // true
console.log(someNode.lastChild == newNode);   // true複製代碼

若是傳入到 appendChild() 中的節點已是文檔的一部分了,那結果就是將該節點從原來的位置轉移到新位置。即便能夠將 DOM 樹當作是由一系列指針鏈接起來的,但任何 DOM 節點也不能同時出如今文檔中的多個位置上。所以,若是在調用 appendChild() 時傳入了父節點的第一個子節點,那麼該節點就會成爲父節點的最後一個子節點,以下面的例子所示。

// someNode 有多個子節點
var returnedNode = someNode.appendChild(someNode.firstChild);
console.log(returnedNode == someNode.firstChild);   // false
console.log(returnedNode == someNode.lastChild);    // true複製代碼

若是須要把節點放在 childNodes 列表中某個特定的位置上,而不是放在末尾,那麼可使用 insertBefore() 方法。這個方法接受兩個參數:要插入的節點和做爲參照的節點。插入節點後,被插入的節點會變成參照節點的前一個同胞節點 previousSibling,同時被方法返回。若是參照節點是 null,則 insertBefore()appendChild() 執行相同的操做,以下面的例子所示。

// 插入後成爲最後一個子節點
returnedNode = someNode.insertBefore(newNode, null);
console.log(newNode == someNode.lastChild);   // true

// 插入後成爲第一個子節點
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
console.log(returnedNode == newNode);         // true
console.log(newNode == someNode.firstChild);  // true

// 插入到最後一個子節點前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
console.log(newNode == someNode.childNodes[someNode.childNodes.length-2]); // true複製代碼

前面介紹的 appendChild()insertBefore() 方法都只插入節點,不會移除節點。而下面要介紹的 replaceChild() 方法接受的兩個參數是:要插入的節點和要替換的節點。要替換的節點將由這個方法返回並從文檔樹中被移除,同時由要插入的節點佔據其位置。來看下面的例子。

// 替換第一個子節點
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);

// 替換最後一個子節點
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);複製代碼

在使用 replaceChild() 插入一個節點時,該節點的全部關係指針都會從被它替換的節點複製過來。儘管從技術上講,被替換的節點仍然還在文檔中,但它在文檔中已經沒有了本身的位置。

若是隻想移除而非替換節點,可使用 removeChild() 方法。這個方法接受一個參數,即要移除的節點。被移除的節點將成爲方法的返回值,以下面的例子所示。

// 移除第一個子節點
var formerFirstChild = someNode.removeChild(someNode.firstChild);

// 移除最後一個子節點
var formerLastChild = someNode.removeChild(someNode.lastChild);複製代碼

與使用 replaceChild() 方法同樣,經過 removeChild() 移除的節點仍然爲文檔全部,只不過在文檔中已經沒有了本身的位置。

前面介紹的四個方法操做的都是某個節點的子節點,也就是說,要使用這幾個方法必須先取得父節點(使用 parentNode 屬性)。另外,並非全部類型的節點都有子節點,若是在不支持子節點的節點上調用了這些方法,將會致使錯誤發生。

Document 類型

JavaScript 經過 Document 類型表示文檔。在瀏覽器中,document 對象是 HTMLDocument(繼承自 Document 類型)的一個實例,表示整個 HTML 頁面。並且,document 對象是 window 對象的一個屬性,所以能夠將其做爲全局對象來訪問。Document 節點具備下列特徵:

  • nodeType 的值爲9;
  • nodeName 的值爲 "#document"
  • nodeValue 的值爲 null
  • parentNode 的值爲 null
  • ownerDocument 的值爲 null
  • 其子節點多是一個 DocumentType(最多一個)、Element(最多一個)、ProcessingInstructionComment

Document 類型能夠表示 HTML 頁面或者其餘基於 XML 的文檔。不過,最多見的應用仍是做爲 HTMLDocument 實例的 document 對象。經過這個文檔對象,不只能夠取得與頁面有關的信息,並且還能操做頁面的外觀及其底層結構。

文檔的子節點

雖然 DOM 標準規定 Document 節點的子節點能夠是DocumentTypeElementProcessingInstructionComment,但還有兩個內置的訪問其子節點的快捷方式。第一個就是documentElement 屬性,該屬性始終指向 HTML 頁面中的 html 元素。另外一個就是經過 childNodes 列表訪問文檔元素,但經過 documentElement 屬性則能更快捷、更直接地訪問該元素。如下面這個簡單的頁面爲例。

複製代碼

這個頁面在通過瀏覽器解析後,其文檔中只包含一個子節點,即 html 元素。能夠經過 documentElementchildNodes 列表來訪問這個元素,以下所示。

var html = document.documentElement;      // 取得對的引用
console.log(html === document.childNodes[0]);   // true
console.log(html === document.firstChild);      // true複製代碼

這個例子說明,documentElementfirstChildchildNodes[0] 的值相同,都指向 元素。

做爲 HTMLDocument 的實例,document 對象還有一個 body 屬性,直接指向 元素。由於開發人員常常要使用這個元素,因此 document.body 在 JavaScript 代碼中出現的頻率很是高,其用法以下。

var body = document.body;    // 取得對的引用複製代碼

全部瀏覽器都支持 document.documentElementdocument.body 屬性。

Document 另外一個可能的子節點是 DocumentType。一般將 標籤當作一個與文檔其餘部分不一樣的實體,能夠經過 doctype 屬性(在瀏覽器中是 document.doctype )來訪問它的信息。

var doctype = document.doctype;     // 取得對的引用複製代碼

瀏覽器對 document.doctype 的支持差異很大,能夠給出以下總結。

  • IE8 及以前版本:若是存在文檔類型聲明,會將其錯誤地解釋爲一個註釋並把它看成 Comment 節點;而 document.doctype 的值始終爲 null
  • IE9+ 及 Firefox:若是存在文檔類型聲明,則將其做爲文檔的第一個子節點;document.doctype 是一個 DocumentType 節點,也能夠經過 document.firstChilddocument.childNodes[0] 訪問同一個節點。
  • Safari、Chrome 和 Opera:若是存在文檔類型聲明,則將其解析,但不做爲文檔的子節點。document.doctype 是一個 DocumentType 節點,但該節點不會出如今 document.childNodes 中。

因爲瀏覽器對 document.doctype 的支持不一致,所以這個屬性的用處頗有限。

文檔信息

做爲 HTMLDocument 的一個實例,document 對象還有一些標準的 Document 對象所沒有的屬性。這些屬性提供了 document 對象所表現的網頁的一些信息。其中第一個屬性就是 title,包含着 元素中的文本——顯示在瀏覽器窗口的標題欄或標籤頁上。經過這個屬性能夠取得當前頁面的標題,也能夠修改當前頁面的標題並反映在瀏覽器的標題欄中。

// 取得文檔標題
var originalTitle = document.title;

// 設置文檔標題
document.title = "New page title";複製代碼

接下來要介紹的3個屬性都與對網頁的請求有關,它們是 URLdomainreferrerURL 屬性中包含頁面完整的 URL(即地址欄中顯示的URL),domain 屬性中只包含頁面的域名,而 referrer 屬性中則保存着連接到當前頁面的那個頁面的 URL。在沒有來源頁面的狀況下,referrer 屬性中可能會包含空字符串。全部這些信息都存在於請求的 HTTP 頭部,只不過是經過這些屬性讓咱們可以在 JavaScrip 中訪問它們而已,以下面的例子所示。

// 取得完整的URL
var url = document.URL;

// 取得域名
var domain = document.domain;

// 取得來源頁面的URL
var referrer = document.referrer;複製代碼

查找元素

說到最多見的 DOM 應用,恐怕就要數取得特定的某個或某組元素的引用,而後再執行一些操做了。取得元素的操做可使用 document 對象的幾個方法來完成。其中,Document 類型爲此提供了兩個方法:getElementById()getElementsByTagName()

第一個方法,getElementById(),接收一個參數:要取得的元素的 ID。若是找到相應的元素則返回該元素,若是不存在帶有相應 ID 的元素,則返回 null。注意,這裏的 ID 必須與頁面中元素的 id 特性(attribute)嚴格匹配,包括大小寫。如下面的元素爲例。

 
Some text複製代碼

可使用下面的代碼取得這個元素:

var div = document.getElementById("myDiv");   // 取得
  
  
  

 
元素的引用
複製代碼

可是,下面的代碼在除 IE7 及更早版本以外的全部瀏覽器中都將返回 null

var div = document.getElementById("mydiv");   // 無效的ID(在IE7及更早版本中能夠)複製代碼

IE8 及較低版本不區分 ID 的大小寫,所以 "myDiv""mydiv" 會被看成相同的元素 ID。若是頁面中多個元素的ID值相同,getElementById() 只返回文檔中第一次出現的元素。

另外一個經常使用於取得元素引用的方法是 getElementsByTagName()。這個方法接受一個參數,即要取得元素的標籤名,而返回的是包含零或多個元素的 NodeList。在HTML文檔中,這個方法會返回一個HTMLCollection 對象,做爲一個「動態」集合,該對象與 NodeList很是相似。例如,下列代碼會取得頁面中全部的 元素,並返回一個 HTMLCollection

var images = document.getElementsByTagName("img");複製代碼

這行代碼會將一個 HTMLCollection 對象保存在 images 變量中。與 NodeList 對象相似,可使用方括號語法或 item() 方法來訪問 HTMLCollection 對象中的項。而這個對象中元素的數量則能夠經過其 length 屬性取得,以下面的例子所示。

console.log(images.length);        // 輸出圖像的數量
console.log(images[0].src);        // 輸出第一個圖像元素的src特性
console.log(images.item(0).src);   // 輸出第一個圖像元素的src特性複製代碼

HTMLCollection 對象還有一個方法,叫作 namedItem(),使用這個方法能夠經過元素的 name 特性取得集合中的項。例如,假設上面提到的頁面中包含以下 元素:

複製代碼

那麼就能夠經過以下方式從 images 變量中取得這個 元素:

var myImage = images.namedItem("myImage");複製代碼

在提供按索引訪問項的基礎上,HTMLCollection 還支持按名稱訪問項,這就爲咱們取得實際想要的元素提供了便利。並且,對命名的項也可使用方括號語法來訪問,以下所示:

var myImage = images["myImage"];複製代碼

HTMLCollection 而言,咱們能夠向方括號中傳入數值或字符串形式的索引值。在後臺,對數值索引就會調用 item(),而對字符串索引就會調用 namedItem()

要想取得文檔中的全部元素,能夠向 getElementsByTagName() 中傳入 "*"。在 JavaScript 及 CSS 中,星號(*)一般表示「所有」。下面看一個例子。

var allElements = document.getElementsByTagName("*");複製代碼

僅此一行代碼返回的 HTMLCollection 中,就包含了整個頁面中的全部元素——按照它們出現的前後順序。換句話說,第一項是 元素,第二項是 元素,以此類推。因爲 IE 將註釋(Comment)實現爲元素(Element),所以在IE中調用 getElementsByTagName("*") 將會返回全部註釋節點。

第三個方法,也是隻有 HTMLDocument 類型纔有的方法,是 getElementsByName()。顧名思義,這個方法會返回帶有給定 name 特性的全部元素。最常使用 getElementsByName() 方法的狀況是取得單選按鈕;爲了確保發送給瀏覽器的值正確無誤,全部單選按鈕必須具備相同的 name 特性,以下面的例子所示。

 
Which color do you prefer?
  • Red
  • Green
  • Blue 複製代碼

    如這個例子所示,其中全部單選按鈕的 name 特性值都是 "color",但它們的 ID 能夠不一樣。ID 的做用在於將 元素應用到每一個單選按鈕,而 name 特性則用以確保三個值中只有一個被髮送給瀏覽器。這樣,咱們就可使用以下代碼取得全部單選按鈕:

    var radios = document.getElementsByName("color");複製代碼

    getElementsByTagName() 相似,getElementsByName() 方法也會返回一個 HTMLCollectioin。可是,對於這裏的單選按鈕來講,namedItem() 方法則只會取得第一項(由於每一項的 name 特性都相同)。

    特殊集合

    除了屬性和方法,document 對象還有一些特殊的集合。這些集合都是 HTMLCollection 對象,爲訪問文檔經常使用的部分提供了快捷方式,包括:

    • document.anchors,包含文檔中全部帶 name 特性的 元素;
    • document.applets,包含文檔中全部的 元素,由於再也不推薦使用 元素,因此這個集合已經不建議使用了;
    • document.forms,包含文檔中全部的
      元素,與document.getElementsByTagName("form")獲得的結果相同;
    • document.images,包含文檔中全部的 元素,與document.getElementsByTagName("img")獲得的結果相同;
    • document.links,包含文檔中全部帶href特性的 元素。

    這個特殊集合始終均可以經過 HTMLDocument 對象訪問到,並且,與 HTMLCollection 對象相似,集合中的項也會隨着當前文檔內容的更新而更新。

    文檔寫入

    有一個 document 對象的功能已經存在不少年了,那就是將輸出流寫入到網頁中的能力。這個能力體如今下列4個方法中:write()writeln()open()close()。其中,write()writeln() 方法都接受一個字符串參數,即要寫入到輸出流中的文本。write() 會原樣寫入,而 writeln() 則會在字符串的末尾添加一個換行符 \n。在頁面被加載的過程當中,可使用這兩個方法向頁面中動態地加入內容,以下面的例子所示。

    document.write() Example
    
    
        

    The current date and time is: document.write("" + (new Date()).toString() + "");

    複製代碼

    這個例子展現了在頁面加載過程當中輸出當前日期和時間的代碼。其中,日期被包含在一個 元素中,就像在 HTML 頁面中包含普通的文本同樣。這樣作會建立一個 DOM 元素,並且能夠在未來訪問該元素。經過 write()writeln() 輸出的任何 HTML 代碼都將如此處理。

    此外,還可使用 write()writeln() 方法動態地包含外部資源,例如 JavaScript 文件等。在包含 JavaScript 文件時,必須注意不能像下面的例子那樣直接包含字符串 "",由於這會致使該字符串被解釋爲腳本塊的結束,它後面的代碼將沒法執行。

    document.write() Example 2
    
    
        
            document.write("");
        
    
    複製代碼

    即便這個文件看起來沒錯,但字符串 "" 將被解釋爲與外部的 document.write("

    字符串 "<\ script="">" 不會被看成外部 window.onload = function(){ document.write("Hello world!"); };

    在這個例子中,咱們使用了 window.onload 事件處理程序,等到頁面徹底加載以後延遲執行函數。函數執行以後,字符串 "Hello world!" 會重寫整個頁面內容。

    方法 open()close() 分別用於打開和關閉網頁的輸出流。若是是在頁面加載期間使用 write()writeln() 方法,則不須要用到這兩個方法。

    關卡

    仔細想一想,下面代碼塊會輸出什麼結果呢?

     
    
    
      
      
      
    
     
    aaabbbccc var d = document.getElementById("t"); document.writeln(d.firstChild.innerHTML); // ??? document.writeln(d.lastChild.innerHTML); // ??? 複製代碼
     
    
    
      
      
      
    
     
    aaabbbccc var d = document.getElementById("t"); document.writeln(d.childNodes[1].innerHTML); // ??? document.writeln(d.parentNode.getAttribute("name")); // ??? 複製代碼
     
    
    
      
      
      
    
     
    aaabbbccc var d = document.getElementById("t").childNodes[1]; document.writeln(d.nextSibling.innerHTML); // ??? document.writeln(d.previousSibling.innerHTML); // ??? 複製代碼

    更多

    關注微信公衆號「劼哥舍」回覆「答案」,獲取關卡詳解。
    關注 github.com/stone0090/j…,獲取最新動態。

    相關文章
    相關標籤/搜索