DOM是所有前端开发每天打交道的东西,但是随着jQuery等库的出现,大大简化了DOM操作,导致大家慢慢的“遗忘”了它的本来面貌。不过,要想深入学习前端知识,对DOM的了解是不可或缺的,所以本文力图系统的讲解下DOM的相关知识,如有遗漏或错误,还请大家指出一起讨论^ ^。
DOM(文档对象模型)是针对HTML和XML文档的一个API,通过DOM可以去改变文档。这个说法很官方,大家可能还不明白。
举个例子:我们有一段HTML,那么如何访问第二层第一个节点呢,如何把最后一个节点移动到第一个节点上面去呢?
DOM就是定义了如何做类似的操作,那么应该怎么做的标准呢,比如用getElementById
来访问节点,用insertBefore
来插入节点。
当浏览器载入HTML时,会生成相应的DOM树。
简而言之,DOM可以理解为一个访问或操作HTML各种表情的实现标准。
对于一个HTML来说,文档节点Document
(这个是看不到的)是他的根节点,对应的对象便是document
对象(严格的讲是子类HTMLDocument
对象,下面单独介绍Document
类型时会指出)。
换句话说存在一个文档节点Document
,然后他有子节点,比如通过document.getElementsByTagName(“html”)
,得到类型为元素节点的Element html。
每一段HTML标记都可以用相应的节点来表示,例如:
HTML元素通过元素节点表示,注释通过注释节点表示,文档类型通过文档类型节点表示等。
一共定义了12种节点类型,而这些类型又都继承自Node
类型。
所以我们首先讲Node
类型,因为这个类型的方法是所有节点都会继承的。
Node
是所有节点的基类型,所有节点都继承自它,所有所有节点都有一些共同的方法和属性。
先讲Node
类型的属性
首先是nodeType
属性,用来表明节点类型的,例如:
document.nodeType
;返回9,其中document
对象为文档节点的Document
的实例。
这里面,9代表的就是DOCUMENT_NODE
节点的意思,可以通过Node.DOCUMENT_NODE
查看对应的数字document.nodeType === Node.DOCUMENT_NODE; //true
至于一共有哪些节点,每个节点对应的数字又是多少,这个可以问问百度。反正最常用的元素节点是Element(对应数字为1)和文本几点Text(对应数字为3)
然后常用的还有nodeName
和nodeValue
对于元素节点nodeName
就是标签名,nodeValue
就是null
对于文本节点nodeName
为”#text”(chrome里面测试的),nodeValue
就是实际的值
每个节点还有childNodes
属性,这是个十分重要的属性,他保存了这个节点所有直接子元素
调用childNodes
返回的就是一个NodeList
对象,它极其像一个数组,但是有一个关键的地方,他是动态查询的,也就是说每次调用他都会对DOM结构查询,所以对它的使用需要谨慎,注意性能。
访问childNodes
可以使用数组下表或者item
方法
然后各个节点还存在各种属性让它们可以相互访问,下图是个很好的总结
比较有用的属性和方法:
1.hasChildNodes()
如果包含子节点就返回true
,比查询childNodes
的length
来的简单。
2.ownerDocument
返回文档节点的引用(在html里面也就是document
对象)
比较有用的操作DOM的方法:
appendChild()
方法可以在节点的childNodes
的末尾添加一个节点,值得注意的是如果这个节点是已经存在于文档中的,那么变回删除原节点,感觉上就像是移动节点一样。insertBefore()
方法接受两个参数,一个是插入的节点,另外一个是参照的节点。如果第二个参数为null
,则insertBefore
和appendChild
的效果一样。否则便会把节点插入到参照节点之前。这里要注意的是,如果第二个参数不为null
,那么插入的节点不能是已经存在的节点,否则结果同上。
replaceChild()
方法可以替换节点,接受两个参数,需要插入的节点和需要替换的节点。返回被替换掉的节点。
removeChild()
移除节点。这里有个常见的需求,比如我有一个节点#node,那么如何移除它呢?
var node = document.getElementById("node");
node.parentNode.removeChild(node); //先拿到父节点,再调用父节点的removeChild删除自己
这里大家需要注意一下,上面的四个方法都是操作某个节点的子节点,也就是说,操作前必须先找到父节点(通过parentNode
来找)
复制节点的方法
cloneNode()
复制节点,接受一个参数true
或者false
。如果true
就是复制那个节点和它的子节点,如果是false
,就是复制节点本身(复制出来的节点没有任何子元素)。这方法返回复制的节点,如果如要操作它,那么需要借助前面讲的四个方法来把这个节点放到html中去。
上面这些就是Node类型中常用的属性和方法。所有的类型都继承自Node类型所以这些属性和方法是所有节点都有的。
最开始讲DOM的时候提到了Document
类型。其实关于这个类型最重要的就是它的一个子类HTMLDocument
有一个实例对象document
。而这个document
对象是我们最常用的一个对象了。
document
对象又是在window
对象上,所以浏览器就可以直接方法document
了。
document对象的常用属性
document.childNodes
继承自上面讲的Node
类型,可以放文档的直接子节点(通常包括文档声明和html节点)
document.documentElement
可以直接拿到html节点的引用(等价于document.getElementsByTagName("html")[0])
document.body
body
节点的引用
document.title
页面title
,可以修改,会改变浏览器标签上的名字
document.URL
页面的url
document.referrer
取得referrer
,也就是打开这个页面的那个页面的地址,做来源统计时候比较有用
document.domain
取得域名,可以设置,但是同城只能设置为不包含子域名的情况,在一些子域名跨域情况下有效。
document对象常用的方法
getElementById
,传入id,得到元素节点。里面的参数区分大小写(IE8不区分)。注意:如果有多个id相同的元素,则返回第一个。IE7里面表单元素的name
也会被当做id来使用。
getElementsByTagName
根据标签取得元素,得到的是HTMLCollection
类型。如果传入的是”*”,则可以得到全部的元素。
还有一个只有HTMLDocument
类型(也就是document
对象)才有的方法getElementsByName
,就是根据那么返回元素。
document
对象还有一些集合,例如document.forms
可以返回所有的form
表单。类型也是HTMLCollection
.
说到HTMLCollection
,HTMLCollection
就是一个包含一个或者多个元素的集合,和讲的NodeList
很像。HTMLCollection
这个类型有两个方法,一个是通过下标(或者.item()
)得到具体元素,还有就是通过['name']
(或者.namedItem()
)获得具体元素。
最后,关于document
对象还有一套比较重要的方法,那就是
write()
, writeln()
, open()
, close()
open
和close
分别是打开和关闭网页的输出流,在页面加载的过程中,就相当于open
状态。这两个方法一般不会去用它。
write
和writeln
都是向页面写入东西,区别就是后者会多一个换行符。
需要注意的是:在页面加载过程中,可以使用这两个方法向页面添加内容。如果页面已经加载完了,再调用write,会重写整个页面。
还有一点,如果要动态的写入脚本,例如这样的,那么就要把
分开拼装,否则会被误以为是脚本结束的标志,导致这个结束符匹配到上面一个开始符。可以这样写“”
下面我们来说一下最重要也是最常见的一个类型,Element
类型。
我们平时所操作的都是Element
类型(实质上是HTMLElement
,这里为了方便理解,就简单这么说),比如
document.getElementById("test")
返回的就是Element
类型。我们平时所说的”DOM对象”,通常也就是指Element
类型的对象。
Element类型常见的属性
最常用的当然就是Node
类型上的那些属性和方法,这里就不在重复说了,主要说一下它独有的
首先是tagName
,这个和继承自Node类型的nodeName
一样。都是返回标签名,通常都是大写,有的浏览器会返回小写。所以在比较的时候最好调用一下类似toLowerCase()
这种方法再做比较。
上面我们提到了HTMLElement
类型,HTMLElement
类型继承自Element类型,也就是HTML元素的实际类型,我们在浏览器里用的元素都是这个类型。
这个类型的元素都具有一些标准属性,比如:
id
元素的唯一标识,title
通常是鼠标移上去时显示的信息,className
类名等等,这几个属性是可读写的,也就是说你改变它们以后会得到相应的效果。
除了属性外,还有几个重要的方法。首先说一下操作节点属性的方法,getAttribute
、setAttribute
、removeAttribute
这三个方法。这些是操作属性最常用的方法了,怎么用就不说了,大家应该都会用。还有一个attributes属性,保存了元素的全部属性。
这里要说一个问题,ele.className
和ele.getAttribute("class")
返回的结果是不是同一个东西?
要理解这个问题,首先要说一个重要的知识点,一个元素的属性结构是这么来的,比如一个input元素
"test" checked="checked" />
那么这个元素的属性被包含在input.attributes
里面,比如你在html
元素上看到的class
、id
或者你自己定义的data-test
这种属性。
然后getAttribute
、setAttribute
、removeAttribute
这三个方法可以认为是快捷的取attributes
集合的方法。而直接input.id
或者input.className
都是直接挂在input
下的属性,和attributes
是同级的,所以返回的东西也许看上去是一样的,但是实质上是不一样的,大家可以试一下input.checked
和input.getAttribute("checked")
,结果是前者返回true
,后者返回checked
。
"btn1" checked="checked"/>
var test = document.getElementById("btn1");
console.log(a.getAttribute("checked")); //checked
console.log(a.checked) //true
总的来说,这三个方法通常是用于处理自定义的属性,而不是id
、class
等这样的共有特性。
下面来说一下创建元素
document.createElement()
可以创建一个元素,比如:document.createElement("div")
创建了一个div
元素,之后的操作一般都是为元素设置属性,常用的两种方法,一种是直接node.property
,另一种是node.setAttribute("propertyName","value")
。
做完这些以后元素并不是在页面中,所以还要通过最上面讲的像appendChild()
这些方法来将元素添加到页面中。
在IE中,还可以直接将整个HTML字符串加进去来创建元素,比如document.createElement("
。
最后,元素节点也支持HTMLDocument
类型的那些查找方法,比如getElementsByTagName
。不过它只会找自己后代的节点,所以代码可以这样写:
document.getElementById("test").getElementsByTagName("div")
//找到id为test元素下面的所有div节点
这个类型比较特殊,也是第三常见的类型(前两个分别是Document
和Element
),这个节点简单的来说就是一个字符串。
它还有个重要的特征就是它没有子元素(文本节点,什么都没有),访问text节点的文本内容,可以通过nodeValue
或者data
属性。
下面加单说一下它的一下方法
appendData(); //在Text末尾添加内容
deleteData(offset,count); //从offset指定的位置开始删除count个字符
还有insertData
、replaceData
、splitText
等方法,因为用的特别少,几乎不用,就不一一说了,用的时候可以再查阅。
它还有一个length
属性,返回的是字符串的长度。
这里说一下一个比较常见的错误,初学者经常会遇到的坑,先来看下面的一段代码:
<ul id="test">
<li>北京li>
<li>上海li>
ul>
<script>
var ul = document.getElementById("test");
var first = ul.firstChild;
console.log(first); //#text
script>
这里我们会发现ul的第一个子节点输出的是#text,这是为什么呢,难道不应该是
吗,不是的,因为
和
之间还有一段空格,这些空格被看做为文本节点,所以会它的第一个节点是文本节点。
这是一个常见的问题就是遍历ul
的childNodes
的时候,遍历的时候一定要判断nodeType
是不是等于1(等于1代表是元素节点),这样才能跳出这个坑。
创建文本节点的方法是document.createTextNode
,然后接下来的操作和Element
类型一样。
这些不常用的就简单的说一下吧,
Comment
是注释节点DocumentType
就是doctype
节点,通过document.doctype
来访问。DocumentFragment
这个节点是一个文档片段,偶尔会用到。 比如一种常见的用法是,在一个ul
中插入三个li
,如果你连续循环插入三次,那么浏览器就渲染三次,对性能有挺大的影响,所以大家一般这么做:
var fragment = document.createDocumentFragment();
创建一个fragment
,用appendChild
把li
插入到fragment
中,最后再把fragment
插入到u
l中,这样就会优化性能。
通过上面说的一些节点类型,大家对DOM的了解也会深一点,下面来说一下关于DOM扩展的一下东西。
浏览器为了方便开发者,扩展了一下DOM功能,因为是浏览器自己扩展的,所以使用的时候一定要做兼容性测试
判断标准模式和混杂模式,通过document.compatMode
和document.documentMode
上面说了一个文本节点是第一子元素的问题,所以浏览器又实现了一个children
属性,这个属性只包含元素节点。
为了方便判断A节点是不是B节点的子节点,引入了contains
方法,比如
B.contains(A); //true代表是,false代表不是
针对访问元素,又提供了四个方法:innerText
、innerHTML
、outerText
、ouerHTML
,通过这些方法,可以读写元素,其中TEXT返回的文本内容,*HTML返回的是html文本,而outer则代表包含元素本身。
重要的是,这几个方法又心梗问题,比如在IE中,通过inner*
删除的节点,其绑定的事件依然存在内存中,会消耗大量的内存。
还有一个技巧是,插入大量的html代码,用innerHTML
是非常快的,建议使用。