從原型鏈看DOM--Node類型


前言:

本系列從原型,原型鏈,屬性類型等方面下手學習了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操作技術在后續系列的總結中。

  1. Node類型
  2. Document類型
  3. Element類型
  4. Text類型
  5. Comment類型
  6. CDATASection類型
  7. DocumentType類型
  8. DocumentFragment類型
  9. Attr類型

DOM操作技術

  1. 動態腳本
  2. 動態樣式
  3. 操作表格
  4. 使用NodeList




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上可以重寫這些方法)

  1. nodeType屬性:

    每個節點(Node原型鏈上的實例對象)都可繼承該屬性(通過 Node.prototype 原型鏈訪問),用於表明節點類型。
    節點類型:由在Node類型中定義的下列12個數值常量表示,任何節點類型必是其一。這些類型屬性是定義在Node構造函數身上的(靜態屬性),可以看到上面的圖有輸出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
    但這些屬性值表示的具體什么類型節點也可通過直接在 Node.prototype 對象上訪問或通過原型鏈訪問。比如要訪問注釋Comment類型節點,三種方式均可
    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(
    "這是一個元素節點");
    }
  2.  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。

    nodeName值是特性的名稱,nodeValue值是特性的值。
    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。

    nodeName值為"#text",nodeValue值為節點所包含的文本。
    (4).對於CDATASection類型節點:該類型只針對基於XML文檔。原型鏈的繼承關系為:CDATASection實例._proto__->CDATASection.prototype->Text.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。

    nodeName值為"#cdata-section",nodeValue值為CDATA區域中的內容。
    (5).對於Comment類型節點:原型鏈的繼承關系為:Comment類型實例.__proto__->Comment.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。

    nodeName值為"#comment",nodeValue值為注釋的內容。
    (6).對於Document類型節點:原型鏈的繼承關系為(以瀏覽器中document為例):document.__proto__->HTMLDocument.prototype->Document.prototype->Node.prototype->EventTarget.prototype。

    可以看到Document.prototype上的屬性和方法很多有176個,返回這個數組是Array類型的實例。







    發現了document節點對象和element元素節點對象的事件屬性還不是統一繼承的,是在各自原型鏈上繼承的事件屬性。
    NodeName值為"#document",NodeValue值為null。
    (7).對於DocumentType類型節點:原型鏈的繼承關系為:document.doctype.__proto__->DocumentType.prototype->Node.prototype->EventTarget.prototype。

    nodeName的值為doctype的名稱,nodeValue的值為null。
    (8).對於DocumentFragment類型的節點:原型鏈的繼承關系為:文檔片段實例.__proto__->DocumentFragment.prototype->Node.prototype->EventTarget.prototype。

    注意到這也和Document.prototype上的方法重合了。
    nodeName的值"#document-fragment",nodeValue的值為null。
  3. childNodes屬性:

    每個節點都可繼承該屬性,其中保存着NodeList對象。
    區別NodeList接口和HTMLCollection接口:
    (1).NodeList接口是為節點的childNodes屬性提供的,原型鏈的關系為:某節點.childNodes.__proto__->NodeList.prototype->Object.prototype。
    符合的有getElementsByName,childNodes,querySelectorAll等

    對於原型上的item方法,返回NodeList對象中指定索引的節點,如果索引越界,則返回 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

    (3)NodeList類型集合大部分時候和HTMLCollection類型集合都是即時更新的,當其所包含的文檔結構發生改變時,它會自動更新。以下圖片為示例,nodechilds集合和lis集合雖然保存的內容一樣,但它兩不相等,是因為nodechilds保存的引用地址和lis保存的引用地址不一樣,但這兩引用地址所指向的內存堆中各的自集合對象里的每一項引用都是同一個element節點對象。所以刪除一個li節點后,nodechilds和lis集合都會發生變化。


    正是因為動態集合,childNodes.length會實時變化,因而
    //刪除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就是靜態的

    所以當你選擇遍歷NodeList中所有項,或緩存列表長度時候,考慮要用哪種。
  4. parentNode屬性:

    該屬性指向文檔樹中的父節點(可能是Document類型也可能是Element類型)
  5. previousSibling和nextSibling屬性:

    childNodes列表中第一個節點的previousSibling屬性值為null,列表中最后一個節點的nextSibling屬性也為null。
  6. firstChild和lastChild屬性:

    父節點的這兩個屬性分別指向其childNodes的第一個和最后一個節點,在只有一個子節點情況下,firstChild和lastChild指向同一個節點。若沒有子節點,這兩屬性為null。
  7. hasChildNodes():

    這個函數的writable屬性看來是允許可寫的,那就重寫該方法試試,我是重寫在節點實例對象上了,當然你也可以在原型Node.prototype上重寫該方法是可以的。

    在節點包含一個或多個子節點情況下返回true,在判斷時這比childNodes.length更簡便。
  8. ownerDocument:

    該屬性指向表示整個文檔的文檔節點。這種關系表示的是任何節點都屬於它所在的文檔,任何節點都不能同時存在兩個或兩個以上文檔中。當我們不必在節點層次中通過層層回溯到達頂端而是可以直接 節點對象.ownerDocument 訪問,但要注意 document.ownerDocument;// null 文檔節點本身的文檔節點為null。
  9. parentNode.appendChild():返回新增節點。
    又是偶然我發現一個好玩的現象,當重寫Node.prototype.appendChild方法后,發現只要可獲得焦點的區域(比如a元素,input元素,button元素等)獲得焦點后就會執行appendChild函數,但是當輸入內容期間並不觸發該函數,然后刪除內容的時候又會觸發該函數執行。每次執行就執行吧它還很奇怪的執行4次。由此可以猜想當在文檔中獲得一個焦點后就相當於觸發了appendChild事件??因為DOM文檔結構本來靜態的,突然插進來一個光標,DOM結構被改動了所以才會觸發appendChild?因為光標一直在DOM結構中的某個位置如果沒有移出的話,在原地方編輯內容因為還是在原地方改動所以並不觸發appendChild。就是不理解為什么每次觸發要執行4次該函數(對於博客園這個編輯頁面來說是4次,其他頁面測試也有3次的)。而我們平常之所以插入光標,輸入內容,刪除內容,離開光標感覺瀏覽器對此並沒什么反應我估計是JS引擎實現appendChild方法內部給做了妥善處理,其實應該還是觸發這個DOM級的事件了吧。

    再次回歸主題:appendChild()用於向childNodes列表的末尾添加一個節點,添加后,childNodes的新增節點,父節點及以前的最后一個子節點的關系指針都會得到相應更新。如果傳入到appendChild()中的節點已經是文檔中的一部分了,那結果就是將該節點從原來位置移動到新位置。
    var a=document.body.firstChild;
    document.body.appendChild(document.body.firstChild)
    ==a;// true
    a==document.body.lastChild;// true
  10. parentNode.insertBefore(要插,參照):插入到childNodes列表中某個特定位置,參數一為要插入的節點,參數二為作為參照的節點。插入節點后被插入的節點會變成參照節點的前一個同胞節點(previousSibling),返回被插入的節點。如果參照節點是null,則insertBefore的效果和appendChild()一樣,可以這么理解:把源節點插入到目標節點null之前,那誰是null節點呢,不就是父節點的最后一個子節點的nextSibling嗎,所以插到這個null節點之前就相當於插入父節點的最后一個子節點之后。基於這個思路可以實現我們自己的insertAfter()
    //要插入到desEle之后,相當於插入desEle.nextSibling之前,返回被插入的srcEle
    Node.prototype.insertAfter=function(srcEle,desEle){
    this.insertBefore(srcEle,desEle.nextSibling);
    return srcEle;
    }
  11. parentNode.replaceChild(newNode,parentNode.child):參數為要插入的新節點,要替換的節點。要替換的節點將由這個方法返回並從文檔樹中移除,要插入的節點占據其位置。新節點的所有關系指針都會從被它替換的節點復制過來。盡管從技術上講被替換的節點仍然還在文檔中,但它在文檔中已經沒有自己的位置。
    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 節點 證明還在文檔樹
  12. removeChild():參數為要移除的節點,返回被移除的節點。被移除的節點仍然為文檔所有,但在文檔中已經沒有自己的位置。
  13. node.cloneNode():用於創建調用這個方法的節點的一個完全相同的副本。參數為true表示進行深復制(復制節點及其整個子節點樹),參數為false進行淺復制(只復制節點本身)。復制后返回的節點屬於文檔所有但並沒有為它指定父節點,可通過appendChild(),insertBefore(),replaceChild()將它添加到文檔中。
    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> 會將事件復制成功),子節點(深復制情況下),其他一切都不會復制。 

  14. normalize():處理文檔樹中的文本節點。由於解析器或DOM操作等原因,可能會出現文本節點不包含文本,或者接連出現兩個文本節點的情況。當在某個節點上調用這個方法時,就會在該節點的后代節點中查找上述兩種情況,如果找到空文本節點就刪除它,如果找到相鄰文本節點則將它合並為一個文本節點。
    空文本節點指的是內容為空才能被刪除,比如
    document.createTextNode('');
    document.createTextNode(
    ' ');
    document.createTextNode(
    ' ');
    ...

     也就是看該文本節點的data(ChacterData.prototype上的屬性)值就可以了,圖片上的data值是回車雖然在呈現上和空文本節點一樣,但並不是空所以不能被刪除了,所以注意這樣編代碼ul的childNodes里的文本節點其實是回車符。

    <ul>
    <li></li>
    <li></li>
    </ul>

 

參考:
《JavaScript高級程序設計》

MDN HTMLCollection
MDN NodeList


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com