从原型链看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操作技术在后续系列的总结中。
- Node类型
- Document类型
- Element类型
- Text类型
- Comment类型
- CDATASection类型
- DocumentType类型
- DocumentFragment类型
- Attr类型
DOM操作技术
- 动态脚本
- 动态样式
- 操作表格
- 使用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上可以重写这些方法)
- 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
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 */
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
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。 - 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); } } } }
(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中所有项,或缓存列表长度时候,考虑要用哪种。 - parentNode属性:
该属性指向文档树中的父节点(可能是Document类型也可能是Element类型) - previousSibling和nextSibling属性:
childNodes列表中第一个节点的previousSibling属性值为null,列表中最后一个节点的nextSibling属性也为null。 - firstChild和lastChild属性:
父节点的这两个属性分别指向其childNodes的第一个和最后一个节点,在只有一个子节点情况下,firstChild和lastChild指向同一个节点。若没有子节点,这两属性为null。 - hasChildNodes():
这个函数的writable属性看来是允许可写的,那就重写该方法试试,我是重写在节点实例对象上了,当然你也可以在原型Node.prototype上重写该方法是可以的。
在节点包含一个或多个子节点情况下返回true,在判断时这比childNodes.length更简便。 - ownerDocument:
该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在两个或两个以上文档中。当我们不必在节点层次中通过层层回溯到达顶端而是可以直接 节点对象.ownerDocument 访问,但要注意 document.ownerDocument;// null 文档节点本身的文档节点为null。 - 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
- 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; }
- 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 节点 证明还在文档树
- removeChild():参数为要移除的节点,返回被移除的节点。被移除的节点仍然为文档所有,但在文档中已经没有自己的位置。
- 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> 会将事件复制成功),子节点(深复制情况下),其他一切都不会复制。
- normalize():处理文档树中的文本节点。由于解析器或DOM操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况,如果找到空文本节点就删除它,如果找到相邻文本节点则将它合并为一个文本节点。
空文本节点指的是内容为空才能被删除,比如document.createTextNode(''); document.createTextNode(' '); document.createTextNode(' '); ...
也就是看该文本节点的data(ChacterData.prototype上的属性)值就可以了,图片上的data值是回车虽然在呈现上和空文本节点一样,但并不是空所以不能被删除了,所以注意这样编代码ul的childNodes里的文本节点其实是回车符。
<ul> <li></li> <li></li> </ul>
参考:
《JavaScript高级程序设计》