先来一颗栗子:javascript
<img src="/sub/123.jpg" alt="test" /> <script type="text/javascript"> var img = document.getElementsByTagName('img')[0]; console.log('src:', img.src); </script>
输出 src: sub/123.jpg?No,输出的是 src: http://127.0.0.1:8020/sub/123.jpg, 但我其实只想要一个pathname而已啊。虽然有一万种办法能够从完整地址中取出pathname,但我仍是想一次获取我们写到属性里面的那个src啊。css
固然,这样的接口必须是有的:html
console.log('src:', img.getAttribute('src'));
// src: /sub/123.jpg
之前总以为HTML里面东西很少,但如今发现实际上是本身了解的很少,仍是too naive啊!DOM结构竟然都这么不熟悉。找到问题,又回去看了看高程3,总以为里面按照DOM123分类很没逻辑,仍是试着按照功能区分总结一下吧。java
先看看DOM结构层次,上个栗子:node
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>DOM</title> </head> <body> <!--wrapper--> <div class="wrp"> wrp: <img src="/sub/123.jpg" alt="test" /> <script type="text/javascript"> var img = document.getElementsByTagName('img')[0]; console.log('src:', img.getAttribute('src')); </script> </div> </body> </html>
这种比较常见的HTML文本,里面就已经包含了咱们经常使用到的一个Node类型。DOM会将HTML文档以节点树的形式组织起来,也就是说Node与HTML文档是对应,即便是代码中的换行这种美观上东西,也会真实地反映到节点树上。chrome
上面的HTML对应的Nodelist:编程
整个文档就是一个document节点,这个咱们用的也比较多了。document中有几个属性用的比较少:URL、domain、referrer,其中domain能够设置,利用这一点能够解决一些子域之间的跨域问题。另外的一些特殊集合,如forms、images、links这些,能够方便操做,作爬虫的时候也能够用来简化模型。跨域
document下面有document type、element、comment、text几种经常使用的节点。数组
这里要注意的是Text节点和element节点的区别,一般咱们会在element节点里面写入字符,但并非说element里面包含字符,而是element节点里面嵌入了一个text节点,text节点里面的内容才是咱们写进的字符。例如 <p>papapa</p>的结构应该是:app
> 换行
另一个常被忽略的是换行。为了美化代码,一般咱们每一个标签之间都会换行,DOM在解析HTML文档时,会把换行也解析成 Textnode 节点!也就是咱们每一行HTML代码后面都自带一个textnode节点\n!
说到关系,也就是父子关系、兄弟关系这两种了。为了定位一个节点,咱们能够须要DOM提供的几个定位接口:firstChild、lastChild、previousSibling、nextSibling,还有parentNode、childNodes。利用这几个接口的组合就能够定位到具体某个节点了。
还有一点就是Nodelist自己,Nodelist是一个动态的类数组对象,动态的意思就是DOM发生改变以后,变更会实时更新到Nodelist上,从这个性质来看,Nodelist应该是一个引用集或者指针集。类数组的意思就是说其实人家不是真正的数组,只是长得有点像。因此遍历的时候用for in也会将某些诸如对象自己的方法属性都遍历出来啦,仍是常规的for i to length就行了,也能够用forEach、for of啦。
每一个node节点都有相应的nodetype、nodeName和nodeValue属性,见名知义,分别表明了节点的类型、名字和值。这里要说一下,element节点的nodeName为标签名,而nodeValue则为null,文本和comment的nodeValue为相应的字符串。
说到操做,无非就是增删改查。而DOM操做中,主体都是element节点,因此基本也是对element节点进行操做。
DOM的查有两种:准确查询和遍历查询。
1) 准确查询是给定一个条件去搜索,主要有两类接口:getElementBy***与querySelector***。
这两种查询的区别在于实时性与非实时性。getElement**的方式返回的是Nodelist,因此具备实时性。而querySelector**返回的是一个快照,DOM表的变化不能实时反映到查询的结果里面。看看这个例子:
<div class="wrp"> <p>papapa</p> </div> <script> var divs = document.getElementsByClassName('wrp'), // 复制7个 // var divs = document.querySelectorAll('.wrp'), // 复制1个 i, div = null; for(i = 0; i < divs.length; i++) { div = divs[0].cloneNode(true); document.body.appendChild(div); if(i > 5) { break; } } </script>
采用getElements***的方式,那divs是动态变化的,因此会复制7个papapa出来。假如采用querySelector***的方式,则divs只保存了刚查询的时候那个状态,所以只会复制一个papapa出来。因此,假如采用getElement的方式查询,那么就须要采用一个len变量来保存当前状态,不然就会形成死循环。按照咱们日常的认知思惟,divs也不该该动态变化,毕竟我后面没有继续查询啊,你怎么能变呢。因此,querySelector的方式更可控,不会搞出一些莫名其妙的bug,并且jQuery风格想必你们仍是喜欢的。
2)遍历
DOM提供了2个遍历迭代器:NodeIterator与TreeWalker,固然你也能够手动实现遍历。
NodeIterator的用法看下面例子:
1 <div class="wrp"> 2 <p>papapa</p> 3 <div> 4 <span>yohohoho</span> 5 <input class='input1' type="text" name="" value="" /> 6 </div> 7 <input class="input2" type="text" name="" value="" /> 8 </div> 9 <script> 10 var div = document.querySelector('.wrp'); 11 var filter = function(node) { 12 return node.tagName.toLowerCase() === 'input' ? 13 NodeFilter.FILTER_ACCEPT : 14 NodeFilter.FILTER_SKIP; 15 } 16 var myIterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false); 17 var n = myIterator.nextNode(); // move to root(div) 18 while(n !== null) { 19 console.log(n.className); 20 n = myIterator.nextNode(); 21 } 22 </script>
TreeWalker遍历:
1 var div = document.querySelector('.wrp'); 2 var filter = function(node) { 3 return node.tagName.toLowerCase() === 'input' ? 4 NodeFilter.FILTER_ACCEPT : 5 NodeFilter.FILTER_SKIP; 6 } 7 8 var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false); 9 var n = walker.firstChild(); 10 while(n !== null) { 11 console.log(n.className); 12 n = walker.nextNode(); 13 }
手动方式,先说说思路:首先要考虑子级中还有子级,那显然这里要用递归的形式来作,判断节点是否有子节点而递归。而后就是一些细节方面的,好比为了剔除换行符和文本节点的影响,那固然是直接使用children来获取子级节点了。另外还须要避免Nodelist的动态变化之类的。看代码:
1 function mywalker(el, filter, cb) { 2 var children = el.children; 3 for(var i = 0, len = children.length; i < len; i++) { 4 var walker = children[i]; 5 if(walker.children.length !== 0) { 6 mywalker(walker, filter, cb); 7 } else if(filter(walker)) { 8 cb(walker); 9 } 10 } 11 } 12 13 var div = document.querySelector('.wrp'); 14 15 function filter(node) { 16 return node.tagName.toLowerCase() === 'input'; 17 } 18 19 function cb(node) { 20 console.log(node.className); 21 } 22 23 mywalker(div, filter, cb);
若是还要考虑文本节点的话,那可使用childNodes来遍历,这个时候就要注意过滤换行符了。
DOM节点的增接口有:appendChild() 和 insertBefore()。但建立节点则须要用到另外两个接口:document.creat***方式和document.cloneNode()。建立节点以后经过添加接口将节点放入文档中。这里除了能够建立普通的节点以外,也能够建立脚本和样式表,这种用法能够实现按需加载,延迟加载等各类资源加载方法。固然要作成像requirejs那样的加载器,那就须要添加不少处理逻辑了。
删除有两个接口:replaceChild、removeChild。
1)内容的更改:innerHTML、innerText,textContent之类的。textContent通常不会用到,他是将节点内部全部文本节点拼接在一块儿的字符串,包括换行符这些也都塞了进去,因此还须要进一步进行字符串处理。
2)属性的更改
文章开头其实就已经提到了最经常使用的两种更改属性的方法,分别是 Element.props=value 和 Element.setAttribute() 系列。其中getAttribute()取的是Attribute节点上的值,也就是对HTML标签的尖括号<>内的这串字符进行查询。假如没定义则返回一个null,有定义的则返回一个字符串。而Element.props是对HTML标签的实例进行查询,也就是对一个实例化后的节点对象进行查询。而这个对象在初始化的时候就会将HTML标签中没定义的属性置为””有定义则进行解析转换。因此对同一个对象进行查询,没定义的属性.prop返回空值而getAttribute返回null;对style和onload一类事件属性进行查询,前者返回一个对象,然后者返回一个字符串或null;对自定义属性的查询,前者返回一个undefined,然后者则返回自定义的值。
而文章开头中两种查询获得的结果不一样,缘由就在于此。
> attributes
节点中统一管理各类属性的是 attributes 属性,它是一个NamedNodeMap,保存了对象已定义的全部属性。这货其实也是一个类数组,这里chrome却是打印的很清楚,一目了然。记得不能用for in遍历哦。
能够经过attributes[‘id’]的形式去查询相应的属性,它自身也有一些方法,but没什么人会用它,并且attributes这个属性自己就不多会被用到。使用它的场景之一(或者能够说惟一)就是要对属性的集中管理,或者遍历或者批量初始化,而日常咱们用的基本都是.prop的形式,直观方便。
> 自定义属性
咱们能够往标签里加入任意的自定义属性,自由虽好但规范仍是要有。HTML5中是以data-prop的形式来定义自定义属性的。挂在data下的属性就能够经过dataset属性统一管理,通常能够将数据放到data上,而后渲染的时候直接读取dataset属性进行渲染了,像knockout这种库也是这么来进行数据绑定的。
> 样式
属性中很是重要的一点就是样式,这里又涉及到内联样式和嵌入、外联两类的样式控制。上文也说到,使用element.stylt.样式能够直接更改样式,也能够经过element.style.cssText来一次读写全部的内联样式。但样式比较复杂的地方是嵌入、外联样式的叠加影响,因此单纯查询style并不能获得元素的最终样式。这个时候能够经过document.defaultView的getComputedStyle()方法来获取计算样式,就像chrome开发者工具那样获得计算样式。
可是,CSS是个大工程,试图经过js来动态控制全部样式的变化是不靠谱的。更为常见的作法是CSS预设各类状态,而js做为控制来控制状态的转换,也就是控制class的变换。DOM编程中除了单纯的className以外,更为强大的接口是classList,这又是一个类数组对象。它提供了几个好用的类管理方法:add、contains、remove、toggle,满满jQuery风啊。但jQuery毕竟是个民间女子,当正统后宫吸取了她的各类奇法淫技以后,失宠也是不可避免的。
琢磨了很久,但写出来仍是各类凌乱,结构仍是不够清晰。DOM的结构层次以Nodelist为基础,不一样nodeType的节点有机组合就构成了一个DOM表。对DOM的操做有各类接口,感受比较混乱,可能这也是各类框架一直致力于弱化开发者对DOM操做的缘由吧,但在超小规模应用上熟悉了DOM操做的话,仍是会比使用框架更加灵活。
对DOM的总结就到这里了,看了下仍是整理为主,背书为辅哈,必须填图了。