通俗易懂的讲解 DOM

1,744 阅读13分钟

       DOM是所有前端开发每天打交道的东西,但是随着jQuery等库的出现,大大简化了DOM操作,导致大家慢慢的“遗忘”了它的本来面貌。不过,要想深入学习前端知识,对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是所有节点的基类型,所有节点都继承自它,所有所有节点都有一些共同的方法和属性。

       先讲Node类型的属性

       首先是nodeType属性,用来表明节点类型的,例如:
document.nodeType;返回9,其中document对象为文档节点的Document的实例。

       这里面,9代表的就是DOCUMENT_NODE节点的意思,可以通过Node.DOCUMENT_NODE查看对应的数字document.nodeType === Node.DOCUMENT_NODE; //true

       至于一共有哪些节点,每个节点对应的数字又是多少,这个可以问问百度。反正最常用的元素节点是Element(对应数字为1)和文本几点Text(对应数字为3)

       然后常用的还有nodeNamenodeValue

       对于元素节点nodeName就是标签名,nodeValue就是null

       对于文本节点nodeName为”#text”(chrome里面测试的),nodeValue就是实际的值

       每个节点还有childNodes属性,这是个十分重要的属性,他保存了这个节点所有直接子元素

       调用childNodes返回的就是一个NodeList对象,它极其像一个数组,但是有一个关键的地方,他是动态查询的,也就是说每次调用他都会对DOM结构查询,所以对它的使用需要谨慎,注意性能。

       访问childNodes可以使用数组下表或者item方法
然后各个节点还存在各种属性让它们可以相互访问,下图是个很好的总结
dom

比较有用的属性和方法:

1.hasChildNodes()
  如果包含子节点就返回true,比查询childNodeslength来的简单。
  
2.ownerDocument
  返回文档节点的引用(在html里面也就是document对象)

比较有用的操作DOM的方法:

  1. appendChild()方法可以在节点的childNodes的末尾添加一个节点,值得注意的是如果这个节点是已经存在于文档中的,那么变回删除原节点,感觉上就像是移动节点一样。
  2. insertBefore()方法接受两个参数,一个是插入的节点,另外一个是参照的节点。如果第二个参数为null,则insertBeforeappendChild的效果一样。否则便会把节点插入到参照节点之前。这里要注意的是,如果第二个参数不为null,那么插入的节点不能是已经存在的节点,否则结果同上。

  3. replaceChild()方法可以替换节点,接受两个参数,需要插入的节点和需要替换的节点。返回被替换掉的节点。

  4. removeChild()移除节点。这里有个常见的需求,比如我有一个节点#node,那么如何移除它呢?

var node = document.getElementById("node");
node.parentNode.removeChild(node);  //先拿到父节点,再调用父节点的removeChild删除自己

       这里大家需要注意一下,上面的四个方法都是操作某个节点的子节点,也就是说,操作前必须先找到父节点(通过parentNode来找)

复制节点的方法

       cloneNode()复制节点,接受一个参数true或者false。如果true就是复制那个节点和它的子节点,如果是false,就是复制节点本身(复制出来的节点没有任何子元素)。这方法返回复制的节点,如果如要操作它,那么需要借助前面讲的四个方法来把这个节点放到html中去。

       上面这些就是Node类型中常用的属性和方法。所有的类型都继承自Node类型所以这些属性和方法是所有节点都有的。

三、Document类型

       最开始讲DOM的时候提到了Document类型。其实关于这个类型最重要的就是它的一个子类HTMLDocument有一个实例对象document。而这个document对象是我们最常用的一个对象了。

       document对象又是在window对象上,所以浏览器就可以直接方法document了。

document对象的常用属性

  1. document.childNodes继承自上面讲的Node类型,可以放文档的直接子节点(通常包括文档声明和html节点)

  2. document.documentElement可以直接拿到html节点的引用(等价于document.getElementsByTagName("html")[0])

  3. document.body body节点的引用

  4. document.title 页面title,可以修改,会改变浏览器标签上的名字

  5. document.URL 页面的url

  6. document.referrer 取得referrer,也就是打开这个页面的那个页面的地址,做来源统计时候比较有用

  7. document.domain 取得域名,可以设置,但是同城只能设置为不包含子域名的情况,在一些子域名跨域情况下有效。

document对象常用的方法

  1. getElementById,传入id,得到元素节点。里面的参数区分大小写(IE8不区分)。注意:如果有多个id相同的元素,则返回第一个。IE7里面表单元素的name也会被当做id来使用。

  2. getElementsByTagName 根据标签取得元素,得到的是HTMLCollection类型。如果传入的是”*”,则可以得到全部的元素。

       还有一个只有HTMLDocument类型(也就是document对象)才有的方法getElementsByName,就是根据那么返回元素。

       document对象还有一些集合,例如document.forms可以返回所有的form表单。类型也是HTMLCollection.

       说到HTMLCollectionHTMLCollection就是一个包含一个或者多个元素的集合,和讲的NodeList很像。HTMLCollection这个类型有两个方法,一个是通过下标(或者.item())得到具体元素,还有就是通过['name'](或者.namedItem())获得具体元素。

       最后,关于document对象还有一套比较重要的方法,那就是
write() , writeln() , open() , close()

       openclose分别是打开和关闭网页的输出流,在页面加载的过程中,就相当于open状态。这两个方法一般不会去用它。

       writewriteln都是向页面写入东西,区别就是后者会多一个换行符。

       需要注意的是:在页面加载过程中,可以使用这两个方法向页面添加内容。如果页面已经加载完了,再调用write,会重写整个页面。

       还有一点,如果要动态的写入脚本,例如这样的,那么就要把分开拼装,否则会被误以为是脚本结束的标志,导致这个结束符匹配到上面一个开始符。可以这样写“”

四、Element类型

       下面我们来说一下最重要也是最常见的一个类型,Element类型。

       我们平时所操作的都是Element类型(实质上是HTMLElement,这里为了方便理解,就简单这么说),比如

       document.getElementById("test")返回的就是Element类型。我们平时所说的”DOM对象”,通常也就是指Element类型的对象。

Element类型常见的属性

       最常用的当然就是Node类型上的那些属性和方法,这里就不在重复说了,主要说一下它独有的

       首先是tagName,这个和继承自Node类型的nodeName一样。都是返回标签名,通常都是大写,有的浏览器会返回小写。所以在比较的时候最好调用一下类似toLowerCase()这种方法再做比较。

       上面我们提到了HTMLElement类型,HTMLElement类型继承自Element类型,也就是HTML元素的实际类型,我们在浏览器里用的元素都是这个类型。

这个类型的元素都具有一些标准属性,比如:

  id元素的唯一标识,title通常是鼠标移上去时显示的信息,className类名等等,这几个属性是可读写的,也就是说你改变它们以后会得到相应的效果。

除了属性外,还有几个重要的方法。首先说一下操作节点属性的方法,getAttributesetAttributeremoveAttribute这三个方法。这些是操作属性最常用的方法了,怎么用就不说了,大家应该都会用。还有一个attributes属性,保存了元素的全部属性。

       这里要说一个问题,ele.classNameele.getAttribute("class")返回的结果是不是同一个东西?

       要理解这个问题,首先要说一个重要的知识点,一个元素的属性结构是这么来的,比如一个input元素

       那么这个元素的属性被包含在input.attributes里面,比如你在html元素上看到的classid或者你自己定义的data-test这种属性。

       然后getAttributesetAttributeremoveAttribute这三个方法可以认为是快捷的取attributes集合的方法。而直接input.id或者input.className都是直接挂在input下的属性,和attributes是同级的,所以返回的东西也许看上去是一样的,但是实质上是不一样的,大家可以试一下input.checkedinput.getAttribute("checked"),结果是前者返回true,后者返回checked


var test = document.getElementById("btn1");
console.log(a.getAttribute("checked"));     //checked
console.log(a.checked)      //true

       总的来说,这三个方法通常是用于处理自定义的属性,而不是idclass等这样的共有特性。

下面来说一下创建元素

       document.createElement()可以创建一个元素,比如:document.createElement("div")创建了一个div元素,之后的操作一般都是为元素设置属性,常用的两种方法,一种是直接node.property,另一种是node.setAttribute("propertyName","value")

       做完这些以后元素并不是在页面中,所以还要通过最上面讲的像appendChild()这些方法来将元素添加到页面中。

       在IE中,还可以直接将整个HTML字符串加进去来创建元素,比如document.createElement("

test
")

       最后,元素节点也支持HTMLDocument类型的那些查找方法,比如getElementsByTagName。不过它只会找自己后代的节点,所以代码可以这样写:

document.getElementById("test").getElementsByTagName("div")
//找到id为test元素下面的所有div节点

五、Text类型

       这个类型比较特殊,也是第三常见的类型(前两个分别是DocumentElement),这个节点简单的来说就是一个字符串。

       它还有个重要的特征就是它没有子元素(文本节点,什么都没有),访问text节点的文本内容,可以通过nodeValue或者data属性。

下面加单说一下它的一下方法

appendData();       //在Text末尾添加内容
deleteData(offset,count);       //从offset指定的位置开始删除count个字符

       还有insertDatareplaceDatasplitText等方法,因为用的特别少,几乎不用,就不一一说了,用的时候可以再查阅。

       它还有一个length属性,返回的是字符串的长度。

       这里说一下一个比较常见的错误,初学者经常会遇到的坑,先来看下面的一段代码:

  • 北京
  • 上海

       这里我们会发现ul的第一个子节点输出的是#text,这是为什么呢,难道不应该是

  • 北京
  • 吗,不是的,因为
  • 北京
    • 之间还有一段空格,这些空格被看做为文本节点,所以会它的第一个节点是文本节点。

             这是一个常见的问题就是遍历ulchildNodes的时候,遍历的时候一定要判断nodeType是不是等于1(等于1代表是元素节点),这样才能跳出这个坑。

             创建文本节点的方法是document.createTextNode,然后接下来的操作和Element类型一样。

      六、其他的一些类型 Comment、DocumentType和DocumentFragment

      这些不常用的就简单的说一下吧,

      • Comment是注释节点
      • DocumentType就是doctype节点,通过document.doctype来访问。
      • DocumentFragment这个节点是一个文档片段,偶尔会用到。

             比如一种常见的用法是,在一个ul中插入三个li,如果你连续循环插入三次,那么浏览器就渲染三次,对性能有挺大的影响,所以大家一般这么做:

      var fragment = document.createDocumentFragment();

             创建一个fragment,用appendChildli插入到fragment中,最后再把fragment插入到ul中,这样就会优化性能。

      七、DOM扩展

             通过上面说的一些节点类型,大家对DOM的了解也会深一点,下面来说一下关于DOM扩展的一下东西。

             浏览器为了方便开发者,扩展了一下DOM功能,因为是浏览器自己扩展的,所以使用的时候一定要做兼容性测试

             判断标准模式和混杂模式,通过document.compatModedocument.documentMode

             上面说了一个文本节点是第一子元素的问题,所以浏览器又实现了一个children属性,这个属性只包含元素节点。

             为了方便判断A节点是不是B节点的子节点,引入了contains方法,比如

      B.contains(A);      //true代表是,false代表不是

             针对访问元素,又提供了四个方法:innerTextinnerHTMLouterTextouerHTML,通过这些方法,可以读写元素,其中TEXT返回的文本内容,*HTML返回的是html文本,而outer则代表包含元素本身。

             重要的是,这几个方法又心梗问题,比如在IE中,通过inner*删除的节点,其绑定的事件依然存在内存中,会消耗大量的内存。

             还有一个技巧是,插入大量的html代码,用innerHTML是非常快的,建议使用。