操作DOM
终于到了JavaScript最为核心的部分了,通常来说,操作DOM,为页面提供更为友好的行为是JavaScript根本目标。
DOM树 - HTML结构的抽象
既然DOM是操纵HTML文档的,那么必然是先要将HTML文档抽象成JavaScript的对象,这样才能使用JavaScript去操作它们。
HTML文档抽象的结果是DOM树 - 一棵与HTML结构对应的JavaScript对象组成的树。
先看一个简单的HTML页面:
<!doctype html> <html> <head> <title>My title</title> </head> <body> <a href="">My link</a> <h1>My header</h1> </body> </html>
我们这里忽略第一行的文档说明<!doctype html>,剩下的HTML其实是一个单根的树,那么抽象出来的JavaScript对象组成的DOM树是:
这棵树上有两种类型的对象:
第一种对象就是Document对象。
Document对象是树的根,可以这么说,所有的DOM操作都起源于Document对象,最终也回归到Document对象。
如何获取这个对象是所有操作的前提,在BOM里面我们已经总结过了。因为Document对象是Window对象的成员,Window对象通常是用window获取到的,而window标识符是可以省略的,所以在程序中我们可以使用document标识符获取到代表当前页面的document对象。Document对象主要提供了访问各种元素的快捷方式,这个在下面的例子中你会看到。完整的成员列表请参看:http://www.w3school.com.cn/jsref/dom_obj_document.asp。
第二种对象就是节点。
这棵树上除了Document外的所有对象都称为“节点”,节点可能是这些对象:Root Element (还是Element),Element,Attribute,Text。
Element对象是HTML元素的抽象,它完成对所有HTML元素的操作,包括获取/设置属性值,添加/删除子元素,获取/设置图形信息等等。完整的成员列表请参看:http://www.w3school.com.cn/jsref/dom_obj_all.asp。
Attribute对象是HTML元素属性的抽象,它完成对HTML元素属性的操作,主要是获取/设置属性值。完整的成员列表请参看:http://www.w3school.com.cn/jsref/dom_obj_attributes.asp。
Text对象是对HTML元素中字符串值的抽象,是最低层的节点。这个对象很简单,在下面的例子中会有所涉及。
从实现上来说,Node是表征节点的基对象,Element,Attribute,Text都是从Node继承的,Node对象封装了节点的基本信息,比如nodeName, nodeType, nodeValue等等。其中nodeName和nodeValue都比较简单,值得一提的是nodeType,一般说来,nodeType有以下取值:
节点类型 NodeType
元素element 1
属性attr 2
文本text 3
注释comments 8
文档document 9
所有nodeType值为1的节点都被处理成了Element对象,也就是HTML元素对象。Element对象是从Node对象继承的,提供了针对对HTML元素的各种扩展方法。
大部分时间,我们都是直接去操作Element对象,偶尔也会直接使用Node对象。document对象针对Node, Element对象提供了不同的方法,我们一般都是选择使用名称中包含Element或者是明确返回Element的各种方法。当然这里并不是说Node对象就不能用,很多场合使用Node对象也是很方便的,这个要具体问题具体分析。
这里DOM4中据说Attribute对象不再是从Node节点继承了,所以使用起来可能稍微有点区别,标准出来以后同学可以自己查询一下。
DOM第一步: 查找目标对象
DOM操作很多,第一步通常是要查找到要操作的DOM对象,这个通用的功能由document对象提供,可以有两种方式达到目的。
第一种方式,从document上直接根据某些条件查找,这种不妨称为“绝对查找”。
这种方式是使用document对象的下列方法实现的:
document.getElementById
element.getElementsByTagName
element.getElementsByClassName
从上面函数的名字我们可以知道:
1. 它们分别是通过元素的id,元素的tag,元素应用的style分别实现查找的。
2. 通过id的方式返回的单一的元素,因为一般一个文档中每个元素的id都是唯一的,如果不唯一那是自寻烦恼。这个方法只属于document对象。
3. 通过tag名字和style名字可以返回多个元素,这两个方法是每个element都有的(当然document对象也有),可以在自己的children成员中查找符合条件的对象。
看下面的例子:
<input type="button" value="Change Name From Long Path" onclick="changeName()"></input> <ul id="listValues"> <li class="big">A</li> <li class="big">B</li> <li class="big">C</li> </ul> <script> function changeName(){ // 根据id查找元素,缩小范围 var list = document.getElementById('listValues'); // 根据tag查找子元素中符合条件的 var values = list.getElementsByTagName('li'); for(var i = 0; i < values.length; i++) { values[i].innerHTML = values[i].innerHTML + 's'; } // 根据style查找符合条件的子元素 values = list.getElementsByClassName('big'); for(var i = 0; i < values.length; i++) { values[i].innerHTML = values[i].innerHTML + 'c'; } } </script>
这个例子通过元素对象的innerHTML属性来修改元素的内容。
第二种方式,从相关元素通过关联关系查找,这种不妨称为“相对查找”。
这种方式是在已知某元素的情况下,通过查找这个元素的子女,兄弟,父亲来找到目标的方式。一般是使用下列成员: parentNode, children, firstChild, lastChild, previousSibling, nextSibling来查找。看改写后的例子:
<input type="button" value="Change Name From Short" onclick="changeNameShort()"></input> <ul id="listValues"> <li class="big">A</li> <li class="big">B</li> <li class="big">C</li> </ul> <script> function changeNameShort(){ // 获得某个元素的方式还是使用getElementById方法 var list = document.getElementById('listValues'); // 使用children方式 var values = list.children; for(var i = 0; i < values.length; i++) { values[i].innerHTML = values[i].innerHTML + 's'; } // 使用childNodes方式 values = list.childNodes; for(var i = 0; i < values.length; i++) { values[i].innerHTML = values[i].innerHTML + 's'; } // 其它的一些方式 // list.firstChild.innerHTML = list.firstChild.innerHTML + 'f'; // list.lastChild.innerHTML = list.lastChild.innerHTML + 'l'; } </script>
DOM第二步: 标准的元素级别的CRUD操作
标准的CRUD操作中,我们已经演示过查找元素并进行更新操作了,下面再看一下创建,删除,克隆操作:
<input type="button" value="Add new" onclick="add()"></input> <input type="button" value="Remove first" onclick="remove()"></input> <input type="button" value="Clone last" onclick="clone()"></input> <ul id="listValues"> <li class="big">A</li> <li class="big">B</li> <li class="big">C</li> </ul> <script> // add and append function add(){ var list = document.getElementById('listValues'); var item = document.createElement('li'); var text = document.createTextNode("D"); item.appendChild(text); list.appendChild(item); } // remove function remove(){ var list = document.getElementById('listValues'); list.removeChild(list.children[0]); } // clone function clone(){ var list = document.getElementById('listValues'); var node = list.lastElementChild.cloneNode(true); list.appendChild(node); } </script>
这个例子中主要是演示了document.createElement, element.appendChild, element.removeChild等方法的作用。
DOM第三步: 属性Attribute的操作
DOM可以操作元素,也可以操作更新的元素的属性,看例子:
<input type="button" value="显示tooltip" onclick="changeAttribute()"></input> <div id='demo'>不带tooltip的文本,点击上面的button来开启tooltip.</div> <script> // get/set attributes function changeAttribute() { var div = document.getElementById('demo'); alert(div.getAttribute('title')); div.setAttribute('title', 'updated tooltip!'); } </script>
这个例子是给div添加了一个title属性,这个属性用于存放元素的tootip值。
说到属性的问题,我们需要知道HTML是支持自定义属性的,这些自定义的属性常常是用来存放页面的逻辑数据,很多的框架都通过这种方式来扩展页面的行为。
DOM第四步: 样式CSS的操作
CSS是特殊的属性,它可以在class属性中存放CSS样式的名字,这种方式经常用于把CSS样式定义在元素之外的情况,也是推荐的写法,除此以外,CSS还可以直接定义在style属性中,这些情况都可以使用DOM直接修改。看个例子:
<head> <title>My title</title> <style> .big { font-size:40px; } .small { font-size:20px; } </style> </head> <body> <input type="button" value="Change CSS" onclick="changeCSS()"></input> <ul id="listValues"> <li class="big">A</li> <li class="big">B</li> <li class="big">C</li> </ul> <script> // modify css function changeCSS() { var list = document.getElementById('listValues'); var values = list.children; for(var i = 0; i < values.length; i++) { // 方式一:通过修改class的方式修改css values[i].className = 'small'; // 方式二:通过直接修改style属性的方式修改css // 经验:一般在css中有-的,在js中-去掉,后面的那个词大写就变成style的属性了 //values[i].style.fontSize = '20px'; // 方式三:通过直接修改style对象的cssText属性 //values[i].style.cssText = 'font-size: 20px; background-color: green'; // 方式四:通过直接调用style对象的setProperty方式修改css //values[i].style.setProperty('background-color', 'red', ''); } } </script> </body>
上面的例子中有修改fontSize的语句,这个语句是直接修改style元素的方式,这里有一个经验,一般来说css中样式的名字去掉短线后,把短线后第一个字符大写就是JavaScript中对应的style对象的属性的名字,比如CSS中的font-size直接对应到style.fontSize上。此外还可以通过sytle对象的cssText属性(一次性可以把全部CSS值都加上),setProperty(一次添加一个值)等方式修改CSS,例子中都提到了。
上面我们简单总结了DOM的常见操作,这些操作很简单,却是很多高级特效的基础。
DOM的级别
上面我们概括了基本的一些操作,其实DOM的操作是在不同的时期,不同的版本中加入到JavaScript中来的,这个一般可以用DOM的级别来划分,DOM的级别代表了DOM操作的演变过程,通常来说:DOM1级主要定义的是HTML和XML文档的底层结构。DOM2和DOM3级别则在这个结构的基础上引入了更多的交互能力,也支持了更高级的XML特性。为此DOM2和DOM3级分为许多模块(模块之间具有某种关联),分别描述了DOM的某个非常具体的子集。我们上面例子中使用的操作都是这些级别的操作。
但是事实上,所有的浏览器都支持所谓的JavaScript DOM0操作的。DOM0与后来的这些操作最大的区别在于它提供了数组的方式直接返回一些特殊的成员,这些成员通常是document对象的images (图片), forms (表单), links (超链接的目标地址), anchors (超链接的名字), cookie (客户度存储), body, title,每个element的id, name, title, style,图片的src等等。看一下具体的例子:
<img src="imgs/1.jpg" alt="images" onclick="changeImage()"></img> <script> function changeImage() { // DOM 0方式访问图片 var image = document.images[0]; image.src = 'imgs/2.jpg'; } </script>
相对于DOM1后面的操作来说,这种方式可以更加方便的访问页面中所有的图片,表单等常用数据,适当的使用这种方式可以加速程序的开发。
特殊的操作innerHTML
我们上面使用innerHTML来修改一个Element的内容,然后又使用appendChild来修改Element的内容,那么这两种方式有什么不同呢?
innerHTML不是DOM的标准操作,这个属性完全是浏览器自己实现的行为,它最初是微软加入的,不过由于使用起来相当方便快捷(直接的字符串操作,比DOM操作要快,特别是需要大量添加HTML元素的时候,而且字符串中的元素可以嵌套多层,DOM实现这种嵌套的话需要书写大量的代码),所以其它主流浏览器都提供了这个属性。
什么时候使用innerHTML与什么时候使用DOM并没有严格的标准,通常是看个人的习惯,一般的知道思想是添加大量确定的静态结构的时候使用innerHTML相当高效,添加不确定数量的动态结构的时候DOM更为灵活。看个例子:
var x = document.getElementById('root'); x.innerHTML = '<ul><li>第一点</li><li>第二点</li><li>第三点</li></ul>';
这段代码写成appendChild的话则是:
var x = document.getElementById('root'); var y = document.createElement('ul'); var z = document.createElement('li'); z.appendChild(document.createTextNode('第一点')); y.appendChild(z); var z1 = document.createElement('li'); z1.appendChild(document.createTextNode('第二点')); y.appendChild(z1); var z2 = document.createElement('li'); z2.appendChild(document.createTextNode('第三点')); y.appendChild(z2); x.appendChild(y);
同学们自己辨别思考一下吧,其实这两种方式并不是互斥的,我们完全可以结合使用这两种方式。
DOM说白了就是一句话:“使用抽象的DOM对象操作页面本身,动态的添加页面的行为”。