从原型链看DOM--Node类型
前言:
本系列從原型,原型鏈,屬性類型等方面下手學(xué)習(xí)了DOM文檔對象模型,旨在弄清我們在DOM中常用的每一個屬性和方法都清楚它從哪里來要到哪里做什么事,這樣對于理解代碼有一定啟發(fā)。全靠自己在總結(jié)中摸索,所以對于一個問題要是深究還真能挖出許多有意思的東西,現(xiàn)在覺得JavaScript這個東西簡直越來越有意思了。
?
正文:
DOM(文檔對象模型)是針對HTML和XML文檔的一個API(應(yīng)用程序編程接口)。DOM描繪了一個層次化的節(jié)點樹,允許開發(fā)人員添加,移除,修改頁面某一部分,現(xiàn)在它已成為表現(xiàn)和操作頁面標(biāo)記的真正的跨平臺,語言中立的方式。
DOM1為基本文檔結(jié)構(gòu)及查詢提供了接口。本系列主要討論JavaScript對DOM1級的實現(xiàn),但是還會穿插一點DOM2和DOM3的內(nèi)容。
?
節(jié)點層次
DOM可以將任何HTML和XML文檔描繪成一個由多層節(jié)點構(gòu)成的結(jié)構(gòu)。節(jié)點分為好幾種不同的類型,每種類型分別表示文檔中不同的信息及標(biāo)記。每個節(jié)點都有各自特點,屬性和方法,及和其他節(jié)點存在的關(guān)系。節(jié)點之間的關(guān)系構(gòu)成了層次,而所有頁面標(biāo)記則表現(xiàn)為一個以特定節(jié)點為根節(jié)點的樹形結(jié)構(gòu)。節(jié)點比元素的概念大,元素只是節(jié)點的一種類型。
文檔節(jié)點:每個文檔的根節(jié)點。HTML文檔中文檔節(jié)點(?window.document=>#document?)只有一個子節(jié)點即?HTML?元素。
文檔元素:文檔中最外層元素,文檔中的其他元素都包含在文檔元素中,每個文檔只能有一個文檔元素。在HTML頁面中,文檔元素始終都是?HTML?元素。XML中,沒有預(yù)定義的元素,任何元素都能成為文檔元素。
每一段標(biāo)記都可通過樹中一個節(jié)點表示:HTML元素通過元素節(jié)點表示,特性通過特性節(jié)點表示,文檔類型通過文檔類型節(jié)點表示,注釋通過注釋節(jié)點表示...共有12種節(jié)點類型,這些類型都繼承自一個基類型Node類型。下面來挨個看這些節(jié)點類型,但是本篇著重學(xué)習(xí)Node類型,其他類型和DOM操作技術(shù)在后續(xù)系列的總結(jié)中。
DOM操作技術(shù)
Node類型:
DOM1級定義了一個Node接口,該接口將由DOM中所有節(jié)點類型實現(xiàn)。這個Node接口在JavaScript中作為Node類型實現(xiàn),JavaScript中所有節(jié)點類型(Element,Attr,Text,CDATASection,Comment,Document,DocumentType,DocumentFragment等)都繼承自Node類型(?Element.prototype instanceof Node;// true?),因此所有節(jié)點類型的實例都共享著原型鏈(某類型實例->某類型.prototype->Node.prototype->EventTarget.prototype->Object.prototype)上的屬性和方法,?document instanceof Node;// true?比如文檔節(jié)點?#document?就是Document類型實例也是Node類型的實例,文檔元素?html?是HTMLElement類型的實例是Element類型實例也是Node類型的實例。
下面這段可以略過,只是我的一個小思考:
document.documentElement.__proto__==HTMLElement.prototype;// false //居然是false,這個html元素節(jié)點實例的原型指向的不是HTMLElement構(gòu)造函數(shù)的prototype??但是... HTMLElement.prototype.isPrototypeOf(document.documentElement);// true //判斷HTMLElement.prototype確實是在html元素的原型鏈上啊有沒有覺得很奇怪,?__proto__?不是指向構(gòu)造這個實例的函數(shù)的原型嗎??為什么會是?false?,而且?document.documentElement.__proto__?和?HTMLelement.prototype?竟然不是同一類型的,按理說?html?元素作為HTMLElement的實例,默認它們指向同一個?HTMLElement.prototype?對象的。
百思不得其解,一度認為我對?__proto__?這個東西是不是理解有誤,查看相關(guān)文檔MDN Object.prototype.__proto__后受了點啟發(fā),
是不是?document.documentElement.__proto__?被JS引擎重寫了!!?讓其重新指向一個為HTMLElement類型的實例對象(假設(shè)就叫?HTMLEleObj?吧,其實是?HTMLHtmlElement.prototype?),查看?HTMLEleObj?的?__proto__?,發(fā)現(xiàn)這個東西類型為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?)又增加了一個對象,現(xiàn)在原型鏈變成(?html.__proto__->HTMLEleObj;HTMLEleObj.__proto__->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype?)。還是畫個圖比較好理解點。但是還是不明白為何JS引擎要在原型鏈上增加這么個對象,有什么用處?發(fā)現(xiàn)html元素身上有兩個屬性,版本和構(gòu)造器,然而并不能直接通過?HTMLHtmlElement.prototype.version?訪問?version?屬性(每個元素的?__proto__?除了?constructor?屬性外其余的這些屬性還都不一樣,不過都是HTML5之前元素上的屬性),需要通過它的實例(html元素)訪問。?constructor?指向?HTMLHtmlElement?接口。
---補充---
通過?document.documentElement.__proto__.constructor?訪問得到,HTMLEleObj對象其實是HTMLHtmlElement接口的原型對象,雖然以上思考有些誤解,但是還是留下這個思考的過程吧。真正的原型鏈?zhǔn)??某元素.__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?屬性訪問到其對應(yīng)的構(gòu)造器。
扯遠了,回歸主題來看Node類型:
Node.prototype上的屬性及方法
注意到?Node.prototype?是?EventTarget?類型的實例對象,怪不得?Node.prototype.__proto__?會指向?EventTarget.prototype?。
Object.getOwnPropertyNames(EventTarget.prototype);// ["addEventListener", "removeEventListener", "dispatchEvent", "constructor"]所有這些關(guān)系總結(jié)來說就是Node和EventTarget是JavaScript中兩種類型,用組合繼承模式實現(xiàn)的話就內(nèi)部實現(xiàn)可能是這樣子:
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?會被初始化上一些屬性和實例。不過事實上我們并不能成功構(gòu)造?Node?和?EventTarget?類型的實例,控制臺會提示報錯不合法的構(gòu)造。那應(yīng)該是JS引擎內(nèi)部自己實現(xiàn)的吧,不然誰都能構(gòu)造這種底層接口的實例那就亂套了。
?
Node.prototype常見屬性和方法:
這些關(guān)系指針屬性大部分都是只讀的因為訪問器屬性的?set?被設(shè)置為?undefiend?了,即使?set?不為?undefiend?,但?set?方法能被使用的前提是該節(jié)點的對應(yīng)要訪問的那個屬性不為?null?。所以DOM提供了一些操作節(jié)點的方法(從第9小點開始總結(jié),這些方法都是可寫的,并且在Node.prototype上可以重寫這些方法)
每個節(jié)點(Node原型鏈上的實例對象)都可繼承該屬性(通過?Node.prototype?原型鏈訪問),用于表明節(jié)點類型。
節(jié)點類型:由在Node類型中定義的下列12個數(shù)值常量表示,任何節(jié)點類型必是其一。這些類型屬性是定義在Node構(gòu)造函數(shù)身上的(靜態(tài)屬性),可以看到上面的圖有輸出Node上面的屬性。加*為重點講解。
* 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.NOTATON_NODE; 12
但這些屬性值表示的具體什么類型節(jié)點也可通過直接在?Node.prototype?對象上訪問或通過原型鏈訪問。比如要訪問注釋Comment類型節(jié)點,三種方式均可
Node.prototype.COMMENT_NODE==Node.COMMENT_NODE;// true Node.prototype.COMMENT_NODE;// 8 Node.COMMENT_NODE;// 8 document.COMMENT_NODE;// 8 應(yīng)用:通過利用節(jié)點類型屬性可以確定節(jié)點的類型,為了兼容那些沒有公開Node類型的構(gòu)造函數(shù)的瀏覽器,我們就不用Node.ELEMENT_NODE形式訪問類型值,而是直接通過數(shù)字值判斷
if(someNode.nodeType==1){console.log("這是一個元素節(jié)點"); }
?nodeName和nodeValue屬性:
表示節(jié)點具體信息。
(1).對于Element元素節(jié)點:原型鏈繼承關(guān)系為:某元素節(jié)點.__proto__->HTML某元素Element.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype。
nodeName保存的為元素的標(biāo)簽名,nodeValue的值為null。
nodeName值是特性的名稱,nodeValue值是特性的值。
var html=document.documentElement; //獲取特性實例所在的對象 html.attributes;//attributes屬性是Element.prototype上的屬性
?
這個對象是NamedNodeMap類型的實例,這個對象的原型鏈關(guān)系為html.attributes.__proto__->NamedNodeMap.prototype->Object.prototype。這個對象里面又有幾個屬性,這幾個屬性才是我們需要的真正特性對象。
nodeName值為"#text",nodeValue值為節(jié)點所包含的文本。
(4).對于CDATASection類型節(jié)點:該類型只針對基于XML文檔。原型鏈的繼承關(guān)系為:CDATASection實例._proto__->CDATASection.prototype->Text.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。
nodeName值為"#cdata-section",nodeValue值為CDATA區(qū)域中的內(nèi)容。
(5).對于Comment類型節(jié)點:原型鏈的繼承關(guān)系為:Comment類型實例.__proto__->Comment.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。
nodeName值為"#comment",nodeValue值為注釋的內(nèi)容。
(6).對于Document類型節(jié)點:原型鏈的繼承關(guān)系為(以瀏覽器中document為例):document.__proto__->HTMLDocument.prototype->Document.prototype->Node.prototype->EventTarget.prototype。
可以看到Document.prototype上的屬性和方法很多有176個,返回這個數(shù)組是Array類型的實例。
發(fā)現(xiàn)了document節(jié)點對象和element元素節(jié)點對象的事件屬性還不是統(tǒng)一繼承的,是在各自原型鏈上繼承的事件屬性。
NodeName值為"#document",NodeValue值為null。
(7).對于DocumentType類型節(jié)點:原型鏈的繼承關(guān)系為:document.doctype.__proto__->DocumentType.prototype->Node.prototype->EventTarget.prototype。
nodeName的值為doctype的名稱,nodeValue的值為null。
(8).對于DocumentFragment類型的節(jié)點:原型鏈的繼承關(guān)系為:文檔片段實例.__proto__->DocumentFragment.prototype->Node.prototype->EventTarget.prototype。
注意到這也和Document.prototype上的方法重合了。
nodeName的值"#document-fragment",nodeValue的值為null。
每個節(jié)點都可繼承該屬性,其中保存著NodeList對象。
區(qū)別NodeList接口和HTMLCollection接口:
(1).NodeList接口是為節(jié)點的childNodes屬性提供的,原型鏈的關(guān)系為:某節(jié)點.childNodes.__proto__->NodeList.prototype->Object.prototype。
符合的有g(shù)etElementsByName,childNodes,querySelectorAll等
對于原型上的item方法,返回NodeList對象中指定索引的節(jié)點,如果索引越界,則返回?null?。等價的寫法是?nodeList[idx]?, 不過這種情況下越界訪問將返回?undefined?(因為是以數(shù)組形式訪問的)。
對arguments對象使用Array.prototype.slice()方法將其轉(zhuǎn)換為數(shù)組,采用同樣方法也可以將NodeList類型集合轉(zhuǎn)換為數(shù)組類型,其實就是就是在類數(shù)組對象的上下文中調(diào)用原生的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參數(shù)不是number類型時,slice返回length之前的項end=collections.length;return Array.prototype.slice.call(collections,start,end);}}} } (2)HTMLCollection接口是為一個包含了元素的通用集合,原型鏈的關(guān)系為:通過某用法(比如getElementsByTagName,getElementsByClassName,getElementsByTagNameNS,document.forms等)獲取的節(jié)點集合.__proto__->HTMLCollection.prototype->Object.prototype
(3)NodeList類型集合大部分時候和HTMLCollection類型集合都是即時更新的,當(dāng)其所包含的文檔結(jié)構(gòu)發(fā)生改變時,它會自動更新。以下圖片為示例,nodechilds集合和lis集合雖然保存的內(nèi)容一樣,但它兩不相等,是因為nodechilds保存的引用地址和lis保存的引用地址不一樣,但這兩引用地址所指向的內(nèi)存堆中各的自集合對象里的每一項引用都是同一個element節(jié)點對象。所以刪除一個li節(jié)點后,nodechilds和lis集合都會發(fā)生變化。
正是因為動態(tài)集合,childNodes.length會實時變化,因而 //刪除childNodes中的所有文本節(jié)點,因為child.length是動態(tài)變化的,所以分情況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也有時候表現(xiàn)為靜態(tài)集合,以意味著對文檔對象模型任何改動都不會影響集合內(nèi)容。querySelectorAll就是靜態(tài)的
所以當(dāng)你選擇遍歷NodeList中所有項,或緩存列表長度時候,考慮要用哪種。
該屬性指向文檔樹中的父節(jié)點(可能是Document類型也可能是Element類型)
childNodes列表中第一個節(jié)點的previousSibling屬性值為null,列表中最后一個節(jié)點的nextSibling屬性也為null。
父節(jié)點的這兩個屬性分別指向其childNodes的第一個和最后一個節(jié)點,在只有一個子節(jié)點情況下,firstChild和lastChild指向同一個節(jié)點。若沒有子節(jié)點,這兩屬性為null。
這個函數(shù)的writable屬性看來是允許可寫的,那就重寫該方法試試,我是重寫在節(jié)點實例對象上了,當(dāng)然你也可以在原型Node.prototype上重寫該方法是可以的。
在節(jié)點包含一個或多個子節(jié)點情況下返回true,在判斷時這比childNodes.length更簡便。
該屬性指向表示整個文檔的文檔節(jié)點。這種關(guān)系表示的是任何節(jié)點都屬于它所在的文檔,任何節(jié)點都不能同時存在兩個或兩個以上文檔中。當(dāng)我們不必在節(jié)點層次中通過層層回溯到達頂端而是可以直接?節(jié)點對象.ownerDocument?訪問,但要注意?document.ownerDocument;// null?文檔節(jié)點本身的文檔節(jié)點為null。
又是偶然我發(fā)現(xiàn)一個好玩的現(xiàn)象,當(dāng)重寫Node.prototype.appendChild方法后,發(fā)現(xiàn)只要可獲得焦點的區(qū)域(比如a元素,input元素,button元素等)獲得焦點后就會執(zhí)行appendChild函數(shù),但是當(dāng)輸入內(nèi)容期間并不觸發(fā)該函數(shù),然后刪除內(nèi)容的時候又會觸發(fā)該函數(shù)執(zhí)行。每次執(zhí)行就執(zhí)行吧它還很奇怪的執(zhí)行4次。由此可以猜想當(dāng)在文檔中獲得一個焦點后就相當(dāng)于觸發(fā)了appendChild事件??因為DOM文檔結(jié)構(gòu)本來靜態(tài)的,突然插進來一個光標(biāo),DOM結(jié)構(gòu)被改動了所以才會觸發(fā)appendChild?因為光標(biāo)一直在DOM結(jié)構(gòu)中的某個位置如果沒有移出的話,在原地方編輯內(nèi)容因為還是在原地方改動所以并不觸發(fā)appendChild。就是不理解為什么每次觸發(fā)要執(zhí)行4次該函數(shù)(對于博客園這個編輯頁面來說是4次,其他頁面測試也有3次的)。而我們平常之所以插入光標(biāo),輸入內(nèi)容,刪除內(nèi)容,離開光標(biāo)感覺瀏覽器對此并沒什么反應(yīng)我估計是JS引擎實現(xiàn)appendChild方法內(nèi)部給做了妥善處理,其實應(yīng)該還是觸發(fā)這個DOM級的事件了吧。
再次回歸主題:appendChild()用于向childNodes列表的末尾添加一個節(jié)點,添加后,childNodes的新增節(jié)點,父節(jié)點及以前的最后一個子節(jié)點的關(guān)系指針都會得到相應(yīng)更新。如果傳入到appendChild()中的節(jié)點已經(jīng)是文檔中的一部分了,那結(jié)果就是將該節(jié)點從原來位置移動到新位置。
var a=document.body.firstChild; document.body.appendChild(document.body.firstChild)==a;// true a==document.body.lastChild;// true
//要插入到desEle之后,相當(dāng)于插入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 節(jié)點 證明還在文檔樹
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()方法不會復(fù)制添加DOM節(jié)點的JavaScript屬性,例如事件處理程序。這個方法只復(fù)制特性(包括通過特性綁定的事件處理程序?<h1 onclick="console.log('xxx')">xxx</h1>?會將事件復(fù)制成功),子節(jié)點(深復(fù)制情況下),其他一切都不會復(fù)制。?
空文本節(jié)點指的是內(nèi)容為空才能被刪除,比如 document.createTextNode(''); document.createTextNode(' '); document.createTextNode(' '); ...
?也就是看該文本節(jié)點的data(ChacterData.prototype上的屬性)值就可以了,圖片上的data值是回車雖然在呈現(xiàn)上和空文本節(jié)點一樣,但并不是空所以不能被刪除了,所以注意這樣編代碼ul的childNodes里的文本節(jié)點其實是回車符。
?
參考:
《JavaScript高級程序設(shè)計》
MDN HTMLCollection
MDN NodeList
轉(zhuǎn)載于:https://www.cnblogs.com/venoral/p/5293575.html
總結(jié)
以上是生活随笔為你收集整理的从原型链看DOM--Node类型的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。