从原型链看DOM--Node类型

前言:html

本系列从原型,原型链,属性类型等方面下手学习了DOM文档对象模型,旨在弄清咱们在DOM中经常使用的每个属性和方法都清楚它从哪里来要到哪里作什么事,这样对于理解代码有必定启发。全靠本身在总结中摸索,因此对于一个问题要是深究还真能挖出许多有意思的东西,如今以为JavaScript这个东西简直愈来愈有意思了。node

 

正文:
DOM(文档对象模型)是针对HTML和XML文档的一个API(应用程序编程接口)。DOM描绘了一个层次化的节点树,容许开发人员添加,移除,修改页面某一部分,如今它已成为表现和操做页面标记的真正的跨平台,语言中立的方式。
DOM1为基本文档结构及查询提供了接口。本系列主要讨论JavaScript对DOM1级的实现,可是还会穿插一点DOM2和DOM3的内容。编程

 

节点层次数组

DOM能够将任何HTML和XML文档描绘成一个由多层节点构成的结构。节点分为好几种不一样的类型,每种类型分别表示文档中不一样的信息及标记。每一个节点都有各自特色,属性和方法,及和其余节点存在的关系。节点之间的关系构成了层次,而全部页面标记则表现为一个以特定节点为根节点的树形结构。节点比元素的概念大,元素只是节点的一种类型。浏览器

文档节点:每一个文档的根节点。HTML文档中文档节点( window.document=>#document )只有一个子节点即 HTML 元素。缓存

文档元素:文档中最外层元素,文档中的其余元素都包含在文档元素中,每一个文档只能有一个文档元素。在HTML页面中,文档元素始终都是 HTML 元素。XML中,没有预约义的元素,任何元素都能成为文档元素。app

每一段标记均可经过树中一个节点表示: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

相关文章
相关标签/搜索