在上篇文章这DOM(一)——简介中我们介绍了Node的所有节点类型,这篇文章我将针对其中比较重要的节点进行介绍。
Node类型
nodeType和nodeName
每一个节点都有一个nodeType
和nodeName
属性,用于表明节点的类型。节点类型共有12种。
ELEMENT_NODE =1
ATTRIBUTE_NODE =2
TEXT_NODE =3
CDATA_SECTION_NODE =4
ENTITY_REFERENCE_NODE =5
ENTITY_NODE =6
PROCESSING_INSTRUCTION_NODE =7
COMMENT_NODE =8
DOCUMENT_NODE =9
DOCUMENT_TYPE_NODE =10
DOCUMENT_FRAGMENT_NODE =11
NOTATION_NODE =12
nodeType
和nodeName
只是同一节点类型的不同方式表达。
nodeType
是用数字表示节点类型,nodeName
是用字符串表示节点类型。实际上他们映射的是同一个节点。
nodeValue
绝大数节点类型(除了Text(文本节点)和Comment(注释节点))的nodeValue属性都返回null。这个节点属性就是获取Text与Comment节点实际文本字符串。
其他节点属性
除了上述的三个节点属性之外,还有一些很重要的节点属性。
- childNodes
- firstChild
- lastChild
- nextSibling
- parentNode
- previousSibling
单纯地介绍太过于抽象,来一张图,或许大家就明白了这些节点属性的作用。
childNodes属性可以用来获取任何一个元素的所有子元素,它是一个包含这个元素全部子元素的数组。
其他的属性,大家看图基本上就知道意思了,所以不多说了。
操纵节点
(常用的)操纵节点的方法有:
appendChild()
在节点内,添加最后一个子节点。(有点类似于伪元素after,是在元素内部最后添加的)
cloneNode()
创建节点的拷贝,并返回该副本。该方法会克隆所有属性以及它们的值。
该方法不会复制DOM节点中的JavaScript属性,但是IE会有bug——它会复制事件处理程序。removeChild()
删除父节点的一个子节点
replaceChild()
用新节点替换某个子节点。这个新节点可以是文档中某个已存在的节点,也可创建新的节点。
insertBefore()
在已有的子节点前插入一个新的子节点。
可以说掌握了上述5中节点的操作方法,基本上就熟悉DOM的节点操作了。
Document类型
Document类型表示文档。在浏览器中,document对象是HTMLDocument(继承自Document)的一个实例。
常见的节点接口还包括:
// '<' 表示'从左侧继承'
Object < Node < Element < HTMLElement
Object < Node < Attr(DOM4中弃用)
Object < Node < CharacterData < Text
Object < Node < Document < HTMLDocument
Object < Node < DocumentFragment
文档信息
document对象常用的属性有四个,分别是URL、domain、referrer和cookie。由于前三个属性与网址都有关系,所以先简单介绍一下网址的组成。
以百度首页为例子:
https://www.baidu.com/
URL格式:
<协议>://<主机>:[端口号]<路径>
协议:指定使用的传输协议
主机和端口号:存放资源的主机的域名。端口号可选,省略是使用默认端口。
路径:主机上的一个目录或文件的地址
协议:https
服务器:www
域名:www.baidu.com
如果你想进一步了解,这篇文章会帮你——《详解URL的组成》
- URL属性
URL——统一资源定位符。该属性可以显示当前页面完整的URL(既地址栏中显示的URL)
在控制台中输入下列代码,可以获取当前页面的完整URL
console.log(document.URL);
referrer属性
referrer属性保存着链接到当前页面的那个页面的URL。该属性是计算流量来源的方法之一,更多相关信息请浏览张鑫旭老师的文章《JS获取上一访问页面URL地址document.referrer实践》domain属性
domain包含了当前页面的域名。
在要介绍的前三个属性里面,只有domain能够进行设置。但是由于安全方面的原因,domain设置的值有限制。如果URL中包含一个子域名,例如www.baidu.com,那么只能将domain设置为”baidu.com”,而不能设置成jingyan.baidu.com。换言之,就是domain只能做“减法”,不能做加法或者完全更改其内容(例如改成youkuaiyun.com)。
domain最大的作用是突破了跨域安全限制。由于跨域安全的限制,来自不同子域的页面无法通过JavaScript通信的。当在页面包含内嵌框架(iframe)或其他子域的时,设置将页面和内部的框架都设置成同一个domain,就可以由同一个js文件进行控制了。
浏览器对domain属性还有一个限制:domain属性只能从紧绷(tight)到松散(loose)。即只能从子域名变成主域名,而不能从主域名到子域名。
//主域名 baidu.com
//子域名1 www.baidu.com
//子域名2 jingyan.baidu.com
//当前页面 jingyan.baidu.com
document.domain="baidu.com";//成功
//当前页面 www.baidu.com
document.domain="baidu.com";//成功
document.domain="www.baidu.com";//失败
- cookie属性
cookie属性可以使一些数据存储到本地电脑上,这样下次再打开指定网页的时候就可以根据记录的信息完成相关操作了(如自动登录)。
cookie的属性操作非常简单,只要是做到“键=值”就可以。
更多信息cookie相关操作,你可以参考《JavaScript Cookie》
文档方法
最常见的文档方法分成两类,一类是查找元素,另一类是文档写入。
查找元素
常见的查找元素的方法有:
document.getElementById()
返回对拥有指定 id 的第一个对象的引用。id按照严格匹配,区分大小写。
如果页面中存在多个同名id的元素,则只返回第一次出现的元素。虽然所有浏览器都支持该方法,但是它仍有一些兼容性问题:
①在IE8及低版本中,将不会区分id的大小写。所以id的命名尽量不要以大小写区分,可能导致兼容性问题。
②在IE7以及更低版本中,会有一个怪异的行为:如果表单元素的name等于指定id,而且该元素在文档中带有给定id的元素前面,那么IE就会返回那个表单元素。
下面这个例子,低版本IE将会获取input元素而非div元素。
<input type="text" name="myElement" vaule="Text field">
<div id="myElement">A div</div>
为了避免出现这种情况,name请不要跟id取相同的字段。
document.getElementsByName()
返回带有指定名称的对象的集合。这个方法大家并不常用,兼容性也有待考究。使用的时候需要注意的有两点:
①返回的是元素的数组,而不是一个元素。
②在IE8及以下版本,只有拥有name属性的元素才能被获取,其他元素(非文本标签元素)是获取不到的。
下面这个例子,低版本IE将无法获取指定元素的。
<div name="f"></div>
document.getElementsByTagName()
返回一个包括所有给定标签名称的元素集合。该方法会返回元素的顺序是它们在文档中的顺序。如果把特殊字符串
"*"
作为参数传给该方法,将获取所有的元素。
注意:
由于IE会将注释(comment)实现为元素,因此当调用getElementsByTagName("*")
将会返回所有注释节点。
文档写入
文档写入的功能在初级开发中大家见得比较多,其中document.write()
应该是大家最熟悉的,document.writeln()
与前者在功能上也无太大差别。
document.write()
write()
方法可向文档写入 HTML 表达式或 JavaScript 代码。document.writeln()
writeln()
方法可向文档写入 HTML 表达式或 JavaScript 代码,同时会在每个表达式后写一个换行符。
Element类型
element类型应该是我们最常见的类型了。它的常见子节点包括:
- Element(HTML元素)
- Text(文本节点)
- Comment(注释节点)
获取节点之后,既可以使用nodeName,也可以使用tagName属性来获取元素的标签名。这两个属性会返回相同的值。
注意:
tagName和nodeName属性返回的值全都是大写字母。在XML(XHTML)中标签名会与源代码一直,所以比较时最好统一转换为大写。
标准特性
标准特性是每个HTML元素都有的属性,包括:
- id。元素唯一标识符。
- title。标题。
- lang。元素内容的语言代码。很少使用。
- dir。语言方向。(例如文字的阅读方向从左到右,还是从右到左)
- className。就是平时所说的class属性。(由于class是ECMA的保留字,所以这里叫className)
操作特性
操作特性分成两步,一是获取特性,二是设置特性。
1.获取特性
- 通过DOM元素本身的属性来获取元素特性。不过这种方式存在兼容性问题。
例如:非公认特性在Safari、Opera、Chrome、Firefox浏览器中是不存在的,但是IE却会为自定义特性也创建属性。
<div id="myid" data-mydata="mydata" my_special_attribute="hello!"></div>
<script>
document.writeln(document.getElementById("myid").id);//所有浏览器都返回myid
document.writeln(document.getElementById("myid").my_special_attribute);//IE返回hello,其他浏览器返回undefined
</script>
getAttribute()
方法可以获取元素的指定特性。这个特性可以是公认的特性,也可以是自定义的特性。
例如下面这个例子:
id
就是公认特性,data-mydata
是html5
新增的自定义特性,my_special_attribute
则是完全自定义特性。这些特性通过getAttribute
均可获得目标值。
<div id="myid" data-mydata="mydata" my_special_attribute="hello!"></div>
虽然该方法确实很好用,但是在获取style
和事件处理程序名(如onclick
)上有兼容性问题,特别是在IE浏览器中,不同版本之间存在不小的差异。所以在使用该属性时,不建议用在获取style
特性和事件处理程序名上。
2.设置特性
- 创建或者修改元素特性。
通过DOM自身属性进行特性的设置和修改。但是这种方法不支持自定义特性(属性)的设置。
<div id="myid"></div>
<script>
document.getElementById("myid").className("myclass");//设置成功
document.getElementById("myid").mydata("mydata");//设置失败(仅在IE中会设置成功)
</script>
setAttribute()
方法也可以创建或改变某个新属性。这个方法接受两个参数:①要设置的特性名②要设置的特性值。
如果特性已存在则替换为新的值,如果不存在则创建一个新的特性并设置它的值。
<div id="myid"></div>
<script>
document.getElementById("myid").setAttribute("class","myclass");
</script>
注意:
①该方法设置的特性名会被统一转换成小写形式。
②IE7及以前版本存在bug,通过这个方法设置class
和style
特性就没有任何效果。
- 移除元素的特性
removeAttribute()
方法接受一个参数——要删除的特性名字。该方法不仅会清除特性的值,还会把特性从元素中删除。
注意: IE6及以下版本不支持该方法。
attributes属性
Element类型是使用attributes
属性的唯一一个DOM节点类型。
attributes
属性返回指定节点属性的集合。
这个属性在平时中并不常用(对我个人而言),除非是用于遍历属性。同时这个属性在IE7及更早版本中,会返回HTML元素中所有可能的特性(可以说是bug),包括没有指定的特性。所以本文就不深入研究该属性了。
创建元素
document对象中有一个方法可以创建元素,那就是createELement()
。
document.createElement()
只接受一个参数——即要创建的元素的标签名。这个标签名不区分大小写。
你可以像下面的代码一样创建元素
var p=document.createElment("p");
此时你仅仅是创建了元素,但是这个元素并不是DOM节点树的组成部分,它只是一个游荡在JavaScript世界里面的一个幽灵。这种情况我们称之为文档碎片(document fragment)。借助节点方法,如appendChild()
、insertBerfore()
、repalceChild()
可以将文档碎片添加进DOM节点树中。这样我们就可以在浏览器窗口中看到这个元素了。
在IE中还有一种document.createElement()
的使用方法,那就是传入完整的元素标签。
例如:
var div=document.createElement("<div id=\"newDiv\" class=\"myClass\"></div>")
这种方法有助于避开IE7及跟早版本中动态创建元素的某些问题,包括:
- 不能设置动态创建的
<iframe>
元素的name特性 - 不能通过表单的reset方法(HTML DOM的方法)重置动态创建的
<input>
元素 - 动态创建的
type
特性值为“reset”的<button>
元素重设不了表单 - 动态创建的一批
name
相同的单选按钮彼此之间毫无关系。name
值相同的一组单选按钮本来应该用于表示同一选项的不同值,但是动态创建的一批这种单选按钮之间却没有这种关系。
元素的子节点
元素的childNodes属性包含了它所有的节点,它返回的是一个存放节点的数组。
对于下面的代码,IE和其他浏览器有不同的解释:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
IE认为<ul>
有三个子节点,分别是三个<li>
节点;
其他浏览器认为有<ul>
有七个子节点,分别是三个<li>
节点和四个文本节点(<li>
之间存在的空白符)。
为了兼容不同浏览器之间的差异,我们建议进行节点属性的检查,来保证所选取的节点的正确性。
例如:
for(var i=0,len=element.childNodes.length;i<len;i++){
if(element.childNodes[i].nodeType==1){//进行节点检测
//do something
}
}
配合上getElementByTagName()
方法可以做到只遍历直接子节点
var ul=document.getElementById("list"),
items=ul.getElementByTagName("li");
for(var i=0,len=items.childNodes.length;i<len;i++){
if(items.childNodes[i].nodeType==1){//进行直接子节点进行检测
//do something
}
}
Text类型
文本节点由Text类型表示。Text节点具有以下特征:
- nodeType的值为3
- nodeName的值为
#text
- nodeValue的值为节点包含的文本内容,该内容可以包含转义后的HTML字符。但不能包含HTML代码。
- 没有子节点
- 父节点(parentNode)是Element节点
创建文本节点
document.createTextNode()
方法可以创建新文本节点,这个方法接受一个参数——要插入节点中的文本。该方法不会转义其参数内容。
<p id="demo">单击按钮</p>
<button onclick="myFunction()">点我</button>
<script>
function myFunction(){
var h=document.createElement("H1");
var t=document.createTextNode("<p>ha ha ha</p>");//内容不会被转义
h.appendChild(t);
document.body.appendChild(h);
};
</script>
获取文本节点值与操作文本节点值
获取文本节点值的方法有两种,一种是data
,另一种是nodeValue
。
能够使用data
和nodeValue
这两种方法获取文本节点值,是因为Text
节点继承了CharacterData
和Node
的所有属性和方法。
使用方法如下:
<p id="demo">文本节点<p>
<script>
var p=document.getElementById("demo").firstChild;
console.log(p.data);//文本节点
console.log(p.nodeValue);//文本节点
</script>
Node
对象或许大家都熟悉了,但对于CharacterData
对象或许还有一些陌生。
- CharacterData
该接口是一个抽象接口(abstract interface),提供了Text
和Comment
节点的常用功能。正是这个接口为我们提供了操作文本的相关方法。
兼容性
Chrome | IE | Firefox | Opera | Safari | W3C |
---|---|---|---|---|---|
1.0 | 6 | 1.0 (1.7 or earlier) | Yes | Yes | Yes |
对象属性
属性 | 描述 |
---|---|
data | 一个 DOMString(由UTF-16组成的String类型),表示该对象中包含的文本数据。 |
length | 该节点包含的字符数。(只读) |
常用方法
方法 | 描述 |
---|---|
appendData() | 为 CharacterData.data 字符串追加指定的 DOMString ;当方法返回时,data 包含的是已合并的 DOMString . |
deleteData() | 在 CharacterData.data 字符串中,从指定位置开始,删除指定数量的字符;当方法返回时,data 包含的是缩短了的 DOMString . |
insertData() | 在 CharacterData.data 字符串中,在指定的位置,插入指定的字符;当方法返回时,data 包含的是已修改的 DOMString . |
replaceData() | 在 CharacterData.data 字符串中,从指定位置开始,把指定数量的字符替换为指定的 DOMString ; 当方法返回时, data 包含的是已修改的 DOMString . |
substringData() | 返回一个包含了从 CharacterData.data 中的指定位置开始,指定长度的 DOMString . |
操作演示
<p id="demo">文本节点<p>
<script>
var p=document.getElementById("demo").firstChild;
p.appendData(",尾部新加的文本");
console.log(p.data);//文本节点,尾部新加的文本
p.deleteData(0,5);
console.log(p.data);//尾部新加的文本
p.insertData(0,"头部插入新文本,");
console.log(p.data);//头部插入新文本,尾部新加的文本
p.replaceData(0,p.length,"全部更新为新文本");
console.log(p.data);//全部更新为新文本
console.log(p.substringData(0,4));//全部更新
</script>
合并与分割文本节点
如果DOM中存在相邻的同胞文本节点,很容易导致分不清哪儿文本节点表示哪个字符串。
来看下面这段代码:
<p id="demo"></p>
<script>
var p = document.getElementById("demo"),
price1=document.createTextNode("大甩卖!大甩卖!不要100!"),
price2=document.createTextNode("只要9.99!只要9.99!");
p.appendChild(price1);
p.appendChild(price2);
console.log(p.childNodes.length)//2
</script>
上述代码在视觉效果上跟下面代码的效果是一致的:
<p id="demo">大甩卖!大甩卖!不要100!只要9.99!只要9.99!</p>
<script>
var p = document.getElementById("demo");
console.log(p.childNodes.length)//1
</script>
这两段代码有一处非常关键的地方不同,那就是p.childNodes.length
。前者是2,后者是1。原因很简单——前者是创建了两个文本节点然后插入到p
元素中的。但是实际上我们只需要一个文本节点就能完成这个需求。同时如果我们要修改文本节点,很容易出现混乱:我们获取的节点是不是想要的文本节点?
合并文本节点
为了规范化文本节点,DOM提供normalize()
这个方法。
normalize()
可以移除空的文本节点,并连接相邻的文本节点。
合并相邻的同胞文本节点:
<p id="demo"></p>
<script>
var p = document.getElementById("demo"),
price1=document.createTextNode("大甩卖!大甩卖!不要100!"),
price2=document.createTextNode("只要9.99!只要9.99!");
p.appendChild(price1);
p.appendChild(price2);
p.normalize();//合并兄弟文本节点
console.log(p.childNodes.length)//1
</script>
这样当我们需要修改文本节点时,就无需顾虑自己获取的文本节点是不是完整的文本节点了。
分割文本节点
splitText()
方法跟normalize()
方法的作用完全相反,它会将一个文本节点分割为两个文本节点,即按照指定的位置分割nodeValue
值。
该方法接受一个参数:要切割的字符串长度。切割的位置从0开始计算,到指定位置位置,分成两个兄弟文本节点。
<p id="demo">大甩卖!不要100!只要9.99!</p>
<script>
var p = document.getElementById("demo");
console.log(p.childNodes.length);//1
console.log(p.firstChild.data);//大甩卖!不要100!只要9.99!
p.firstChild.splitText(10);//进行切割
console.log(p.childNodes.length);//2
console.log(p.firstChild.data);//大甩卖!不要100!
console.log(p.lastChild.nodeValue);//只要9.99!
</script>
其他节点类型
其他节点类型包括Comment
类型、CDATASection
类型、DocumentType
类型、DocumentFragment
类型,以及Attr
类型。这些节点绝大多数都不常用,放在一起讲,了解个大概就好了。
Comment类型
Comment
类型是用于表示注释节点,其nodeName
为#comment
,nodeType
为8
。除此之外,它跟Text
节点并无其他不同,两者共用一套方法。
CDATASection类型
CDATASection
类型只针对XML文档,表示的是CDATA区域。
DocumentType类型
DocumentType
类型包含着与文档的doctype有关的所有信息。这是在web浏览器中并不常用,且浏览器对它的兼容性不好,所以在此不进行深入了解。
DocumentFragment类型
在所有节点类型中,只有DocumentFragment
在文档中没有对应的标记。DOM规定文档碎片(document fragment)是一种轻量级的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。
这是一个相对常用的节点类型,DocumentFragment
节点具有以下特征:
nodeType
的值为11
nodeName
的值为#document-Fragment
nodeValue
的值为null
parentNode
的值为null
- 子节点可以是
Element
、Comment
、Text
、CDATASection
等等
DocumentFragment
节点最大的作用就是作为“仓库”来使用。由于文档碎片继承了Node的所有方法,所以我们可以执行那些针对文档的DOM操作之后再添加到DOM节点树中,这样做将会带给我们以下这些好处:
①提高性能。减少DOM变换导致的回流(reflow)和重绘(repaint)。
②不会出错。文档碎片添加进DOM节点树中,只会将文档碎片的所有子节点添加到相应位置,文档碎片永远不会成为DOM节点树的一部分。这就避免了我们获取到错误的节点类型。
Attr类型
元素的特性在DOM中以Attr类型来表示。但实际上在DOM4中,已经该节点类型明确废弃,被合并到Element
节点的attribute
属性中。如果需要操作元素的特性,我们更加推荐使用getAttribute()
、setAttribute()
等相关方法(请参考上文的Element
节点的attribute
属性的操作方法)。
参考资料:
JavaScript DOM编程艺术(第2版)
JavaScript 高级程序设计(第3版)
DOM启蒙
DOM
DOM4-ELEMENT-ATTR
CharacterData
DOMString
CharacterData
Document