DOM
DOM(文檔對(duì)象模型)是針對(duì)HTML 和XML 文檔的一個(gè)API(應(yīng)用程序編程接口)。
DOM描繪了一個(gè)層次化的節(jié)點(diǎn)樹纺荧,允許開發(fā)人員添加旭愧、移除和修改頁面的某一部分。
節(jié)點(diǎn)層次
DOM可以將任何HTML文檔描繪成多層節(jié)點(diǎn)構(gòu)成的對(duì)象模型
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
上面的html內(nèi)容轉(zhuǎn)換成DOM結(jié)構(gòu)就是下面這張圖的樣子
Document節(jié)點(diǎn)是每個(gè)文檔的根節(jié)點(diǎn), 它只有一個(gè)子節(jié)點(diǎn), 即<html>元素, 我們叫它文檔元素宙暇。其他所有元素都包含在文檔元素中输枯。
對(duì)于每一段html標(biāo)記都可以通過樹中的一個(gè)節(jié)點(diǎn)表示: HTMl元素通過元素節(jié)點(diǎn)表示, 特性通過屬性節(jié)點(diǎn)表示, 文檔通過文檔節(jié)點(diǎn)表示, 除了這些還有諸如注釋節(jié)點(diǎn)等總共12中節(jié)點(diǎn)類型, 這些類型都繼承于一個(gè)基類型
Node類型
DOM1級(jí)定義了一個(gè)Node類型, 所有的節(jié)點(diǎn)類型都繼承自Node類型, 所有節(jié)點(diǎn)類型都共享著相同的基本屬性和方法。
每個(gè)節(jié)點(diǎn)都有一個(gè)nodeType屬性, 用于表示節(jié)點(diǎn)類型, 通常這個(gè)屬性會(huì)返回一個(gè)常數(shù), 每個(gè)常數(shù)對(duì)應(yīng)一種類型, 任何節(jié)點(diǎn)都是這12種的其中一個(gè):
- 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)
通過上面的常量, 可以很容易的確定節(jié)點(diǎn)類型, 如:
if (someNode.nodeType == 1){ //適用于所有瀏覽器
alert("Node is an element.");
}
我們可以通過節(jié)點(diǎn)的nodeName屬性得到元素的標(biāo)簽名, 對(duì)于元素節(jié)點(diǎn), nodeValue的值始終是null
節(jié)點(diǎn)關(guān)系
文檔中所有元素之前都存在著父子關(guān)系, 如
- <body>元素是<html>的子元素
- <html>元素師<body>的父親元素
- <head>和<body>則是同胞元素, 因?yàn)樗麄兌际?lt;html>的直接子元素
每個(gè)節(jié)點(diǎn)都有一個(gè)childNodes
屬性, 其中保存著一個(gè)NodeList偽數(shù)組
, NodeList中有序的保存著該節(jié)點(diǎn)的所有子節(jié)點(diǎn), 可以通過偽數(shù)組位置來訪問某個(gè)子節(jié)點(diǎn), NodeList最顯著地特點(diǎn)是他是基于DOM結(jié)構(gòu)動(dòng)態(tài)執(zhí)行查詢結(jié)果的, 會(huì)實(shí)時(shí)反映DOM結(jié)構(gòu)的變化占贫。下面例子是childNodes的典型應(yīng)用:
var firstChild = someNode.childNodes[0]
var secondChild = someNode.childNodes.item(1)
var count = someNode.childNodes.length
這里需要注意的一點(diǎn), 使用childNodes時(shí), 同胞元素之間的空格和回車會(huì)被識(shí)別為文本節(jié)點(diǎn)
除了childNodes外, 每個(gè)節(jié)點(diǎn)都有一個(gè)parentNode
屬性, 該屬性用來獲取該節(jié)點(diǎn)的父節(jié)點(diǎn), 此外還有一個(gè)previousSibling
用來獲取前一個(gè)同胞節(jié)點(diǎn), nextSibling
用來獲取后一個(gè)同胞節(jié)點(diǎn)
這里同樣需要注意, 由于元素間的空格和回車會(huì)被識(shí)別為文本節(jié)點(diǎn), 所有通過
previousSibling
和nextSibling
獲取同胞節(jié)點(diǎn)時(shí), 當(dāng)遇到這些文本節(jié)點(diǎn)時(shí), 一定要向前或向后獲取兩次同胞節(jié)點(diǎn)來跳過這些文本節(jié)點(diǎn)
父節(jié)點(diǎn)還可以通過firstChild
和lastChild
來獲得第一個(gè)子節(jié)點(diǎn)和最后一個(gè)子節(jié)點(diǎn)
下面這張圖就是上面這些屬性的總結(jié)
除了上面這些, 我很還可以通過hasChildNodes()
方法來判斷節(jié)點(diǎn)是否有子節(jié)點(diǎn)
所有節(jié)點(diǎn)都有的最后一個(gè)屬性是ownerDocument桃熄,該屬性指向表示整個(gè)文檔的文檔節(jié)點(diǎn)。這種關(guān)系表示的是任何節(jié)點(diǎn)都屬于它所在的文檔型奥,任何節(jié)點(diǎn)都不能同時(shí)存在于兩個(gè)或更多個(gè)文檔中瞳收。
操作節(jié)點(diǎn)
appendChild(newNode)
DOM提供了很多操作節(jié)點(diǎn)的方法, 其中最常用的方法就是appendChild(), 用于向childNodes列表末尾添加一個(gè)節(jié)點(diǎn), 如下面的例子就是向body的末尾添加一個(gè)button子節(jié)點(diǎn)
let el = document.querySelector('body')
let btn = document.createElement('button')
el.appendChild(btn)
如果傳入appendChild()中的節(jié)點(diǎn)已經(jīng)是文檔的一部分了, 那么該節(jié)點(diǎn)會(huì)從原來的位置轉(zhuǎn)移到新位置, 原因是DOM中的任何元素不能同時(shí)出現(xiàn)在文檔中的多個(gè)位置上, 如下面的例子
//someNode 有多個(gè)子節(jié)點(diǎn)
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true
insertBefore(insertNode, positionNode)
該方法是把某個(gè)放到childNodes列表的某個(gè)特定位置上, 第一個(gè)參數(shù)是要插入的節(jié)點(diǎn), 第二個(gè)參數(shù)是作為參照的節(jié)點(diǎn)。執(zhí)行后要插入的節(jié)點(diǎn)就會(huì)被放在參照節(jié)點(diǎn)的前一個(gè)位置
replaceChild(newNode, replaceNode)
該方法接受兩個(gè)參數(shù), 第一個(gè)是要插入的節(jié)點(diǎn), 第二個(gè)是要被替換的節(jié)點(diǎn), 返回值為被替換的節(jié)點(diǎn)
removeChild(Node)
該方法接受一個(gè)參數(shù), 就是要移除的節(jié)點(diǎn), 返回值是被移除的節(jié)點(diǎn), removeChild和replaceChild一樣, 被刪除的節(jié)點(diǎn)并沒有在文檔中被刪除, 只是斷開了所有鏈接
cloneNode(boolean)
該方法接受兩個(gè)參數(shù)為true時(shí)對(duì)元素進(jìn)行深拷貝, 拷貝對(duì)象包括其子節(jié)點(diǎn), 為false時(shí)為淺拷貝, 只拷貝元素本身
// 假設(shè)我們有一個(gè)<ul>元素, 里面包含三個(gè)<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()方法不會(huì)復(fù)制添加到DOM 節(jié)點(diǎn)中的JavaScript 屬性厢汹,例如事件處理程序等螟深。這個(gè)方法只復(fù)制特性、(在明確指定的情況下也復(fù)制)子節(jié)點(diǎn)烫葬,其他一切都不會(huì)復(fù)制界弧。IE 在此存在一個(gè)bug凡蜻,即它會(huì)復(fù)制事件處理程序,所以建議在復(fù)制之前最好先移除事件處理序夹纫。
最后一個(gè)方法是normalize()咽瓷,這個(gè)方法唯一的作用就是處理文檔樹中的文本節(jié)點(diǎn)。由于解析器的實(shí)現(xiàn)或DOM操作等原因舰讹,可能會(huì)出現(xiàn)文本節(jié)點(diǎn)不包含文本茅姜,或者接連出現(xiàn)兩個(gè)文本節(jié)點(diǎn)的情況。當(dāng)在某個(gè)節(jié)點(diǎn)上調(diào)用這個(gè)方法時(shí)月匣,就會(huì)在該節(jié)點(diǎn)的后代節(jié)點(diǎn)中查找上述兩種情況钻洒。如果找到了空文本節(jié)點(diǎn),則刪除它锄开;如果找到相鄰的文本節(jié)點(diǎn)素标,則將它們合并為一個(gè)文本節(jié)點(diǎn)
Document類型節(jié)點(diǎn)相關(guān)操作
在瀏覽器中, document
對(duì)象是HTMLDocument(繼承自Document類型)的一個(gè)實(shí)例, 表示整個(gè)HTML頁面, document對(duì)象是window對(duì)象的一個(gè)屬性, 可以通過全局訪問
我們可以通過其documentElement
屬性或者childNodes
來訪問<html>元素
document對(duì)象還有一個(gè)body屬性指向<body>元素。document.body
在js代碼中出現(xiàn)的頻率非常高, 請(qǐng)大家熟練掌握
document我們用的最多的是查找元素相關(guān)操作
我們可以通過document.getElementById()
和document.getElementsByTagName()
這兩個(gè)方法獲取相關(guān)節(jié)點(diǎn)
現(xiàn)在更多的用
document.querySelector()
和document.querySelectorAll()
這兩個(gè)方法來獲取元素
Element類型
除了document類型外, Element類型就要算是web編程中最常用的類型了萍悴。
nodeName
和tagName
這兩個(gè)屬性都返回標(biāo)簽名, 返回的是大寫, 例如:
<div id="myDiv"></div>
var div = document.getElementById("myDiv");
alert(div.tagName); //"DIV"
alert(div.tagName == div.nodeName); //true
所以判斷時(shí)不要忘記用toLowerCase()
轉(zhuǎn)換成小寫, 不然很容易出錯(cuò)
if (element.tagName == "div"){ //不能這樣比較头遭,很容易出錯(cuò)!
//在此執(zhí)行某些操作
}
if (element.tagName.toLowerCase() == "div"){ //這樣最好(適用于任何文檔)
//在此執(zhí)行某些操作
}
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" 元素內(nèi)的語言代碼, 很少使用
alert(div.dir); //"ltr" 語言方向, 癣诱,值為"ltr"(left-to-right计维,從左至右)或"rtl"(right-to-left,從右至左, 很少使用撕予。
當(dāng)然鲫惶,像下面這樣通過為每個(gè)屬性賦予新的值,也可以修改對(duì)應(yīng)的每個(gè)特性:
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!
特性的名稱是不區(qū)分大小寫的实抡,即"ID"和"id"代表的都是同一個(gè)特性欠母。另外也要注意,根據(jù)HTML5 規(guī)范吆寨,自定義特性應(yīng)該加上data-前綴以便驗(yàn)證赏淌。
自定義的屬性只能通過getAttribute獲取, 不能直接用屬性名獲取, 下面的代碼就獲取自定義屬性得到的是undefined
alert(div.id); //"myDiv"
alert(div.my_special_attribute); //undefined(IE 除外)
alert(div.align); //"left"
style和事件綁定getAttribute和直接調(diào)用屬性名返回的內(nèi)容是不同的
- 在通過getAttribute()訪問時(shí),返回的style 特性值中包含的是CSS 文本啄清,而通過屬性來訪問它則會(huì)返回一個(gè)對(duì)象猜敢。
- 通過getAttribute()訪問事件時(shí),會(huì)返回相應(yīng)代碼的字符串盒延。而在訪問s事件屬性(如onclick)時(shí),則會(huì)返回一個(gè)JavaScript 函數(shù)(如果未在元素中指定相應(yīng)特性鼠冕,則返回null)
設(shè)置特性
與getAttribute()
對(duì)應(yīng)的方法是setAttribute()
這個(gè)方法接受兩個(gè)參數(shù):要設(shè)置的特性名和值添寺。如果特性已經(jīng)存在,setAttribute()會(huì)以指定的值替換現(xiàn)有的值懈费;如果特性不存在计露,setAttribute()則創(chuàng)建該屬性并設(shè)置相應(yīng)的值。來看下面的例子:
div.setAttribute("id", "someOtherId");
div.setAttribute("class", "ft");
div.setAttribute("title", "Some other text");
div.setAttribute("lang","fr");
div.setAttribute("dir", "rtl");
也可以通過直接給屬性賦值設(shè)置元素的特性
div.id = "someOtherId";
div.align = "left";
直接給屬性賦值對(duì)于自定義屬性依然不適用
removeAttribute()
,這個(gè)方法用于徹底刪除元素的特性票罐。
Element類型的attributes屬性
-
getNamedItem(name)
:返回nodeName 屬性等于name 的節(jié)點(diǎn)叉趣; -
removeNamedItem(name)
:從列表中移除nodeName 屬性等于name 的節(jié)點(diǎn); -
setNamedItem(node)
:向列表中添加節(jié)點(diǎn)该押,以節(jié)點(diǎn)的nodeName 屬性為索引疗杉; -
item(pos)
:返回位于數(shù)字pos 位置處的節(jié)點(diǎn)。
創(chuàng)建元素
使用document.createElement()
方法可以創(chuàng)建新元素蚕礼。這個(gè)方法只接受一個(gè)參數(shù)烟具,即要?jiǎng)?chuàng)建元素的標(biāo)簽名
var div = document.createElement("div");
元素的子節(jié)點(diǎn)
正如我們之前說的, 元素的childNodes 屬性中包含了它的所有子節(jié)點(diǎn),這些子節(jié)點(diǎn)有可能是元素奠蹬、文本節(jié)點(diǎn)朝聋、注釋或處理指令。不同瀏覽器在看待這些節(jié)點(diǎn)方面存在顯著的不同囤躁,以下面的代碼為例冀痕。
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul >
在瀏覽器中,<ul>元素都會(huì)有7 個(gè)元素狸演,包括3 個(gè)<li>元素和4 個(gè)文本節(jié)點(diǎn)(表示<li>元素之間的空
白符)言蛇。如果像下面這樣將元素間的空白符刪除,那么所有瀏覽器都會(huì)返回相同數(shù)目的子節(jié)點(diǎn)严沥。
<ul id="myList"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>
如果需要通過childNodes 屬性遍歷子節(jié)點(diǎn)猜极,那么一定不要忘記有些瀏覽器會(huì)識(shí)別標(biāo)簽間的空格。這意味著在執(zhí)行某項(xiàng)操作以前消玄,通常都要先檢查一下nodeTpye 屬性跟伏,如下面的例子所示。
for (var i=0, len=element.childNodes.length; i < len; i++){
if (element.childNodes[i].nodeType == 1){
//執(zhí)行某些操作
}
}
DOM操作技術(shù)
很多時(shí)候翩瓜,DOM 操作都比較簡明受扳,因此用JavaScript 生成那些通常原本是用HTML代碼生成的內(nèi)容并不麻煩。不過兔跌,也有一些時(shí)候勘高,操作DOM 并不像表面上看起來那么簡單。由于瀏覽器中充斥著隱藏的陷阱和不兼容問題坟桅,用JavaScript 代碼處理DOM 的某些部分要比處理其他部分更復(fù)雜一些华望。
動(dòng)態(tài)腳本
我們都知道使用<script>標(biāo)簽可以向頁面中插入JavaScript代碼, 一種方式是通過src特性包含外部文件, 另一種方式就是用這個(gè)元素本身來包含代碼。
- 我們可以通過以下方式引入外部JavaScript文件
<script type="text/javascript" src="client.js"></script>
通過js代碼創(chuàng)建這個(gè)節(jié)點(diǎn)的代碼如下:
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);
我們可以用一個(gè)函數(shù)講代碼封裝
function loadScript(url) {
let script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
document.body.appendChild(script)
}
然后調(diào)用loadScript('client.js')
就能加載外部js文件了
- 我們還可以直接在<script>標(biāo)簽中指定js代碼來運(yùn)行js
<script type="text/javascript">
function sayHi(){
alert("hi");
}
</script>
同樣的我們也可以通過js代碼創(chuàng)建這個(gè)標(biāo)簽
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)
}
之后調(diào)用就行了loadScriptString('(function() {console.log("a")})()')
以這種方式加載的代碼會(huì)在全局作用域中執(zhí)行仅乓,而且當(dāng)腳本執(zhí)行后將立即可用赖舟。實(shí)際上,這樣執(zhí)行 代碼與在全局作用域中把相同的字符串傳遞給 eval()是一樣的夸楣。
動(dòng)態(tài)樣式
動(dòng)態(tài)樣式的套路跟上面以上, 也是用js代碼動(dòng)態(tài)插入標(biāo)簽, 我們知道引給頁面添加css樣式有兩種方式, 一種<link>引入外部css文件, 另一種<style>直接嵌入樣式
如果我們想動(dòng)態(tài)引入外部css文件, 我們可以通過一個(gè)函數(shù)封裝來實(shí)現(xiàn)
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則使用下面的封裝來實(shí)現(xiàn)
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)
}
// 想下面這樣調(diào)用函數(shù)即可
loadStyleString("body{background-color:red}")
NodeList
NodeList是"動(dòng)態(tài)的", 每當(dāng)文檔結(jié)構(gòu)發(fā)生變化時(shí), 它都會(huì)更新宾抓。本質(zhì)上來說, NodeList對(duì)象是在訪問DOM文檔時(shí)實(shí)時(shí)運(yùn)行的查詢, 例如, 下面的代碼就會(huì)無限循環(huán):
let divs = document.getElementsByTagName("div"), i, div;
for(i=0; i < divs.length; i++) {
div = document.createElement("div")
document.body.appendChild(div)
}
第一行代碼會(huì)取得文檔中所有<div>元素的 HTMLCollection子漩。由于這個(gè)集合是“動(dòng)態(tài)的”,因此, 只要有新<div>元素被添加到頁面中石洗,這個(gè)元素也會(huì)被添加到該集合中幢泼。瀏覽器不會(huì)將創(chuàng)建的所有集合都保存在一個(gè)列表中,而是在下一次訪問集合時(shí)再更新集合讲衫。結(jié)果缕棵,在遇到上例中所示的循環(huán)代碼時(shí),就會(huì)導(dǎo)致一個(gè)有趣的問題焦人。每次循環(huán)都要對(duì)條件 i < divs.length 求值挥吵,意味著會(huì)運(yùn)行取得所有 <div> 元素的詢。 考慮到循環(huán)體每次都會(huì)創(chuàng)建一個(gè)新 <div> 元素并將其添加到文檔中花椭, 因此 divs.length 的值在每次循環(huán)后都會(huì)遞增忽匈。既然 i 和 divs.length 每次都會(huì)同時(shí)遞增,結(jié)果它們的 值永遠(yuǎn)也不會(huì)相等矿辽。
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);
}
這個(gè)例子中初始化了第二個(gè)變量 len丹允。由于 len 中保存著對(duì) divs.length 最初的值,因此就會(huì)避免上一個(gè)例子中出現(xiàn)的無限循環(huán)問題袋倔。
一般來說雕蔽,應(yīng)該盡量減少訪問 NodeList 的次數(shù)。因?yàn)槊看卧L問 NodeList宾娜,都會(huì)運(yùn)行一次基于文檔的查詢批狐。所以,可以考慮將從 NodeList 中取得的值緩存起來前塔。