一、DOM概述
- D: Document 文檔 一份文檔就是一棵節點樹,每個節點都是一個對象
- O:Object 對象
JavaScript語言里對象可以分為三種類型:
(1)用戶定義的對象
(user-defined object) 有程序員自己創建的對象
(2)內建對象
(native object) 內建在JavaScript語言里的對象,如Array、Math、Date等
(3)宿主對象
(host object) 由瀏覽器提供的對象,如 window對象 - M: 模型,文檔的表現形式。
節點:元素[標簽]節點、文本節點和屬性節點
-
標簽節點
就是html里的標簽,如<body>、<p>
之類的標簽。標簽的名字就是元素的名字 -
文本節點
就是文本,如<p>
標簽包含this is content
文本,那么這個文本就是文本節點 -
屬性節點
被放在起始標簽里,如<p title="title">xxxx</p>
,那么title=”title”就是屬性節點
二、CSS(層疊樣式表)告訴瀏覽器應該如何顯示一份文檔的內容
如何引用CSS樣式:
(1)直接在html里寫
<head>
<style type="text/css">
body {
background-color: red;
}
p {
margin-left: 20px
}
</style>
<head>
(2)引入CSS文件
<head>
<link rel="stylesheet" type="text/css" href="mystyle.css">
</head>
元素樣式:
body{
color:yellow;
background-color: black;
}
元素樣式還有繼承的功能,類似DOM,CSS也罷文檔的內容視為一棵節點樹,節點樹上的各個元素將繼承其父元素的樣式屬性,比如上面的我們為body元素定義了一些字體和背景顏色,這些顏色不僅作用于那些直接包含在標簽里的內容, 還將作用于嵌套在body元素內部的所有元素。
class樣式(給帶有class屬性的元素設置樣式)
<p class"special">element p has special class</p>
<h2 class="special">element h2 has special class</p>
如下面的樣式,就會對上面的兩個標簽起作用,內容都變成了斜體。
.special{
font-style: italic;
}
如果只想給h2標簽設置斜體,p標簽不受影響,可以這樣設置樣式:
h2.special{
font-style: italic;
}
如果即存在.special又存在h2.special,.special樣式還是會影響到所有h2標簽。
id樣式 (給具有唯一id標識的元素設置樣式)
<ul id="purchases">
<li>支付寶</li>
<li>微信</li>
<li>信用卡</li>
</ul>
通過如下方式設置【#】:
#purchases{
border: 1px solid white;
background-color: #333;
color: #ccc;
padding: 2em;
}
如果你想給id為purchases的節點的子節點設置單獨設置樣式,可以這樣:
#purchases li{
font-weight: bold;
}
三、DOM操作節點的方法:
1.getElementById(id);
這個方法返回一個對象,這個對象對應著document對象里一個獨一無二的元素。【實際上,document中的每個元素都是一個對象。利用DOM提供的方法能得到任何一個對象】
節點上設置id屬性,id應該設置為獨一無二的,但是你非得在html節點上加上兩個相同的id,chrome上只會返回第一個。
一般說來,用不著為文檔里的每個元素都定義一個獨一無二的對象。DOM提供了另一個方法來獲取那些沒有id屬性的對象。
2.getElementsByTagName(tag);
getElementsByTagName方法會返回一個對象數組,每個對象分別對應著文檔里有著給定標簽的一個元素。參數tag就是文檔的標簽,比如body、ul、ui等標簽。調用可以參考demo里的getChildNodes()方法。
getElementsByTagName還可以傳遞通配符*
,它會返回文檔里的所有元素節點,document.getElementsByTagName(“*”)。
getElementsByTagName還可以被其他元素節點調用,比如
var payWays = document.getElementById("purchases");
var items = payWays.getElementsByTagName("*");
因為在例子中id為purchases元素節點是ul,那么payWays.getElementsByTagName(“*”);返回的是ul里面有多少個ui節點。
3.getElementsByClassName(class);
HTML5 DOM中新增的一個方法。這個方法能夠通過class屬性來訪問元素。該方法返回的也是數組。
如果你想輸入多個className可以用空格隔開。如:
document.getElementsByClassName("name1 name2")
和getElementsByTagName(tag)一樣,getElementsByClassName也可以被其他元素調用,但是不支持*通配符:
var payWays = document.getElementById("purchases");
var items = payWays.getElementsByClassName("sale");
因為getElementsByClassName方法HTML5才出來的,老點的瀏覽器可能不支持,下面方法兼容老版本:
function getElementsByClassName(node, classname){
//如果支持,使用新方法
if(node.getElementsByClassName){
return node.getElementsByClassName(classname);
} else{
var results = new Array();
var es = node.getElementsByTagName("*");
for(var i=0;i<es.length;i++){
if(es[i].className.index(classname)!=-1){
results[results.length] = es[i];
}
}
}
}
4.getAttribute()、setAttribute();
這個兩個方法都是用來獲取和設置節點上的屬性。不屬于document對象。
5.childNodes獲取節點下的所有類型的子元素,該屬性返回一個數組
文檔里的節點不止元素節點、文本節點,屬性節點三個類型的節點。文檔里幾乎每一樣東西都是一個節點。甚至連空格
和換行
都會被解釋為節點,而他們也會全部包含在childNodes屬性返回的數組當中。
上面我們通過getElementsByTagName(““)也可以獲取某個節點下子元素, 注意getElementsByTagName(““)返回的不僅僅是直接子節點,如果子節點包含了節點,也會計算在內的。
雖然childNodes返回所有類型的節點,我們可以通過nodeType
獲取節點的類型,但是該屬性只返回整形
。
6.nodeType 節點的類型
元素節點的nodeType屬性值是1.
屬性節點的nodeType屬性值是2.
文本節點的nodeType屬性值是3.
也就這是三個節點有使用價值。
7.nodeValue 屬性返回文本節點的值。
比如要獲取<p id="xxx">我是標簽p里的值</p>
里面的文本的值,其實這里包含了兩個節點,一個元素節點p和里面的文本節點,可以通過下面方式獲取里面的文本:
document.getElementById("xxx").childNodes[0].nodeValue; //document.getElementById("xxx").firstNode.nodeValue;
nodeValue除了可以獲取文本節點的值,還可以修改文本節點的值,如:
p1.firstChild.nodeValue = "通過nodeValue設置新的值"
8.firstChild 和 lastChild 獲取節點數組中首尾節點
var e = document.getElementById(“xxx”);
e.firstChild 相當于 e.childNodes[0]
e.lastChild 相當于 e.childNodes[e.childNodes.length-1]
四、JavaScript最佳實踐
例如我們想點擊一個url然后在新的窗口彈出(這個窗口的大小我們可以設定),可以通過JavaScript偽協議
和內嵌的事件處理函數
來實現。
什么是JavaScript偽協議?
真
協議就是英特網上的如http、ftp
等,偽
協議則是一種非標準化的協議。javascript:
偽協議讓我們可以通過一個鏈接來調用JavaScript函數。如:
<a href="javascript:openWindow()">openWindow</a>
這條語句在支持javascript:
偽協議的瀏覽器中運行正常,較老的瀏覽器則會去嘗試打開那個鏈接但失敗。支持這種偽協議但禁用JavaScript功能的瀏覽器將什么都不做。
總之,在HTML文檔中通過javascript:
偽協議調用JavaScript代碼的做法非常不好。
內嵌的事件處理函數
<a href="#" onclick="openWindow();return false">openWindow</a>
在HTML指令里使用了return false
語句,瀏覽器不會試圖去打開這個鏈接。
使用內嵌的事件處理函數和使用JavaScript偽協議的做法很糟糕。因為他們都不能平穩的退化,原因有兩點:
(1)因為他們都不能平穩退化,如果用戶已經禁用了瀏覽器的JavaScript功能,這樣的鏈接將毫無用處。
(2)href
屬性值不是合法的鏈接,可能會影響搜索引擎上的排名
所以最好的方式就是href設置為合法的url,修改一下openwindow
,把url作為參數傳遞進去而不是在方法里寫死。
<a onclick="openwindow('http://www.baidu.com');return false">openWindow</a>
可以共用前面設置的url
<a onclick="openwindow(this.getAtrribute('href'));return false">openWindow</a>
看起來比較多,還可以簡化下(this可以代表任何一種當前元素)
<a onclick="openwindow(this.href);return false">openWindow</a>
JavaScript分離
如何把JavaScript代碼調用行為與HTML文檔的結構和內容分離開,這樣的話,網頁就會健壯多。那么,能否使用下面的語句來實現點擊事件(里面并沒有onclick屬性):
<a class="popup">open window</a>
JavaScript語言不要求事件必須在HTML文檔里處理,我們可以在外部JavaScript文件里把一個事件添加到HTML文檔中的某個元素上,如下所示:
element.event = action…
如何獲取element上面我們已經講了很多操作方法比如:getElementById(id)、getElementsByTagName(tag)…
那么,如何實現上面的點擊效果呢?
(1)獲取html的a標簽
(2)遍歷a標簽數組
(3)如果某個a標簽的class屬性等于popup,就表示需要給它設置處理點擊事件的函數
var as = document.getElementsByTagName("a");
for(var i = 0; i < as.length; i++){
if(as[i].getAttribute("class") == 'popup'){
as[i].onclick = function(){
openwindow(this.getAttribute("href"));
return false;
}
}
}
測試的時候,發現并沒有用。因為上面的js代碼的第一行是:
var as = document.getElementsByTagName("a");
這行語句將在JavaScript文件被加載時立刻執行,如果引用外部js的代碼<script>
標簽調用是在放在標簽<head>
里,它將在HTML文檔之前加載到瀏覽器里;如果是</body>
標簽之前,就不能保證那個文件先結束加載(瀏覽器可能一次加載多個)。因為文檔加載時文檔可能不完整,所以模型也不完整。沒有完整的DOM,getElementsByTagName
函數就不能正常工作。
所以必須讓這些代碼在HTML文檔全部加載到瀏覽器之后馬上開始執行。還在,HTML文檔全部加載完畢時將觸發一個事件 window.onload,當觸發onload事件時,document對象已經存在了。所以把外部js改成如下形式就可以了:
window.onload = preLinks;
function preLinks(){
var as = document.getElementsByTagName("a");
for(var i = 0; i < as.length; i++){
if(as[i].getAttribute("class") == 'popup'){
as[i].onclick = function(){
openwindow2(this.getAttribute("href"));
return false;
}
}
}
}
function openwindow2(url){
window.open(url,"openwindow","width=600,height=600");
}
這樣我們就成功的把行為和結構分離了
問題又來了,如果瀏覽器沒有啟用JavaScript功能。這樣的話可以確保那些“古老的”瀏覽器不會因為我們的腳本代碼而出問題,這樣做是為了讓腳本有良好的向后兼容性。那些只支持一部分JavaScript功能但不支持DOM的瀏覽器認可訪問我們的網頁。
(我覺得,現在瀏覽器都非常新了,而且沒有不支持DOM的吧。 但是還是記錄下這個知識點。可能這本書比較老,這也體現了這本書作者的思維嚴謹性,值得學習)
判斷某個js方法是否可用可以通過if(functionName),如:
if(!document.getElementById(id)) return false
性能考慮:
(1)盡量減少DOM的操作,比如document.getElementById(id),回去搜索整個DOM樹。所以如果能夠復用操作盡量復用,減少DOM操作。
(2)如果引入多個外部js文件,如果可以的話,把多個js合并到一個js文件,減少加載頁面時發出請求的次數。
(3)壓縮腳本,所謂壓縮腳本,指的是把腳本文件不要的字節,如空格和注釋等統統刪除,達到壓縮文件的目的。很多工具都可以替你來做這件事。有的壓縮工具甚至會重寫你的部分代碼,使用更短的變量名,從而減少整體的文件大小。壓縮后的代碼版本雖然不容易看懂,卻能大幅減少文件的大小。所以要保存兩個版本,一個一個是工作副本(可以修改代碼并添加注釋);另一個是精簡副本,用于存放站點。通常為了與非精簡版本區分開,最好在精簡副本的文件名中加上min字樣。如:
<script src="scriptName.min.js">
下面推薦幾個代表性的壓縮工具:
五、DOM Core 和 HTML-DOM
至此,我們在編寫JavaScript代碼時只用到了一下幾個DOM方法:
getElementById();
getElementsByTagName();
getAttribute();
setAttribute();
這些方法都是DOM Core的組成部分,它們并不專屬于JavaScript,支持DOM的任何一種程序設計語言都可以使用它們。它們的用途也并非僅限于處理網頁。
在使用JS語言和DOM為HTML編寫腳本的時候,還有很多屬性可供選擇,例如我們使用onclick,這是屬于HTML-DOM。例如HTML-DOM提供了一個forms對象:document.getElementsByTagName(“form”),可以簡化為:document.forms
。類似的還有element.getAttribute(“src”)簡化為element.src
,還有href屬性等。所以你要能夠看懂別人的HTML,明白這兩種寫法。
動態創建DOM節點
上面我們講了通過一系列的方法可以找到DOM的節點、改變節點的屬性(element.setAttribute)。那么如何創建節點呢?
1、使用傳統方法:
(1)document.write()
該方法必須放在body標簽里面,且必須是script標簽里。如:
<body id="content">
<script type="text/javascript">
document.write("<p>document write something</p>");
</script>
</body>
document.write的最大缺點是它違背了“行為應該和表現分離
”的原則。所以盡量避免使用。
(2)innerHTML屬性
現如今的瀏覽器幾乎都支持該屬性,但這個屬性并不是W3C DOM標準的組成部分,但現在已經在HTML5的規范中。最早見于IE4瀏覽器中,
該屬性可以用來讀、寫某個給定元素的HTML內容。比如你可以把某個標簽里的所有HTML代碼全部替換成某個文本。但是這樣就沒有細節可言了,如果想要精準的控制還是必須使用DOM的方法和屬性。
2、使用DOM方法
案例一:比如我們想在一個id為container的div節點添加一個p節點,p節點里有個文本“this dynamic node”。怎么實現?
需要用到的DOM方法:createElement、appendChild、createTextNode
方法。
首先我們要創建一個element,可以通過document.createElement(nodeName);
所以創建p節點很簡單:
var p = document.createElement(“p”);
并且需要在p節點里添加一個文本,這就要用到創建文本節點方法createTextNode.
vat txt = document.createTextNode(“this dynamic node”);
此時txt節點還是個孤零零的節點,和p節點沒有任何關系,需要通過appendChild為p節點添加txt節點。
p.appendChild(txt);
同樣的,p節點也是孤零零的存在,和div節點沒有任何關系,所以也需要appendChild();
var container = document.getElementById("container");
container.appendChild(p);
案例二:在已有元素前插入一個新元素
DOM提供了名為insertBefore(newElement,targetElement)方法。該方法將把一個新元素插入到一個現有元素的前面。調用該方法需要知道三件事:
(1)新元素:你想插入的元素(newElement)
(2)目標元素:你想把這個新元素插入到哪個元素(targetElement)之前。
(3)父元素:目標元素的父元素(parentElement)。可以通過node.parentNode屬性獲得parentElement
parentElement.inertBefore(newElement,targetElement)
具體的例子可以參考源碼里的testInsertBefore
方法,往input之前加入文本
案例三:在已有元素前插入一個新元素
很遺憾,DOM并沒有提供這個方法。所以需要我們自己實現。
結合insertBefore方法和element.nextSibling屬性可以實現。
具體的例子可以參考源碼里的insertAfter
方法。源碼鏈接在文章最后給出。
六、CSS DOM
我們在瀏覽器里看到的網頁是由一下三層信息構成的共同體:
結構體 (HTML)
表示層 (CSS)
行為層 (Javascript、DOM)
style屬性
文檔中的每個元素都是一個對象,每個對象又有著各種各樣的屬性。有些屬性告訴我們元素節點樹上的位置節點,比如,parentNode、nextSibling、previousSibling、childNodes、firstChild和lastChild這些屬性,告訴我們文檔中各節點之間關系。
除此之外,文檔的每個元素都有一個style屬性,style屬性包含著元素的樣式,比如給p節點加上一個樣式:
<p id="localStyle" style="color:grey;font-family: 'Arial',sans-serif;"> this is local style</p>
如果想要獲取p節點的樣式里的顏色怎么辦?
查詢style屬性將返回一個對象,而不是一個簡單的字符串。樣式都存放在這個style對象的屬性里:
element.style.propertyName
所以獲取里面的顏色很簡單:
p.style.color;
需要注意的是在p標簽里我們設置樣式里的顏色是grey,如果我設置的是十六進制的值“#999999”,p.style.color返回的值是RGB格式rgb(153,153,153)
但是獲取font-family,要使用駝峰的方式,CSS是使用-
來分隔兩個單詞的,我們通過DOM來獲取有-
的style屬性必須使用去掉-
然后第二個單詞首字母大寫。如CSS屬性background-color對應的DOM屬性為backgroundColor;CSS屬性font-weight對應這DOM屬性fontWeight;CSS屬性margin-top-width對應著DOM屬性marginTopWidth。
獲取font-family:
p.style.fontFamily
DOM可以獲取style屬性,同樣可以修改樣式:
p.style.color = “black”;
以上我們是通過DOM來操作內嵌樣式,但是這樣的內嵌樣式有很大的局限性。只有把CSS style屬性插入到標記里(標簽),才可以用DOM style屬性去查詢那些信息:
<p id="localStyle" style="color:grey;font-family: 'Arial',sans-serif;"> this is local style</p>
這樣不是使用CSS的好辦法(表現信息與結構混雜在一起了)。更好的辦法是用一個外部樣式去設置樣式:
p#localStyle{
color: grey;
font-family: 'Arial',sans-serif;
}
把這段CSS代碼放到一個單獨的文件,然后在HTML引入即可:
<link href="my.css" rel="stylesheet" type="text/css" />
但是這樣就無法通過DOM獲取style里的屬性了,因為p標簽沒有style屬性節點了。
className屬性
在前面的例子,我們通過DOM直接設置或修改樣式,這種做法讓“行為層”干“表示層”的活,并不是理想的工作方式。
這里有一種簡明的解決方案:與其使用DOM直接修改某個元素的樣式,不同通過JavaScript代碼去更新這個元素的class屬性。
比如我們已經有了下面的樣式:
.special{
font-style: italic;
}
我們只需要給某個節點設置class屬性為special即可。設置節點的className屬性可以兩種方式:
element.setAttribute(“class”,”special”);
element.className = “special”;
如果想要設置多個className,請空格隔開:
element.className = “special special2”;
七、總結
本博客的內容主要來自《JavaScript DOM編程藝術》。本博客主要總結了如下知識點:
DOM的獲取Element的方法和屬性:
getElementById(id)
getElementsByTagName(tag)
getElementsByClassName(class)
parentNode
childNodes
firstChild
lastChild
nextSibling
previousSibling
getAttribute()
setAttribute()
DOM 動態創建Element
createElement
appendChild
createTextNode
insertBefore
Element屬性
nodeType
nodeValue
style
className
DOM 操作style里CSS屬性以及JavaScript的最佳實踐
代碼可以參考我的githubhttps://github.com/chiclaim/html-javascript-css