DOM
DOM(文档对象模型)是针对HTML 和XML 文档的一个API(应用程序编程接口)。
DOM描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。
节点层次
DOM可以将任何HTML文档描绘成多层节点构成的对象模型
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
上面的html内容转换成DOM结构就是下面这张图的样子
Document节点是每个文档的根节点, 它只有一个子节点, 即<html>元素, 我们叫它文档元素。其他所有元素都包含在文档元素中。
对于每一段html标记都可以通过树中的一个节点表示: HTMl元素通过元素节点表示, 特性通过属性节点表示, 文档通过文档节点表示, 除了这些还有诸如注释节点等总共12中节点类型, 这些类型都继承于一个基类型
Node类型
DOM1级定义了一个Node类型, 所有的节点类型都继承自Node类型, 所有节点类型都共享着相同的基本属性和方法。
每个节点都有一个nodeType属性, 用于表示节点类型, 通常这个属性会返回一个常数, 每个常数对应一种类型, 任何节点都是这12种的其中一个:
- 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.NOTATION_NODE(12)
通过上面的常量, 可以很容易的确定节点类型, 如:
if (someNode.nodeType == 1){ //适用于所有浏览器
alert("Node is an element.");
}
我们可以通过节点的nodeName属性得到元素的标签名, 对于元素节点, nodeValue的值始终是null
节点关系
文档中所有元素之前都存在着父子关系, 如
- <body>元素是<html>的子元素
- <html>元素师<body>的父亲元素
- <head>和<body>则是同胞元素, 因为他们都是<html>的直接子元素
每个节点都有一个childNodes
属性, 其中保存着一个NodeList伪数组
, NodeList中有序的保存着该节点的所有子节点, 可以通过伪数组位置来访问某个子节点, NodeList最显著地特点是他是基于DOM结构动态执行查询结果的, 会实时反映DOM结构的变化。下面例子是childNodes的典型应用:
var firstChild = someNode.childNodes[0]
var secondChild = someNode.childNodes.item(1)
var count = someNode.childNodes.length
这里需要注意的一点, 使用childNodes时, 同胞元素之间的空格和回车会被识别为文本节点
除了childNodes外, 每个节点都有一个parentNode
属性, 该属性用来获取该节点的父节点, 此外还有一个previousSibling
用来获取前一个同胞节点, nextSibling
用来获取后一个同胞节点
这里同样需要注意, 由于元素间的空格和回车会被识别为文本节点, 所有通过
previousSibling
和nextSibling
获取同胞节点时, 当遇到这些文本节点时, 一定要向前或向后获取两次同胞节点来跳过这些文本节点
父节点还可以通过firstChild
和lastChild
来获得第一个子节点和最后一个子节点
下面这张图就是上面这些属性的总结
除了上面这些, 我很还可以通过hasChildNodes()
方法来判断节点是否有子节点
所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。
操作节点
appendChild(newNode)
DOM提供了很多操作节点的方法, 其中最常用的方法就是appendChild(), 用于向childNodes列表末尾添加一个节点, 如下面的例子就是向body的末尾添加一个button子节点
let el = document.querySelector('body')
let btn = document.createElement('button')
el.appendChild(btn)
如果传入appendChild()中的节点已经是文档的一部分了, 那么该节点会从原来的位置转移到新位置, 原因是DOM中的任何元素不能同时出现在文档中的多个位置上, 如下面的例子
//someNode 有多个子节点
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true
insertBefore(insertNode, positionNode)
该方法是把某个放到childNodes列表的某个特定位置上, 第一个参数是要插入的节点, 第二个参数是作为参照的节点。执行后要插入的节点就会被放在参照节点的前一个位置
replaceChild(newNode, replaceNode)
该方法接受两个参数, 第一个是要插入的节点, 第二个是要被替换的节点, 返回值为被替换的节点
removeChild(Node)
该方法接受一个参数, 就是要移除的节点, 返回值是被移除的节点, removeChild和replaceChild一样, 被删除的节点并没有在文档中被删除, 只是断开了所有链接
cloneNode(boolean)
该方法接受两个参数为true时对元素进行深拷贝, 拷贝对象包括其子节点, 为false时为浅拷贝, 只拷贝元素本身
// 假设我们有一个<ul>元素, 里面包含三个<li>, 我们用myList保存获取的<ul>
var deepList = myList.cloneNode(true);
alert(deepList.childNodes.length); //3(IE < 9)或7(其他浏览器)
var shallowList = myList.cloneNode(false);
alert(shallowList.childNodes.length);
cloneNode()方法不会复制添加到DOM 节点中的JavaScript 属性,例如事件处理程序等。这个方法只复制特性、(在明确指定的情况下也复制)子节点,其他一切都不会复制。IE 在此存在一个bug,即它会复制事件处理程序,所以建议在复制之前最好先移除事件处理序。
最后一个方法是normalize(),这个方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或DOM操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点
Document类型节点相关操作
在浏览器中, document
对象是HTMLDocument(继承自Document类型)的一个实例, 表示整个HTML页面, document对象是window对象的一个属性, 可以通过全局访问
我们可以通过其documentElement
属性或者childNodes
来访问<html>元素
document对象还有一个body属性指向<body>元素。document.body
在js代码中出现的频率非常高, 请大家熟练掌握
document我们用的最多的是查找元素相关操作
我们可以通过document.getElementById()
和document.getElementsByTagName()
这两个方法获取相关节点
现在更多的用
document.querySelector()
和document.querySelectorAll()
这两个方法来获取元素
Element类型
除了document类型外, Element类型就要算是web编程中最常用的类型了。
nodeName
和tagName
这两个属性都返回标签名, 返回的是大写, 例如:
<div id="myDiv"></div>
var div = document.getElementById("myDiv");
alert(div.tagName); //"DIV"
alert(div.tagName == div.nodeName); //true
所以判断时不要忘记用toLowerCase()
转换成小写, 不然很容易出错
if (element.tagName == "div"){ //不能这样比较,很容易出错!
//在此执行某些操作
}
if (element.tagName.toLowerCase() == "div"){ //这样最好(适用于任何文档)
//在此执行某些操作
}
HTML元素
html一般有以下表示特性的属性, 我们用例子来表示:
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
var div = document.getElementById("myDiv");
alert(div.id); //"myDiv"
alert(div.className); //"bd"
alert(div.title); //"Body text"
alert(div.lang); //"en" 元素内的语言代码, 很少使用
alert(div.dir); //"ltr" 语言方向, ,值为"ltr"(left-to-right,从左至右)或"rtl"(right-to-left,从右至左, 很少使用。
当然,像下面这样通过为每个属性赋予新的值,也可以修改对应的每个特性:
div.id = "someOtherId";
div.className = "ft";
div.title = "Some other text";
div.lang = "fr";
div.dir ="rtl";
取得特性
元素有很多的特性, 我们主要通过getAttribute()
, setAttribute()
和removeAttribute()
来操作特性
var div = document.getElementById("myDiv");
alert(div.getAttribute("id")); //"myDiv"
alert(div.getAttribute("class")); //"bd"
alert(div.getAttribute("title")); //"Body text"
alert(div.getAttribute("lang")); //"en"
alert(div.getAttribute("dir")); //"ltr"
getAttribute也可以获取自定义属性
<div id="myDiv" my_special_attribute="hello!"></div>
var value = div.getAttribute("my_special_attribute"); //hello!
特性的名称是不区分大小写的,即"ID"和"id"代表的都是同一个特性。另外也要注意,根据HTML5 规范,自定义特性应该加上data-前缀以便验证。
自定义的属性只能通过getAttribute获取, 不能直接用属性名获取, 下面的代码就获取自定义属性得到的是undefined
alert(div.id); //"myDiv"
alert(div.my_special_attribute); //undefined(IE 除外)
alert(div.align); //"left"
style和事件绑定getAttribute和直接调用属性名返回的内容是不同的
- 在通过getAttribute()访问时,返回的style 特性值中包含的是CSS 文本,而通过属性来访问它则会返回一个对象。
- 通过getAttribute()访问事件时,会返回相应代码的字符串。而在访问s事件属性(如onclick)时,则会返回一个JavaScript 函数(如果未在元素中指定相应特性,则返回null)
设置特性
与getAttribute()
对应的方法是setAttribute()
这个方法接受两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()会以指定的值替换现有的值;如果特性不存在,setAttribute()则创建该属性并设置相应的值。来看下面的例子:
div.setAttribute("id", "someOtherId");
div.setAttribute("class", "ft");
div.setAttribute("title", "Some other text");
div.setAttribute("lang","fr");
div.setAttribute("dir", "rtl");
也可以通过直接给属性赋值设置元素的特性
div.id = "someOtherId";
div.align = "left";
直接给属性赋值对于自定义属性依然不适用
removeAttribute()
,这个方法用于彻底删除元素的特性。
Element类型的attributes属性
-
getNamedItem(name)
:返回nodeName 属性等于name 的节点; -
removeNamedItem(name)
:从列表中移除nodeName 属性等于name 的节点; -
setNamedItem(node)
:向列表中添加节点,以节点的nodeName 属性为索引; -
item(pos)
:返回位于数字pos 位置处的节点。
创建元素
使用document.createElement()
方法可以创建新元素。这个方法只接受一个参数,即要创建元素的标签名
var div = document.createElement("div");
元素的子节点
正如我们之前说的, 元素的childNodes 属性中包含了它的所有子节点,这些子节点有可能是元素、文本节点、注释或处理指令。不同浏览器在看待这些节点方面存在显著的不同,以下面的代码为例。
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul >
在浏览器中,<ul>元素都会有7 个元素,包括3 个<li>元素和4 个文本节点(表示<li>元素之间的空
白符)。如果像下面这样将元素间的空白符删除,那么所有浏览器都会返回相同数目的子节点。
<ul id="myList"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>
如果需要通过childNodes 属性遍历子节点,那么一定不要忘记有些浏览器会识别标签间的空格。这意味着在执行某项操作以前,通常都要先检查一下nodeTpye 属性,如下面的例子所示。
for (var i=0, len=element.childNodes.length; i < len; i++){
if (element.childNodes[i].nodeType == 1){
//执行某些操作
}
}
DOM操作技术
很多时候,DOM 操作都比较简明,因此用JavaScript 生成那些通常原本是用HTML代码生成的内容并不麻烦。不过,也有一些时候,操作DOM 并不像表面上看起来那么简单。由于浏览器中充斥着隐藏的陷阱和不兼容问题,用JavaScript 代码处理DOM 的某些部分要比处理其他部分更复杂一些。
动态脚本
我们都知道使用<script>标签可以向页面中插入JavaScript代码, 一种方式是通过src特性包含外部文件, 另一种方式就是用这个元素本身来包含代码。
- 我们可以通过以下方式引入外部JavaScript文件
<script type="text/javascript" src="client.js"></script>
通过js代码创建这个节点的代码如下:
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);
我们可以用一个函数讲代码封装
function loadScript(url) {
let script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
document.body.appendChild(script)
}
然后调用loadScript('client.js')
就能加载外部js文件了
- 我们还可以直接在<script>标签中指定js代码来运行js
<script type="text/javascript">
function sayHi(){
alert("hi");
}
</script>
同样的我们也可以通过js代码创建这个标签
let script = document.createElement("script")
script.type = "text/javascript"
script.text = "(function() {alert('hi')})()"
document.body.appendChild(script);
同样我们可以封装
function loadScriptString(code){
let script = document.createElement("script")
script.type = "text/javascript"
script.text = code
document.body.appendChild(script)
}
之后调用就行了loadScriptString('(function() {console.log("a")})()')
以这种方式加载的代码会在全局作用域中执行,而且当脚本执行后将立即可用。实际上,这样执行 代码与在全局作用域中把相同的字符串传递给 eval()是一样的。
动态样式
动态样式的套路跟上面以上, 也是用js代码动态插入标签, 我们知道引给页面添加css样式有两种方式, 一种<link>引入外部css文件, 另一种<style>直接嵌入样式
如果我们想动态引入外部css文件, 我们可以通过一个函数封装来实现
function loadStyles(url){
let link = document.createElement("link")
link.rel = "stylesheet"
link.type = "text/css"
link.href = url
let head = document.getElementsByTagName("head")[0]
head.appendChild(link)
}
如果想直接嵌入css则使用下面的封装来实现
function loadStyleString(css){
let style = document.createElement("style")
style.type = "text/css"
style.styleSheet.cssText = css
let head = document.getElementsByTagName("head")[0]
head.appendChild(style)
}
// 想下面这样调用函数即可
loadStyleString("body{background-color:red}")
NodeList
NodeList是"动态的", 每当文档结构发生变化时, 它都会更新。本质上来说, NodeList对象是在访问DOM文档时实时运行的查询, 例如, 下面的代码就会无限循环:
let divs = document.getElementsByTagName("div"), i, div;
for(i=0; i < divs.length; i++) {
div = document.createElement("div")
document.body.appendChild(div)
}
第一行代码会取得文档中所有<div>元素的 HTMLCollection。由于这个集合是“动态的”,因此, 只要有新<div>元素被添加到页面中,这个元素也会被添加到该集合中。浏览器不会将创建的所有集合都保存在一个列表中,而是在下一次访问集合时再更新集合。结果,在遇到上例中所示的循环代码时,就会导致一个有趣的问题。每次循环都要对条件 i < divs.length 求值,意味着会运行取得所有 <div> 元素的询。 考虑到循环体每次都会创建一个新 <div> 元素并将其添加到文档中, 因此 divs.length 的值在每次循环后都会递增。既然 i 和 divs.length 每次都会同时递增,结果它们的 值永远也不会相等。
var divs = document.getElementsByTagName("div"), i, len, div;
for(i=0, len=divs.length; i < len; i++) {
div = document.createElement("div");
document.body.appendChild(div);
}
这个例子中初始化了第二个变量 len。由于 len 中保存着对 divs.length 最初的值,因此就会避免上一个例子中出现的无限循环问题。
一般来说,应该尽量减少访问 NodeList 的次数。因为每次访问 NodeList,都会运行一次基于文档的查询。所以,可以考虑将从 NodeList 中取得的值缓存起来。