從這篇筆記開始整理JavaScript的第三部分:文檔對象模型DOM(Document Object Model)。DOM是針對HTML和XML文檔的一個API,脫胎於DHTML,由W3C負責制定相關標准,現在已經成為表現和操作頁面標記的真正的跨平台、語言中立的一種標准,除了JavaScript外,其它一些語言比如SVG、MathML等也不同程度的實現了各自的DOM。
1、DOM組成和級別
DOM分為三個組成部分和三個級別:
組成部分 | 說明 |
核心DOM | 用於任何結構化文檔的標准模型 |
XML DOM | 用於XML文檔的標准模型,定義了所有XML元素的對象和屬性,以及訪問它們的方法(接口),換句話說,XML DOM是用於獲取、更改、添加或刪除XML元素的標准 |
HMTL DOM | 用於HTML文檔的標准模型,定義了所有HTML元素的對象和屬性,以及訪問它們的方法(接口) |
DOM級別 | 瀏覽器支持情況 | 功能模塊 | 說明 |
0級 | IE4、Netscape4 | 在W3C標准中,是沒有0級的,通常所謂DOM 0級指的就是在DOM 1級規范之前的在IE4和Netscape Navigator4中支持的DHTML | |
1級 | 幾乎所有現代瀏覽器 | DOM核心(DOM Core) | 規定的是如何映射基於XML的文檔結構,以便簡化對文檔中任意部分的訪問和操作 |
DOM HTML | 在DOM核心基礎上加以擴展,添加了針對HTML的對象和方法 | ||
2級 | IE 9+ Opera 7-9.9(部分支持),OPera 10+ Safari 2+(部分支持) Chrome 1+(部分支持) Firefox 1+(幾乎全部) |
DOM視圖(DOM Views) | 定義了跟蹤不同文檔(例如,應用CSS前后的文檔)視圖的接口 |
DOM事件(DOM Events) | 定義了事件和事件處理的接口 | ||
DOM樣式(DOM Style) | 定義了基於CSS為元素應用樣式的接口 | ||
DOM遍歷和范圍(DOM Traversal and Range) | 定義了遍歷和操作文檔樹的接口 | ||
DOM核心和HTML擴展 | 開始支持XML命名空間等 | ||
3級 | IE9+ Opera9+(部分支持) Firefox1+(部分支持) |
DOM加載和保存(DOM Load and Save) | 引入了以統一方式加載和保存文檔的方法 |
DOM驗證(DOM Validation) | 驗證文檔的方法 | ||
DOM核心和HTML擴展 | 開始支持XML1.0規范,涉及XML Infoset、XPath和XMLBase等 |
2、文檔映射
DOM將HTML和XML文檔映射成一個由不同節點組成的樹型機構,每種節點都對應於文檔中的信息或標記,節點有自己的屬性和方法,並和其他節點存在某種關系,節點之間的關系構成了節點層次,例如:
<html> <head> <title>標題</title> </head> <body> <p>測試</p> </body> </html> (1)文檔節點Document是每個文檔的根節點。 |
|
3、Node接口
類別 | 屬性/方法 | 值/類型/返回類型 | 說明 |
屬性 | nodeName | String | 節點名字,根據節點類型定義。對於元素節點,就是原始的標簽名 |
nodeValue | String | 節點值,根據節點類型定義。對於元素節點為null | |
nodeType | Number | 節點類型,返回12種節點類型值之一 | |
ownerDocument | Document | 指向這個節點所屬的文檔,可以利用它直接訪問文檔節點,而不用層層回溯 | |
parentNode | Node | 文檔樹中的父節點 | |
childNodes | NodeList | 所有直接子節點組成的列表,不同瀏覽器對空白字符和<html>外的注釋有不同處理,會導致childNodes不一致 | |
firstChild | Node | 第一個直接子節點,沒有子節點返回null | |
lastChild | Node | 最后一個直接子節點,沒有子節點返回null | |
previousSibiling | Node | 前一個兄弟節點,沒有則返回null | |
nextSibiling | Node | 后一個兄弟節點,沒有則返回null | |
方法 | hasChildNodes() | Boolean | 有子節點時返回true,否則返回false |
appendChild(node) | Node | 在末尾添加子節點,返回新添加的節點。如果傳入參數已經是文檔中一部分,結果將是將該節點從原來的位置移向新位置(任何DOM節點不能同時出現在多個位置上) | |
removeChild(node) | Node | 移除節點並返回這個節點。刪除后,節點仍然屬於原來的文檔,只是沒有了位置 | |
replaceChild(node,node) | Node | 傳入新添加的節點和被替換的節點,返回被替換的節點。被替換的節點仍然屬於原來的文檔,只是沒有了位置 | |
insertBefore(node,node) | Node | 傳入新添加的節點和參照節點,新添加節點會成為參照節點的前一個兄弟節點,如果參照節點為null,則插入到最后,相當於appendChild()。返回新添加的節點 | |
cloneNode(boolean) | Node | 復制節點,參數為true時,復制節點及其所有子節點樹,為false時,只復制節點本身。返回的節點屬於文檔所有,但沒有指定父節點 | |
normalize() | 處理文檔樹中的文本節點,刪除空文檔節點或合並兩個相鄰的文本節點等 |
說明:
(1)關於節點類型nodeType,在DOM1中定義了12種常量,是作為Node類型構造函數的屬性定義的(靜態屬性),它們對應於各自的節點類型:
節點 | 節點類型(nodeType,Node的靜態屬性) | 節點名稱(nodeName) | 節點值(nodeValue) | 父節點(parentNode) | 子節點(childNodes) | 說明 |
Document | Node.DOCUMENT_NODE(9) | #document | null | null | DocumentType(最多一個)|Element(最多一個) |Comment|ProcessingInstruction |
下有進一步敘述 |
Element | Node.ELEMENT_NODE(1) | 元素的標簽名 | null | Document|Element | Element|Text|Comment| ProcessingInstruction| CDATASection|EntityReference |
|
Text | Node.TEXT_NODE(3) | #text | 節點所包含的文本 | Element | (不支持)沒有子節點 | |
Comment | Node.COMMENT_NODE(8) | #comment | 注釋的內容 | Document|Element | (不支持)沒有子節點 | 和Text繼承自相同基類,擁有除splitText()外所有字符串方法,可通過nodeValue或data屬性獲取注釋內容 |
CDATASection | Node.CDATA_SECTION_NODE(4) | #cdata-section | CDATA區域的內容 | Document|Element | (不支持)沒有子節點 | 繼承自Text類型,擁有除splitText()外所有字符串方法 |
DocumentType | Node.DOCUMENT_TYPE_NODE(10) | doctype的名稱 | null | Document | (不支持)沒有子節點 | 在DOM1中不能動態創建,DocumentType對象的3個屬性: name表示文檔類型名稱 entities表示描述文檔類型實體的NamedNodeMap notations表示文檔類型描述的符號的NamedNodeMap |
DocumentFragment | Node.DOCUMENT_FRAGMENT_NODE(11) | #document-fragment | null | null | 同Element類型 | 下有進一步敘述 |
Attr | Node.ATTRIBUTE_NODE(2) | 特性名稱 | 特性值 | null | HTML中不支持,XML中可以是Text或EntityReference | 有3個自己的屬性: name等於nodeName value等於nodeValue specified表示是否為默認設置 |
EntityReference | Node.ENTITY_REFERENCE_NODE(5) | 引用的實體名稱 | null |
實體引用節點 | ||
Entity | Node.ENTITY_NODE(6) | entity name | null |
實體節點 | ||
ProcessingInstruction | Node.PROCESSING_INSTRUCTION_NODE(7) | 與 ProcessingInstruction.target 相同 |
與 ProcessingInstruction.data 相同 |
處理指令 | ||
Notation | Node.NOTATION_NODE(12) | notation name | null |
DTD中定義的符號 |
這些節點類型都實現了Node接口,因此都可以訪問Node類型的屬性和方法(不支持子節點的節點類型上調用appendChild()、insertBefore()、replaceChild()、removeChild()等方法時會拋出異常)。經過測試在IE9中已經可以直接訪問Node類型中定義的常量值,並且IE9中這些值不能改變,而在Firefox15的版本中仍然可以修改,這應該是Firefox實現的一個Bug(原書說IE中不可訪問Node的論述似有不妥)。
Node.DOCUMENT_NODE = 2; console.info(Node.DOCUMENT_NODE);//IE9輸出9,而Firefox15輸出的是2
(2)關於NodeList類型,它也是一個類數組類型,有length屬性,也可以通過方括號語法訪問,還可以通過item()方法訪問,但並不是Array的實例,如果要對其使用數組方法,必須像轉換arguments對象一樣來轉換NodeList對象:
var node = document;//任意一個節點,這里使用文檔根節點測試 console.info(node.childNodes.length);//2,直接子節點 console.info(node.childNodes[0].nodeName);//通過方括號語法訪問第1個元素,索引從0開始 console.info(node.childNodes.item(1).nodeName);//通過item()方法訪問第2個元素,索引從0開始 var arr = Array.prototype.slice.call(node.childNodes,0);//轉換為真正的數組 console.info(arr.join(','));//可以使用數組方法了
需要特別注意的是,NodeList對象類型是一個有生命、有呼吸的對象,它的屬性和元素是跟隨文檔變化而變化的:
var node = document; var nodeList = node.childNodes; var src = nodeList.length; node.removeChild(nodeList[0]);//修改文檔,會同時修改NodeList對象的屬性和元素,即便是已經將其保存為另外一個變量 console.info(nodeList.length == src - 1);//true
類似NodeList這種動態變化的對象對象還有HTMLCollection、NamedNodeMap等,其中HTMLCollection還有一個namedItem()方法,可以通過元素的name獲取集合中的項。
4、Document類型
在JavaScript中,通過Document類型表示文檔,而我們通常使用的document對象則是HTMLDocument(繼承自Document類型)的一個實例,表示整個HTML頁面。同時,docuemnt對象也是window對象的一個屬性,可以作為全局對象來訪問。document對象的主要屬性和方法有:
類別 | 屬性/方法 | 說明 | 備注 |
屬性 | documentElement | 指向HTML頁面中的<html>元素 | 作為document的子節點,<html>元素還可以通過document.firstChild或document.childNodes[0]等方式訪問 |
body | 直接指向HTML頁面中的<body>元素 | document.body的使用頻率非常高 | |
doctype | 表示<!DOCTYPE>相關信息,不同瀏覽器差異比較大,因此用處不大 | 有些瀏覽器會把<!DOCTYPE>作為注釋處理 | |
title | 包含<title>元素中的文本,顯示在瀏覽器窗口的標題和標簽頁上,可以通過它修改標題 | 修改title屬性的值不會改變<title>元素 | |
URL | 頁面完整的URL,即地址欄中顯示的URL | 這些信息都存在於請求的HTTP頭部,這些屬性是為了方便在JavaScript中訪問,domain的設置需要注意: 1、不能將domain設置為URL不包含的域,如URL='p2p.wrox.com',domain可以設置為wrox.com,但不能設置為ncz.net 2、不能將domain由松散的設置為緊綳的,如原來domain='wrox.com',不能設置為domain='p2p.wrox.com' |
|
domain | 頁面的域名,可以設置 | ||
referrer | 鏈接到當前頁面的那個頁面的URL,沒有來源頁面時,為空字符串 | ||
implementation | 提供瀏覽器對DOM實現情況的描述對象,在DOM1只有一個hasFeature()方法 | hasFeature()接受兩個參數:DOM功能和版本號。由於瀏覽器實現問題,這個方法並非百分百准確 | |
文檔元素集合 | anchors | 包含文檔中所有帶name特性的<a>元素 | 這些集合都是HTMLCollection對象,集合中的項會隨文檔的變化而更新 |
applets | 包含文檔中所有<applet>元素,已經不推薦使用 | ||
forms | 包含文檔中所有<form>元素,相當於document.getElementsByTagName('form') | ||
images | 包含文檔中所有<img>元素,相當於document.getElementsByTagName('img') | ||
links | 包含文檔中所有帶href特性的<a>元素 | ||
元素獲取方法 | getElementById() | 接受一個參數:要獲取元素的ID,匹配大小寫,找不到時返回null,若有多個相同ID元素,返回第一個 | 在IE8-中不區分大小寫,並且表單元素(如<input>)中的name也被作為ID去查詢 |
getElementsByTagName() | 接受一個參數:要獲取元素的標簽名,返回包含0或多個元素的NodeList,在HTML中,返回HTMLCollection | 傳入*號時,則返回文檔中所有元素,可以通過索引(調用item())、字符串(調用namedItem())、以及直接使用item()方法訪問結果集 | |
getElementsByName() | HTMLDocument特有方法,返回name特性為傳入參數值的元素集合,返回HTMLCollection對象 | 使用namedItem()時,只返回第一項,因為是按name獲取的集合,集合中的name全部相同 | |
文檔寫入方法 | write() | 輸出文本,接受一個參數:寫入到輸出流的文本 | write()和writeln()被用來向頁面動態地輸入內容,需要注意的是: 1、如果輸出內容中含'</script>',為了防止做為標簽解析而發生錯誤,需要特殊處理,可以寫成'<\/script>'或拆分 2、如果是頁面加載完成之后調用,會重寫整個頁面 |
writeln() | 輸出文本,並添加換行符(\n),接受一個參數:寫入到輸出流的文本 | ||
open() | 打開網頁輸出流 | ||
close() | 關閉網頁輸出流 | ||
創建方法 | createElement() | 創建新元素,並設置ownerDocument屬性,接受一個參數:要創建元素的標簽名(在HTML中不分大小寫,在XML中區分) | 在IE中還可以傳入包含屬性的完整的元素標簽來創建新元素 |
createTextNode() | 創建文本節點,接受一個參數:要插入節點中的文本(會按HTML/XML格式編碼),會同時設置ownerDocument | 除非把新節點添加到文檔中,否則不會顯示新節點 | |
createComment() | 創建新注釋,接受注釋文本 | 瀏覽器不會識別<html>后代注釋,如要訪問注釋節點,需要保證是<html>元素的后代 | |
createCDATASection() | 在XML文件中創建CDATA區域,傳入節點內容 | CDATA區域只會出現XML文檔中,因此多數瀏覽器會報CDATA區域錯誤地解析為Comment或Element | |
createDocumentFragment() | 創建文檔片段 | ||
createAttribute() | 創建新特性節點 |
說明:
(1)一般情況下,不用在document對象上調用appendChild()、insertBefore()、removeChild()、replaceChild()等方法,因為文檔類型(如果存在的話)是只讀的,而且它只能有一個元素子節點。
(2)document對象的創建方法,是實現動態加載腳本或樣式的基礎,從而可以進一步對代碼模塊化,實現按需加載,提升性能,這在Ext4庫中已經很好地應用了。動態加載的一般方法:
function loadScript(url){//動態加載外部js文件,也可以類似的加載js代碼 var script = document.createElement("script"); script.type = "text/javascript"; script.src = url; document.body.appendChild(script); } function loadStyle(url){//動態加載外部css文件,也可以類似的通過style元素加載css代碼 var link = document.createElement("link"); link.rel = "stylesheet"; link.type = "text/css"; link.href = url; var head = document.getElementsByTagName("head")[0]; head.appendChild(link); }
5、Element類型
Element類型用於表現XML或HTML元素,提供了對元素標簽名、子節點以及特性的訪問。在HTML中,元素由HTMLElement類型(或其子類型)表示,HTMLElement繼承了Element類型,並添加了對應每個HTML元素都存在的標准特性的屬性。HTMLElement類型常用屬性和方法總結如下:
類別 | 屬性/特性 | 說明 | |
Element類型屬性 | tagName | 這個屬性是所有Element都有的,值和nodeName相同,表示元素標簽名,在HTML中,返回的標簽名始終大寫,在XML中,和源代碼一致 | |
HTML元素標准特性 | id | id | 元素在文檔中的唯一標識符 |
title | title | 有關元素的附件說明信息,一般通過工具條顯示出來 | |
lang | lang | 元素內容的語言代碼,很少使用 | |
dir | dir | 語言的方向,ltr:從左至右,rtl:從右至左,也很少使用 | |
className | class | 元素的CSS類,因為class是保留字,所以屬性命名為className | |
特性屬性 | attributes | 這個是屬性只有Element類型使用,是一個NamedNodeMap值,屬於一個“動態”集合 | |
特性方法 | getAttribute() | 參數:特性名。參數必須與實際的特性名相同,因此是class而不是className,給定特性不存在時,返回null,這個方法也可以獲取自定義特性,特性名不區分大小寫 | |
setAttribute() | 參數:特性名和特性值。如果已存在該特性,替換現有的值,如果不存在,就創建並賦值。通過這個方法設置特性時,會把特性名轉換為小寫形式 | ||
removeAttribute() | 參數:特性名。徹底刪除元素的特性,不僅會清除特性的值,也會從元素對象中刪除特性 | ||
特性節點方法 | getAttributeNode() | 返回對應特性的Attr節點,element.getAttributeNode('align')相當於element.attributes['align'] | |
setAttributeNode() | 設置元素的Attr節點 | ||
其它方法 | getElementsByTagName() | 在當前元素的后代節點中搜索,其它用法和Document類型的同名方法一樣 |
說明:
(1)特殊特性:
A、class特性,其對應DOM對象的屬性為className,在特性方法操作中參數需要傳入class,而對象屬性操作需要設置className屬性。
B、style特性,在通過getAttribute()訪問時,返回的是style特性值中包含的CSS文本,而通過屬性來訪問它則會返回一個對象。
C、事件處理特性,比如onclick,當在元素上使用時,onclick特性中包含的是JavaScript代碼,如果通過getAttribute()訪問,返回相應代碼的字符串,而在訪問onclick屬性時,會返回一個JavaScript函數(沒有相應特性時返回null)。
(2)關於自定義特性和屬性,在HTML5規范中,需加上“data-”前綴。
A、給節點設置特性值時,會同步修改相應DOM對象的屬性值,但是設置自定義特性的值時,一般瀏覽器是不會為相應的DOM對象添加屬性的,然而IE也會為自定義特性創建屬性。
B、直接給DOM對象設置屬性值時,會同步修改相應節點元素的特性值,但是一般瀏覽器中自定義屬性不會成為元素的特性,使用getArrtibute()時會返回null,然而IE會為自定義屬性創建特性。
(3)HTMLElement類型是HTML中元素的基類型,對應不同的標簽,還有很多更加具體的子類型,這些子類型也有與之相關的特性和方法,比如對應<body>標簽有HTMLBodyElement類型、對應<table>標簽有HTMLTableElement類型等。
(4)元素的子節點,可以有任意數目的子節點和后代節點,childNodes屬性則包括了所有直接子節點,但是由於不同瀏覽器在處理注釋、文本等節點的不同,會使得childNodes也不同,因此,如果需要遍歷元素子節點的話,需要添加節點類型判斷:
for(var i=0,l=element.childNodes.length; i < l; i++){ if(element.childNodes[i].nodeType === 1)//過濾元素子節點 { //對元素子節點做操作 } }
(5)元素的每一個特性都由一個Attr節點表示,每個節點都保存在一個NamedNodeMap對象中,這個對象和NodeList與HTMLCollection類似,是一個“動態”的,隨文檔變化而變化,它有下面的一些方法:
方法 | 說明 |
getNamesItem(name) | 返回nodeName等於name的節點 |
removeNamedItem(name) | 從列表中移除nodeName等於那么的節點,調用removeNamedItem()與在元素上調用removeAttribute()效果相同,只是前者返回被移除的Attr節點 |
setNamedItem(node) | 向列表中添加節點,以節點的nodeName屬性為索引 |
item(pos) | 返回位於數字pos位置處的節點 |
元素的attributes屬性返回的就是一個NamedNodeMap對象,其中包含一系列Attr節點,每個節點的nodeName就是特性名稱,nodeValue就是特性值,可以使用attributes屬性來遍歷元素的特性(原書第268頁):
function outputAttributes(element){ var pairs = new Array(), attr; for(var i=0, len=element.attributes.length; i < len; i++){ attr = element.attributes[i]; if(attr.specified){//specified屬性表示特性值是設置還是默認的,為true表示設置的 pairs.push(attr.nodeName + "=\"" +attr.nodeValue + "\""); } } return pairs.join(" ");//以空格連接各個特性並返回 }
6、Text類型
文本節點由Text類型表示,包含的是可以按字面解釋的純文本內容,可以包含轉義后的HTML字符,但不能包含HTML代碼,Text類型的主要屬性和方法有:
類別 | 屬性/方法 | 說明 |
屬性 | length | 文本字符數 |
data | 文本字符,和nodeValue值相同 | |
方法 | appendData(text) | 將text添加到文本節點末尾 |
deleteData(offset,count) | 從offset指定的位置開始刪除count個字符 | |
insertData(offset,text) | 從offset指定的位置插入text | |
replaceData(offset,count,text) | 用text替換從offset指定的位置開始到offset+count為止處的文本 | |
splitText(offset) | 從offset指定的位置將當前文本節點分成兩個文本節點 | |
substringData(offset,count) | 提取從offset指定的位置開始到offset+count為止處的字符串 |
說明:
(1)在Node類型中定義了一個normalize()方法,用於將相鄰的兩個或多個文本子節點合並,需要注意的是這個方法需要在文本節點的父節點上調用;與之相反的是,Text類型提供了splitText()方法,它會將原文本節點分成兩個文本節點,原文本節點將包含從開始到指定位置之前的內容,新文本節點包含剩下的內容,最終返回新文本節點。
(2)默認情況下,每個可以包含內容的元素最多只能有一個文本子節點,而且文本不能為空。通過DOM腳本操作時,可能存在有多個文件子節點的情況。
(3)設置文本節點時需要注意,字符串會經過HTML或XML編碼。
7、DocumentFragment類型
在所有節點類型中,只有DocumentFragment是沒有對應的標記的,DOM規定DocumentFragment是一種“輕量級”的文檔,可以包含和控制節點,但不會像完整的文檔那樣占用額外的資源。DocumentFragment類型可以作為一個容器來使用,可以把后面要對文檔進行的添加、修改、移除等操作先對DocumentFragment進行,然后再將DocumentFragment添加至文檔中,從而避免DOM視圖的多次渲染。例如:
function addItems(){ var fragment = document.createDocumentFragment();//創建一個文檔片段作為容器 var ul = document.getElementById("myList"); var li = null; for (var i=0; i < 3; i++){ li = document.createElement("li");//創建列表元素 li.appendChild(document.createTextNode("Item " + (i+1)));//在列表元素中添加文本 fragment.appendChild(li);//將列表元素添加中容器中,此時不會渲染頁面 } ul.appendChild(fragment); //將容器中的節點添加到文檔中(但容器本身不會添加至文檔樹),這樣只需要渲染一次頁面 }
需要注意的是,在DocumentFragment中的節點不屬於文檔,如果將文本中的節點添加至DocumentFragment中,該節點將會從文檔樹中移除。
8、操作表格
<table>元素是HTML中最復雜的結構之一,在HTML DOM中還為<table>、<tbody>、<tr>等元素添加了一些屬性和方法(請注意table、tbody、tr及td之間的關系):
元素 | 類別 | 屬性/方法 | 說明 |
<table> | 屬性 | caption | 保存着對<caption>元素(如果有)的指針 |
tBodies | 是一個<tbody>元素的HTMLCollection | ||
tHead | 保存着對<thead>元素(如果有)的指針 | ||
tFoot | 保存着對<tfoot>元素(如果有)的指針 | ||
rows | 表格中所有行的HTMLCollection | ||
方法 | createTHead() | 創建<thead>元素,將其放到表格中,返回引用 | |
createTFoot() | 創建<tfoot>元素,將其放到表格中,返回引用 | ||
createCaption() | 創建<caption>元素,將其放到表格中,返回引用 | ||
deleteTHead() | 刪除<thead>元素 | ||
deleteTFoot() | 刪除<tfoot>元素 | ||
deleteCaption() | 刪除<caption>元素 | ||
deleteRow(pos) | 刪除指定位置的行 | ||
insertRow(pos) | 向rows集合中的指定位置插入一行 | ||
<tbody> | 屬性 | rows | 保存着<tbody>元素中行的HTMLCollection |
方法 | deleteRow(pos) | 刪除指定位置的行 | |
insertRow(pos) | 向rows集合中的指定位置插入一行,返回新插入行的引用 | ||
<tr> | 屬性 | cells | 保存着<tr>元素中單元格的HTMLCollection |
方法 | deleteCell(pos) | 刪除指定位置的單元格 | |
insertCell(pos) | 想cells集合中的指定位置插入一個單元格,返回新插入單元格的引用 |
注意,上表中列出的是原書中提及元素的屬性和方法,並不全面,比如對應<td>元素還有HTMLTableCellElement對象,而<tr>對應對象還有rowIndex屬性等,實際使用時可以查閱相關的DOM參考手冊。這里旨在通過主要的一些屬性和方法明確概念,而非參考大全。