前言:
本系列從原型,原型鏈,屬性類型等方面下手學習了DOM文檔對象模型,旨在弄清我們在DOM中常用的每一個屬性和方法都清楚它從哪裡來要到哪裡做什麼事,這樣對於理解代碼有一定啟發。全靠自己在總結中摸索,所以對於一個問題要是深究還真能挖出許多有意思的東西,現在覺得JavaScript這個東西簡直越來越有意思了。
正文:
DOM(文檔對象模型)是針對HTML和XML文檔的一個API(應用程序編程接口)。DOM描繪了一個層次化的節點樹,允許開發人員添加,移除,修改頁面某一部分,現在它已成為表現和操作頁面標記的真正的跨平台,語言中立的方式。
DOM1為基本文檔結構及查詢提供了接口。本系列主要討論JavaScript對DOM1級的實現,但是還會穿插一點DOM2和DOM3的內容。
節點層次
DOM可以將任何HTML和XML文檔描繪成一個由多層節點構成的結構。節點分為好幾種不同的類型,每種類型分別表示文檔中不同的信息及標記。每個節點都有各自特點,屬性和方法,及和其他節點存在的關系。節點之間的關系構成了層次,而所有頁面標記則表現為一個以特定節點為根節點的樹形結構。節點比元素的概念大,元素只是節點的一種類型。
文檔節點:每個文檔的根節點。HTML文檔中文檔節點( window.document=>#document )只有一個子節點即 HTML 元素。
文檔元素:文檔中最外層元素,文檔中的其他元素都包含在文檔元素中,每個文檔只能有一個文檔元素。在HTML頁面中,文檔元素始終都是 HTML 元素。XML中,沒有預定義的元素,任何元素都能成為文檔元素。
每一段標記都可通過樹中一個節點表示:HTML元素通過元素節點表示,特性通過特性節點表示,文檔類型通過文檔類型節點表示,注釋通過注釋節點表示...共有12種節點類型,這些類型都繼承自一個基類型Node類型。下面來挨個看這些節點類型,但是本篇著重學習Node類型,其他類型和DOM操作技術在後續系列的總結中。
DOM操作技術
Node類型:
DOM1級定義了一個Node接口,該接口將由DOM中所有節點類型實現。這個Node接口在JavaScript中作為Node類型實現,JavaScript中所有節點類型(Element,Attr,Text,CDATASection,Comment,Document,DocumentType,DocumentFragment等)都繼承自Node類型( Element.prototype instanceof Node;// true ),因此所有節點類型的實例都共享著原型鏈(某類型實例->某類型.prototype->Node.prototype->EventTarget.prototype->Object.prototype)上的屬性和方法, document instanceof Node;// true 比如文檔節點 #document 就是Document類型實例也是Node類型的實例,文檔元素 html 是HTMLElement類型的實例是Element類型實例也是Node類型的實例。
下面這段可以略過,只是我的一個小思考:
document.documentElement.__proto__==HTMLElement.prototype;// false //居然是false,這個html元素節點實例的原型指向的不是HTMLElement構造函數的prototype??但是... HTMLElement.prototype.isPrototypeOf(document.documentElement);// true //判斷HTMLElement.prototype確實是在html元素的原型鏈上啊
有沒有覺得很奇怪, __proto__ 不是指向構造這個實例的函數的原型嗎??為什麼會是 false ,而且 document.documentElement.__proto__ 和 HTMLelement.prototype 竟然不是同一類型的,按理說 html 元素作為HTMLElement的實例,默認它們指向同一個 HTMLElement.prototype 對象的。
百思不得其解,一度認為我對 __proto__ 這個東西是不是理解有誤,查看相關文檔MDN Object.prototype.__proto__後受了點啟發,
是不是 document.documentElement.__proto__ 被JS引擎重寫了!!?讓其重新指向一個為HTMLElement類型的實例對象(假設就叫 HTMLEleObj 吧,其實是 HTMLHtmlElement.prototype ),查看 HTMLEleObj 的 __proto__ ,發現這個東西類型為Element類型實例,想到 HTMLElement.prototype 也是Element類型的,那這兩個是不是同一個對象?
好像是這樣的: document.documentElement.__proto__.__proto__==HTMLElement.prototype;// true 。這也就能解釋為什麼html元素改變了 [[prototype]] 指向但還仍在原來的原型鏈上,JS引擎是給這個本身默認的原型鏈( html.__proto__->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype )又增加了一個對象,現在原型鏈變成( html.__proto__->HTMLEleObj;HTMLEleObj.__proto__->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype )。還是畫個圖比較好理解點。但是還是不明白為何JS引擎要在原型鏈上增加這麼個對象,有什麼用處?發現html元素身上有兩個屬性,版本和構造器,然而並不能直接通過 HTMLHtmlElement.prototype.version 訪問 version 屬性(每個元素的 __proto__ 除了 constructor 屬性外其余的這些屬性還都不一樣,不過都是HTML5之前元素上的屬性),需要通過它的實例(html元素)訪問。 constructor 指向 HTMLHtmlElement 接口。
---補充---
通過 document.documentElement.__proto__.constructor 訪問得到,HTMLEleObj對象其實是HTMLHtmlElement接口的原型對象,雖然以上思考有些誤解,但是還是留下這個思考的過程吧。真正的原型鏈是 某元素.__proto__->HTML某元素Element.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype 。比如對於html元素就是 document.documentElement.__proto__->HTMLHtmlElement.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype 。對於body元素就是 document.body.__proto__->HTMLBodyElement.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype 。其實都是可以通過 某元素.__proto__.constructor 屬性訪問到其對應的構造器。
扯遠了,回歸主題來看Node類型:
Node.prototype上的屬性及方法
注意到 Node.prototype 是 EventTarget 類型的實例對象,怪不得 Node.prototype.__proto__ 會指向 EventTarget.prototype 。
Object.getOwnPropertyNames(EventTarget.prototype);// ["addEventListener", "removeEventListener", "dispatchEvent", "constructor"]
所有這些關系總結來說就是Node和EventTarget是JavaScript中兩種類型,用組合繼承模式實現的話就內部實現可能是這樣子:
function EventTarget(){ //初始化實例的屬性和方法 } function Node(){ //初始化實例的屬性和方法 } Node.prototype=new EventTarget(); Node.prototype.construct=Node;
//以Element類型舉例 Element.prototype=new Node(); Element.construct=Element; ...
Node.prototype 指向 EventTarget.prototype ,並且 Node.prototype 會被初始化上一些屬性和實例。不過事實上我們並不能成功構造 Node 和 EventTarget 類型的實例,控制台會提示報錯不合法的構造。那應該是JS引擎內部自己實現的吧,不然誰都能構造這種底層接口的實例那就亂套了。
Node.prototype常見屬性和方法:
這些關系指針屬性大部分都是只讀的因為訪問器屬性的 set 被設置為 undefiend 了,即使 set 不為 undefiend ,但 set 方法能被使用的前提是該節點的對應要訪問的那個屬性不為 null 。所以DOM提供了一些操作節點的方法(從第9小點開始總結,這些方法都是可寫的,並且在Node.prototype上可以重寫這些方法)
Node.prototype.COMMENT_NODE==Node.COMMENT_NODE;// true Node.prototype.COMMENT_NODE;// 8 Node.COMMENT_NODE;// 8 document.COMMENT_NODE;// 8應用:通過利用節點類型屬性可以確定節點的類型,為了兼容那些沒有公開Node類型的構造函數的浏覽器,我們就不用Node.ELEMENT_NODE形式訪問類型值,而是直接通過數字值判斷
if(someNode.nodeType==1){ console.log("這是一個元素節點"); }
nodeName和nodeValue屬性:
表示節點具體信息。
(1).對於Element元素節點:原型鏈繼承關系為:某元素節點.__proto__->HTML某元素Element.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype。
nodeName保存的為元素的標簽名,nodeValue的值為null。
var someNode=document.documentElement;// 獲取HTML元素 if(someNode.nodeType==1){ console.log(someNode.nodeName+"是元素節點名"); console.log("它的nodeValue:"+someNode.nodeValue); } /*HTML是元素節點名 它的nodeValue:null */(2).對於Attr類型節點:原型鏈繼承關系為:某特性節點.__proto__->Attr.prototype->Node.prototype->EventTarget.prototype。
var html=document.documentElement; //獲取特性實例所在的對象 html.attributes;//attributes屬性是Element.prototype上的屬性
這個對象是NamedNodeMap類型的實例,這個對象的原型鏈關系為html.attributes.__proto__->NamedNodeMap.prototype->Object.prototype。這個對象裡面又有幾個屬性,這幾個屬性才是我們需要的真正特性對象。
html.attributes["0"];// lang="zh-cn" 是一個特性節點 html.attributes["1"];// style="overflow:hidden;" 是另一個特性節點 html.attributes["lang"].nodeName;// "lang" lang特性節點的nodeName值 html.attributes["lang"].nodeValue;// "zh-cn" html.attributes["0"] instanceof Attr;// true(3).對於Text類型節點:原型鏈的繼承關系為:某文本節點.__proto__->Text.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。
返回 null 。
等價的寫法是 nodeList[idx] , 不過這種情況下越界訪問將返回 undefined (因為是以數組形式訪問的)。
對arguments對象使用Array.prototype.slice()方法將其轉換為數組,采用同樣方法也可以將NodeList類型集合轉換為數組類型,其實就是就是在類數組對象的上下文中調用原生的slice方法。function transToArr(collections,start,end){ var length=arguments.length; if(length==0){ return; }else if(length==1){ return Array.prototype.slice.call(collections); }else{ //判斷start,end類型 if(typeof arguments[1]=='number'){ if(typeof arguments[2]=='number'){ return Array.prototype.slice.call(collections,start,end); }else{ //end參數不是number類型時,slice返回length之前的項 end=collections.length; return Array.prototype.slice.call(collections,start,end); } } } }(2)HTMLCollection接口是為一個包含了元素的通用集合,原型鏈的關系為:通過某用法(比如getElementsByTagName,getElementsByClassName,getElementsByTagNameNS,document.forms等)獲取的節點集合.__proto__->HTMLCollection.prototype->Object.prototype
//刪除childNodes中的所有文本節點,因為child.length是動態變化的,所以分情況i++ var child=parent.childNodes; for(var i=0;i<child.length;){ if(child[i].nodeType==3){ ul.removeChild(child[i]); //不用i=0回歸到開時就用上次的i就可 }else{ i++; } }但NodeList也有時候表現為靜態集合,以意味著對文檔對象模型任何改動都不會影響集合內容。querySelectorAll就是靜態的
var a=document.body.firstChild; document.body.appendChild(document.body.firstChild)==a;// true a==document.body.lastChild;// true
//要插入到desEle之後,相當於插入desEle.nextSibling之前,返回被插入的srcEle Node.prototype.insertAfter=function(srcEle,desEle){ this.insertBefore(srcEle,desEle.nextSibling); return srcEle; }
var parent=$('#hdtb-msb'); var first=parent.firstChild; var last=parent.lastChild; var firstnode=parent.replaceChild(last,first); firstnode;// <div class=?"hdtb-mitem hdtb-msel hdtb-imb">?全部?</div>? firstnode.ownerDocument;// #document 節點 證明還在文檔樹
var clone1=last.cloneNode(true); clone1;// <a class=?"hdtb-tl" id=?"hdtb-tls" role=?"button" tabindex=?"0" data-ved=?"0ahUKEwiYht2SptbLAhVDao4KHSbJBqIQ2x8ICigF">?搜索工具?</a>? var clone2=last.cloneNode(false); clone2;// <a class=?"hdtb-tl" id=?"hdtb-tls" role=?"button" tabindex=?"0" data-ved=?"0ahUKEwiYht2SptbLAhVDao4KHSbJBqIQ2x8ICigF">?</a>? clone1.ownerDocument==clone2.ownerDocument // true
cloneNode()方法不會復制添加DOM節點的JavaScript屬性,例如事件處理程序。這個方法只復制特性(包括通過特性綁定的事件處理程序 <h1 onclick="console.log('xxx')">xxx</h1> 會將事件復制成功),子節點(深復制情況下),其他一切都不會復制。
document.createTextNode(''); document.createTextNode(' '); document.createTextNode(' '); ...
也就是看該文本節點的data(ChacterData.prototype上的屬性)值就可以了,圖片上的data值是回車雖然在呈現上和空文本節點一樣,但並不是空所以不能被刪除了,所以注意這樣編代碼ul的childNodes裡的文本節點其實是回車符。
<ul> <li></li> <li></li> </ul>
參考:
《JavaScript高級程序設計》
MDN HTMLCollection
MDN NodeList