本內容是《Web前端開發之Javascript影片》的課件,請配合大師哥《Javascript》影片課程學習。
DOM(Document Object Model,文件物件模型):
DOM是一個使程式和指令碼能夠動態地訪問和更新文件的內容、結構以及樣式,並獨立於平臺和語言的介面;其定義了訪問和處理文件的標準方法;是針對HTML和XML文件的一個API;
DOM描繪了一個層次化的節點樹,允許開發者新增、移除和修改頁面的某一部分,從而重構整個文件。
DOM脫胎於Netscape及微軟創始的DHTML,但現在已經成為表現和操作頁面標記的真正的跨平臺、語言中立的方式;
DOM與具體的程式語言無關,可以在C、Javascript、ActionScript、Java等語言中實現;
DOM標準的目標是讓“任何一種程式設計語言”能操控使用“任何一種標記語言”編寫出的“任何一份文件”;
DOM 級別:
DOM Level 1:由兩個模組組成:DOM核心(DOM Core)和DOM HTML;專注於 HTML 和 XML 文件模型,它含有文件導航和處理功能;
DOM Level 2:對 DOM1級做了擴充套件,添加了樣式表物件模型,並定義了操作附於文件之上的樣式資訊的功能性;同時還定義了一個事件模型,並提供了對 XML 名稱空間的支援;
DOM Level 3:對DOM2級做了擴充套件,規定了內容模型 (DTD 和 Schemas) 和文件驗證,同時規定了文件載入和儲存、文件檢視、文件格式化和關鍵事件;
DOM Level 0:確切來說,不存在DOM0級,因為它不是 W3C 規範,而僅僅是對在 Netscape Navigator 3.0 和 Microsoft Internet Explorer 3.0 中的等價功能性的一種定義,實際上指的就是DHTML;
1998年10月 DOM1級規範成為W3C的標準,為基本的文件結構及查詢提供了介面;
目前主流的瀏覽器都已實現了DOM1、基本實現了DOM2和3;
DOM組成:
Core DOM:定義了一套標準的針對任何結構化文件的物件,即用於XML和HTML的共用介面;HTML DOM:在 DOM 核心的基礎上加以擴充套件,定義了一套標準的針對HTML文件的介面物件;XML:
XML 指可擴充套件標記語言(EXtensible Markup Language),它是一種標記語言,類似於HTML,它的設計宗旨是傳輸資料,而非顯示資料;
XML 標籤沒有被預定義,需要開發者自定義標籤;
XML 被設計為具有自我描述性,是W3C的推薦標準;
XML 的用途:把資料從 HTML 分離、簡化資料共享、簡化資料傳輸、簡化平臺的變更;
DOM樹:
DOM可以將任何HTML或XML文件描繪成一個由多層節點構成的結構;
如HTML:
樹狀圖
每一個標籤是是文件的一個節點,它表示一個Node物件;
節點分為幾種不同的型別,每種型別分別表示文件中不同的資訊或標記;
每種節點都擁有各自的特點、資料和方法,並且也與其他節點存在某種關係;
節點之間的關係構成了層次,而所有頁面標記則表現為一個以特定節點為根節點的樹形結構;
文件節點(Document):是每個文件的根節點;文件節點只有一個子節點,即<html>元素,也稱為文件元素,它是文件最外層元素,其他所有元素都包含在文件元素中;每個文件只能有一個文件元素;在XML中,沒有預定義的文件元素,因此任何元素都可能成為文件元素;
Node介面:
每一段標記都可以透過樹中的一個節點來表示,這個節點稱為Node;
DOM1級定義一個Node介面,其用於抽象地表示文件中一個獨立的部分;
HTML元素透過元素節點表示,特性(attribute)透過特性節點表示,文件型別透過文件型別節點表示,註釋透過註釋節點表示,共12種節點型別,這些型別都繼承自一個基型別Node型別,因此所有節點型別都共享著相同的基本屬性和方法;
Node型別圖:
Node型別圖
Document和Element型別與HTMLDocument和HTMLElement型別之間是有嚴格的區分的;Document型別代表一個HTML或XML文件,而HTMLDocment只是代表一個HTML文件,XMLDocument代表是XML文件;Element型別代表該文件中的一個元素,而HTMLElement只是HTML文件中的元素,不是XMLDocument中的元素;
HTMLElement的很多子型別代表HTML元素的具體型別,每個型別具有多個Javascript屬性,這些屬性對應具體的元素或元素組的HTML元素特性;這些具體的元素類也定義了額外的屬性和方法,它們並不是簡單的對映HTML元素及HTML元素特性;
每個節點都有一個nodeType屬性,用於表明節點的型別;
節點型別由在Node型別中定義的下列12個常數值來表示;
nodeName : String,節點的名字,取決於節點的型別;
nodeValue : String,節點的值,取決於節點的型別;
nodeType : Number,節點的型別常數值之一;
注:對於元素節點,nodeName儲存的始終是元素的標籤名,而nodeValue的值為null;因此在使用nodeName及nodeValue時,最好先檢測一下節點的型別;
childNodes屬性:
返回NodeList型別的所有子節點集合;
NodeList是一種類陣列的物件,用於儲存一組有序的節點,可以透過方括號來訪問儲存在其中的節點,其擁有length 屬性;
NodeList物件的特點:是基於DOM結構動態執行查詢的結果,因此DOM結構的變化能夠自動反映在NodeList物件中;
var mydiv = document.getElementById("mydiv");console.log(mydiv.childNodes.length); // 7var p = document.createElement("p");p.innerText = "大師哥王唯";mydiv.appendChild(p);console.log(mydiv.childNodes.length); // 8
在實際應用中,有可能需要將NodeList物件轉換成陣列:
var mydiv = document.getElementById("mydiv");var arrNodes = Array.prototype.slice.call(mydiv.childNodes,0);console.log(arrNodes);
children屬性:
IE與其他瀏覽器對文字節點中空白符的解釋不一致,導致了children屬性出現;這個屬性是HTMLCollection的例項,其中只包含元素的子節點中那些也是元素的節點,即children列表中只包含Element元素;
children並不是標準屬性,但所有的瀏覽器都實現了該屬性;
var myList = document.getElementById("myList");var lis = myList.children;console.log(lis);console.log(lis.length);
Text和Comment節點沒有children屬性;
HTMLCollection型別:
是一個介面,表示HTML 元素的集合,與NodeList非常類似,也是個類陣列物件;元素在 HTMLCollection 物件中的順序與它們在文件原始碼中出現的順序一樣;
HTMLCollection型別的屬性和方法:
item()方法:返回 HTMLCollection 中指定索引的元素;
length屬性:返回 HTMLCollection 中元素的數量;
namedItem()方法:返回 HTMLCollection 中指定 ID 或 name 屬性的元素;
console.log(lis.item(0));console.log(lis[0]); // 一般用這個代替console.log(lis.namedItem("myli"));console.log(lis["myli"]); // 一般用這個代替
與NodeList型別一樣,HTMLCollection物件也是實時動態的;
parentNode屬性:
指向節點的父節點;
一個元素節點的父節點可能是一個元素(Element )節點,也可能是一個文件(Document )節點,或者是個文件碎片(DocumentFragment)節點;
對於Document、DocumentFragment和Attr物件來說,其parentNode屬性為null,因為它們沒有父節點;
另外,如果當前節點剛剛被建立,還沒有被插入到DOM樹中,則該節點的parentNode屬性也返回null;
var elt = document.getElementById("elt");if(elt.parentNode){ elt.parentNode.removeChild(elt);}
parentElement屬性:
返回當前節點的父元素節點,如果該元素沒有父節點,或者父節點不是一個 DOM 元素,則返回 null;parentElement是一個DOM元素物件(HTMLElement物件);
if(elt.parentElement) elt.parentElement.style.backgroundColor = "purple";
在早期,parentElement是ie專用的,而現在所有的瀏覽器都已經實現了,並且被納入了最新的DOM4規範中;
parentElement匹配的是parent為Element的情況,而parentNode匹配的則是parent為Node的情況;Element是包含在Node裡的,它的nodeType是1;一般情況parentNode可以取代parentElement的所有功能;
最能體現兩者的區別是以下兩行程式碼:
console.log(document.documentElement); // <html>console.log(document.documentElement.parentNode); // #documentconsole.log(document.documentElement.parentElement); // null
previousSibling: 前一個兄弟節點;如果這個節點就是第一個兄弟節點,那麼該值為null;
nextSibling : 後一個兄弟節點:如果這個節點就是最後一個兄弟節點,那麼該值為null;
var mydiv = document.getElementById("mydiv");console.log(mydiv.parentNode); // <body>var firstChild = mydiv.childNodes[0]; // 把0換成1或2console.log(firstChild.previousSibling);console.log(firstChild.nextSibling);if(firstChild.nextSibling === null) console.log("child is last node");elseconsole.log("child is't last node");// 遍歷var elt = document.getElementById("elt");while(elt){ console.log(elt.nodeName); elt = elt.nextSibling;}
firstChild : 指向在childNodes集合中的第一個節點;
lastChild : 指向在childNodes集合中最後一個節點;
即firstChild始終指向childNodes[0];lastChild指向childNodes[someNode.childNodes.length - 1];
var mydiv = document.getElementById("mydiv");var firstChild = mydiv.firstChild;var lastChild = mydiv.lastChild;console.log(firstChild);console.log(lastChild);
注:當只有一個子節點的情況下,firstChild和lastChild指向同一個節點;如果沒有子節點,均為null;並不是每種節點都有子節點;
<div id="mydiv"><h2>零點<span>程式設計師</span></h2></div><script> var textChild = mydiv.firstChild.firstChild; console.log(textChild.nodeType); // 3 文字節佔 console.log(textChild.childNodes); // 空的NodeList[]</script>
textChild指的是零點,而不是零點<span>程式設計師</span>;
ownerDocument 屬性:
指向這個節點所屬的文件節點(也就是頂層節點);
任何節點都屬於它所在的文件,任何節點都不能同時存在於兩個或多個文件中,透過這個屬性不必層層回溯到頂端,而是直接訪問到文件節點;
var mydiv = document.getElementById("mydiv");console.log(mydiv.ownerDocument); // #documentvar innerDiv = document.getElementById("innerDiv");console.log(innerDiv.ownerDocument); // #documentconsole.log(mydiv.ownerDocument.documentElement); // <html>
節點層次關係圖:
節點層次關係圖
Node方法(操作節點):
appendChild(node) : 將node新增到childNodes的末尾; 新增節點後,childNodes的新添節點、父節點及以前的最後一個子節點的關係指標都會相應地得到更新;更新後,appendChild()返回新增的節點;
var mydiv = document.getElementById("mydiv");var p = document.createElement("p");p.innerText = "大師哥王唯";var returnP = mydiv.appendChild(p);console.log(mydiv.lastChild); // <p>大師哥王唯</p>console.log(p === returnP); // true
如果傳入appendChild()的節點已經是文件的一部分了,就將該節點從原來的位置轉移到新位置;即任何DOM節點不能同時出現在文件中的多個位置上;
<script>var mydiv = document.getElementById("mydiv");var yourdiv = document.getElementById("yourdiv");// 注:把最後的子元素的空白符刪除var returnDiv = yourdiv.appendChild(mydiv.lastChild);console.log(returnDiv === mydiv.lastChild); // falseconsole.log(returnDiv === yourdiv.firstChild); // true</script>
如果在呼叫appendChild()方法時,傳入了父節點的第一個子節點,那麼,該節點就會成為父節點中的最後一個子節點,如:
// 注意html中的換行空格var mydiv = document.getElementById("mydiv");console.log(mydiv.firstChild);mydiv.appendChild(mydiv.firstChild);console.log(mydiv.firstChild);
需要注意的問題;
var divs = document.getElementsByTagName("div");var btn = document.createElement("input");btn.type = "button";btn.value = "按鈕";for(var i=0; i<divs.length; i++){ console.log(divs[i]); divs[i].appendChild(btn);}
本來的意圖是為了給所有的div新增input子元素,可卻終只是最後的div添加了,原因是一個元素只能有一個父元素,起先,第一個div的確添加了input的元素,便是迴圈中的appendChild()會讓元素從原來的位置轉移到新位置;
改寫:把建立btn的程式碼放到for迴圈內,即可達到目的;
由於appendChild()返回的是被追加的子元素,所以在鏈式呼叫時不能隨便使用;
var elt = document.createElement('p').appendChild(document.createElement('b'));console.log(elt); // elt為<b></b>
本意是返回一個p節點,並且這個p元素包含一個b的子節點,但實際上elt為b;
// 改成var elt = document.createElement('p')elt.appendChild(document.createElement('b'));console.log(elt); // <p>document.body.appendChild(elt);
insertBefore(newnode, refnode)方法:
在childNodes中的refnode之前插入newcode,並返回這個節點;
插入節點後,被插入的節點(新節點)會變成參照節點的前一個兄弟節點;如果參照節點是null,則與appendChild()執行相同的操作;
var mydiv = document.getElementById("mydiv");var p = document.createElement("p");p.innerText = "大師哥王唯";// 或者參照mydiv.firstChild節點var returnP = mydiv.insertBefore(p,null); // 插入後成為最後一個子節點console.log(mydiv.lastChild); // <p>大師哥王唯</p>var returnP = mydiv.insertBefore(p, mydiv.firstChild); // 插入後成為第一個子節點console.log(mydiv.firstChild); // <p>大師哥王唯</p>var returnP = mydiv.insertBefore(p, mydiv.lastChild); // 插入到最後子節點的前面console.log(mydiv.childNodes[mydiv.childNodes.length - 2]);// <p>大師哥王唯</p>var returnP = mydiv.insertBefore(p,mydiv.childNodes[1]); // 插入到任意節點的前面console.log(mydiv.childNodes[1]); // <p>大師哥王唯</p>var yourdiv = document.getElementById("yourdiv");yourdiv.insertBefore(mydiv.childNodes[1],null);//把mydiv的子節點插入到yourdiv中
參照節點refnode是必選引數,如果沒有,則傳null,否則會丟擲異常;如果傳遞undefined,則會隱式轉換;
// mydiv.insertBefore(elt); // 異常mydiv.insertBefore(elt,undefined); // undefined隱式轉換// mydiv.insertBefore(elt,"undefined"); // 異常
如果給定的子節點是文件中現有的節點,insertBefore() 會將其從當前位置移動到新位置;
var elt = document.getElementById("elt");var mydiv = document.getElementById("mydiv");mydiv.insertBefore(elt,mydiv.firstChild); // elt被移到mydiv內的第一個位置
提供一個使用索引位置插入節點的簡單函式;
移除子節點,並返回被移除節點
var mydiv = document.getElementById("mydiv");var deleteChild = mydiv.removeChild(mydiv.firstChild);console.log(deleteChild);// 刪除的節點還可以再次使用var yourdiv = document.getElementById("yourdiv");yourdiv.appendChild(deleteChild);console.log(yourdiv.firstChild);
被移除的節點仍然還在文件中,但它在文件中已經沒有了自己的位置,但如果被刪除節點沒有變數引用它,在一定的時間內將會被記憶體管理器回收;
var mydiv = document.getElementById("mydiv");var deleteChild = mydiv.removeChild(mydiv.firstChild);console.log(deleteChild === mydiv.firstChild); // false// 改成var deleteChild = mydiv.firstChild;var returnChild = mydiv.removeChild(deleteChild);console.log(deleteChild === returnChild); // true
removeChild()方法是在父節點上呼叫的,所以刪除一個節點,一定先定位好父節點再刪除子節點,比如要刪除當前的元素,可以:
var mydiv = document.getElementById("mydiv");for(var i=mydiv.childNodes.length-1; i>=0; i--){ console.log(mydiv.childNodes[i]); mydiv.removeChild(mydiv.childNodes[i]);}console.log(mydiv);
為什麼由後往前刪除,因為childNodes返回的列表是動態的,每一次訪問它都是被刪除一個後的列表;因此,如果只是單純的刪除所有子節點,可以:
// 更簡單的刪除,或者把firstChild換成lastChild也可以while(mydiv.firstChild) mydiv.removeChild(mydiv.firstChild);
replaceChild(newnode, oldnode) 方法:
<div id="yourdiv">temp</div><script>var mydiv = document.getElementById("mydiv");var p = document.createElement("p");p.innerText = "大師哥王唯";var child = mydiv.replaceChild(p, mydiv.firstChild);console.log(child);var yourdiv = document.getElementById("yourdiv");if(yourdiv.hasChildNodes()){ var child = yourdiv.replaceChild(mydiv.lastChild,yourdiv.firstChild);}</script>
定義一個環繞節點的函式:
function roundNode(outerNodeString,innerNode){ // 假如引數為字串而不是節點,將其當做元素的id if(typeof innerNode == "string") innerNode = document.getElementById(innerNode); var parent = innerNode.parentNode; // 取得父節點 var outerNode = document.createElement(outerNodeString); parent.replaceChild(outerNode, innerNode); outerNode.appendChild(innerNode); return outerNode;}var mydiv = document.getElementById("mydiv");roundNode("div",mydiv).style.color = "red";
cloneNode()方法:
用於建立呼叫這個方法的節點的一個完全相同的副本;
其接受一個引數,表示是否執行深複製,true為深複製,即複製節點及其整個子節點樹; false,則為淺複製,即只複製節點本身,節點所包含的文字也不會被複制,預設為false;
var mydiv = document.getElementById("mydiv");var cloneDiv = mydiv.cloneNode(true);console.log(cloneDiv);
或
<ul id="mylist"> <li>HTML5</li> <li>CSS3</li> <li>Javascript</li></ul><script>var myList = document.getElementById("mylist");var deepList = myList.cloneNode(true);var shallowList = myList.cloneNode(false);console.log(deepList.childNodes.length); // 7console.log(shallowList.childNodes.length); // 0</script>
複製後的節點副本屬於文件所有,但並沒有父節點,因此需要透過appendChild()、insertBefore或replaceChild將它新增文件中;
console.log(mydiv.parentNode); // <body>console.log(cloneDiv.parentNode); // nullvar yourdiv = document.getElementById("yourdiv");yourdiv.appendChild(cloneDiv); // 不符合標準,因為有兩個id為mydiv元素console.log(cloneDiv.parentNode); // <div id="yourdiv">
cloneNode()方法不會複製新增到DOM節點中的Javascript屬性,如事件處理程式等,但會複製特性、子節點;但如果是在標籤中直接新增的on事件,也會被複制,因為它是被當作特性複製的;
var myList = document.getElementsByTagName("ul")[0];var li3 = myList.childNodes[5]; // 第3個lili3.addEventListener("click",function(e){ alert(this.innerText);},false);var deepList = myList.cloneNode(true);var yourdiv = document.getElementById("yourdiv");yourdiv.appendChild(deepList);
如果原始節點設定了ID,並且克隆節點會被插入到相同的文件中,那麼就應該修改克隆節點的ID以保證其唯一性;
console.log(deepList.id); // mylistdeepList.id = "deeplist";console.log(deepList.id); // deeplist
normalize()方法:
合併節點,該方法唯一的作用就是處理文件樹中的文字節點;由於解析器的實現或DOM操作等原因,可能會出現文字節點不包含文字,或者接連出現兩個文字節點的情況;當在某個節點上呼叫該方法時,就會在該節點的後代節點中查詢上述兩種情況;如果找到了空文字節點,則刪除它;如果找到相鄰的文字節點,則將它們合併為一個文字節點;
hasChildNodes()方法:
判斷節點是否有子節點,會返回一個布林值;
var mydiv = document.getElementById("mydiv");console.log(mydiv.childNodes.length);console.log(mydiv.hasChildNodes());if(mydiv.hasChildNodes()) console.log("有子節點");elseconsole.log("無");// 如果原始檔中有空白符,刪除的是空白文字節點if(mydiv.hasChildNodes()) mydiv.removeChild(mydiv.firstChild);// 全部刪除while(mydiv.hasChildNodes()) mydiv.removeChild(mydiv.firstChild);
總結:判斷一個節點是否有子節點,有三種方式;
node.firstChild !== null、node.childNodes.length > 0、node.hasChildNodes()方法;
contains()方法:
在實際開發中,經常需要知道某個節點是不是另一個節點的後代; 其接受一個後代節點,用於判斷是否為當前元素的後代節點,如果是,返回true,否則為false;