<h2 id="5.1">DOM節點</h2>
## DOM的概念
DOM是文檔對象模型(Document Object Model)的簡稱,它的基本思想是把結構化文檔(比如HTML和XML)解析成一系列的節點,再由這些節點組成一個樹狀結構(DOM Tree)。所有的節點和最終的樹狀結構,都有規范的對外接口,以達到使用編程語言操作文檔的目的(比如增刪內容)。所以,DOM可以理解成文檔(HTML文檔、XML文檔和SVG文檔)的編程接口。
DOM有自己的國際標準,目前的通用版本是[DOM 3](http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html),下一代版本[DOM 4](http://www.w3.org/TR/dom/)正在擬定中。本章介紹的就是JavaScript對DOM標準的實現和用法。
嚴格地說,DOM不屬于JavaScript,但是操作DOM是JavaScript最常見的任務,而JavaScript也是最常用于DOM操作的語言。所以,DOM往往放在JavaScript里面介紹。
## 節點的概念
DOM的最小組成單位叫做節點(node),一個文檔的樹形結構(DOM樹),就是由各種不同類型的節點組成。
對于HTML文檔,節點主要有以下六種類型:Document節點、DocumentType節點、Element節點、Attribute節點、Text節點和DocumentFragment節點。
節點|名稱|含義
----|----|----
Document | 文檔節點 | 整個文檔(window.document)
DocumentType | 文檔類型節點 | 文檔的類型(比如<!DOCTYPE html>)
Element | 元素節點 | HTML元素(比如<body>、<a>等)
Attribute | 屬性節點| HTML元素的屬性(比如class="right")
Text | 文本節點 | HTML文檔中出現的文本
DocumentFragment | 文檔碎片節點 | 文檔的片段
瀏覽器原生提供一個Node對象,上表所有類型的節點都是Node對象派生出來的。也就是說,它們都繼承了Node的屬性和方法。
## Node節點的屬性
### nodeName,nodeType
`nodeName`屬性返回節點的名稱,`nodeType`屬性返回節點類型的常數值。具體的返回值,可查閱下方的表格。
類型 | nodeName | nodeType
-----|----------|---------
DOCUMENT_NODE | #document | 9
ELEMENT_NODE | 大寫的HTML元素名 | 1
ATTRIBUTE_NODE | 等同于Attr.name | 2
TEXT_NODE | #text | 3
DOCUMENT_FRAGMENT_NODE | #document-fragment | 11
DOCUMENT_TYPE_NODE | 等同于DocumentType.name |10
以`document`節點為例,它的`nodeName`屬性等于`#document`,`nodeType`屬性等于9。
```javascript
document.nodeName // "#document"
document.nodeType // 9
```
通常來說,使用`nodeType`屬性確定一個節點的類型,比較方便。
```javascript
document.querySelector('a').nodeType === 1
// true
document.querySelector('a').nodeType === Node.ELEMENT_NODE
// true
```
上面兩種寫法是等價的。
### ownerDocument,nextSibling,previousSibling,parentNode,parentElement
以下屬性返回當前節點的相關節點。
**(1)ownerDocument**
ownerDocument屬性返回當前節點所在的頂層文檔對象,即document對象。
```javascript
var d = p.ownerDocument;
d === document // true
```
document對象本身的ownerDocument屬性,返回null。
**(2)nextSibling**
nextSibling屬性返回緊跟在當前節點后面的第一個同級節點。如果當前節點后面沒有同級節點,則返回null。注意,該屬性還包括文本節點和評論節點。因此如果當前節點后面有空格,該屬性會返回一個文本節點,內容為空格。
```javascript
var el = document.getElementById('div-01').firstChild;
var i = 1;
while (el) {
console.log(i + '. ' + el.nodeName);
el = el.nextSibling;
i++;
}
```
上面代碼遍歷`div-01`節點的所有子節點。
**(3)previousSibling**
previousSibling屬性返回當前節點前面的、距離最近的一個同級節點。如果當前節點前面沒有同級節點,則返回null。
```javascript
// html代碼如下
// <a><b1 id="b1"/><b2 id="b2"/></a>
document.getElementById("b1").previousSibling // null
document.getElementById("b2").previousSibling.id // "b1"
```
對于當前節點前面有空格,則`previousSibling`屬性會返回一個內容為空格的文本節點。
**(4)parentNode**
`parentNode`屬性返回當前節點的父節點。對于一個節點來說,它的父節點只可能是三種類型:`element`節點、`document`節點和`documentfragment`節點。
下面代碼是如何從父節點移除指定節點。
```javascript
if (node.parentNode) {
node.parentNode.removeChild(node);
}
```
對于document節點和documentfragment節點,它們的父節點都是null。另外,對于那些生成后還沒插入DOM樹的節點,父節點也是null。
**(5)parentElement**
parentElement屬性返回當前節點的父Element節點。如果當前節點沒有父節點,或者父節點類型不是Element節點,則返回null。
```javascript
if (node.parentElement) {
node.parentElement.style.color = "red";
}
```
上面代碼設置指定節點的父Element節點的CSS屬性。
在IE瀏覽器中,只有Element節點才有該屬性,其他瀏覽器則是所有類型的節點都有該屬性。
### textContent,nodeValue
以下屬性返回當前節點的內容。
**(1)textContent**
textContent屬性返回當前節點和它的所有后代節點的文本內容。
```javascript
// HTML代碼為
// <div id="divA">This is <span>some</span> text</div>
document.getElementById("divA").textContent
// This is some text
```
上面代碼的textContent屬性,自動忽略當前節點內部的HTML標簽,返回所有文本內容。
該屬性是可讀寫的,設置該屬性的值,會用一個新的文本節點,替換所有它原來的子節點。它還有一個好處,就是自動對HTML標簽轉義。這很適合用于用戶提供的內容。
```javascript
document.getElementById('foo').textContent = '<p>GoodBye!</p>';
```
上面代碼在插入文本時,會將p標簽解釋為文本,即&lt;p&gt;,而不會當作標簽處理。
對于Text節點和Comment節點,該屬性的值與nodeValue屬性相同。對于其他類型的節點,該屬性會將每個子節點的內容連接在一起返回,但是不包括Comment節點。如果一個節點沒有子節點,則返回空字符串。
document節點和doctype節點的textContent屬性為null。如果要讀取整個文檔的內容,可以使用`document.documentElement.textContent`。
在IE瀏覽器,所有Element節點都有一個innerText屬性。它與textContent屬性基本相同,但是有幾點區別。
- innerText受CSS影響,textContent不受。比如,如果CSS規則隱藏(hidden)了某段文本,innerText就不會返回這段文本,textContent則照樣返回。
- innerText返回的文本,會過濾掉空格、換行和回車鍵,textContent則不會。
- innerText屬性不是DOM標準的一部分,Firefox瀏覽器甚至沒有部署這個屬性,而textContent是DOM標準的一部分。
**(2)nodeValue**
nodeValue屬性返回或設置當前節點的值,格式為字符串。但是,該屬性只對Text節點、Comment節點、XML文檔的CDATA節點有效,其他類型的節點一律返回null。
因此,nodeValue屬性一般只用于Text節點。對于那些返回null的節點,設置nodeValue屬性是無效的。
### childNodes,firstChild,lastChild
以下屬性返回當前節點的子節點。
**(1)childNodes**
childNodes屬性返回一個NodeList集合,成員包括當前節點的所有子節點。注意,除了HTML元素節點,該屬性返回的還包括Text節點和Comment節點。如果當前節點不包括任何子節點,則返回一個空的NodeList集合。由于NodeList對象是一個動態集合,一旦子節點發生變化,立刻會反映在返回結果之中。
```javascript
var ulElementChildNodes = document.querySelector('ul').childNodes;
```
**(2)firstChild**
`firstChild`屬性返回當前節點的第一個子節點,如果當前節點沒有子節點,則返回`null`。
```html
<p id="para-01"><span>First span</span></p>
<script type="text/javascript">
console.log(
document.getElementById('para-01').firstChild.nodeName
) // "span"
</script>
```
上面代碼中,`p`元素的第一個子節點是`span`元素。
注意,`firstChild`返回的除了HTML元素子節點,還可能是文本節點或評論節點。
```html
<p id="para-01">
<span>First span</span>
</p>
<script type="text/javascript">
console.log(
document.getElementById('para-01').firstChild.nodeName
) // "#text"
</script>
```
上面代碼中,`p`元素與`span`元素之間有空白字符,這導致`firstChild`返回的是文本節點。
**(3)lastChild**
lastChild屬性返回當前節點的最后一個子節點,如果當前節點沒有子節點,則返回null。
### baseURI
baseURI屬性返回一個字符串,由當前網頁的協議、域名和所在的目錄組成,表示當前網頁的絕對路徑。如果無法取到這個值,則返回null。瀏覽器根據這個屬性,計算網頁上的相對路徑的URL。該屬性為只讀。
通常情況下,該屬性由當前網址的URL(即window.location屬性)決定,但是可以使用HTML的<base>標簽,改變該屬性的值。
```html
<base href="http://www.example.com/page.html">
<base target="_blank" href="http://www.example.com/page.html">
```
該屬性不僅document對象有(`document.baseURI`),元素節點也有(`element.baseURI`)。通常情況下,它們的值是相同的。
## Node節點的方法
### appendChild(),hasChildNodes()
以下方法與子節點相關。
**(1)appendChild()**
appendChild方法接受一個節點對象作為參數,將其作為最后一個子節點,插入當前節點。
```javascript
var p = document.createElement("p");
document.body.appendChild(p);
```
如果參數節點是文檔中現有的其他節點,appendChild方法會將其從原來的位置,移動到新位置。
hasChildNodes方法返回一個布爾值,表示當前節點是否有子節點。
```javascript
var foo = document.getElementById("foo");
if ( foo.hasChildNodes() ) {
foo.removeChild( foo.childNodes[0] );
}
```
上面代碼表示,如果foo節點有子節點,就移除第一個子節點。
**(2)hasChildNodes()**
hasChildNodes方法結合firstChild屬性和nextSibling屬性,可以遍歷當前節點的所有后代節點。
```javascript
function DOMComb (oParent, oCallback) {
if (oParent.hasChildNodes()) {
for (var oNode = oParent.firstChild; oNode; oNode = oNode.nextSibling) {
DOMComb(oNode, oCallback);
}
}
oCallback.call(oParent);
}
```
上面代碼的DOMComb函數的第一個參數是某個指定的節點,第二個參數是回調函數。這個回調函數會依次作用于指定節點,以及指定節點的所有后代節點。
```javascript
function printContent () {
if (this.nodeValue) {
console.log(this.nodeValue);
}
}
DOMComb(document.body, printContent);
```
### cloneNode(),insertBefore(),removeChild(),replaceChild()
下面方法與節點操作有關。
**(1)cloneNode()**
cloneNode方法用于克隆一個節點。它接受一個布爾值作為參數,表示是否同時克隆子節點,默認是false,即不克隆子節點。
```javascript
var cloneUL = document.querySelector('ul').cloneNode(true);
```
需要注意的是,克隆一個節點,會拷貝該節點的所有屬性,但是會喪失addEventListener方法和on-屬性(即`node.onclick = fn`),添加在這個節點上的事件回調函數。
克隆一個節點之后,DOM樹有可能出現兩個有相同ID屬性(即`id="xxx"`)的HTML元素,這時應該修改其中一個HTML元素的ID屬性。
**(2)insertBefore()**
insertBefore方法用于將某個節點插入當前節點的指定位置。它接受兩個參數,第一個參數是所要插入的節點,第二個參數是當前節點的一個子節點,新的節點將插在這個節點的前面。該方法返回被插入的新節點。
```javascript
var text1 = document.createTextNode('1');
var li = document.createElement('li');
li.appendChild(text1);
var ul = document.querySelector('ul');
ul.insertBefore(li,ul.firstChild);
```
上面代碼在ul節點的最前面,插入一個新建的li節點。
如果insertBefore方法的第二個參數為null,則新節點將插在當前節點的最后位置,即變成最后一個子節點。
將新節點插在當前節點的最前面(即變成第一個子節點),可以使用當前節點的firstChild屬性。
```javascript
parentElement.insertBefore(newElement, parentElement.firstChild);
```
上面代碼中,如果當前節點沒有任何子節點,`parentElement.firstChild`會返回null,則新節點會插在當前節點的最后,等于是第一個子節點。
由于不存在insertAfter方法,如果要插在當前節點的某個子節點后面,可以用insertBefore方法結合nextSibling屬性模擬。
```javascript
parentDiv.insertBefore(s1, s2.nextSibling);
```
上面代碼可以將s1節點,插在s2節點的后面。如果s2是當前節點的最后一個子節點,則`s2.nextSibling`返回null,這時s1節點會插在當前節點的最后,變成當前節點的最后一個子節點,等于緊跟在s2的后面。
**(3)removeChild()**
removeChild方法接受一個子節點作為參數,用于從當前節點移除該節點。它返回被移除的節點。
```javascript
var divA = document.getElementById('A');
divA.parentNode.removeChild(divA);
```
上面代碼是如何移除一個指定節點。
下面是如何移除當前節點的所有子節點。
```javascript
var element = document.getElementById("top");
while (element.firstChild) {
element.removeChild(element.firstChild);
}
```
被移除的節點依然存在于內存之中,但是不再是DOM的一部分。所以,一個節點移除以后,依然可以使用它,比如插入到另一個節點。
**(4)replaceChild()**
replaceChild方法用于將一個新的節點,替換當前節點的某一個子節點。它接受兩個參數,第一個參數是用來替換的新節點,第二個參數將要被替換走的子節點。它返回被替換走的那個節點。
```javascript
replacedNode = parentNode.replaceChild(newChild, oldChild);
```
下面是一個例子。
```javascript
var divA = document.getElementById('A');
var newSpan = document.createElement('span');
newSpan.textContent = 'Hello World!';
divA.parentNode.replaceChild(newSpan,divA);
```
上面代碼是如何替換指定節點。
### contains(),compareDocumentPosition(),isEqualNode()
下面方法用于節點的互相比較。
**(1)contains()**
contains方法接受一個節點作為參數,返回一個布爾值,表示參數節點是否為當前節點的后代節點。
```javascript
document.body.contains(node)
```
上面代碼檢查某個節點,是否包含在當前文檔之中。
注意,如果將當前節點傳入contains方法,會返回true。雖然從意義上說,一個節點不應該包含自身。
```javascript
nodeA.contains(nodeA) // true
```
**(2)compareDocumentPosition()**
compareDocumentPosition方法的用法,與contains方法完全一致,返回一個7個比特位的二進制值,表示參數節點與當前節點的關系。
二進制值 | 數值 | 含義
---------|------|-----
000000 | 0 | 兩個節點相同
000001 | 1 | 兩個節點不在同一個文檔(即有一個節點不在當前文檔)
000010 | 2 | 參數節點在當前節點的前面
000100 | 4 | 參數節點在當前節點的后面
001000 | 8 | 參數節點包含當前節點
010000 | 16 | 當前節點包含參數節點
100000 | 32 | 瀏覽器的私有用途
```javascript
// HTML代碼為
// <div id="writeroot">
// <form>
// <input id="test" />
// </form>
// </div>
var x = document.getElementById('writeroot');
var y = document.getElementById('test');
x.compareDocumentPosition(y) // 20
y.compareDocumentPosition(x) // 10
```
上面代碼中,節點x包含節點y,而且節點y在節點x的后面,所以第一個compareDocumentPosition方法返回20(010100),第二個compareDocumentPosition方法返回10(0010010)。
由于compareDocumentPosition返回值的含義,定義在每一個比特位上,所以如果要檢查某一種特定的含義,就需要使用比特位運算符。
```javascript
var head = document.head;
var body = document.body;
if (head.compareDocumentPosition(body) & 4) {
console.log("文檔結構正確");
} else {
console.log("<head> 不能在 <body> 前面");
}
```
上面代碼中,compareDocumentPosition的返回值與4(又稱掩碼)進行與運算(&),得到一個布爾值,表示head是否在body前面。
在這個方法的基礎上,可以部署一些特定的函數,檢查節點的位置。
```javascript
Node.prototype.before = function (arg) {
return !!(this.compareDocumentPosition(arg) & 2)
}
nodeA.before(nodeB)
```
上面代碼在Node對象上部署了一個before方法,返回一個布爾值,表示參數節點是否在當前節點的前面。
**(3)isEqualNode()**
isEqualNode方法返回一個布爾值,用于檢查兩個節點是否相等。所謂相等的節點,指的是兩個節點的類型相同、屬性相同、子節點相同。
```javascript
var targetEl = document.getElementById("targetEl");
var firstDiv = document.getElementsByTagName("div")[0];
targetEl.isEqualNode(firstDiv)
```
### normalize()
normailize方法用于清理當前節點內部的所有Text節點。它會去除空的文本節點,并且將毗鄰的文本節點合并成一個。
```javascript
var wrapper = document.createElement("div");
wrapper.appendChild(document.createTextNode("Part 1 "));
wrapper.appendChild(document.createTextNode("Part 2 "));
wrapper.childNodes.length // 2
wrapper.normalize();
wrapper.childNodes.length // 1
```
上面代碼使用normalize方法之前,wrapper節點有兩個Text子節點。使用normalize方法之后,兩個Text子節點被合并成一個。
該方法是`Text.splitText`的逆方法,可以查看《Text節點》章節,了解更多內容。
## NodeList接口,HTMLCollection接口
節點對象都是單個節點,但是有時會需要一種數據結構,能夠容納多個節點。DOM提供兩種接口,用于部署這種節點的集合:NodeList接口和HTMLCollection接口。
### NodeList接口
有些屬性和方法返回的是一組節點,比如Node.childNodes、document.querySelectorAll()。它們返回的都是一個部署了NodeList接口的對象。
NodeList接口有時返回一個動態集合,有時返回一個靜態集合。所謂動態集合就是一個活的集合,DOM樹刪除或新增一個相關節點,都會立刻反映在NodeList接口之中。Node.childNodes返回的,就是一個動態集合。
```javascript
var parent = document.getElementById('parent');
parent.childNodes.length // 2
parent.appendChild(document.createElement('div'));
parent.childNodes.length // 3
```
上面代碼中,`parent.childNodes`返回的是一個部署了NodeList接口的對象。當parent節點新增一個子節點以后,該對象的成員個數就增加了1。
document.querySelectorAll方法返回的是一個靜態,DOM內部的變化,并不會實時反映在該方法的返回結果之中。
NodeList接口提供length屬性和數字索引,因此可以像數組那樣,使用數字索引取出每個節點,但是它本身并不是數組,不能使用pop或push之類數組特有的方法。
```javascript
// 數組的繼承鏈
myArray --> Array.prototype --> Object.prototype --> null
// NodeList的繼承鏈
myNodeList --> NodeList.prototype --> Object.prototype --> null
```
從上面的繼承鏈可以看到,NodeList接口對象并不繼承Array.prototype,因此不具有數組接口提供的方法。如果要在NodeList接口使用數組方法,可以將NodeList接口對象轉為真正的數組。
```javascript
var div_list = document.querySelectorAll('div');
var div_array = Array.prototype.slice.call(div_list);
```
也可以通過下面的方法調用。
```javascript
var forEach = Array.prototype.forEach;
forEach.call(element.childNodes, function(child){
child.parentNode.style.color = '#0F0';
});
```
上面代碼讓數組的forEach方法在NodeList接口對象上調用。
不過,遍歷NodeList接口對象的首選方法,還是使用for循環。
```javascript
for (var i = 0; i < myNodeList.length; ++i) {
var item = myNodeList[i];
}
```
不要使用for...in循環去遍歷NodeList接口對象,因為for...in循環會將非數字索引的length屬性和下面要講到的item方法,也遍歷進去,而且不保證各個成員遍歷的順序。
ES6新增的for...of循環,也可以正確遍歷NodeList接口對象。
```javascript
var list = document.querySelectorAll( 'input[type=checkbox]' );
for (var item of list) {
item.checked = true;
}
```
NodeList接口提供item方法,接受一個數字索引作為參數,返回該索引對應的成員。如果取不到成員,或者索引不合法,則返回null。
```javascript
nodeItem = nodeList.item(index)
// 實例
var divs = document.getElementsByTagName("div");
var secondDiv = divs.item(1);
```
上面代碼中,由于數字索引從零開始計數,所以取出第二個成員,要使用數字索引1。
所有類似數組的對象,都可以使用方括號運算符取出成員,所以一般情況下,都是使用下面的寫法,而不使用item方法。
```javascript
nodeItem = nodeList[index]
```
### HTMLCollection接口
HTMLCollection接口與NodeList接口類似,也是節點的集合,但是集合成員都是Element節點。該接口都是動態集合,節點的變化會實時反映在集合中。document.links、docuement.forms、document.images等屬性,返回的都是HTMLCollection接口對象。
部署了該接口的對象,具有length屬性和數字索引,因此是一個類似于數組的對象。
item方法根據成員的位置參數(從0開始),返回該成員。如果取不到成員或數字索引不合法,則返回null。
```javascript
var c = document.images;
var img1 = c.item(10);
// 等價于下面的寫法
var img1 = c[1];
```
namedItem方法根據成員的ID屬性或name屬性,返回該成員。如果沒有對應的成員,則返回null。
```javascript
// HTML代碼為
// <form id="myForm"></form>
var elem = document.forms.namedItem("myForm");
// 等價于下面的寫法
var elem = document.forms["myForm"];
```
由于item方法和namedItem方法,都可以用方括號運算符代替,所以建議一律使用方括號運算符。
## ParentNode接口,ChildNode接口
不同的節點除了繼承Node接口以外,還會繼承其他接口。ParentNode接口用于獲取當前節點的Element子節點,ChildNode接口用于處理當前節點的子節點(包含但不限于Element子節點)。
### ParentNode接口
ParentNode接口用于獲取Element子節點。Element節點、Document節點和DocumentFragment節點,部署了ParentNode接口。凡是這三類節點,都具有以下四個屬性,用于獲取Element子節點。
**(1)children**
children屬性返回一個動態的HTMLCollection集合,由當前節點的所有Element子節點組成。
下面代碼遍歷指定節點的所有Element子節點。
```javascript
if (el.children.length) {
for (var i = 0; i < el.children.length; i++) {
// ...
}
}
```
**(2)firstElementChild**
firstElementChild屬性返回當前節點的第一個Element子節點,如果不存在任何Element子節點,則返回null。
```javascript
document.firstElementChild.nodeName
// "HTML"
```
上面代碼中,document節點的第一個Element子節點是<HTML>。
**(3)lastElementChild**
lastElementChild屬性返回當前節點的最后一個Element子節點,如果不存在任何Element子節點,則返回null。
```javascript
document.lastElementChild.nodeName
// "HTML"
```
上面代碼中,document節點的最后一個Element子節點是<HTML>。
**(4)childElementCount**
childElementCount屬性返回當前節點的所有Element子節點的數目。
### ChildNode接口
ChildNode接口用于處理子節點(包含但不限于Element子節點)。Element節點、DocumentType節點和CharacterData接口,部署了ChildNode接口。凡是這三類節點(接口),都可以使用下面四個方法。但是現實的情況是,除了第一個remove方法,目前沒有瀏覽器支持后面三個方法。
**(1)remove()**
remove方法用于移除當前節點。
```javascript
el.remove()
```
上面方法在DOM中移除了el節點。注意,調用這個方法的節點,是被移除的節點本身,而不是它的父節點。
**(2)before()**
before方法用于在當前節點的前面,插入一個同級節點。如果參數是節點對象,插入DOM的就是該節點對象;如果參數是文本,插入DOM的就是參數對應的文本節點。
**(3)after()**
after方法用于在當前節點的后面,插入一個同級節點。如果參數是節點對象,插入DOM的就是該節點對象;如果參數是文本,插入DOM的就是參數對應的文本節點。
**(4)replaceWith()**
replaceWith方法使用參數指定的節點,替換當前節點。如果參數是節點對象,替換當前節點的就是該節點對象;如果參數是文本,替換當前節點的就是參數對應的文本節點。
## html元素
`html`元素是網頁的根元素,`document.documentElement`就指向這個元素。
**(1)clientWidth屬性,clientHeight屬性**
這兩個屬性返回視口(viewport)的大小,單位為像素。所謂“視口”,是指用戶當前能夠看見的那部分網頁的大小
`document.documentElement.clientWidth`和`document.documentElement.clientHeight`,基本上與`window.innerWidth`和`window.innerHeight`同義。只有一個區別,前者不將滾動條計算在內(很顯然,滾動條和工具欄會減小視口大小),而后者包括了滾動條的高度和寬度。
**(2)offsetWidth屬性,offsetHeight屬性**
這兩個屬性返回html元素的寬度和高度,即網頁的總寬度和總高度。
### dataset屬性
`dataset`屬性用于操作HTML標簽元素的`data-*`屬性。下面是一個有`data-*`屬性的`div`節點。
```html
<div id="myDiv" data-id="myId"></div>
```
要讀取`data-id`屬性,可以從當前節點的`dataset.id`屬性讀取。
```javascript
var id = document.getElementById("myDiv").dataset.id;
```
要設置`data-id`屬性,可以直接對`dataset.id`賦值。如果該屬性不存在,將會被新建。
```javascript
document.getElementById('myDiv').dataset.id = 'hello';
```
刪除一個`data-*`屬性,可以直接使用`delete`命令。
```javascript
delete document.getElementById("myDiv").dataset.id;
```
除了`dataset`屬性,也可以用`getAttribute('data-foo')`、`removeAttribute('data-foo')`、`setAttribute('data-foo')`、`hasAttribute('data-foo')`等方法操作`data-*`屬性。
需要注意的是,`dataset`屬性使用駱駝拼寫法表示屬性名,這意味著`data-hello-world`會用`dataset.helloWorld`表示。而如果此時存在一個`data-helloWorld`屬性,該屬性將無法讀取,也就是說,`data-*`屬性本身只能使用連詞號,不能使用駱駝拼寫法。
### tabindex屬性
`tabindex`屬性用來指定,當前HTML元素節點是否被tab鍵遍歷,以及遍歷的優先級。
```javascript
var b1 = document.getElementById("button1");
b1.tabIndex = 1;
```
如果 tabindex = -1 ,tab鍵跳過當前元素。
如果 tabindex = 0 ,表示tab鍵將遍歷當前元素。如果一個元素沒有設置tabindex,默認值就是0。
如果 tabindex 大于0,表示tab鍵優先遍歷。值越大,就表示優先級越大。
### 頁面位置相關屬性
**(1)offsetParent屬性、offsetTop屬性和offsetLeft屬性**
這三個屬性提供Element對象在頁面上的位置。
- offsetParent:當前HTML元素的最靠近的、并且CSS的position屬性不等于static的父元素。
- offsetTop:當前HTML元素左上角相對于offsetParent的垂直位移。
- offsetLeft:當前HTML元素左上角相對于offsetParent的水平位移。
如果Element對象的父對象都沒有將position屬性設置為非static的值(比如absolute或relative),則offsetParent屬性指向body元素。另外,計算offsetTop和offsetLeft的時候,是從邊框的左上角開始計算,即Element對象的border寬度不計入offsetTop和offsetLeft。
### style屬性
style屬性用來讀寫頁面元素的行內CSS屬性,詳見本章《CSS操作》一節。
### Element對象的方法
**(1)選擇子元素的方法**
Element對象也部署了document對象的4個選擇子元素的方法,而且用法完全一樣。
- querySelector方法
- querySelectorAll方法
- getElementsByTagName方法
- getElementsByClassName方法
上面四個方法只用于選擇Element對象的子節點。因此,可以采用鏈式寫法來選擇子節點。
```javascript
document.getElementById('header').getElementsByClassName('a')
```
各大瀏覽器對這四個方法都支持良好,IE的情況如下:IE 6開始支持getElementsByTagName,IE 8開始支持querySelector和querySelectorAll,IE 9開始支持getElementsByClassName。
**(2)elementFromPoint方法**
該方法用于選擇在指定坐標的最上層的Element對象。
```javascript
document.elementFromPoint(50,50)
```
上面代碼了選中在(50,50)這個坐標的最上層的那個HTML元素。
**(3)HTML元素的屬性相關方法**
- hasAttribute():返回一個布爾值,表示Element對象是否有該屬性。
- getAttribute()
- setAttribute()
- removeAttribute()
**(4)matchesSelector方法**
該方法返回一個布爾值,表示Element對象是否符合某個CSS選擇器。
```javascript
document.querySelector('li').matchesSelector('li:first-child')
```
這個方法需要加上瀏覽器前綴,需要寫成mozMatchesSelector()、webkitMatchesSelector()、oMatchesSelector()、msMatchesSelector()。
**(5)focus方法**
focus方法用于將當前頁面的焦點,轉移到指定元素上。
```javascript
document.getElementById('my-span').focus();
```
### table元素
表格有一些特殊的DOM操作方法。
- **insertRow()**:在指定位置插入一個新行(tr)。
- **deleteRow()**:在指定位置刪除一行(tr)。
- **insertCell()**:在指定位置插入一個單元格(td)。
- **deleteCell()**:在指定位置刪除一個單元格(td)。
- **createCaption()**:插入標題。
- **deleteCaption()**:刪除標題。
- **createTHead()**:插入表頭。
- **deleteTHead()**:刪除表頭。
下面是使用JavaScript生成表格的一個例子。
```javascript
var table = document.createElement('table');
var tbody = document.createElement('tbody');
table.appendChild(tbody);
for (var i = 0; i <= 9; i++) {
var rowcount = i + 1;
tbody.insertRow(i);
tbody.rows[i].insertCell(0);
tbody.rows[i].insertCell(1);
tbody.rows[i].insertCell(2);
tbody.rows[i].cells[0].appendChild(document.createTextNode('Row ' + rowcount + ', Cell 1'));
tbody.rows[i].cells[1].appendChild(document.createTextNode('Row ' + rowcount + ', Cell 2'));
tbody.rows[i].cells[2].appendChild(document.createTextNode('Row ' + rowcount + ', Cell 3'));
}
table.createCaption();
table.caption.appendChild(document.createTextNode('A DOM-Generated Table'));
document.body.appendChild(table);
```
這些代碼相當易讀,其中需要注意的就是insertRow和insertCell方法,接受一個表示位置的參數(從0開始的整數)。
table元素有以下屬性:
- **caption**:標題。
- **tHead**:表頭。
- **tFoot**:表尾。
- **rows**:行元素對象,該屬性只讀。
- **rows.cells**:每一行的單元格對象,該屬性只讀。
- **tBodies**:表體,該屬性只讀。
<h2 id="5.2">document節點</h2>
## document節點概述
`document`節點是文檔的根節點,每張網頁都有自己的`document`節點。`window.document`屬性就指向這個節點。也就是說,只要瀏覽器開始載入HTML文檔,這個節點對象就存在了,可以直接調用。
document節點有不同的辦法可以獲取。
- 對于正常的網頁,直接使用`document`或`window.document`。
- 對于`iframe`載入的網頁,使用`iframe`節點的`contentDocument`屬性。
- 對Ajax操作返回的文檔,使用XMLHttpRequest對象的`responseXML`屬性。
- 對于某個節點包含的文檔,使用該節點的`ownerDocument`屬性。
上面這四種`document`節點,都部署了[Document接口](http://dom.spec.whatwg.org/#interface-document),因此有共同的屬性和方法。當然,各自也有一些自己獨特的屬性和方法,比如HTML和XML文檔的`document`節點就不一樣。
## document節點的屬性
document節點有很多屬性,用得比較多的是下面這些。
### doctype,documentElement,defaultView,body,head,activeElement
以下屬性指向文檔內部的某個節點。
**(1)doctype**
對于HTML文檔來說,document對象一般有兩個子節點。第一個子節點是document.doctype,它是一個對象,包含了當前文檔類型(Document Type Declaration,簡寫DTD)信息。對于HTML5文檔,該節點就代表<!DOCTYPE html>。如果網頁沒有聲明DTD,該屬性返回null。
```javascript
var doctype = document.doctype;
doctype // "<!DOCTYPE html>"
doctype.name // "html"
```
document.firstChild通常就返回這個節點。
**(2)documentElement**
document.documentElement屬性,表示當前文檔的根節點(root)。它通常是document節點的第二個子節點,緊跟在`document.doctype`節點后面。
對于HTML網頁,該屬性返回HTML節點,代表<html lang="en">。
**(3)defaultView**
defaultView屬性,在瀏覽器中返回document對象所在的window對象,否則返回null。
```javascript
var win = document.defaultView;
```
**(4)body**
body屬性返回當前文檔的body或frameset節點,如果不存在這樣的節點,就返回null。這個屬性是可寫的,如果對其寫入一個新的節點,會導致原有的所有子節點被移除。
**(4)head**
head屬性返回當前文檔的head節點。如果當前文檔有多個head,則返回第一個。
```javascript
document.head === document.querySelector('head')
```
**(5)activeElement**
activeElement屬性返回當前文檔中獲得焦點的那個元素。用戶通常可以使用tab鍵移動焦點,使用空格鍵激活焦點,比如如果焦點在一個鏈接上,此時按一下空格鍵,就會跳轉到該鏈接。
### documentURI,URL,domain,lastModified,location,referrer,title,characterSet
以下屬性返回文檔信息。
**(1)documentURI,URL**
`documentURI`屬性和`URL`屬性都返回當前文檔的網址。不同之處是`documentURI`屬性是所有文檔都具備的,`URL`屬性則是HTML文檔獨有的。
```javascript
document.documentURI === document.URL
// true
```
**(2)domain**
`domain`屬性返回當前文檔的域名。比如,某張網頁的網址是 http://www.example.com/hello.html ,`domain`屬性就等于`www.example.com`。如果無法獲取域名,該屬性返回`null`。
```javascript
var badDomain = 'www.example.xxx';
if (document.domain === badDomain)
window.close();
```
上面代碼判斷,如果當前域名等于指定域名,則關閉窗口。
二級域名的情況下,domain屬性可以設置為對應的一級域名。比如,當前域名是sub.example.com,則domain屬性可以設置為example.com。除此之外的寫入,都是不可以的。
**(3)lastModified**
lastModified屬性返回當前文檔最后修改的時間戳,格式為字符串。
```javascript
document.lastModified
// Tuesday, July 10, 2001 10:19:42
```
注意,`lastModified`屬性的值是字符串,所以不能用來直接比較,兩個文檔誰的日期更新,需要用`Date.parse`方法轉成時間戳格式,才能進行比較。
```javascript
if (Date.parse(doc1.lastModified) > Date.parse(doc2.lastModified)) {
// ...
}
```
**(4)location**
`document.location`屬性返回一個只讀的`location`對象,提供了當前文檔的URL信息。
```javascript
// 當前網址為 http://user:passwd@www.example.com:4097/path/a.html?x=111#part1
document.location.href // "http://user:passwd@www.example.com:4097/path/a.html?x=111#part1"
document.location.protocol // "http:"
document.location.host // "www.example.com:4097"
document.location.hostname // "www.example.com"
document.location.port // "4097"
document.location.pathname // "/path/a.html"
document.location.search // "?x=111"
document.location.hash // "#part1"
document.location.user // "user"
document.location.password // "passed"
// 跳轉到另一個網址
document.location.assign('http://www.google.com')
// 優先從服務器重新加載
document.location.reload(true)
// 優先從本地緩存重新加載(默認值)
document.location.reload(false)
// 將location對象轉為字符串,等價于document.location.href
document.location.toString()
```
雖然`location`屬性返回的對象是只讀的,但是可以將`URL`賦值給這個屬性,網頁就會自動跳轉到指定網址。
```javascript
document.location = 'http://www.example.com';
// 等同于
document.location.href = 'http://www.example.com';
```
注意,采用上面的方法重置URL,跟用戶點擊鏈接跳轉的效果是一樣的。上一個網頁依然將保存在瀏覽器歷史之中,點擊“后退”按鈕就可以回到前一個網頁。如果不希望用戶看到前一個網頁,可以使用`location.replace`方法,瀏覽器`history`對象就會用新的網址,取代當前網址,這樣的話,“后退”按鈕就不會回到當前網頁了。
```javascript
window.location.replace('http://www.example.com/otherpage.html');
```
`location`對象的`search`屬性代表URL的查詢字符串(包括`?`)。
```javascript
// 查詢字符串為 ?id=x&sort=name
var search = window.location.search;
search = search.slice(1); // 得到 'id=x&sort=name'
search = search.split('&'); // 得到數組 ['id=x', 'sort=name']
```
`document.location`屬性與`window.location`屬性等價。
```javascript
document.location === window.location //true
```
歷史上,IE曾經不允許對document.location賦值,為了保險起見,建議優先使用`window.location`。如果只是單純地獲取當前網址,建議使用`document.URL`,語義性更好。
**(5)referrer**
referrer屬性返回一個字符串,表示當前文檔的訪問來源,如果是無法獲取來源或是用戶直接鍵入網址,而不是從其他網頁點擊,則返回一個空字符串。
**(6)title**
`title`屬性返回當前文檔的標題,該屬性是可寫的。
```javascript
document.title = '新標題';
```
**(7)characterSet**
characterSet屬性返回渲染當前文檔的字符集,比如UTF-8、ISO-8859-1。
### readyState,designMode
以下屬性與文檔行為有關。
**(1)readyState**
readyState屬性返回當前文檔的狀態,共有三種可能的值,加載HTML代碼階段(尚未完成解析)是“loading”,加載外部資源階段是“interactive”,全部加載完成是“complete”。
下面的代碼用來檢查網頁是否加載成功。
```javascript
// 基本檢查
if (document.readyState === 'complete') {
// ...
}
// 輪詢檢查
var interval = setInterval(function() {
if (document.readyState === 'complete') {
clearInterval(interval);
// ...
}
}, 100);
```
**(2)designMode**
designMode屬性控制當前document是否可編輯。通常會打開iframe的designMode屬性,將其變為一個所見即所得的編輯器。
```javascript
iframe_node.contentDocument.designMode = "on";
```
### implementation,compatMode
以下屬性返回文檔的環境信息。
**(1)implementation**
implementation屬性返回一個對象,用來甄別當前環境部署了哪些DOM相關接口。implementation屬性的hasFeature方法,可以判斷當前環境是否部署了特定版本的特定接口。
```javascript
document.implementation.hasFeature( 'HTML', '2.0')
// true
document.implementation.hasFeature('MutationEvents','2.0')
// true
```
上面代碼表示,當前環境部署了DOM HTML 2.0版和MutationEvents的2.0版。
**(2)compatMode**
compatMode屬性返回瀏覽器處理文檔的模式,可能的值為BackCompat(向后兼容模式)和 CSS1Compat(嚴格模式)。
### anchors,embeds,forms,images,links,scripts,styleSheets
以下屬性返回文檔內部特定元素的集合(即HTMLCollection對象,詳見下文)。這些集合都是動態的,原節點有任何變化,立刻會反映在集合中。
**(1)anchors**
anchors屬性返回網頁中所有的a節點元素。注意,只有指定了name屬性的a元素,才會包含在anchors屬性之中。
**(2)embeds**
embeds屬性返回網頁中所有嵌入對象,即embed標簽,返回的格式為類似數組的對象(nodeList)。
**(3)forms**
forms屬性返回頁面中所有表單。
```javascript
var selectForm = document.forms[index];
var selectFormElement = document.forms[index].elements[index];
```
上面代碼獲取指定表單的指定元素。
**(4)images**
images屬性返回頁面所有圖片元素(即img標簽)。
```javascript
var ilist = document.images;
for(var i = 0; i < ilist.length; i++) {
if(ilist[i].src == "banner.gif") {
// ...
}
}
```
上面代碼在所有img標簽中,尋找特定圖片。
**(4)links**
links屬性返回當前文檔所有的鏈接元素(即a標簽,或者說具有href屬性的元素)。
**(5)scripts**
scripts屬性返回當前文檔的所有腳本(即script標簽)。
```javascript
var scripts = document.scripts;
if (scripts.length !== 0 ) {
console.log("當前網頁有腳本");
}
```
**(6)styleSheets**
styleSheets屬性返回一個類似數組的對象,包含了當前網頁的所有樣式表。該屬性提供了樣式表操作的接口。然后,每張樣式表對象的cssRules屬性,返回該樣式表的所有CSS規則。這又方便了操作具體的CSS規則。
```javascript
var allSheets = [].slice.call(document.styleSheets);
```
上面代碼中,使用slice方法將document.styleSheets轉為數組,以便于進一步處理。
### document.cookie
`document.cookie`屬性用來操作瀏覽器Cookie,詳見《瀏覽器環境》一章的《Cookie》部分。
## document對象的方法
document對象主要有以下一些方法。
### open(),close(),write(),writeln()
document.open方法用于新建一個文檔,供write方法寫入內容。它實際上等于清除當前文檔,重新寫入內容。不要將此方法與window.open()混淆,后者用來打開一個新窗口,與當前文檔無關。
document.close方法用于關閉open方法所新建的文檔。一旦關閉,write方法就無法寫入內容了。如果再調用write方法,就等同于又調用open方法,新建一個文檔,再寫入內容。
`document.write`方法用于向當前文檔寫入內容。只要當前文檔還沒有用`close`方法關閉,它所寫入的內容就會追加在已有內容的后面。
```javascript
// 頁面顯示“helloworld”
document.open();
document.write('hello');
document.write('world');
document.close();
```
如果頁面已經渲染完成(DOMContentLoaded事件發生之后),再調用write方法,它會先調用open方法,擦除當前文檔所有內容,然后再寫入。
```javascript
document.addEventListener("DOMContentLoaded", function(event) {
document.write('<p>Hello World!</p>');
});
// 等同于
document.addEventListener("DOMContentLoaded", function(event) {
document.open();
document.write('<p>Hello World!</p>');
document.close();
});
```
如果在頁面渲染過程中調用`write`方法,并不會調用`open`方法。(可以理解成,open方法已調用,但close方法還未調用。)
```html
<html>
<body>
hello
<script type="text/javascript">
document.write("world")
</script>
</body>
</html>
```
在瀏覽器打開上面網頁,將會顯示“hello world”。
需要注意的是,雖然調用close方法之后,無法再用write方法寫入內容,但這時當前頁面的其他DOM節點還是會繼續加載。
```html
<html>
<head>
<title>write example</title>
<script type="text/javascript">
document.open();
document.write("hello");
document.close();
</script>
</head>
<body>
world
</body>
</html>
```
在瀏覽器打開上面網頁,將會顯示“hello world”。
總之,除了某些特殊情況,應該盡量避免使用`document.write`這個方法。
`document.writeln`方法與`write`方法完全一致,除了會在輸出內容的尾部添加換行符。
```js
document.write(1);
document.write(2);
// 12
document.writeln(1);
document.writeln(2);
// 1
// 2
//
```
注意,`writeln`方法添加的是ASCII碼的換行符,渲染成HTML網頁時不起作用。
### hasFocus()
document.hasFocus方法返回一個布爾值,表示當前文檔之中是否有元素被激活或獲得焦點。
```javascript
focused = document.hasFocus();
```
注意,有焦點的文檔必定被激活(active),反之不成立,激活的文檔未必有焦點。比如如果用戶點擊按鈕,從當前窗口跳出一個新窗口,該新窗口就是激活的,但是不擁有焦點。
### querySelector(),getElementById(),querySelectorAll(),getElementsByTagName(),getElementsByClassName(),getElementsByName(),elementFromPoint()
以下方法用來選中當前文檔中的元素。
**(1)querySelector()**
`querySelector`方法返回匹配指定的CSS選擇器的元素節點。如果有多個節點滿足匹配條件,則返回第一個匹配的節點。如果沒有發現匹配的節點,則返回`null`。
```javascript
var el1 = document.querySelector('.myclass');
var el2 = document.querySelector('#myParent > [ng-click]');
```
`querySelector`方法無法選中CSS偽元素。
**(2)getElementById()**
`getElementById`方法返回匹配指定ID屬性的元素節點。如果沒有發現匹配的節點,則返回null。
```javascript
var elem = document.getElementById("para1");
```
注意,在搜索匹配節點時,`id`屬性是大小寫敏感的。比如,如果某個節點的`id`屬性是`main`,那么`document.getElementById("Main")`將返回`null`,而不是指定節點。
`getElementById`方法與`querySelector`方法都能獲取元素節點,不同之處是`querySelector`方法的參數使用CSS選擇器語法,`getElementById`方法的參數是HTML標簽元素的id屬性。
```javascript
document.getElementById('myElement')
document.querySelector('#myElement')
```
上面代碼中,兩個方法都能選中id為myElement的元素,但是getElementById()比querySelector()效率高得多。
**(3)querySelectorAll()**
`querySelectorAll`方法返回匹配指定的CSS選擇器的所有節點,返回的是NodeList類型的對象。NodeList對象不是動態集合,所以元素節點的變化無法實時反映在返回結果中。
```javascript
elementList = document.querySelectorAll(selectors);
```
querySelectorAll方法的參數,可以是逗號分隔的多個CSS選擇器,返回所有匹配其中一個選擇器的元素。
```javascript
var matches = document.querySelectorAll('div.note, div.alert');
```
上面代碼返回class屬性是note或alert的div元素。
querySelectorAll方法支持復雜的CSS選擇器。
```javascript
// 選中data-foo-bar屬性等于someval的元素
document.querySelectorAll('[data-foo-bar="someval"]');
// 選中myForm表單中所有不通過驗證的元素
document.querySelectorAll('#myForm :invalid');
// 選中div元素,那些class含ignore的除外
document.querySelectorAll('DIV:not(.ignore)');
// 同時選中div,a,script三類元素
document.querySelectorAll('DIV, A, SCRIPT');
```
如果`querySelectorAll`方法和`getElementsByTagName`方法的參數是字符串`*`,則會返回文檔中的所有HTML元素節點。
與querySelector方法一樣,querySelectorAll方法無法選中CSS偽元素。
**(4)getElementsByClassName()**
getElementsByClassName方法返回一個類似數組的對象(HTMLCollection類型的對象),包括了所有class名字符合指定條件的元素(搜索范圍包括本身),元素的變化實時反映在返回結果中。這個方法不僅可以在document對象上調用,也可以在任何元素節點上調用。
```javascript
// document對象上調用
var elements = document.getElementsByClassName(names);
// 非document對象上調用
var elements = rootElement.getElementsByClassName(names);
```
getElementsByClassName方法的參數,可以是多個空格分隔的class名字,返回同時具有這些節點的元素。
```javascript
document.getElementsByClassName('red test');
```
上面代碼返回class同時具有red和test的元素。
**(5)getElementsByTagName()**
getElementsByTagName方法返回所有指定標簽的元素(搜索范圍包括本身)。返回值是一個HTMLCollection對象,也就是說,搜索結果是一個動態集合,任何元素的變化都會實時反映在返回的集合中。這個方法不僅可以在document對象上調用,也可以在任何元素節點上調用。
```javascript
var paras = document.getElementsByTagName("p");
```
上面代碼返回當前文檔的所有p元素節點。
注意,getElementsByTagName方法會將參數轉為小寫后,再進行搜索。
**(6)getElementsByName()**
getElementsByName方法用于選擇擁有name屬性的HTML元素,比如form、img、frame、embed和object,返回一個NodeList格式的對象,不會實時反映元素的變化。
```javascript
// 表單為 <form name="x"></form>
var forms = document.getElementsByName("x");
forms[0].tagName // "FORM"
```
注意,在IE瀏覽器使用這個方法,會將沒有name屬性、但有同名id屬性的元素也返回,所以name和id屬性最好設為不一樣的值。
**(7)elementFromPoint()**
elementFromPoint方法返回位于頁面指定位置的元素。
```javascript
var element = document.elementFromPoint(x, y);
```
上面代碼中,elementFromPoint方法的參數x和y,分別是相對于當前窗口左上角的橫坐標和縱坐標,單位是CSS像素。elementFromPoint方法返回位于這個位置的DOM元素,如果該元素不可返回(比如文本框的滾動條),則返回它的父元素(比如文本框)。如果坐標值無意義(比如負值),則返回null。
### createElement(),createTextNode(),createAttribute(),createDocumentFragment()
以下方法用于生成元素節點。
**(1)createElement()**
createElement方法用來生成HTML元素節點。
```javascript
var element = document.createElement(tagName);
// 實例
var newDiv = document.createElement("div");
```
createElement方法的參數為元素的標簽名,即元素節點的tagName屬性。如果傳入大寫的標簽名,會被轉為小寫。如果參數帶有尖括號(即<和>)或者是null,會報錯。
**(2)createTextNode()**
`document.createTextNode`方法用來生成文本節點,參數為所要生成的文本節點的內容。
```javascript
var newDiv = document.createElement('div');
var newContent = document.createTextNode('Hello');
newDiv.appendChild(newContent);
```
上面代碼新建一個`div`節點和一個文本節點,然后將文本節點插入`div`節點。
這個方法可以確保返回的節點,被瀏覽器當作txt文本渲染,而不是當作HTML代碼渲染。因此,可以用來展示用戶的輸入,避免XSS攻擊。
```javascript
var div = document.createElement('div');
div.appendChild(document.createTextNode('<span>Foo & bar</span>'));
console.log(div.innerHTML)
// <span>Foo & bar</span>
```
上面代碼中,`createTextNode`方法對大于號和小于號進行轉義,從而保證即使用戶輸入的內容包含惡意代碼,也能正確顯示。
需要注意的是,該方法不對單引號和雙引號轉義,所以不能用來對HTML屬性賦值。
```html
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
};
var userWebsite = '" onmouseover="alert(\'derp\')" "';
var profileLink = '<a href="' + escapeHtml(userWebsite) + '">Bob</a>';
var div = document.getElemenetById('target');
div.innerHtml = profileLink;
// <a href="" onmouseover="alert('derp')" "">Bob</a>
```
上面代碼中,由于`createTextNode`方法不轉義雙引號,導致`onmouseover`方法被注入了代碼。
**(3)createAttribute()**
`document.createAttribute`方法生成一個新的屬性對象節點,并返回它。
```javascript
attribute = document.createAttribute(name);
```
createAttribute方法的參數name,是屬性的名稱。
```javascript
var node = document.getElementById("div1");
var a = document.createAttribute("my_attrib");
a.value = "newVal";
node.setAttributeNode(a);
// 等同于
var node = document.getElementById("div1");
node.setAttribute("my_attrib", "newVal");
```
**(4)createDocumentFragment()**
createDocumentFragment方法生成一個DocumentFragment對象。
```javascript
var docFragment = document.createDocumentFragment();
```
DocumentFragment對象是一個存在于內存的DOM片段,但是不屬于當前文檔,常常用來生成較復雜的DOM結構,然后插入當前文檔。這樣做的好處在于,因為DocumentFragment不屬于當前文檔,對它的任何改動,都不會引發網頁的重新渲染,比直接修改當前文檔的DOM有更好的性能表現。
```javascript
var docfrag = document.createDocumentFragment();
[1, 2, 3, 4].forEach(function(e) {
var li = document.createElement("li");
li.textContent = e;
docfrag.appendChild(li);
});
document.body.appendChild(docfrag);
```
### createEvent()
createEvent方法生成一個事件對象,該對象可以被element.dispatchEvent方法使用,觸發指定事件。
```javascript
var event = document.createEvent(type);
```
createEvent方法的參數是事件類型,比如UIEvents、MouseEvents、MutationEvents、HTMLEvents。
```javascript
var event = document.createEvent('Event');
event.initEvent('build', true, true);
document.addEventListener('build', function (e) {
// ...
}, false);
document.dispatchEvent(event);
```
### createNodeIterator(),createTreeWalker()
以下方法用于遍歷元素節點。
**(1)createNodeIterator()**
createNodeIterator方法返回一個DOM的子節點遍歷器。
```javascript
var nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT
);
```
上面代碼返回body元素的遍歷器。createNodeIterator方法的第一個參數為遍歷器的根節點,第二個參數為所要遍歷的節點類型,這里指定為元素節點。其他類型還有所有節點(NodeFilter.SHOW_ALL)、文本節點(NodeFilter.SHOW_TEXT)、評論節點(NodeFilter.SHOW_COMMENT)等。
所謂“遍歷器”,在這里指可以用nextNode方法和previousNode方法依次遍歷根節點的所有子節點。
```javascript
var nodeIterator = document.createNodeIterator(document.body);
var pars = [];
var currentNode;
while (currentNode = nodeIterator.nextNode()) {
pars.push(currentNode);
}
```
上面代碼使用遍歷器的nextNode方法,將根節點的所有子節點,按照從頭部到尾部的順序,讀入一個數組。nextNode方法先返回遍歷器的內部指針所在的節點,然后會將指針移向下一個節點。所有成員遍歷完成后,返回null。previousNode方法則是先將指針移向上一個節點,然后返回該節點。
```javascript
var nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_ELEMENT
);
var currentNode = nodeIterator.nextNode();
var previousNode = nodeIterator.previousNode();
currentNode === previousNode // true
```
上面代碼中,currentNode和previousNode都指向同一個的節點。
有一個需要注意的地方,遍歷器返回的第一個節點,總是根節點。
**(2)createTreeWalker()**
createTreeWalker方法返回一個DOM的子樹遍歷器。它與createNodeIterator方法的區別在于,后者只遍歷子節點,而它遍歷整個子樹。
createTreeWalker方法的第一個參數,是所要遍歷的根節點,第二個參數指定所要遍歷的節點類型。
```javascript
var treeWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT
);
var nodeList = [];
while(treeWalker.nextNode()) nodeList.push(treeWalker.currentNode);
```
上面代碼遍歷body節點下屬的所有元素節點,將它們插入nodeList數組。
### adoptNode(),importNode()
以下方法用于獲取外部文檔的節點。
**(1)adoptNode()**
adoptNode方法將某個節點,從其原來所在的文檔移除,插入當前文檔,并返回插入后的新節點。
```javascript
node = document.adoptNode(externalNode);
```
importNode方法從外部文檔拷貝指定節點,插入當前文檔。
```javascript
var node = document.importNode(externalNode, deep);
```
**(2)importNode()**
importNode方法用于創造一個外部節點的拷貝,然后插入當前文檔。它的第一個參數是外部節點,第二個參數是一個布爾值,表示對外部節點是深拷貝還是淺拷貝,默認是淺拷貝(false)。雖然第二個參數是可選的,但是建議總是保留這個參數,并設為true。
另外一個需要注意的地方是,importNode方法只是拷貝外部節點,這時該節點的父節點是null。下一步還必須將這個節點插入當前文檔的DOM樹。
```javascript
var iframe = document.getElementsByTagName("iframe")[0];
var oldNode = iframe.contentWindow.document.getElementById("myNode");
var newNode = document.importNode(oldNode, true);
document.getElementById("container").appendChild(newNode);
```
上面代碼從iframe窗口,拷貝一個指定節點myNode,插入當前文檔。
### addEventListener(),removeEventListener(),dispatchEvent()
以下三個方法與Document節點的事件相關。這些方法都繼承自EventTarget接口,詳細介紹參見《Event對象》章節的《EventTarget》部分。
```javascript
// 添加事件監聽函數
document.addEventListener('click', listener, false);
// 移除事件監聽函數
document.removeEventListener('click', listener, false);
// 觸發事件
var event = new Event('click');
document.dispatchEvent(event);
```
<h2 id="5.3">Element對象</h2>
Element對象對應網頁的HTML標簽元素。每一個HTML標簽元素,在DOM樹上都會轉化成一個Element節點對象(以下簡稱元素節點)。
元素節點的`nodeType`屬性都是1,但是不同HTML標簽生成的元素節點是不一樣的。JavaScript內部使用不同的構造函數,生成不同的Element節點,比如`<a>`標簽的節點對象由`HTMLAnchorElement()`構造函數生成,`<button>`標簽的節點對象由`HTMLButtonElement()`構造函數生成。因此,元素節點不是一種對象,而是一組對象。
## 屬性
### attributes,id,tagName
以下屬性返回元素節點的性質。
**(1)attributes**
attributes屬性返回一個類似數組的對象,成員是當前元素節點的所有屬性節點,每個數字索引對應一個屬性節點(Attribute)對象。返回值中,所有成員都是動態的,即屬性的變化會實時反映在結果集。
下面是一個HTML代碼。
```html
<p id="para">Hello World</p>
```
獲取attributes成員的代碼如下。
```javascript
var para = document.getElementById('para');
var attr = para.attributes[0];
attr.name // id
attr.value // para
```
上面代碼說明,通過attributes屬性獲取屬性節點對象(attr)以后,可以通過name屬性獲取屬性名(id),通過value屬性獲取屬性值(para)。
注意,屬性節點的name屬性和value屬性,等同于nodeName屬性和nodeValue屬性。
下面代碼是遍歷一個元素節點的所有屬性。
```javascript
var para = document.getElementsByTagName("p")[0];
if (para.hasAttributes()) {
var attrs = para.attributes;
var output = "";
for(var i = attrs.length - 1; i >= 0; i--) {
output += attrs[i].name + "->" + attrs[i].value;
}
result.value = output;
} else {
result.value = "No attributes to show";
}
```
**(2)id屬性**
id屬性返回指定元素的id標識。該屬性可讀寫。
**(3)tagName屬性**
tagName屬性返回指定元素的大寫的標簽名,與nodeName屬性的值相等。
```javascript
// 假定HTML代碼如下
// <span id="span">Hello</span>
var span = document.getElementById("span");
span.tagName // "SPAN"
```
### innerHTML,outerHTML
以下屬性返回元素節點的HTML內容。
**(1)innerHTML**
innerHTML屬性返回該元素包含的HTML代碼。該屬性可讀寫,常用來設置某個節點的內容。
如果將該屬性設為空,等于刪除所有它包含的所有節點。
```javascript
el.innerHTML = '';
```
上面代碼等于將el節點變成了一個空節點,el原來包含的節點被全部刪除。
注意,如果文本節點中包含&、小于號(<)和大于號(%gt;),innerHTML屬性會將它們轉為實體形式&、<、>。
```javascript
// HTML代碼如下 <p id="para"> 5 > 3 </p>
document.getElementById('para').innerHTML
// 5 > 3
```
由于上面這個原因,導致在innerHTML插入<script>標簽,不會被執行。
```javascript
var name = "<script>alert('haha')</script>";
el.innerHTML = name;
```
上面代碼將腳本插入內容,腳本并不會執行。但是,innerHTML還是有安全風險的。
```javascript
var name = "<img src=x onerror=alert(1)>";
el.innerHTML = name;
```
上面代碼中,alert方法是會執行的。因此為了安全考慮,如果插入的是文本,最好用textContent屬性代替innerHTML。
**(2)outerHTML**
outerHTML屬性返回一個字符串,內容為指定元素的所有HTML代碼,包括它自身和包含的所有子元素。
```javascript
// 假定HTML代碼如下
// <div id="d"><p>Hello</p></div>
d = document.getElementById("d");
dump(d.outerHTML);
// '<div id="d"><p>Hello</p></div>'
```
outerHTML屬性是可讀寫的,對它進行賦值,等于替換掉當前元素。
```javascript
// 假定HTML代碼如下
// <div id="container"><div id="d">Hello</div></div>
container = document.getElementById("container");
d = document.getElementById("d");
container.firstChild.nodeName // "DIV"
d.nodeName // "DIV"
d.outerHTML = "<p>Hello</p>";
container.firstChild.nodeName // "P"
d.nodeName // "DIV"
```
上面代碼中,outerHTML屬性重新賦值以后,內層的div元素就不存在了,被p元素替換了。但是,變量d依然指向原來的div元素,這表示被替換的DIV元素還存在于內存中。
如果指定元素沒有父節點,對它的outerTHML屬性重新賦值,會拋出一個錯誤。
```javascript
document.documentElement.outerHTML = "test"; // DOMException
```
### children,childElementCount,firstElementChild,lastElementChild
以下屬性與元素節點的子元素相關。
**(1)children**
children屬性返回一個類似數組的動態對象(實時反映變化),包括當前元素節點的所有子元素。如果當前元素沒有子元素,則返回的對象包含零個成員。
```javascript
// para是一個p元素節點
if (para.children.length) {
var children = para.children;
for (var i = 0; i < children.length; i++) {
// ...
}
}
```
**(2)childElementCount**
childElementCount屬性返回當前元素節點包含的子元素節點的個數。
**(3)firstElementChild**
firstElementChild屬性返回第一個子元素,如果沒有,則返回null。
**(4)lastElementChild**
lastElementChild屬性返回最后一個子元素,如果沒有,則返回null。
### nextElementSibling,previousElementSibling
以下屬性與元素節點的同級元素相關。
**(1)nextElementSibling**
nextElementSibling屬性返回指定元素的后一個同級元素,如果沒有則返回null。
```javascript
// 假定HTML代碼如下
// <div id="div-01">Here is div-01</div>
// <div id="div-02">Here is div-02</div>
var el = document.getElementById('div-01');
el.nextElementSibling
// <div id="div-02">Here is div-02</div>
```
**(2)previousElementSibling**
previousElementSibling屬性返回指定元素的前一個同級元素,如果沒有則返回null。
### className,classList
className屬性用來讀取和設置當前元素的class屬性。它的值是一個字符串,每個class之間用空格分割。
classList屬性則返回一個類似數組的對象,當前元素節點的每個class就是這個對象的一個成員。
```html
<div class="one two three" id="myDiv"></div>
```
上面這個div元素的節點對象的className屬性和classList屬性,分別如下。
```javascript
document.getElementById('myDiv').className
// "one two three"
document.getElementById('myDiv').classList
// {
// 0: "one"
// 1: "two"
// 2: "three"
// length: 3
// }
```
從上面代碼可以看出,className屬性返回一個空格分隔的字符串,而classList屬性指向一個類似數組的對象,該對象的length屬性(只讀)返回當前元素的class數量。
classList對象有下列方法。
- add():增加一個class。
- remove():移除一個class。
- contains():檢查當前元素是否包含某個class。
- toggle():將某個class移入或移出當前元素。
- item():返回指定索引位置的class。
- toString():將class的列表轉為字符串。
```javascript
myDiv.classList.add('myCssClass');
myDiv.classList.add('foo', 'bar');
myDiv.classList.remove('myCssClass');
myDiv.classList.toggle('myCssClass'); // 如果myCssClass不存在就加入,否則移除
myDiv.classList.contains('myCssClass'); // 返回 true 或者 false
myDiv.classList.item(0); // 返回第一個Class
myDiv.classList.toString();
```
下面比較一下,className和classList在添加和刪除某個類時的寫法。
```javascript
// 添加class
document.getElementById('foo').className += 'bold';
document.getElementById('foo').classList.add('bold');
// 刪除class
document.getElementById('foo').classList.remove('bold');
document.getElementById('foo').className =
document.getElementById('foo').className.replace(/^bold$/, '');
```
toggle方法可以接受一個布爾值,作為第二個參數。如果為`true`,則添加該屬性;如果為`false`,則去除該屬性。
```javascript
el.classList.toggle('abc', boolValue);
// 等同于
if (boolValue){
el.classList.add('abc');
} else {
el.classList.remove('abc');
}
```
### clientHeight,clientLeft,clientTop,clientWidth
以下屬性與元素節點的可見區域的坐標相關。
**(1)clientHeight**
clientHeight屬性返回元素節點的可見高度,包括padding、但不包括水平滾動條、邊框和margin的高度,單位為像素。該屬性可以計算得到,等于元素的CSS高度,加上CSS的padding高度,減去水平滾動條的高度(如果存在水平滾動條)。
如果一個元素是可以滾動的,則clientHeight只計算它的可見部分的高度。
**(2)clientLeft**
clientLeft屬性等于元素節點左邊框(border)的寬度,單位為像素,包括垂直滾動條的寬度,不包括左側的margin和padding。但是,除非排版方向是從右到左,且發生元素寬度溢出,否則是不可能存在左側滾動條。如果該元素的顯示設為`display: inline`,clientLeft一律為0,不管是否存在左邊框。
**(3)clientTop**
clientTop屬性等于網頁元素頂部邊框的寬度,不包括頂部的margin和padding。
**(4)clientWidth**
clientWidth屬性等于網頁元素的可見寬度,即包括padding、但不包括垂直滾動條(如果有的話)、邊框和margin的寬度,單位為像素。
如果一個元素是可以滾動的,則clientWidth只計算它的可見部分的寬度。
### scrollHeight,scrollWidth,scrollLeft,scrollTop
以下屬性與元素節點占據的總區域的坐標相關。
**(1)scrollHeight**
scrollHeight屬性返回指定元素的總高度,包括由于溢出而無法展示在網頁的不可見部分。如果一個元素是可以滾動的,則scrollHeight包括整個元素的高度,不管是否存在垂直滾動條。scrollHeight屬性包括padding,但不包括border和margin。該屬性為只讀屬性。
如果不存在垂直滾動條,scrollHeight屬性與clientHeight屬性是相等的。如果存在滾動條,scrollHeight屬性總是大于clientHeight屬性。當滾動條滾動到內容底部時,下面的表達式為true。
```javascript
element.scrollHeight - element.scrollTop === element.clientHeight
```
如果滾動條沒有滾動到內容底部,上面的表達式為false。這個特性結合`onscroll`事件,可以判斷用戶是否滾動到了指定元素的底部,比如是否滾動到了《使用須知》區塊的底部。
```javascript
var rules = document.getElementById("rules");
rules.onscroll = checking;
function checking(){
if (this.scrollHeight - this.scrollTop === this.clientHeight) {
console.log('謝謝閱讀');
} else {
console.log('您還未讀完');
}
}
```
**(2)scrollWidth**
scrollWidth屬性返回元素的總寬度,包括由于溢出容器而無法顯示在網頁上的那部分寬度,不管是否存在水平滾動條。該屬性是只讀屬性。
**(3)scrollLeft**
scrollLeft屬性設置或返回水平滾動條向右側滾動的像素數量。它的值等于元素的最左邊與其可見的最左側之間的距離。對于那些沒有滾動條或不需要滾動的元素,該屬性等于0。該屬性是可讀寫屬性,設置該屬性的值,會導致瀏覽器將指定元素自動滾動到相應的位置。
**(4)scrollTop**
scrollTop屬性設置或返回垂直滾動條向下滾動的像素數量。它的值等于元素的頂部與其可見的最高位置之間的距離。對于那些沒有滾動條或不需要滾動的元素,該屬性等于0。該屬性是可讀寫屬性,設置該屬性的值,會導致瀏覽器將指定元素自動滾動到相應位置。
```javascript
document.querySelector('div').scrollTop = 150;
```
上面代碼將div元素向下滾動150像素。
## 方法
### hasAttribute(),getAttribute(),removeAttribute(),setAttribute()
以下方法與元素節點的屬性相關。
**(1)hasAttribute()**
`hasAttribute`方法返回一個布爾值,表示當前元素節點是否包含指定的HTML屬性。
```javascript
var d = document.getElementById("div1");
if (d.hasAttribute("align")) {
d.setAttribute("align", "center");
}
```
上面代碼檢查`div`節點是否含有`align`屬性。如果有,則設置為“居中對齊”。
**(2)getAttribute()**
`getAttribute`方法返回當前元素節點的指定屬性。如果指定屬性不存在,則返回`null`。
```javascript
var div = document.getElementById('div1');
div.getAttribute('align') // "left"
```
**(3)removeAttribute()**
removeAttribute方法用于從當前元素節點移除屬性。
```javascript
// 原來的HTML代碼
// <div id="div1" align="left" width="200px">
document.getElementById("div1").removeAttribute("align");
// 現在的HTML代碼
// <div id="div1" width="200px">
```
**(4)setAttribute()**
`setAttribute`方法用于為當前元素節點新增屬性,或編輯已存在的屬性。
```javascript
var d = document.getElementById('d1');
d.setAttribute('align', 'center');
```
該方法會將所有屬性名,都當作小寫處理。對于那些已存在的屬性,該方法是編輯操作,否則就會新建屬性。
下面是一個對`img`元素的`src`屬性賦值的例子。
```javascript
var myImage = document.querySelector('img');
myImage.setAttribute ('src', 'path/to/example.png');
```
大多數情況下,直接對屬性賦值比使用該方法更好。
```javascript
el.value = 'hello';
// or
el.setAttribute('value', 'hello');
```
### querySelector(),querySelectorAll(),getElementsByClassName(),getElementsByTagName()
以下方法與獲取當前元素節點的子元素相關。
**(1)querySelector()**
querySelector方法接受CSS選擇器作為參數,返回父元素的第一個匹配的子元素。
```javascript
var content = document.getElementById('content');
var el = content.querySelector('p');
```
上面代碼返回content節點的第一個p元素。
注意,如果CSS選擇器有多個組成部分,比如`div p`,querySelector方法會把父元素考慮在內。假定HTML代碼如下。
```html
<div id="outer">
<p>Hello</p>
<div id="inner">
<p>World</p>
</div>
</div>
```
那么,下面代碼會選中第一個p元素。
```javascript
var outer = document.getElementById('outer');
var el = outer.querySelector('div p');
```
**(2)querySelectorAll()**
querySelectorAll方法接受CSS選擇器作為參數,返回一個NodeList對象,包含所有匹配的子元素。
```javascript
var el = document.querySelector('#test');
var matches = el.querySelectorAll('div.highlighted > p');
```
在CSS選擇器有多個組成部分時,querySelectorAll方法也是會把父元素本身考慮在內。
還是以上面的HTML代碼為例,下面代碼會同時選中兩個p元素。
```javascript
var outer = document.getElementById('outer');
var el = outer.querySelectorAll('div p');
```
**(3)getElementsByClassName()**
getElementsByClassName方法返回一個HTMLCollection對象,成員是當前元素節點的所有匹配指定class的子元素。該方法與document.getElementsByClassName方法的用法類似,只是搜索范圍不是整個文檔,而是當前元素節點。
**(4)getElementsByTagName()**
getElementsByTagName方法返回一個HTMLCollection對象,成員是當前元素節點的所有匹配指定標簽名的子元素。該方法與document.getElementsByClassName方法的用法類似,只是搜索范圍不是整個文檔,而是當前元素節點。此外,該方法搜索之前,會統一將標簽名轉為小寫。
### closest(),matches()
**(1)closest()**
closest方法返回當前元素節點的最接近的父元素(或者當前節點本身),條件是必須匹配給定的CSS選擇器。如果不滿足匹配,則返回null。
假定HTML代碼如下。
```html
<article>
<div id="div-01">Here is div-01
<div id="div-02">Here is div-02
<div id="div-03">Here is div-03</div>
</div>
</div>
</article>
```
div-03節點的closet方法的例子如下。
```javascript
var el = document.getElementById('div-03');
el.closest("#div-02") // div-02
el.closest("div div") // div-03
el.closest("article > div") //div-01
el.closest(":not(div)") // article
```
上面代碼中,由于closet方法將當前元素節點也考慮在內,所以第二個closet方法返回div-03。
**(2)match()**
match方法返回一個布爾值,表示當前元素是否匹配給定的CSS選擇器。
```javascript
if (el.matches(".someClass")) {
console.log("Match!");
}
```
該方法帶有瀏覽器前綴,下面的函數可以兼容不同的瀏覽器,并且在瀏覽器不支持時,自行部署這個功能。
```javascript
function matchesSelector(el, selector) {
var p = Element.prototype;
var f = p.matches
|| p.webkitMatchesSelector
|| p.mozMatchesSelector
|| p.msMatchesSelector
|| function(s) {
return [].indexOf.call(document.querySelectorAll(s), this) !== -1;
};
return f.call(el, selector);
}
// 用法
matchesSelector(
document.getElementById('myDiv'),
'div.someSelector[some-attribute=true]'
)
```
### addEventListener(),removeEventListener(),dispatchEvent()
以下三個方法與Element節點的事件相關。這些方法都繼承自EventTarget接口,詳細介紹參見《Event對象》章節的《EventTarget》部分。
```javascript
// 添加事件監聽函數
el.addEventListener('click', listener, false);
// 移除事件監聽函數
el.removeEventListener('click', listener, false);
// 觸發事件
var event = new Event('click');
el.dispatchEvent(event);
```
### getBoundingClientRect(),getClientRects()
以下方法返回元素節點的CSS盒狀模型信息。
**(1)getBoundingClientRect()**
getBoundingClientRect方法返回一個對象,該對象提供當前元素節點的大小、它相對于視口(viewport)的位置等信息,基本上就是CSS盒狀模型的內容。
```javascript
var rect = obj.getBoundingClientRect();
```
上面代碼中,getBoundingClientRect方法返回的對象,具有以下屬性(全部為只讀)。
- bottom:元素底部相對于視口的縱坐標。
- height:元素高度(等于bottom減去top)。
- left:元素左上角相對于視口的坐標。
- right:元素右邊界相對于視口的橫坐標。
- top:元素頂部相對于視口的縱坐標。
- width:元素寬度(等于right減去left)。
由于元素相對于視口(viewport)的位置,會隨著頁面滾動變化,因此表示位置的四個屬性值,都不是固定不變的。
注意,getBoundingClientRect方法的所有屬性,都把邊框(border屬性)算作元素的一部分。也就是說,都是從邊框外緣的各個點來計算。因此,width和height包括了元素本身 + padding + border。
**(1)getClientRects()**
getClientRects方法返回一個類似數組的對象,里面是當前元素在頁面上形成的所有矩形。每個矩形都有`bottom`、`height`、`left`、`right`、`top`和`width`六個屬性,表示它們相對于視口的四個坐標,以及本身的高度和寬度。
對于盒狀元素(比如div和p),該方法返回的對象中只有該元素一個成員。對于行內元素(比如span、a、em),該方法返回的對象有多少個成員,取決于該元素在頁面上占據多少行。
```html
<span id="inline">
Hello World
Hello World
Hello World
</span>
```
上面代碼是一個行內元素span,如果它在頁面上占據三行,getClientRects方法返回的對象就有三個成員,如果它在頁面上占據一行,getClientRects方法返回的對象就只有一個成員。
```javascript
var el = document.getElementById('inline');
el.getClientRects().length // 3
el.getClientRects()[0].left // 8
el.getClientRects()[0].right // 113.908203125
el.getClientRects()[0].bottom // 31.200000762939453
el.getClientRects()[0].height // 23.200000762939453
el.getClientRects()[0].width // 105.908203125
```
這個方法主要用于判斷行內元素是否換行,以及行內元素的每一行的位置偏移。
### insertAdjacentHTML(),remove()
以下方法操作元素節點的DOM樹。
**(1)insertAdjacentHTML()**
insertAdjacentHTML方法解析字符串,然后將生成的節點插入DOM樹的指定位置。
```javascript
element.insertAdjacentHTML(position, text);
```
該方法接受兩個參數,第一個是指定位置,第二個是待解析的字符串。
指定位置共有四個。
- beforebegin:在當前元素節點的前面。
- afterbegin:在當前元素節點的里面,插在它的第一個子元素之前。
- beforeend:在當前元素節點的里面,插在它的最后一個子元素之后。
- afterend:在當前元素節點的后面。'
```javascript
// 原來的HTML代碼:<div id="one">one</div>
var d1 = document.getElementById('one');
d1.insertAdjacentHTML('afterend', '<div id="two">two</div>');
// 現在的HTML代碼:
// <div id="one">one</div><div id="two">two</div>
```
該方法不是徹底置換現有的DOM結構,這使得它的執行速度比innerHTML操作快得多。所有瀏覽器都支持這個方法,包括IE 6。
**(2)remove()**
remove方法用于將當前元素節點從DOM樹刪除。
```javascript
var el = document.getElementById('div-01');
el.remove();
```
### scrollIntoView()
scrollIntoView方法滾動當前元素,進入瀏覽器的可見區域。
```javascript
el.scrollIntoView(); // 等同于el.scrollIntoView(true)
el.scrollIntoView(false);
```
該方法可以接受一個布爾值作為參數。如果為true,表示元素的頂部與當前區域的可見部分的頂部對齊(前提是當前區域可滾動);如果為false,表示元素的底部與當前區域的可見部分的尾部對齊(前提是當前區域可滾動)。如果沒有提供該參數,默認為true。
<h2 id="5.4">Text節點和DocumentFragment節點</h2>
## Text節點的概念
Text節點代表Element節點和Attribute節點的文本內容。如果一個節點只包含一段文本,那么它就有一個Text子節點,代表該節點的文本內容。通常我們使用Element節點的firstChild、nextSibling等屬性獲取Text節點,或者使用Document節點的createTextNode方法創造一個Text節點。
```javascript
// 獲取Text節點
var textNode = document.querySelector('p').firstChild;
// 創造Text節點
var textNode = document.createTextNode('Hi');
document.querySelector('div').appendChild(textNode);
```
瀏覽器原生提供一個Text構造函數。它返回一個Text節點。它的參數就是該Text節點的文本內容。
```javascript
var text1 = new Text();
var text2 = new Text("This is a text node");
```
注意,由于空格也是一個字符,所以哪怕只有一個空格,也會形成Text節點。
Text節點除了繼承Node節點的屬性和方法,還繼承了CharacterData接口。Node節點的屬性和方法請參考《Node節點》章節,這里不再重復介紹了,以下的屬性和方法大部分來自CharacterData接口。
## Text節點的屬性
### data
data屬性等同于nodeValue屬性,用來設置或讀取Text節點的內容。
```javascript
// 讀取文本內容
document.querySelector('p').firstChild.data
// 等同于
document.querySelector('p').firstChild.nodeValue
// 設置文本內容
document.querySelector('p').firstChild.data = 'Hello World';
```
### wholeText
wholeText屬性將當前Text節點與毗鄰的Text節點,作為一個整體返回。大多數情況下,wholeText屬性的返回值,與data屬性和textContent屬性相同。但是,某些特殊情況會有差異。
舉例來說,HTML代碼如下。
```html
<p id="para">A <em>B</em> C</p>
```
這時,Text節點的wholeText屬性和data屬性,返回值相同。
```javascript
var el = document.getElementById("para");
el.firstChild.wholeText // "A "
el.firstChild.data // "A "
```
但是,一旦移除em節點,wholeText屬性與data屬性就會有差異,因為這時其實P節點下面包含了兩個毗鄰的Text節點。
```javascript
el.removeChild(para.childNodes[1]);
el.firstChild.wholeText // "A C"
el.firstChild.data // "A "
```
### length
length屬性返回當前Text節點的文本長度。
```javascript
(new Text('Hello')).length // 5
```
### nextElementSibling
nextElementSibling屬性返回緊跟在當前Text節點后面的那個同級Element節點。如果取不到這樣的節點,則返回null。
```javascript
// HTML為
// <div>Hello <em>World</em></div>
var tn = document.querySelector('div').firstChild;
tn.nextElementSibling
// <em>World</em>
```
### previousElementSibling
previousElementSibling屬性返回當前Text節點前面最近的那個Element節點。如果取不到這樣的節點,則返回null。
## Text節點的方法
### appendData(),deleteData(),insertData(),replaceData(),subStringData()
以下5個方法都是編輯Text節點文本內容的方法。
appendData方法用于在Text節點尾部追加字符串。
deleteData方法用于刪除Text節點內部的子字符串,第一個參數為子字符串位置,第二個參數為子字符串長度。
insertData方法用于在Text節點插入字符串,第一個參數為插入位置,第二個參數為插入的子字符串。
replaceData方法用于替換文本,第一個參數為替換開始位置,第二個參數為需要被替換掉的長度,第三個參數為新加入的字符串。
subStringData方法用于獲取子字符串,第一個參數為子字符串在Text節點中的開始位置,第二個參數為子字符串長度。
```javascript
// HTML代碼為
// <p>Hello World</p>
var pElementText = document.querySelector('p').firstChild;
pElementText.appendData('!');
// 頁面顯示 Hello World!
pElementText.deleteData(7,5);
// 頁面顯示 Hello W
pElementText.insertData(7,'Hello ');
// 頁面顯示 Hello WHello
pElementText.replaceData(7,5,'World');
// 頁面顯示 Hello WWorld
pElementText.substringData(7,10);
// 頁面顯示不變,返回"World "
```
### remove()
remove方法用于移除當前Text節點。
```javascript
// HTML代碼為
// <p>Hello World</p>
document.querySelector('p').firstChild.remove()
// 現在頁面代碼為
// <p></p>
```
### splitText(),normalize()
splitText方法將Text節點一分為二,變成兩個毗鄰的Text節點。它的參數就是分割位置(從零開始),分割到該位置的字符前結束。如果分割位置不存在,將報錯。
分割后,該方法返回分割位置后方的字符串,而原Text節點變成只包含分割位置前方的字符串。
```javascript
// html代碼為 <p id="p">foobar</p>
var p = document.getElementById('p');
var textnode = p.firstChild;
var newText = textnode.splitText(3);
newText // "bar"
textnode // "foo"
```
normalize方法可以將毗鄰的兩個Text節點合并。
接上面的例子,splitText方法將一個Text節點分割成兩個,normalize方法可以實現逆操作,將它們合并。
```javascript
p.childNodes.length // 2
// 將毗鄰的兩個Text節點合并
p.normalize();
p.childNodes.length // 1
```
## DocumentFragment節點
DocumentFragment節點代表一個文檔的片段,本身就是一個完整的DOM樹形結構。它沒有父節點,不屬于當前文檔,操作DocumentFragment節點,要比直接操作DOM樹快得多。
它一般用于構建一個DOM結構,然后插入當前文檔。document.createDocumentFragment方法,以及瀏覽器原生的DocumentFragment構造函數,可以創建一個空的DocumentFragment節點。然后再使用其他DOM方法,向其添加子節點。
```javascript
var docFrag = document.createDocumentFragment();
// or
var docFrag = new DocumentFragment();
var li = document.createElement("li");
li.textContent = "Hello World";
docFrag.appendChild(li);
document.queryselector('ul').appendChild(docFrag);
```
上面代碼創建了一個DocumentFragment節點,然后將一個li節點添加在它里面,最后將DocumentFragment節點移動到原文檔。
一旦DocumentFragment節點被添加進原文檔,它自身就變成了空節點(textContent屬性為空字符串)。如果想要保存DocumentFragment節點的內容,可以使用cloneNode方法。
```javascript
document
.queryselector('ul')
.appendChild(docFrag.cloneNode(true));
```
DocumentFragment節點對象沒有自己的屬性和方法,全部繼承自Node節點和ParentNode接口。也就是說,DocumentFragment節點比Node節點多出以下四個屬性。
- children:返回一個動態的HTMLCollection集合對象,包括當前DocumentFragment對象的所有子元素節點。
- firstElementChild:返回當前DocumentFragment對象的第一個子元素節點,如果沒有則返回null。
- lastElementChild:返回當前DocumentFragment對象的最后一個子元素節點,如果沒有則返回null。
- childElementCount:返回當前DocumentFragment對象的所有子元素數量。
另外,Node節點的所有方法,都接受DocumentFragment節點作為參數(比如Node.appendChild、Node.insertBefore)。這時,DocumentFragment的子節點(而不是DocumentFragment節點本身)將插入當前節點。
<h2 id="5.5">Event對象</h2>
事件是一種異步編程的實現方式,本質上是程序各個組成部分之間的通信。DOM支持大量的事件,本節介紹DOM的事件編程。
## EventTarget接口
DOM的事件操作(監聽和觸發),都定義在`EventTarget`接口。`Element`節點、`document`節點和`window`對象,都部署了這個接口。此外,XMLHttpRequest、AudioNode、AudioContext等瀏覽器內置對象,也部署了這個接口。
該接口就是三個方法,`addEventListener`和`removeEventListener`用于綁定和移除監聽函數,`dispatchEvent`用于觸發事件。
### addEventListener()
`addEventListener`方法用于在當前節點或對象上,定義一個特定事件的監聽函數。
```javascript
target.addEventListener(type, listener[, useCapture]);
```
上面是使用格式,addEventListener方法接受三個參數。
- type,事件名稱,大小寫不敏感。
- listener,監聽函數。指定事件發生時,會調用該監聽函數。
- useCapture,監聽函數是否在捕獲階段(capture)觸發(參見后文《事件的傳播》部分)。該參數是一個布爾值,默認為false(表示監聽函數只在冒泡階段被觸發)。老式瀏覽器規定該參數必寫,較新版本的瀏覽器允許該參數可選。為了保持兼容,建議總是寫上該參數。
下面是一個例子。
```javascript
function hello(){
console.log('Hello world');
}
var button = document.getElementById("btn");
button.addEventListener('click', hello, false);
```
上面代碼中,addEventListener方法為button節點,綁定click事件的監聽函數hello,該函數只在冒泡階段觸發。
可以使用addEventListener方法,為當前對象的同一個事件,添加多個監聽函數。這些函數按照添加順序觸發,即先添加先觸發。如果為同一個事件多次添加同一個監聽函數,該函數只會執行一次,多余的添加將自動被去除(不必使用removeEventListener方法手動去除)。
```javascript
function hello(){
console.log('Hello world');
}
document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);
```
執行上面代碼,點擊文檔只會輸出一行“Hello world”。
如果希望向監聽函數傳遞參數,可以用匿名函數包裝一下監聽函數。
```javascript
function print(x) {
console.log(x);
}
var el = document.getElementById("div1");
el.addEventListener("click", function(){print('Hello')}, false);
```
上面代碼通過匿名函數,向監聽函數print傳遞了一個參數。
### removeEventListener()
removeEventListener方法用來移除addEventListener方法添加的事件監聽函數。
```javascript
div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);
```
removeEventListener方法的參數,與addEventListener方法完全一致。它對第一個參數“事件類型”,也是大小寫不敏感。
注意,removeEventListener方法移除的監聽函數,必須與對應的addEventListener方法的參數完全一致,而且在同一個元素節點,否則無效。
### dispatchEvent()
`dispatchEvent`方法在當前節點上觸發指定事件,從而觸發監聽函數的執行。該方法返回一個布爾值,只要有一個監聽函數調用了`Event.preventDefault()`,則返回值為`false`,否則為`true`。
```javascript
target.dispatchEvent(event)
```
`dispatchEvent`方法的參數是一個`Event`對象的實例。
```javascript
para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);
```
上面代碼在當前節點觸發了`click`事件。
如果`dispatchEvent`方法的參數為空,或者不是一個有效的事件對象,將報錯。
下面代碼根據`dispatchEvent`方法的返回值,判斷事件是否被取消了。
```javascript
var canceled = !cb.dispatchEvent(event);
if (canceled) {
console.log('事件取消');
} else {
console.log('事件未取消');
}
}
```
## 監聽函數
監聽函數(listener)是事件發生時,程序所要執行的函數。它是事件驅動編程模式的主要編程方式。
DOM提供三種方法,可以用來為事件綁定監聽函數。
### HTML標簽的on-屬性
HTML語言允許在元素標簽的屬性中,直接定義某些事件的監聽代碼。
```html
<body onload="doSomething()">
<div onclick="console.log('觸發事件')">
```
上面代碼為`body`節點的`load`事件、`div`節點的`click`事件,指定了監聽函數。
使用這個方法指定的監聽函數,只會在冒泡階段觸發。
注意,使用這種方法時,`on-`屬性的值是將會執行的代碼,而不是“監聽函數”。
```html
<!-- 正確 -->
<body onload="doSomething()">
<!-- 錯誤 -->
<body onload="doSomething">
```
一旦指定的事件發生,`on-`屬性的值是原樣傳入JavaScript引擎執行。因此如果要執行函數,不要忘記加上一對圓括號。
另外,Element節點的`setAttribute`方法,其實設置的也是這種效果。
```javascript
el.setAttribute('onclick', 'doSomething()');
```
### Element節點的事件屬性
Element節點有事件屬性,可以定義監聽函數。
```javascript
window.onload = doSomething;
div.onclick = function(event){
console.log('觸發事件');
};
```
使用這個方法指定的監聽函數,只會在冒泡階段觸發。
### addEventListener方法
通過`Element`節點、`document`節點、`window`對象的`addEventListener`方法,也可以定義事件的監聽函數。
```javascript
window.addEventListener('load', doSomething, false);
```
addEventListener方法的詳細介紹,參見本節EventTarget接口的部分。
在上面三種方法中,第一種“HTML標簽的on-屬性”,違反了HTML與JavaScript代碼相分離的原則;第二種“Element節點的事件屬性”的缺點是,同一個事件只能定義一個監聽函數,也就是說,如果定義兩次onclick屬性,后一次定義會覆蓋前一次。因此,這兩種方法都不推薦使用,除非是為了程序的兼容問題,因為所有瀏覽器都支持這兩種方法。
addEventListener是推薦的指定監聽函數的方法。它有如下優點:
- 可以針對同一個事件,添加多個監聽函數。
- 能夠指定在哪個階段(捕獲階段還是冒泡階段)觸發回監聽函數。
- 除了DOM節點,還可以部署在window、XMLHttpRequest等對象上面,等于統一了整個JavaScript的監聽函數接口。
### this對象的指向
實際編程中,監聽函數內部的this對象,常常需要指向觸發事件的那個Element節點。
addEventListener方法指定的監聽函數,內部的this對象總是指向觸發事件的那個節點。
```javascript
// HTML代碼為
// <p id="para">Hello</p>
var id = 'doc';
var para = document.getElementById('para');
function hello(){
console.log(this.id);
}
para.addEventListener('click', hello, false);
```
執行上面代碼,點擊p節點會輸出para。這是因為監聽函數被“拷貝”成了節點的一個屬性,使用下面的寫法,會看得更清楚。
```javascript
para.onclick = hello;
```
如果將監聽函數部署在Element節點的on-屬性上面,this不會指向觸發事件的元素節點。
```html
<p id="para" onclick="hello()">Hello</p>
<!-- 或者使用JavaScript代碼 -->
<script>
pElement.setAttribute('onclick', 'hello()');
</script>
```
執行上面代碼,點擊p節點會輸出doc。這是因為這里只是調用hello函數,而hello函數實際是在全局作用域執行,相當于下面的代碼。
```javascript
para.onclick = function(){
hello();
}
```
一種解決方法是,不引入函數作用域,直接在on-屬性寫入所要執行的代碼。因為on-屬性是在當前節點上執行的。
```html
<p id="para" onclick="console.log(id)">Hello</p>
<!-- 或者 -->
<p id="para" onclick="console.log(this.id)">Hello</p>
```
上面兩行,最后輸出的都是para。
總結一下,以下寫法的this對象都指向Element節點。
```javascript
// JavaScript代碼
element.onclick = print
element.addEventListener('click', print, false)
element.onclick = function () {console.log(this.id);}
// HTML代碼
<element onclick="console.log(this.id)">
```
以下寫法的this對象,都指向全局對象。
```javascript
// JavaScript代碼
element.onclick = function (){ doSomething() };
element.setAttribute('onclick', 'doSomething()');
// HTML代碼
<element onclick="doSomething()">
```
## 事件的傳播
### 傳播的三個階段
當一個事件發生以后,它會在不同的DOM節點之間傳播(propagation)。這種傳播分成三個階段:
- **第一階段**:從window對象傳導到目標節點,稱為“捕獲階段”(capture phase)。
- **第二階段**:在目標節點上觸發,稱為“目標階段”(target phase)。
- **第三階段**:從目標節點傳導回window對象,稱為“冒泡階段”(bubbling phase)。
這種三階段的傳播模型,會使得一個事件在多個節點上觸發。比如,假設div節點之中嵌套一個p節點。
```html
<div>
<p>Click Me</p>
</div>
```
如果對這兩個節點的click事件都設定監聽函數,則click事件會被觸發四次。
```javascript
var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);
function callback(event) {
var tag = event.currentTarget.tagName;
var phase = phases[event.eventPhase];
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 點擊以后的結果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
```
上面代碼表示,click事件被觸發了四次:p節點的捕獲階段和冒泡階段各1次,div節點的捕獲階段和冒泡階段各1次。
1. 捕獲階段:事件從div向p傳播時,觸發div的click事件;
2. 目標階段:事件從div到達p時,觸發p的click事件;
3. 目標階段:事件離開p時,觸發p的click事件;
4. 冒泡階段:事件從p傳回div時,再次觸發div的click事件。
注意,用戶點擊網頁的時候,瀏覽器總是假定click事件的目標節點,就是點擊位置的嵌套最深的那個節點(嵌套在div節點的p節點)。
事件傳播的最上層對象是window,接著依次是document,html(document.documentElement)和body(document.dody)。也就是說,如果body元素中有一個div元素,點擊該元素。事件的傳播順序,在捕獲階段依次為window、document、html、body、div,在冒泡階段依次為div、body、html、document、window。
### 事件的代理
由于事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
```javascript
var ul = document.querySelector('ul');
ul.addEventListener('click', function(event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
```
上面代碼的click事件的監聽函數定義在ul節點,但是實際上,它處理的是子節點li的click事件。這樣做的好處是,只要定義一個監聽函數,就能處理多個子節點的事件,而且以后再添加子節點,監聽函數依然有效。
如果希望事件到某個節點為止,不再傳播,可以使用事件對象的stopPropagation方法。
```javascript
p.addEventListener('click', function(event) {
event.stopPropagation();
});
```
使用上面的代碼以后,click事件在冒泡階段到達p節點以后,就不再向上(父節點的方向)傳播了。
但是,stopPropagation方法不會阻止p節點上的其他click事件的監聽函數。如果想要不再觸發那些監聽函數,可以使用stopImmediatePropagation方法。
```javascript
p.addEventListener('click', function(event) {
event.stopImmediatePropagation();
});
p.addEventListener('click', function(event) {
// 不會被觸發
});
```
## Event對象
事件發生以后,會生成一個事件對象,作為參數傳給監聽函數。瀏覽器原生提供一個Event對象,所有的事件都是這個對象的實例,或者說繼承了`Event.prototype`對象。
Event對象本身就是一個構造函數,可以用來生成新的實例。
```javascript
event = new Event(typeArg, eventInit);
```
Event構造函數接受兩個參數。第一個參數是字符串,表示事件的名稱;第二個參數是一個對象,表示事件對象的配置。該參數可以有以下兩個屬性。
- bubbles:布爾值,可選,默認為false,表示事件對象是否冒泡。
- cancelable:布爾值,可選,默認為false,表示事件是否可以被取消。
```javascript
var ev = new Event("look", {"bubbles":true, "cancelable":false});
document.dispatchEvent(ev);
```
上面代碼新建一個look事件實例,然后使用dispatchEvent方法觸發該事件。
IE8及以下版本,事件對象不作為參數傳遞,而是通過window對象的event屬性讀取,并且事件對象的target屬性叫做srcElement屬性。所以,以前獲取事件信息,往往要寫成下面這樣。
```javascript
function myEventHandler(event) {
var actualEvent = event || window.event;
var actualTarget = actualEvent.target || actualEvent.srcElement;
// ...
}
```
上面的代碼只是為了說明以前的程序為什么這樣寫,在新代碼中,這樣的寫法不應該再用了。
以下介紹Event實例的屬性和方法。
### bubbles,eventPhase
以下屬性與事件的階段有關。
**(1)bubbles**
bubbles屬性返回一個布爾值,表示當前事件是否會冒泡。該屬性為只讀屬性,只能在新建事件時改變。除非顯式聲明,Event構造函數生成的事件,默認是不冒泡的。
```javascript
function goInput(e) {
if (!e.bubbles) {
passItOn(e);
} else {
doOutput(e);
}
}
```
上面代碼根據事件是否冒泡,調用不同的函數。
**(2)eventPhase**
eventPhase屬性返回一個整數值,表示事件目前所處的節點。
```javascript
var phase = event.eventPhase;
```
- 0,事件目前沒有發生。
- 1,事件目前處于捕獲階段,即處于從祖先節點向目標節點的傳播過程中。該過程是從Window對象到Document節點,再到HTMLHtmlElement節點,直到目標節點的父節點為止。
- 2,事件到達目標節點,即target屬性指向的那個節點。
- 3,事件處于冒泡階段,即處于從目標節點向祖先節點的反向傳播過程中。該過程是從父節點一直到Window對象。只有bubbles屬性為true時,這個階段才可能發生。
### cancelable,defaultPrevented
以下屬性與事件的默認行為有關。
**(1)cancelable**
cancelable屬性返回一個布爾值,表示事件是否可以取消。該屬性為只讀屬性,只能在新建事件時改變。除非顯式聲明,Event構造函數生成的事件,默認是不可以取消的。
```javascript
var bool = event.cancelable;
```
如果要取消某個事件,需要在這個事件上面調用preventDefault方法,這會阻止瀏覽器對某種事件部署的默認行為。
**(2)defaultPrevented**
defaultPrevented屬性返回一個布爾值,表示該事件是否調用過preventDefault方法。
```javascript
if (e.defaultPrevented) {
// ...
}
```
### currentTarget,target
以下屬性與事件的目標節點有關。
**(1)currentTarget**
currentTarget屬性返回事件當前所在的節點,即正在執行的監聽函數所綁定的那個節點。作為比較,target屬性返回事件發生的節點。如果監聽函數在捕獲階段和冒泡階段觸發,那么這兩個屬性返回的值是不一樣的。
```javascript
function hide(e){
console.log(this === e.currentTarget); // true
e.currentTarget.style.visibility = "hidden";
}
para.addEventListener('click', hide, false);
```
上面代碼中,點擊para節點,該節點會不可見。另外,在監聽函數中,currentTarget屬性實際上等同于this對象。
**(2)target**
target屬性返回觸發事件的那個節點,即事件最初發生的節點。如果監聽函數不在該節點觸發,那么它與currentTarget屬性返回的值是不一樣的。
```javascript
function hide(e){
console.log(this === e.target); // 有可能不是true
e.target.style.visibility = "hidden";
}
// HTML代碼為
// <p id="para">Hello <em>World</em></p>
para.addEventListener('click', hide, false);
```
上面代碼中,如果在para節點的em子節點上面點擊,則`e.target`指向em子節點,導致em子節點(即World部分)會不可見,且輸出false。
在IE6—IE8之中,該屬性的名字不是target,而是srcElement,因此經常可以看到下面這樣的代碼。
```javascript
function hide(e) {
var target = e.target || e.srcElement;
target.style.visibility = 'hidden';
}
```
### type,detail,timeStamp,isTrusted
以下屬性與事件對象的其他信息相關。
**(1)type**
type屬性返回一個字符串,表示事件類型,具體的值同addEventListener方法和removeEventListener方法的第一個參數一致,大小寫不敏感。
```javascript
var string = event.type;
```
**(2)detail**
detail屬性返回一個數值,表示事件的某種信息。具體含義與事件類型有關,對于鼠標事件,表示鼠標按鍵在某個位置按下的次數,比如對于dblclick事件,detail屬性的值總是2。
```javascript
function giveDetails(e) {
this.textContent = e.detail;
}
el.onclick = giveDetails;
```
**(3)timeStamp**
`timeStamp`屬性返回一個毫秒時間戳,表示事件發生的時間。
```javascript
var number = event.timeStamp;
```
Chrome在49版以前,這個屬性返回的是一個整數,單位是毫秒(millisecond),表示從Unix紀元開始的時間戳。從49版開始,該屬性返回的是一個高精度時間戳,也就是說,毫秒之后還帶三位小數,精確到微秒。并且,這個值不再從Unix紀元開始計算,而是從`PerformanceTiming.navigationStart`開始計算,即表示距離用戶導航至該網頁的時間。如果想將這個值轉為Unix紀元時間戳,就要計算`event.timeStamp + performance.timing.navigationStart`。
下面是一個計算鼠標移動速度的例子,顯示每秒移動的像素數量。
```javascript
var previousX;
var previousY;
var previousT;
window.addEventListener('mousemove', function(event) {
if (!(previousX === undefined ||
previousY === undefined ||
previousT === undefined)) {
var deltaX = event.screenX - previousX;
var deltaY = event.screenY - previousY;
var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
var deltaT = event.timeStamp - previousT;
console.log(deltaD / deltaT * 1000);
}
previousX = event.screenX;
previousY = event.screenY;
previousT = event.timeStamp;
});
```
**(4)isTrusted**
isTrusted屬性返回一個布爾值,表示該事件是否可以信任。
```javascript
var bool = event.isTrusted;
```
Firefox瀏覽器中,用戶觸發的事件會返回true,腳本觸發的事件返回false;IE瀏覽器中,除了使用createEvent方法生成的事件,所有其他事件都返回true;Chrome瀏覽器不支持該屬性。
### preventDefault()
preventDefault方法取消瀏覽器對當前事件的默認行為,比如點擊鏈接后,瀏覽器跳轉到指定頁面,或者按一下空格鍵,頁面向下滾動一段距離。該方法生效的前提是,事件的cancelable屬性為true,如果為false,則調用該方法沒有任何效果。
該方法不會阻止事件的進一步傳播(stopPropagation方法可用于這個目的)。只要在事件的傳播過程中(捕獲階段、目標階段、冒泡階段皆可),使用了preventDefault方法,該事件的默認方法就不會執行。
```javascript
// HTML代碼為
// <input type="checkbox" id="my-checkbox" />
var cb = document.getElementById('my-checkbox');
cb.addEventListener(
'click',
function (e){ e.preventDefault(); },
false
);
```
上面代碼為點擊單選框的事件,設置監聽函數,取消默認行為。由于瀏覽器的默認行為是選中單選框,所以這段代碼會導致無法選中單選框。
利用這個方法,可以為文本輸入框設置校驗條件。如果用戶的輸入不符合條件,就無法將字符輸入文本框。
```javascript
function checkName(e) {
if (e.charCode < 97 || e.charCode > 122) {
e.preventDefault();
}
}
```
上面函數設為文本框的keypress監聽函數后,將只能輸入小寫字母,否則輸入事件的默認事件(寫入文本框)將被取消。
如果監聽函數最后返回布爾值false(即return false),瀏覽器也不會觸發默認行為,與preventDefault方法有等同效果。
### stopPropagation()
stopPropagation方法阻止事件在DOM中繼續傳播,防止再觸發定義在別的節點上的監聽函數,但是不包括在當前節點上新定義的事件監聽函數。
```javascript
function stopEvent(e) {
e.stopPropagation();
}
el.addEventListener('click', stopEvent, false);
```
將上面函數指定為監聽函數,會阻止事件進一步冒泡到el節點的父節點。
### stopImmediatePropagation()
stopImmediatePropagation方法阻止同一個事件的其他監聽函數被調用。
如果同一個節點對于同一個事件指定了多個監聽函數,這些函數會根據添加的順序依次調用。只要其中有一個監聽函數調用了stopImmediatePropagation方法,其他的監聽函數就不會再執行了。
```javascript
function l1(e){
e.stopImmediatePropagation();
}
function l2(e){
console.log('hello world');
}
el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);
```
上面代碼在el節點上,為click事件添加了兩個監聽函數l1和l2。由于l1調用了stopImmediatePropagation方法,所以l2不會被調用。
## 鼠標事件
### 事件種類
鼠標事件指與鼠標相關的事件,主要有以下一些。
**(1)click事件**
`click`事件當用戶在Element節點、document節點、window對象上,單擊鼠標(或者按下回車鍵)時觸發。
“鼠標單擊”定義為,用戶在同一個位置完成一次`mousedown`動作和`mouseup`動作。它們的觸發順序是:`mousedown`首先觸發,`mouseup`接著觸發,`click`最后觸發。
下面是一個設置click事件監聽函數的例子。
```javascript
div.addEventListener("click", function( event ) {
// 顯示在該節點,鼠標連續點擊的次數
event.target.innerHTML = "click count: " + event.detail;
}, false);
```
下面的代碼是利用click事件進行CSRF攻擊(Cross-site request forgery)的一個例子。
```html
<a href="http://www.harmless.com/" onclick="
var f = document.createElement('form');
f.style.display = 'none';
this.parentNode.appendChild(f);
f.method = 'POST';
f.action = 'http://www.example.com/account/destroy';
f.submit();
return false;">偽裝的鏈接</a>
```
**(2)dblclick事件**
`dblclick`事件當用戶在`element`、`document`、`window`對象上,雙擊鼠標時觸發。該事件會在`mousedown`、`mouseup`、`click`之后觸發。
**(3)mouseup事件,mousedown事件**
mouseup事件在釋放按下的鼠標鍵時觸發。
mousedown事件在按下鼠標鍵時觸發。
**(4)mousemove事件**
mousemove事件當鼠標在一個節點內部移動時觸發。當鼠標持續移動時,該事件會連續觸發。為了避免性能問題,建議對該事件的監聽函數做一些限定,比如限定一段時間內只能運行一次代碼。
**(5)mouseover事件,mouseenter事件**
mouseover事件和mouseenter事件,都是鼠標進入一個節點時觸發。
兩者的區別是,mouseover事件會冒泡,mouseenter事件不會。子節點的mouseover事件會冒泡到父節點,進而觸發父節點的mouseover事件。mouseenter事件就沒有這種效果,所以進入子節點時,不會觸發父節點的監聽函數。
下面的例子是mouseenter事件與mouseover事件的區別。
```javascript
// HTML代碼為
// <ul id="test">
// <li>item 1</li>
// <li>item 2</li>
// <li>item 3</li>
// </ul>
var test = document.getElementById("test");
// 進入test節點以后,該事件只會觸發一次
test.addEventListener("mouseenter", function( event ) {
event.target.style.color = "purple";
setTimeout(function() {
event.target.style.color = "";
}, 500);
}, false);
// 接入test節點以后,只要在子Element節點上移動,該事件會觸發多次
test.addEventListener("mouseover", function( event ) {
event.target.style.color = "orange";
setTimeout(function() {
event.target.style.color = "";
}, 500);
}, false);
```
上面代碼中,由于mouseover事件會冒泡,所以子節點的mouseover事件會觸發父節點的監聽函數。
**(6)mouseout事件,mouseleave事件**
mouseout事件和mouseleave事件,都是鼠標離開一個節點時觸發。
兩者的區別是,mouseout事件會冒泡,mouseleave事件不會。子節點的mouseout事件會冒泡到父節點,進而觸發父節點的mouseout事件。mouseleave事件就沒有這種效果,所以離開子節點時,不會觸發父節點的監聽函數。
**(7)contextmenu**
`contextmenu`事件在一個節點上點擊鼠標右鍵時觸發,或者按下“上下文菜單”鍵時觸發。
### MouseEvent對象
鼠標事件使用MouseEvent對象表示,它繼承UIEvent對象和Event對象。瀏覽器提供一個MouseEvent構造函數,用于新建一個MouseEvent實例。
```javascript
event = new MouseEvent(typeArg, mouseEventInit);
```
MouseEvent構造函數的第一個參數是事件名稱(可能的值包括click、mousedown、mouseup、mouseover、mousemove、mouseout),第二個參數是一個事件初始化對象。該對象可以配置以下屬性。
- screenX,設置鼠標相對于屏幕的水平坐標(但不會移動鼠標),默認為0,等同于MouseEvent.screenX屬性。
- screenY,設置鼠標相對于屏幕的垂直坐標,默認為0,等同于MouseEvent.screenY屬性。
- clientX,設置鼠標相對于窗口的水平坐標,默認為0,等同于MouseEvent.clientX屬性。
- clientY,設置鼠標相對于窗口的垂直坐標,默認為0,等同于MouseEvent.clientY屬性。
- ctrlKey,設置是否按下ctrl鍵,默認為false,等同于MouseEvent.ctrlKey屬性。
- shiftKey,設置是否按下shift鍵,默認為false,等同于MouseEvent.shiftKey屬性。
- altKey,設置是否按下alt鍵,默認為false,等同于MouseEvent.altKey屬性。
- metaKey,設置是否按下meta鍵,默認為false,等同于MouseEvent.metaKey屬性。
- button,設置按下了哪一個鼠標按鍵,默認為0。-1表示沒有按鍵,0表示按下主鍵(通常是左鍵),1表示按下輔助鍵(通常是中間的鍵),2表示按下次要鍵(通常是右鍵)。
- buttons,設置按下了鼠標哪些鍵,是一個3個比特位的二進制值,默認為0。1表示按下主鍵(通常是左鍵),2表示按下次要鍵(通常是右鍵),4表示按下輔助鍵(通常是中間的鍵)。
- relatedTarget,設置一個Element節點,在mouseenter和mouseover事件時,表示鼠標剛剛離開的那個Element節點,在mouseout和mouseleave事件時,表示鼠標正在進入的那個Element節點。默認為null,等同于MouseEvent.relatedTarget屬性。
以下屬性也是可配置的,都繼承自UIEvent構造函數和Event構造函數。
- bubbles,布爾值,設置事件是否冒泡,默認為false,等同于Event.bubbles屬性。
- cancelable,布爾值,設置事件是否可取消,默認為false,等同于Event.cancelable屬性。
- view,設置事件的視圖,一般是window或document.defaultView,等同于Event.view屬性。
- detail,設置鼠標點擊的次數,等同于Event.detail屬性。
下面是一個例子。
```javascript
function simulateClick() {
var event = new MouseEvent('click', {
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
cb.dispatchEvent(event);
}
```
上面代碼生成一個鼠標點擊事件,并觸發該事件。
以下介紹MouseEvent實例的屬性。
### altKey,ctrlKey,metaKey,shiftKey
以下屬性返回一個布爾值,表示鼠標事件發生時,是否按下某個鍵。
- altKey屬性:alt鍵
- ctrlKey屬性:key鍵
- metaKey屬性:Meta鍵(Mac鍵盤是一個四瓣的小花,Windows鍵盤是Windows鍵)
- shiftKey屬性:Shift鍵
```javascript
// HTML代碼為
// <body onclick="showkey(event);">
function showKey(e){
console.log("ALT key pressed: " + e.altKey);
console.log("CTRL key pressed: " + e.ctrlKey);
console.log("META key pressed: " + e.metaKey);
console.log("SHIFT key pressed: " + e.shiftKey);
}
```
上面代碼中,點擊網頁會輸出是否同時按下Alt鍵。
### button,buttons
以下屬性返回事件的鼠標鍵信息。
**(1)button**
button屬性返回一個數值,表示按下了鼠標哪個鍵。
- -1:沒有按下鍵。
- 0:按下主鍵(通常是左鍵)。
- 1:按下輔助鍵(通常是中鍵或者滾輪鍵)。
- 2:按下次鍵(通常是右鍵)。
```javascript
// HTML代碼為
// <button onmouseup="whichButton(event);">點擊</button>
var whichButton = function (e) {
switch (e.button) {
case 0:
console.log('Left button clicked.');
break;
case 1:
console.log('Middle button clicked.');
break;
case 2:
console.log('Right button clicked.');
break;
default:
console.log('Unexpected code: ' + e.button);
}
}
```
**(2)buttons**
buttons屬性返回一個3個比特位的值,表示同時按下了哪些鍵。它用來處理同時按下多個鼠標鍵的情況。
- 1:二進制為001,表示按下左鍵。
- 2:二進制為010,表示按下右鍵。
- 4:二進制為100,表示按下中鍵或滾輪鍵。
同時按下多個鍵的時候,每個按下的鍵對應的比特位都會有值。比如,同時按下左鍵和右鍵,會返回3(二進制為011)。
### clientX,clientY,movementX,movementY,screenX,screenY
以下屬性與事件的位置相關。
**(1)clientX,clientY**
clientX屬性返回鼠標位置相對于瀏覽器窗口左上角的水平坐標,單位為像素,與頁面是否橫向滾動無關。
clientY屬性返回鼠標位置相對于瀏覽器窗口左上角的垂直坐標,單位為像素,與頁面是否縱向滾動無關。
```javascript
// HTML代碼為
// <body onmousedown="showCoords(event)">
function showCoords(evt){
console.log(
"clientX value: " + evt.clientX + "\n" +
"clientY value: " + evt.clientY + "\n"
);
}
```
**(2)movementX,movementY**
movementX屬性返回一個水平位移,單位為像素,表示當前位置與上一個mousemove事件之間的水平距離。在數值上,等于currentEvent.movementX = currentEvent.screenX - previousEvent.screenX。
movementY屬性返回一個垂直位移,單位為像素,表示當前位置與上一個mousemove事件之間的垂直距離。在數值上,等于currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。
**(3)screenX,screenY**
screenX屬性返回鼠標位置相對于屏幕左上角的水平坐標,單位為像素。
screenY屬性返回鼠標位置相對于屏幕左上角的垂直坐標,單位為像素。
```javascript
// HTML代碼為
// <body onmousedown="showCoords(event)">
function showCoords(evt){
console.log(
"screenX value: " + evt.screenX + "\n"
+ "screenY value: " + evt.screenY + "\n"
);
}
```
### relatedTarget
relatedTarget屬性返回事件的次要相關節點。對于那些沒有次要相關節點的事件,該屬性返回null。
下表列出不同事件的target屬性和relatedTarget屬性含義。
|事件名稱 |target屬性 |relatedTarget屬性 |
|---------|-----------|------------------|
|focusin |接受焦點的節點 |喪失焦點的節點 |
|focusout |喪失焦點的節點 |接受焦點的節點 |
|mouseenter |將要進入的節點 |將要離開的節點 |
|mouseleave |將要離開的節點 |將要進入的節點 |
|mouseout |將要離開的節點 |將要進入的節點 |
|mouseover |將要進入的節點 |將要離開的節點 |
|dragenter |將要進入的節點 |將要離開的節點 |
|dragexit |將要離開的節點 |將要進入的節點 |
下面是一個例子。
```javascript
// HTML代碼為
// <div id="outer" style="height:50px;width:50px;border-width:1px solid black;">
// <div id="inner" style="height:25px;width:25px;border:1px solid black;"></div>
// </div>
var inner = document.getElementById("inner");
inner.addEventListener("mouseover", function (){
console.log('進入' + event.target.id + " 離開" + event.relatedTarget.id);
});
inner.addEventListener("mouseenter", function (){
console.log('進入' + event.target.id + " 離開" + event.relatedTarget.id);
});
inner.addEventListener("mouseout", function (){
console.log('離開' + event.target.id + " 進入" + event.relatedTarget.id);
});
inner.addEventListener("mouseleave", function (){
console.log('離開' + event.target.id + " 進入" + event.relatedTarget.id);
});
// 鼠標從outer進入inner,輸出
// 進入inner 離開outer
// 進入inner 離開outer
// 鼠標從inner進入outer,輸出
// 離開inner 進入outer
// 離開inner 進入outer
```
### wheel事件
wheel事件是與鼠標滾輪相關的事件,目前只有一個wheel事件。用戶滾動鼠標的滾輪,就觸發這個事件。
該事件除了繼承了MouseEvent、UIEvent、Event的屬性,還有幾個自己的屬性。
- deltaX:返回一個數值,表示滾輪的水平滾動量。
- deltaY:返回一個數值,表示滾輪的垂直滾動量。
- deltaZ:返回一個數值,表示滾輪的Z軸滾動量。
- deltaMode:返回一個數值,表示滾動的單位,適用于上面三個屬性。0表示像素,1表示行,2表示頁。
瀏覽器提供一個WheelEvent構造函數,可以用來生成滾輪事件的實例。它接受兩個參數,第一個是事件名稱,第二個是配置對象。
```javascript
var syntheticEvent = new WheelEvent("syntheticWheel", {"deltaX": 4, "deltaMode": 0});
```
## 鍵盤事件
鍵盤事件用來描述鍵盤行為,主要有keydown、keypress、keyup三個事件。
- keydown:按下鍵盤時觸發該事件。
- keypress:只要按下的鍵并非Ctrl、Alt、Shift和Meta,就接著觸發keypress事件。
- keyup:松開鍵盤時觸發該事件。
下面是一個例子,對文本框設置keypress監聽函數,只允許輸入數字。
```javascript
// HTML代碼為
// <input type="text"
// name="myInput"
// onkeypress="return numbersOnly(this, event);"
// onpaste="return false;"
// />
function numbersOnly(oToCheckField, oKeyEvent) {
return oKeyEvent.charCode === 0
|| /\d/.test(String.fromCharCode(oKeyEvent.charCode));
}
```
如果用戶一直按鍵不松開,就會連續觸發鍵盤事件,觸發的順序如下。
1. keydown
1. keypress
1. keydown
1. keypress
1. (重復以上過程)
1. keyup
鍵盤事件使用KeyboardEvent對象表示,該對象繼承了UIEvent和MouseEvent對象。瀏覽器提供KeyboardEvent構造函數,用來新建鍵盤事件的實例。
```javascript
event = new KeyboardEvent(typeArg, KeyboardEventInit);
```
KeyboardEvent構造函數的第一個參數是一個字符串,表示事件類型,第二個參數是一個事件配置對象,可配置以下字段。
- key,對應KeyboardEvent.key屬性,默認為空字符串。
- ctrlKey,對應KeyboardEvent.ctrlKey屬性,默認為false。
- shiftKey,對應KeyboardEvent.shiftKey屬性,默認為false。
- altKey,對應KeyboardEvent.altKey屬性,默認為false。
- metaKey,對應KeyboardEvent.metaKey屬性,默認為false。
下面就是KeyboardEvent實例的屬性介紹。
### altKey,ctrlKey,metaKey,shiftKey
以下屬性返回一個布爾值,表示是否按下對應的鍵。
- altKey:alt鍵
- ctrlKey:ctrl鍵
- metaKey:meta鍵(mac系統是一個四瓣的小花,windows系統是windows鍵)
- shiftKey:shift鍵
```javascript
function showChar(e){
console.log("ALT: " + e.altKey);
console.log("CTRL: " + e.ctrlKey);
console.log("Meta: " + e.metaKey);
console.log("Meta: " + e.shiftKey);
}
```
### key,charCode
key屬性返回一個字符串,表示按下的鍵名。如果同時按下一個控制鍵和一個符號鍵,則返回符號鍵的鍵名。比如,按下Ctrl+a,則返回a。如果無法識別鍵名,則返回字符串Unidentified。
主要功能鍵的鍵名(不同的瀏覽器可能有差異):Backspace,Tab,Enter,Shift,Control,Alt,CapsLock,CapsLock,Esc,Spacebar,PageUp,PageDown,End,Home,Left,Right,Up,Down,PrintScreen,Insert,Del,Win,F1~F12,NumLock,Scroll等。
charCode屬性返回一個數值,表示keypress事件按鍵的Unicode值,keydown和keyup事件不提供這個屬性。注意,該屬性已經從標準移除,雖然瀏覽器還支持,但應該盡量不使用。
## 進度事件
進度事件用來描述一個事件進展的過程,比如XMLHttpRequest對象發出的HTTP請求的過程、<img>、<audio>、<video>、<style>、<link>加載外部資源的過程。下載和上傳都會發生進度事件。
進度事件有以下幾種。
- abort事件:當進度事件被中止時觸發。如果發生錯誤,導致進程中止,不會觸發該事件。
- error事件:由于錯誤導致資源無法加載時觸發。
- load事件:進度成功結束時觸發。
- loadstart事件:進度開始時觸發。
- loadend事件:進度停止時觸發,發生順序排在error事件\abort事件\load事件后面。
- progress事件:當操作處于進度之中,由傳輸的數據塊不斷觸發。
- timeout事件:進度超過限時觸發。
```javascript
image.addEventListener('load', function(event) {
image.classList.add('finished');
});
image.addEventListener('error', function(event) {
image.style.display = 'none';
});
```
上面代碼在圖片元素加載完成后,為圖片元素的class屬性添加一個值“finished”。如果加載失敗,就把圖片元素的樣式設置為不顯示。
有時候,圖片加載會在腳本運行之前就完成,尤其是當腳本放置在網頁底部的時候,因此有可能使得load和error事件的監聽函數根本不會被執行。所以,比較可靠的方式,是用complete屬性先判斷一下是否加載完成。
```javascript
function loaded() {
// code after image loaded
}
if (image.complete) {
loaded();
} else {
image.addEventListener('load', loaded);
}
```
由于DOM沒有提供像complete屬性那樣的,判斷是否發生加載錯誤的屬性,所以error事件的監聽函數最好放在img元素的HTML屬性中,這樣才能保證發生加載錯誤時百分之百會執行。
```html
<img src="/wrong/url" onerror="this.style.display='none';" />
```
error事件有一個特殊的性質,就是不會冒泡。這樣的設計是正確的,防止引發父元素的error事件監聽函數。
進度事件使用ProgressEvent對象表示。ProgressEvent實例有以下屬性。
- lengthComputable:返回一個布爾值,表示當前進度是否具有可計算的長度。如果為false,就表示當前進度無法測量。
- total:返回一個數值,表示當前進度的總長度。如果是通過HTTP下載某個資源,表示內容本身的長度,不含HTTP頭部的長度。如果lengthComputable屬性為false,則total屬性就無法取得正確的值。
- loaded:返回一個數值,表示當前進度已經完成的數量。該屬性除以total屬性,就可以得到目前進度的百分比。
下面是一個例子。
```javascript
var xhr = new XMLHttpRequest();
xhr.addEventListener("progress", updateProgress, false);
xhr.addEventListener("load", transferComplete, false);
xhr.addEventListener("error", transferFailed, false);
xhr.addEventListener("abort", transferCanceled, false);
xhr.open();
function updateProgress (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
} else {
console.log('不能計算進度');
}
}
function transferComplete(e) {
console.log('傳輸結束');
}
function transferFailed(evt) {
console.log('傳輸過程中發生錯誤');
}
function transferCanceled(evt) {
console.log('用戶取消了傳輸');
}
```
loadend事件的監聽函數,可以用來取代abort事件/load事件/error事件的監聽函數。
```javascript
req.addEventListener("loadend", loadEnd, false);
function loadEnd(e) {
console.log('傳輸結束,成功失敗未知');
}
```
loadend事件本身不提供關于進度結束的原因,但可以用它來做所有進度結束場景都需要做的一些操作。
另外,上面是下載過程的進度事件,還存在上傳過程的進度事件。這時所有監聽函數都要放在XMLHttpRequest.upload對象上面。
```javascript
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", updateProgress, false);
xhr.upload.addEventListener("load", transferComplete, false);
xhr.upload.addEventListener("error", transferFailed, false);
xhr.upload.addEventListener("abort", transferCanceled, false);
xhr.open();
```
瀏覽器提供一個ProgressEvent構造函數,用來生成進度事件的實例。
```javascript
progressEvent = new ProgressEvent(type, {
lengthComputable: aBooleanValue,
loaded: aNumber,
total: aNumber
});
```
上面代碼中,ProgressEvent構造函數的第一個參數是事件類型(字符串),第二個參數是配置對象,用來指定lengthComputable屬性(默認值為false)、loaded屬性(默認值為0)、total屬性(默認值為0)。
## 拖拉事件
拖拉指的是,用戶在某個對象上按下鼠標鍵不放,拖動它到另一個位置,然后釋放鼠標鍵,將該對象放在那里。
拖拉的對象有好幾種,包括Element節點、圖片、鏈接、選中的文字等等。在HTML網頁中,除了Element節點默認不可以拖拉,其他(圖片、鏈接、選中的文字)都是可以直接拖拉的。為了讓Element節點可拖拉,可以將該節點的draggable屬性設為true。
```html
<div draggable="true">
此區域可拖拉
</div>
```
draggable屬性可用于任何Element節點,但是圖片(img元素)和鏈接(a元素)不加這個屬性,就可以拖拉。對于它們,用到這個屬性的時候,往往是將其設為false,防止拖拉。
注意,一旦某個Element節點的draggable屬性設為true,就無法再用鼠標選中該節點內部的文字或子節點了。
### 事件種類
當Element節點或選中的文本被拖拉時,就會持續觸發拖拉事件,包括以下一些事件。
- **drag事件**:拖拉過程中,在被拖拉的節點上持續觸發。
- **dragstart事件**:拖拉開始時在被拖拉的節點上觸發,該事件的target屬性是被拖拉的節點。通常應該在這個事件的監聽函數中,指定拖拉的數據。
- **dragend事件**:拖拉結束時(釋放鼠標鍵或按下escape鍵)在被拖拉的節點上觸發,該事件的target屬性是被拖拉的節點。它與dragStart事件,在同一個節點上觸發。不管拖拉是否跨窗口,或者中途被取消,dragend事件總是會觸發的。
- **dragenter事件**:拖拉進入當前節點時,在當前節點上觸發,該事件的target屬性是當前節點。通常應該在這個事件的監聽函數中,指定是否允許在當前節點放下(drop)拖拉的數據。如果當前節點沒有該事件的監聽函數,或者監聽函數不執行任何操作,就意味著不允許在當前節點放下數據。在視覺上顯示拖拉進入當前節點,也是在這個事件的監聽函數中設置。
- **dragover事件**:拖拉到當前節點上方時,在當前節點上持續觸發,該事件的target屬性是當前節點。該事件與dragenter事件基本類似,默認會重置當前的拖拉事件的效果(DataTransfer對象的dropEffect屬性)為none,即不允許放下被拖拉的節點,所以如果允許在當前節點drop數據,通常會使用preventDefault方法,取消重置拖拉效果為none。
- **dragleave事件**:拖拉離開當前節點范圍時,在當前節點上觸發,該事件的target屬性是當前節點。在視覺上顯示拖拉離開當前節點,就在這個事件的監聽函數中設置。
- **drop事件**:被拖拉的節點或選中的文本,釋放到目標節點時,在目標節點上觸發。注意,如果當前節點不允許drop,即使在該節點上方松開鼠標鍵,也不會觸發該事件。如果用戶按下Escape鍵,取消這個操作,也不會觸發該事件。該事件的監聽函數負責取出拖拉數據,并進行相關處理。
關于拖拉事件,有以下幾點注意事項。
- 拖拉過程只觸發以上這些拖拉事件,盡管鼠標在移動,但是鼠標事件不會觸發。
- 將文件從操作系統拖拉進瀏覽器,不會觸發dragStart和dragend事件。
- dragenter和dragover事件的監聽函數,用來指定可以放下(drop)拖拉的數據。由于網頁的大部分區域不適合作為drop的目標節點,所以這兩個事件的默認設置為當前節點不允許drop。如果想要在目標節點上drop拖拉的數據,首先必須阻止這兩個事件的默認行為,或者取消這兩個事件。
```html
<div ondragover="return false">
<div ondragover="event.preventDefault()">
```
上面代碼中,如果不取消拖拉事件或者阻止默認行為,就不可能在div節點上drop被拖拉的節點。
拖拉事件用一個DragEvent對象表示,該對象繼承MouseEvent對象,因此也就繼承了UIEvent和Event對象。DragEvent對象只有一個獨有的屬性DataTransfer,其他都是繼承的屬性。DataTransfer屬性用來讀寫拖拉事件中傳輸的數據,詳見下文《DataTransfer對象》的部分。
下面的例子展示,如何動態改變被拖動節點的背景色。
```javascript
div.addEventListener("dragstart", function(e) {
this.style.backgroundColor = "red";
}, false);
div.addEventListener("dragend", function(e) {
this.style.backgroundColor = "green";
}, false);
```
上面代碼中,div節點被拖動時,背景色會變為紅色,拖動結束,又變回綠色。
下面是一個例子,顯示如何實現將一個節點從當前父節點,拖拉到另一個父節點中。
```javascript
// HTML代碼為
// <div class="dropzone">
// <div id="draggable" draggable="true">
// 該節點可拖拉
// </div>
// </div>
// <div class="dropzone"></div>
// <div class="dropzone"></div>
// <div class="dropzone"></div>
// 被拖拉節點
var dragged;
document.addEventListener("dragstart", function( event ) {
// 保存被拖拉節點
dragged = event.target;
// 被拖拉節點的背景色變透明
event.target.style.opacity = 0.5;
// 兼容Firefox
event.dataTransfer.setData('text/plain', 'anything');
}, false);
document.addEventListener('dragend', function( event ) {
// 被拖拉節點的背景色恢復正常
event.target.style.opacity = '';
}, false);
document.addEventListener('dragover', function( event ) {
// 防止拖拉效果被重置,允許被拖拉的節點放入目標節點
event.preventDefault();
}, false);
document.addEventListener('dragenter', function( event ) {
// 目標節點的背景色變紫色
// 由于該事件會冒泡,所以要過濾節點
if ( event.target.className == 'dropzone' ) {
event.target.style.background = 'purple';
}
}, false);
document.addEventListener('dragleave', function( event ) {
// 目標節點的背景色恢復原樣
if ( event.target.className == 'dropzone' ) {
event.target.style.background = "";
}
}, false);
document.addEventListener('drop', function( event ) {
// 防止事件默認行為(比如某些Elment節點上可以打開鏈接)
event.preventDefault();
if ( event.target.className === 'dropzone' ) {
// 恢復目標節點背景色
event.target.style.background = '';
// 將被拖拉節點插入目標節點
dragged.parentNode.removeChild( dragged );
event.target.appendChild( dragged );
}
}, false);
```
### DataTransfer對象概述
所有的拖拉事件都有一個dataTransfer屬性,用來保存需要傳遞的數據。這個屬性的值是一個DataTransfer對象。
拖拉的數據保存兩方面的數據:數據的種類(又稱格式)和數據的值。數據的種類是一個MIME字符串,比如 text/plain或者image/jpeg,數據的值是一個字符串。一般來說,如果拖拉一段文本,則數據默認就是那段文本;如果拖拉一個鏈接,則數據默認就是鏈接的URL。
當拖拉事件開始的時候,可以提供數據類型和數據值;在拖拉過程中,通過dragenter和dragover事件的監聽函數,檢查數據類型,以確定是否允許放下(drop)被拖拉的對象。比如,在只允許放下鏈接的區域,檢查拖拉的數據類型是否為text/uri-list。
發生drop事件時,監聽函數取出拖拉的數據,對其進行處理。
### DataTransfer對象的屬性
DataTransfer對象有以下屬性。
**(1)dropEffect**
dropEffect屬性設置放下(drop)被拖拉節點時的效果,可能的值包括copy(復制被拖拉的節點)、move(移動被拖拉的節點)、link(創建指向被拖拉的節點的鏈接)、none(無法放下被拖拉的節點)。設置除此以外的值,都是無效的。
```javascript
target.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
});
```
dropEffect屬性一般在dragenter和dragover事件的監聽函數中設置,對于dragstart、drag、dragleave這三個事件,該屬性不起作用。進入目標節點后,拖拉行為會初始化成用戶設定的效果,用戶可以通過按下Shift鍵和Control鍵,改變初始設置,在copy、move、link三種效果中切換。
鼠標箭頭會根據dropEffect屬性改變形狀,提示目前正處于哪一種效果。這意味著,通過鼠標就能判斷是否可以在當前節點drop被拖拉的節點。
**(2)effectAllowed**
effectAllowed屬性設置本次拖拉中允許的效果,可能的值包括copy(復制被拖拉的節點)、move(移動被拖拉的節點)、link(創建指向被拖拉節點的鏈接)、copyLink(允許copy或link)、copyMove(允許copy或move)、linkMove(允許link或move)、all(允許所有效果)、none(無法放下被拖拉的節點)、uninitialized(默認值,等同于all)。如果某種效果是不允許的,用戶就無法在目標節點中達成這種效果。
dragstart事件的監聽函數,可以設置被拖拉節點允許的效果;dragenter和dragover事件的監聽函數,可以設置目標節點允許的效果。
```javascript
event.dataTransfer.effectAllowed = "copy";
```
dropEffect屬性和effectAllowed屬性,往往配合使用。
```javascript
event.dataTransfer.effectAllowed = "copyMove";
event.dataTransfer.dropEffect = "copy";
```
上面代碼中,copy是指定的效果,但是可以通過Shift或Ctrl鍵(根據平臺而定),將效果切換成move。
只要dropEffect屬性和effectAllowed屬性之中,有一個為none,就無法在目標節點上完成drop操作。
**(3)files**
files屬性是一個FileList對象,包含一組本地文件,可以用來在拖拉操作中傳送。如果本次拖拉不涉及文件,則屬性為空的FileList對象。
下面就是一個接收拖拉文件的例子。
```javascript
// HTML代碼為
// <div id="output" style="min-height: 200px;border: 1px solid black;">
// 文件拖拉到這里
// </div>
var div = document.getElementById('output');
div.addEventListener("dragenter", function( event ) {
div.textContent = '';
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("dragover", function( event ) {
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("drop", function( event ) {
event.stopPropagation();
event.preventDefault();
var files = event.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
div.textContent += files[i].name + ' ' + files[i].size + '字節\n';
}
}, false);
```
上面代碼中,通過files屬性讀取拖拉文件的信息。如果想要讀取文件內容,就要使用FileReader對象。
```javascript
div.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
var fileList = e.dataTransfer.files;
if (fileList.length > 0) {
var file = fileList[0];
var reader = new FileReader();
reader.onloadend = function(e) {
if (e.target.readyState == FileReader.DONE) {
var content = reader.result;
contentDiv.innerHTML = "File: " + file.name + "\n\n" + content;
}
}
reader.readAsBinaryString(file);
}
});
```
**(4)types**
types屬性是一個數組,保存每一次拖拉的數據格式,比如拖拉文件,則格式信息就為File。
下面是一個例子,通過檢查dataTransfer屬性的類型,決定是否允許在當前節點執行drop操作。
```javascript
function contains(list, value){
for( var i = 0; i < list.length; ++i ){
if(list[i] === value) return true;
}
return false;
}
function doDragOver(event){
var isLink = contains( event.dataTransfer.types, "text/uri-list");
if (isLink) event.preventDefault();
}
```
上面代碼中,只有當被拖拉的節點是一個鏈接時,才允許在當前節點放下。
### DataTransfer對象的方法
DataTransfer對象有以下方法。
**(1)setData()**
setData方法用來設置事件所帶有的指定類型的數據。它接受兩個參數,第一個是數據類型,第二個是具體數據。如果指定的類型在現有數據中不存在,則該類型將寫入types屬性;如果已經存在,在該類型的現有數據將被替換。
```javascript
event.dataTransfer.setData("text/plain", "Text to drag");
```
上面代碼為事件加入純文本格式的數據。
如果拖拉文本框或者拖拉選中的文本,會默認將文本數據添加到dataTransfer屬性,不用手動指定。
```html
<div draggable="true" ondragstart="
event.dataTransfer.setData('text/plain', 'bbb')">
aaa
</div>
```
上面代碼中,拖拉數據實際上是bbb,而不是aaa。
下面是添加其他類型的數據。由于text/plain是最普遍支持的格式,為了保證兼容性,建議最后總是將數據保存一份純文本的格式。
```javascript
var dt = event.dataTransfer;
// 添加鏈接
dt.setData("text/uri-list", "http://www.example.com");
dt.setData("text/plain", "http://www.example.com");
// 添加HTML代碼
dt.setData("text/html", "Hello there, <strong>stranger</strong>");
dt.setData("text/plain", "Hello there, <strong>stranger</strong>");
// 添加圖像的URL
dt.setData("text/uri-list", imageurl);
dt.setData("text/plain", imageurl);
```
可以一次提供多種格式的數據。
```javascript
var dt = event.dataTransfer;
dt.setData("application/x-bookmark", bookmarkString);
dt.setData("text/uri-list", "http://www.example.com");
dt.setData("text/plain", "http://www.example.com");
```
上面代碼中,通過在同一個事件上面,存放三種類型的數據,使得拖拉事件可以在不同的對象上面,drop不同的值。注意,第一種格式是一個自定義格式,瀏覽器默認無法讀取,這意味著,只有某個部署了特定代碼的節點,才可能drop(讀取到)這個數據。
**(2)getData()**
getData方法接受一個字符串(表示數據類型)作為參數,返回事件所帶的指定類型的數據(通常是用setData方法添加的數據)。如果指定類型的數據不存在,則返回空字符串。通常只有drop事件觸發后,才能取出數據。如果取出另一個域名存放的數據,將會報錯。
下面是一個drop事件的監聽函數,用來取出指定類型的數據。
```javascript
function onDrop(event){
var data = event.dataTransfer.getData("text/plain");
event.target.textContent = data;
event.preventDefault();
}
```
上面代碼取出拖拉事件的文本數據,將其替換成當前節點的文本內容。注意,這時還必須取消瀏覽器的默認行為,因為假如用戶拖拉的是一個鏈接,瀏覽器默認會在當前窗口打開這個鏈接。
getData方法返回的是一個字符串,如果其中包含多項數據,就必須手動解析。
```javascript
function doDrop(event){
var lines = event.dataTransfer.getData("text/uri-list").split("\n");
for (let line of lines) {
let link = document.createElement("a");
link.href = line;
link.textContent = line;
event.target.appendChild(link);
}
event.preventDefault();
}
```
上面代碼中,getData方法返回的是一組鏈接,就必須自行解析。
類型值指定為URL,可以取出第一個有效鏈接。
```javascript
var link = event.dataTransfer.getData("URL");
```
下面是一次性取出多種類型的數據。
```javascript
function doDrop(event){
var types = event.dataTransfer.types;
var supportedTypes = ["text/uri-list", "text/plain"];
types = supportedTypes.filter(function (value) types.includes(value));
if (types.length)
var data = event.dataTransfer.getData(types[0]);
event.preventDefault();
}
```
**(3)clearData()**
clearData方法接受一個字符串(表示數據類型)作為參數,刪除事件所帶的指定類型的數據。如果沒有指定類型,則刪除所有數據。如果指定類型不存在,則原數據不受影響。
```javascript
event.dataTransfer.clearData("text/uri-list");
```
上面代碼清除事件所帶的URL數據。
**(4)setDragImage()**
拖動過程中(dragstart事件觸發后),瀏覽器會顯示一張圖片跟隨鼠標一起移動,表示被拖動的節點。這張圖片是自動創造的,通常顯示為被拖動節點的外觀,不需要自己動手設置。setDragImage方法可以用來自定義這張圖片,它接受三個參數,第一個是img圖片元素或者canvas元素,如果省略或為null則使用被拖動的節點的外觀,第二個和第三個參數為鼠標相對于該圖片左上角的橫坐標和右坐標。
下面是一個例子。
```javascript
// HTML代碼為
// <div id="drag-with-image" class="dragdemo" draggable="true">
drag me
// </div>
var div = document.getElementById("drag-with-image");
div.addEventListener("dragstart", function(e) {
var img = document.createElement("img");
img.src = "http://path/to/img";
e.dataTransfer.setDragImage(img, 0, 0);
}, false);
```
## 觸摸事件
觸摸API由三個對象組成。
- Touch
- TouchList
- TouchEvent
Touch對象表示觸摸點(一根手指或者一根觸摸筆),用來描述觸摸動作,包括位置、大小、形狀、壓力、目標元素等屬性。有時,觸摸動作由多個觸摸點(多根手指或者多根觸摸筆)組成,多個觸摸點的集合由TouchList對象表示。TouchEvent對象代表由觸摸引發的事件,只有觸摸屏才會引發這一類事件。
很多時候,觸摸事件和鼠標事件同時觸發,即使這個時候并沒有用到鼠標。這是為了讓那些只定義鼠標事件、沒有定義觸摸事件的代碼,在觸摸屏的情況下仍然能用。如果想避免這種情況,可以用preventDefault方法阻止發出鼠標事件。
### Touch對象
Touch對象代表一個觸摸點。觸摸點可能是一根手指,也可能是一根觸摸筆。它有以下屬性。
**(1)identifier**
identifier屬性表示Touch實例的獨一無二的識別符。它在整個觸摸過程中保持不變。
```javascript
var id = touchItem.identifier;
```
TouchList對象的identifiedTouch方法,可以根據這個屬性,從一個集合里面取出對應的Touch對象。
**(2)screenX,screenY,clientX,clientY,pageX,pageY**
screenX屬性和screenY屬性,分別表示觸摸點相對于屏幕左上角的橫坐標和縱坐標,與頁面是否滾動無關。
clientX屬性和clientY屬性,分別表示觸摸點相對于瀏覽器視口左上角的橫坐標和縱坐標,與頁面是否滾動無關。
pageX屬性和pageY屬性,分別表示觸摸點相對于當前頁面左上角的橫坐標和縱坐標,包含了頁面滾動帶來的位移。
**(3)radiusX,radiusY,rotationAngle**
radiusX屬性和radiusY屬性,分別返回觸摸點周圍受到影響的橢圓范圍的X軸和Y軸,單位為像素。
rotationAngle屬性表示觸摸區域的橢圓的旋轉角度,單位為度數,在0到90度之間。
上面這三個屬性共同定義了用戶與屏幕接觸的區域,對于描述手指這一類非精確的觸摸,很有幫助。指尖接觸屏幕,觸摸范圍會形成一個橢圓,這三個屬性就用來描述這個橢圓區域。
**(4)force**
force屬性返回一個0到1之間的數值,表示觸摸壓力。0代表沒有壓力,1代表硬件所能識別的最大壓力。
**(5)target**
target屬性返回一個Element節點,代表觸摸發生的那個節點。
### TouchList對象
TouchList對象是一個類似數組的對象,成員是與某個觸摸事件相關的所有觸摸點。比如,用戶用三根手指觸摸,產生的TouchList對象就有三個成員,每根手指對應一個Touch對象。
TouchList實例的length屬性,返回TouchList對象的成員數量。
TouchList實例的identifiedTouch方法和item方法,分別使用id屬性和索引值(從0開始)作為參數,取出指定的Touch對象。
### TouchEvent對象
TouchEvent對象繼承Event對象和UIEvent對象,表示觸摸引發的事件。除了被繼承的屬性以外,它還有一些自己的屬性。
**(1)鍵盤相關屬性**
以下屬性都為只讀屬性,返回一個布爾值,表示觸摸的同時,是否按下某個鍵。
- altKey 是否按下alt鍵
- ctrlKey 是否按下ctrl鍵
- metaKey 是否按下meta鍵
- shiftKey 是否按下shift鍵
**(2)changedTouches**
changedTouches屬性返回一個TouchList對象,包含了由當前觸摸事件引發的所有Touch對象(即相關的觸摸點)。
對于touchstart事件,它代表被激活的觸摸點;對于touchmove事件,代表發生變化的觸摸點;對于touchend事件,代表消失的觸摸點(即不再被觸碰的點)。
```javascript
var touches = touchEvent.changedTouches;
```
**(3)targetTouches**
targetTouches屬性返回一個TouchList對象,包含了觸摸的目標Element節點內部,所有仍然處于活動狀態的觸摸點。
```javascript
var touches = touchEvent.targetTouches;
```
**(4)touches**
touches屬性返回一個TouchList對象,包含了所有仍然處于活動狀態的觸摸點。
```javascript
var touches = touchEvent.touches;
```
### 觸摸事件的種類
觸摸引發的事件,有以下幾類。可以通過TouchEvent.type屬性,查看到底發生的是哪一種事件。
- touchstart:用戶接觸觸摸屏時觸發,它的target屬性返回發生觸摸的Element節點。
- touchend:用戶不再接觸觸摸屏時(或者移出屏幕邊緣時)觸發,它的target屬性與touchstart事件的target屬性是一致的,它的changedTouches屬性返回一個TouchList對象,包含所有不再觸摸的觸摸點(Touch對象)。
- touchmove:用戶移動觸摸點時觸發,它的target屬性與touchstart事件的target屬性一致。如果觸摸的半徑、角度、力度發生變化,也會觸發該事件。
- touchcancel:觸摸點取消時觸發,比如在觸摸區域跳出一個情態窗口(modal window)、觸摸點離開了文檔區域(進入瀏覽器菜單欄區域)、用戶放置更多的觸摸點(自動取消早先的觸摸點)。
下面是一個例子。
```javascript
var el = document.getElementsByTagName("canvas")[0];
el.addEventListener("touchstart", handleStart, false);
el.addEventListener("touchmove", handleMove, false);
function handleStart(evt) {
// 阻止瀏覽器繼續處理觸摸事件,
// 也阻止發出鼠標事件
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
console.log(touches[i].pageX, touches[i].pageY);
}
}
function handleMove(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
var id = touches[i].identifier;
var touch = touches.identifiedTouch(id);
console.log(touch.pageX, touch.pageY);
}
}
```
## 表單事件
### Input事件,select事件,change事件
以下事件與表單成員的值變化有關。
**(1)input事件**
input事件當<input>、<textarea>的值發生變化時觸發。此外,打開contenteditable屬性的元素,只要值發生變化,也會觸發input事件。
input事件的一個特點,就是會連續觸發,比如用戶每次按下一次按鍵,就會觸發一次input事件。
**(2)select事件**
select事件當在<input>、<textarea>中選中文本時觸發。
```javascript
// HTML代碼為
// <input id="test" type="text" value="Select me!" />
var elem = document.getElementById('test');
elem.addEventListener('select', function() {
console.log('Selection changed!');
}, false);
```
**(3)Change事件**
Change事件當<input>、<select>、<textarea>的值發生變化時觸發。它與input事件的最大不同,就是不會連續觸發,只有當全部修改完成時才會觸發,而且input事件必然會引發change事件。具體來說,分成以下幾種情況。
- 激活單選框(radio)或復選框(checkbox)時觸發。
- 用戶提交時觸發。比如,從下列列表(select)完成選擇,在日期或文件輸入框完成選擇。
- 當文本框或textarea元素的值發生改變,并且喪失焦點時觸發。
下面是一個例子。
```javascript
// HTML代碼為
// <select size="1" onchange="changeEventHandler(event);">
// <option>chocolate</option>
// <option>strawberry</option>
// <option>vanilla</option>
// </select>
function changeEventHandler(event) {
console.log('You like ' + event.target.value + ' ice cream.');
}
```
### reset事件,submit事件
以下事件發生在表單對象上,而不是發生在表單的成員上。
**(1)reset事件**
reset事件當表單重置(所有表單成員變回默認值)時觸發。
**(2)submit事件**
submit事件當表單數據向服務器提交時觸發。注意,submit事件的發生對象是form元素,而不是button元素(即使它的類型是submit),因為提交的是表單,而不是按鈕。
## 文檔事件
### beforeunload事件,unload事件,load事件,error事件,pageshow事件,pagehide事件
以下事件與網頁的加載與卸載相關。
**(1)beforeunload事件**
beforeunload事件當窗口將要關閉,或者document和網頁資源將要卸載時觸發。它可以用來防止用戶不當心關閉網頁。
該事件的默認動作就是關閉當前窗口或文檔。如果在監聽函數中,調用了`event.preventDefault()`,或者對事件對象的returnValue屬性賦予一個非空的值,就會自動跳出一個確認框,讓用戶確認是否關閉網頁。如果用戶點擊“取消”按鈕,網頁就不會關閉。監聽函數所返回的字符串,會顯示在確認對話框之中。
```javascript
window.onbeforeunload = function() {
if (textarea.value != textarea.defaultValue) {
return '你確認要離開嗎?';
}
};
```
上面代碼表示,當用戶關閉網頁,會跳出一個確認對話框,上面顯示“你確認要離開嗎?”。
下面的兩種寫法,具有同樣效果。
```javascript
window.addEventListener('beforeunload', function( event ) {
event.returnValue = '你確認要離開嗎?';
});
// 等同于
window.addEventListener('beforeunload', function( event ) {
event.preventDefault();
});
```
上面代碼中,事件對象的returnValue屬性的值,將會成為確認框的提示文字。
只要定義了beforeunload事件的監聽函數,網頁不會被瀏覽器緩存。
**(2)unload事件**
unload事件在窗口關閉或者document對象將要卸載時觸發,發生在window、body、frameset等對象上面。它的觸發順序排在beforeunload、pagehide事件后面。unload事件只在頁面沒有被瀏覽器緩存時才會觸發,換言之,如果通過按下“前進/后退”導致頁面卸載,并不會觸發unload事件。
當unload事件發生時,document對象處于一個特殊狀態。所有資源依然存在,但是對用戶來說都不可見,UI互動(window.open、alert、confirm方法等)全部無效。這時即使拋出錯誤,也不能停止文檔的卸載。
```javascript
window.addEventListener('unload', function(event) {
console.log('文檔將要卸載');
});
```
如果在window對象上定義了該事件,網頁就不會被瀏覽器緩存。
**(3)load事件,error事件**
load事件在頁面加載成功時觸發,error事件在頁面加載失敗時觸發。注意,頁面從瀏覽器緩存加載,并不會觸發load事件。
這兩個事件實際上屬于進度事件,不僅發生在document對象,還發生在各種外部資源上面。瀏覽網頁就是一個加載各種資源的過程,圖像(image)、樣式表(style sheet)、腳本(script)、視頻(video)、音頻(audio)、Ajax請求(XMLHttpRequest)等等。這些資源和document對象、window對象、XMLHttpRequestUpload對象,都會觸發load事件和error事件。
**(4)pageshow事件,pagehide事件**
默認情況下,瀏覽器會在當前會話(session)緩存頁面,當用戶點擊“前進/后退”按鈕時,瀏覽器就會從緩存中加載頁面。
pageshow事件在頁面加載時觸發,包括第一次加載和從緩存加載兩種情況。如果要指定頁面每次加載(不管是不是從瀏覽器緩存)時都運行的代碼,可以放在這個事件的監聽函數。
第一次加載時,它的觸發順序排在load事件后面。從緩存加載時,load事件不會觸發,因為網頁在緩存中的樣子通常是load事件的監聽函數運行后的樣子,所以不必重復執行。同理,如果是從緩存中加載頁面,網頁內初始化的JavaScript腳本(比如DOMContentLoaded事件的監聽函數)也不會執行。
```javascript
window.addEventListener('pageshow', function(event) {
console.log('pageshow: ', event);
});
```
pageshow事件有一個persisted屬性,返回一個布爾值。頁面第一次加載時,這個屬性是false;當頁面從緩存加載時,這個屬性是true。
```javascript
window.addEventListener('pageshow', function(event){
if (event.persisted) {
// ...
}
});
```
pagehide事件與pageshow事件類似,當用戶通過“前進/后退”按鈕,離開當前頁面時觸發。它與unload事件的區別在于,如果在window對象上定義unload事件的監聽函數之后,頁面不會保存在緩存中,而使用pagehide事件,頁面會保存在緩存中。
pagehide事件的event對象有一個persisted屬性,將這個屬性設為true,就表示頁面要保存在緩存中;設為false,表示網頁不保存在緩存中,這時如果設置了unload事件的監聽函數,該函數將在pagehide事件后立即運行。
如果頁面包含frame或iframe元素,則frame頁面的pageshow事件和pagehide事件,都會在主頁面之前觸發。
### DOMContentLoaded事件,readystatechange事件
以下事件與文檔狀態相關。
**(1)DOMContentLoaded事件**
當HTML文檔下載并解析完成以后,就會在document對象上觸發DOMContentLoaded事件。這時,僅僅完成了HTML文檔的解析(整張頁面的DOM生成),所有外部資源(樣式表、腳本、iframe等等)可能還沒有下載結束。也就是說,這個事件比load事件,發生時間早得多。
```javascript
document.addEventListener("DOMContentLoaded", function(event) {
console.log("DOM生成");
});
```
注意,網頁的JavaScript腳本是同步執行的,所以定義DOMContentLoaded事件的監聽函數,應該放在所有腳本的最前面。否則腳本一旦發生堵塞,將推遲觸發DOMContentLoaded事件。
**(2)readystatechange事件**
readystatechange事件發生在Document對象和XMLHttpRequest對象,當它們的readyState屬性發生變化時觸發。
```javascript
document.onreadystatechange = function () {
if (document.readyState == "interactive") {
// ...
}
}
```
IE8不支持DOMContentLoaded事件,但是支持這個事件。因此,可以使用readystatechange事件,在低版本的IE中代替DOMContentLoaded事件。
### scroll事件,resize事件
以下事件與窗口行為有關。
**(1)scroll事件**
scroll事件在文檔或文檔元素滾動時觸發。
由于該事件會連續地大量觸發,所以它的監聽函數之中不應該有非常耗費計算的操作。推薦的做法是使用requestAnimationFrame或setTimeout控制該事件的觸發頻率,然后可以結合customEvent拋出一個新事件。
```javascript
(function() {
var throttle = function(type, name, obj) {
var obj = obj || window;
var running = false;
var func = function() {
if (running) { return; }
running = true;
requestAnimationFrame(function() {
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
};
// 將scroll事件重定義為optimizedScroll事件
throttle("scroll", "optimizedScroll");
})();
window.addEventListener("optimizedScroll", function() {
console.log("Resource conscious scroll callback!");
});
```
上面代碼中,throttle函數用于控制事件觸發頻率,requestAnimationFrame方法保證每次頁面重繪(每秒60次),只會觸發一次scroll事件的監聽函數。改用setTimeout方法,可以放置更大的時間間隔。
```javascript
(function() {
window.addEventListener("scroll", scrollThrottler, false);
var scrollTimeout;
function scrollThrottler() {
if ( !scrollTimeout ) {
scrollTimeout = setTimeout(function() {
scrollTimeout = null;
actualScrollHandler();
}, 66);
}
}
function actualScrollHandler() {
// ...
}
}());
```
上面代碼中,setTimeout指定scroll事件的監聽函數,每66毫秒觸發一次(每秒15次)。
**(2)resize事件**
resize事件在改變瀏覽器窗口大小時觸發,發生在window、body、frameset對象上面。
```javascript
var resizeMethod = function(){
if (document.body.clientWidth < 768) {
console.log('移動設備');
}
};
window.addEventListener("resize", resizeMethod, true);
```
該事件也會連續地大量觸發,所以最好像上面的scroll事件一樣,通過throttle函數控制事件觸發頻率。
### hashchange事件,popstate事件
以下事件與文檔的URL變化相關。
**(1)hashchange事件**
hashchange事件在URL的hash部分(即#號后面的部分,包括#號)發生變化時觸發。如果老式瀏覽器不支持該屬性,可以通過定期檢查location.hash屬性,模擬該事件,下面就是代碼。
```javascript
(function(window) {
if ( "onhashchange" in window.document.body ) { return; }
var location = window.location;
var oldURL = location.href;
var oldHash = location.hash;
// 每隔100毫秒檢查一下URL的hash
setInterval(function() {
var newURL = location.href;
var newHash = location.hash;
if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});
oldURL = newURL;
oldHash = newHash;
}
}, 100);
})(window);
```
hashchange事件對象除了繼承Event對象,還有oldURL屬性和newURL屬性,分別表示變化前后的URL。
**(2)popstate事件**
popstate事件在瀏覽器的history對象的當前記錄發生顯式切換時觸發。注意,調用history.pushState()或history.replaceState(),并不會觸發popstate事件。該事件只在用戶在history記錄之間顯式切換時觸發,比如鼠標點擊“后退/前進”按鈕,或者在腳本中調用history.back()、history.forward()、history.go()時觸發。
該事件對象有一個state屬性,保存history.pushState方法和history.replaceState方法為當前記錄添加的state對象。
```javascript
window.onpopstate = function(event) {
console.log("state: " + event.state);
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // state: {"page":1}
history.back(); // state: null
history.go(2); // state: {"page":3}
```
上面代碼中,pushState方法向history添加了兩條記錄,然后replaceState方法替換掉當前記錄。因此,連續兩次back方法,會讓當前條目退回到原始網址,它沒有附帶state對象,所以事件的state屬性為null,然后前進兩條記錄,又回到replaceState方法添加的記錄。
瀏覽器對于頁面首次加載,是否觸發popstate事件,處理不一樣,Firefox不觸發該事件。
### cut事件,copy事件,paste事件
以下三個事件屬于文本操作觸發的事件。
- cut事件:在將選中的內容從文檔中移除,加入剪貼板后觸發。
- copy事件:在選中的內容加入剪貼板后觸發。
- paste事件:在剪貼板內容被粘貼到文檔后觸發。
這三個事件都有一個clipboardData只讀屬性。該屬性存放剪貼的數據,是一個DataTransfer對象,具體的API接口和操作方法,請參見《觸摸事件》的DataTransfer對象章節。
### 焦點事件
焦點事件發生在Element節點和document對象上面,與獲得或失去焦點相關。它主要包括以下四個事件。
- focus事件:Element節點獲得焦點后觸發,該事件不會冒泡。
- blur事件:Element節點失去焦點后觸發,該事件不會冒泡。
- focusin事件:Element節點將要獲得焦點時觸發,發生在focus事件之前。該事件會冒泡。Firefox不支持該事件。
- focusout事件:Element節點將要失去焦點時觸發,發生在blur事件之前。該事件會冒泡。Firefox不支持該事件。
這四個事件的事件對象,帶有target屬性(返回事件的目標節點)和relatedTarget屬性(返回一個Element節點)。對于focusin事件,relatedTarget屬性表示失去焦點的節點;對于focusout事件,表示將要接受焦點的節點;對于focus和blur事件,該屬性返回null。
由于focus和blur事件不會冒泡,只能在捕獲階段觸發,所以addEventListener方法的第三個參數需要設為true。
```javascript
form.addEventListener("focus", function( event ) {
event.target.style.background = "pink";
}, true);
form.addEventListener("blur", function( event ) {
event.target.style.background = "";
}, true);
```
上面代碼設置表單的文本輸入框,在接受焦點時設置背景色,在失去焦點時去除背景色。
瀏覽器提供一個FocusEvent構造函數,可以用它生成焦點事件的實例。
```javascript
var focusEvent = new FocusEvent(typeArg, focusEventInit);
```
上面代碼中,FocusEvent構造函數的第一個參數為事件類型,第二個參數是可選的配置對象,用來配置FocusEvent對象。
## 自定義事件和事件模擬
除了瀏覽器預定義的那些事件,用戶還可以自定義事件,然后手動觸發。
```javascript
// 新建事件實例
var event = new Event('build');
// 添加監聽函數
elem.addEventListener('build', function (e) { ... }, false);
// 觸發事件
elem.dispatchEvent(event);
```
上面代碼觸發了自定義事件,該事件會層層向上冒泡。在冒泡過程中,如果有一個元素定義了該事件的監聽函數,該監聽函數就會觸發。
由于IE不支持這個API,如果在IE中自定義事件,需要使用后文的“老式方法”。
### CustomEvent()
Event構造函數只能指定事件名,不能在事件上綁定數據。如果需要在觸發事件的同時,傳入指定的數據,需要使用CustomEvent構造函數生成自定義的事件對象。
```javascript
var event = new CustomEvent('build', { 'detail': 'hello' });
function eventHandler(e) {
console.log(e.detail);
}
```
上面代碼中,CustomEvent構造函數的第一個參數是事件名稱,第二個參數是一個對象,該對象的detail屬性會綁定在事件對象之上。
下面是另一個例子。
```javascript
var myEvent = new CustomEvent("myevent", {
detail: {
foo: "bar"
},
bubbles: true,
cancelable: false
});
el.addEventListener('myevent', function(event) {
console.log('Hello ' + event.detail.foo);
});
el.dispatchEvent(myEvent);
```
IE不支持這個方法,可以用下面的墊片函數模擬。
```javascript
(function () {
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
})();
```
### 事件的模擬
有時,需要在腳本中模擬觸發某種類型的事件,這時就必須使用這種事件的構造函數。
下面是一個通過MouseEvent構造函數,模擬觸發click鼠標事件的例子。
```javascript
function simulateClick() {
var event = new MouseEvent('click', {
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
cb.dispatchEvent(event);
}
```
### 自定義事件的老式寫法
老式瀏覽器不一定支持各種類型事件的構造函數。因此,有時為了兼容,會用到一些非標準的方法。這些方法未來會被逐步淘汰,但是目前瀏覽器還廣泛支持。除非是為了兼容老式瀏覽器,盡量不要使用。
**(1)document.createEvent()**
document.createEvent方法用來新建指定類型的事件。它所生成的Event實例,可以傳入dispatchEvent方法。
```javascript
// 新建Event實例
var event = document.createEvent('Event');
// 事件的初始化
event.initEvent('build', true, true);
// 加上監聽函數
document.addEventListener('build', doSomething, false);
// 觸發事件
document.dispatchEvent(event);
```
createEvent方法接受一個字符串作為參數,可能的值參見下表“數據類型”一欄。使用了某一種“事件類型”,就必須使用對應的事件初始化方法。
|事件類型|事件初始化方法|
|--------|--------------|
|UIEvents|event.initUIEvent|
|MouseEvents|event.initMouseEvent|
|MutationEvents|event.initMutationEvent|
|HTMLEvents|event.initEvent|
|Event|event.initEvent|
|CustomEvent|event.initCustomEvent|
|KeyboardEvent|event.initKeyEvent|
**(2)event.initEvent()**
事件對象的initEvent方法,用來初始化事件對象,還能向事件對象添加屬性。該方法的參數必須是一個使用`Document.createEvent()`生成的Event實例,而且必須在dispatchEvent方法之前調用。
```javascript
var event = document.createEvent('Event');
event.initEvent('my-custom-event', true, true, {foo:'bar'});
someElement.dispatchEvent(event);
```
initEvent方法可以接受四個參數。
- type:事件名稱,格式為字符串。
- bubbles:事件是否應該冒泡,格式為布爾值。可以使用event.bubbles屬性讀取它的值。
- cancelable:事件是否能被取消,格式為布爾值。可以使用event.cancelable屬性讀取它的值。
- option:為事件對象指定額外的屬性。
### 事件模擬的老式寫法
事件模擬的非標準做法是,對document.createEvent方法生成的事件對象,使用對應的事件初始化方法進行初始化。比如,click事件對象屬于MouseEvent對象,也屬于UIEvent對象,因此要用initMouseEvent方法或initUIEvent方法進行初始化。
**(1)event.initMouseEvent()**
initMouseEvent方法用來初始化Document.createEvent方法新建的鼠標事件。該方法必須在事件新建(document.createEvent方法)之后、觸發(dispatchEvent方法)之前調用。
initMouseEvent方法有很長的參數。
```javascript
event.initMouseEvent(type, canBubble, cancelable, view,
detail, screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey,
button, relatedTarget
);
```
上面這些參數的含義,參見MouseEvent構造函數的部分。
模仿并觸發click事件的寫法如下。
```javascript
var simulateDivClick = document.createEvent('MouseEvents');
simulateDivClick.initMouseEvent('click',true,true,
document.defaultView,0,0,0,0,0,false,
false,false,0,null,null
);
divElement.dispatchEvent(simulateDivClick);
```
**(2)UIEvent.initUIEvent()**
`UIEvent.initUIEvent()`用來初始化一個UI事件。該方法必須在事件新建(document.createEvent方法)之后、觸發(dispatchEvent方法)之前調用。
```javascript
event.initUIEvent(type, canBubble, cancelable, view, detail)
```
該方法的參數含義,可以參見MouseEvent構造函數的部分。其中,detail參數是一個數值,含義與事件類型有關,對于鼠標事件,這個值表示鼠標按鍵在某個位置按下的次數。
```javascript
var e = document.createEvent("UIEvent");
e.initUIEvent("click", true, true, window, 1);
```
<h2 id="5.6">CSS操作</h2>
CSS與JavaScript是兩個有著明確分工的領域,前者負責頁面的視覺效果,后者負責與用戶的行為互動。但是,它們畢竟同屬網頁開發的前端,因此不可避免有著交叉和互相配合。本節介紹如果通過JavaScript操作CSS。
## HTML元素的style屬性
操作Element節點的CSS樣式,最簡單的方法之一就是使用節點對象的`getAttribute`方法、`setAttribute`方法和`removeAttribute`方法,讀寫或刪除HTML元素的`style`屬性。
```javascript
div.setAttribute('style',
'background-color:red;' + 'border:1px solid black;'
);
```
這三個方法的詳細用法,詳見《Node節點》一節。
## Element節點的style屬性
### 基本用法
Element節點本身還提供`style`屬性,用來操作CSS樣式。`style`屬性指向一個對象,用來讀寫頁面元素的行內CSS樣式。
```javascript
var divStyle = document.querySelector('div').style;
divStyle.backgroundColor = 'red';
divStyle.border = '1px solid black';
divStyle.width = '100px';
divStyle.height = '100px';
divStyle.fontSize = '10em';
divStyle.backgroundColor // red
divStyle.border // 1px solid black
divStyle.height // 100px
divStyle.width // 100px
```
從上面代碼可以看到,`style`對象的屬性與CSS規則名一一對應,但是需要改寫。具體規則是將橫杠從CSS屬性名中去除,然后將橫杠后的第一個字母大寫,比如`background-color`寫成`backgroundColor`。如果CSS屬性名是JavaScript保留字,則規則名之前需要加上字符串“css”,比如`float`寫成`cssFloat`。
注意,`style`對象的屬性值都是字符串,而且包括單位。所以,`divStyle.width`不能設置為`100`,而要設置為`100px`。
### cssText屬性
style對象的`cssText`可以用來讀寫或刪除整個style屬性。
```javascript
var divStyle = document.querySelector('div').style;
divStyle.cssText = 'background-color: red;'
+ 'border: 1px solid black;'
+ 'height: 100px;'
+ 'width: 100px;';
```
注意,`cssText`的屬性值不用改寫CSS屬性名。
### CSS模塊的偵測
CSS的規格發展太快,新的模塊層出不窮。不同瀏覽器的不同版本,對CSS模塊的支持情況都不一樣。有時候,需要知道當前瀏覽器是否支持某個模塊,這就叫做“CSS模塊的偵測”。
一個比較普遍適用的方法是,判斷某個DOM元素的`style`對象的某個屬性值是否為字符串。
```javascript
typeof element.style.animationName === 'string';
typeof element.style.transform === 'string';
```
如果該CSS屬性確實存在,會返回一個字符串。即使該屬性實際上并未設置,也會返回一個空字符串。如果該屬性不存在,則會返回`undefined`。
```javascript
document.body.style['maxWidth'] // ""
document.body.style['maximumWidth'] // undefined
```
需要注意的是,不管CSS屬性名帶不帶連詞線,`style`對象都會顯示該屬性存在。
```javascript
document.body.style['backgroundColor'] // ""
document.body.style['background-color'] // ""
```
所有瀏覽器都能用這個方法,但是使用的時候,需要把不同瀏覽器的CSS規則前綴也考慮進去。
```javascript
var content = document.getElementById("content");
typeof content.style['webkitAnimation'] === 'string'
```
這種偵測方法可以寫成一個函數。
```javascript
function isPropertySupported(property){
if (property in document.body.style) return true;
var prefixes = ['Moz', 'Webkit', 'O', 'ms', 'Khtml'];
var prefProperty = property.charAt(0).toUpperCase() + property.substr(1);
for(var i = 0; i < prefixes.length; i++){
if((prefixes[i] + prefProperty) in document.body.style) return true;
}
return false;
}
isPropertySupported('background-clip')
// true
```
此外,部分瀏覽器(Firefox 22+, Chrome 28+, Opera 12.1+)目前部署了supports API,可以返回一個布爾值,表示是否支持某條CSS規則。但是,這個API還沒有成為標準。
```javascript
CSS.supports('transform-origin', '5px');
CSS.supports('(display: table-cell) and (display: list-item)');
```
### setProperty(),getPropertyValue(),removeProperty()
`style`對象的以下三個方法,用來讀寫行內CSS規則。
- `setProperty(propertyName,value)`:設置某個CSS屬性。
- `getPropertyValue(propertyName)`:讀取某個CSS屬性。
- `removeProperty(propertyName)`:刪除某個CSS屬性。
這三個方法的第一個參數,都是CSS屬性名,且不用改寫連詞線。
```javascript
var divStyle = document.querySelector('div').style;
divStyle.setProperty('background-color','red');
divStyle.getPropertyValue('background-color');
divStyle.removeProperty('background-color');
```
## CSS偽元素
CSS偽元素是通過CSS向DOM添加的元素,主要方法是通過`:before`和`:after`選擇器生成偽元素,然后用`content`屬性指定偽元素的內容。
以如下HTML代碼為例。
```html
<div id="test">Test content</div>
```
CSS添加偽元素的寫法如下。
```css
#test:before {
content: 'Before ';
color: #FF0;
}
```
DOM節點的`style`對象無法讀寫偽元素的樣式,這時就要用到`window`對象的`getComputedStyle`方法(詳見下面介紹)。JavaScript獲取偽元素,可以使用下面的方法。
```javascript
var test = document.querySelector('#test');
var result = window.getComputedStyle(test, ':before').content;
var color = window.getComputedStyle(test, ':before').color;
```
此外,也可以使用window.getComputedStyle對象的getPropertyValue方法,獲取偽元素的屬性。
```javascript
var result = window.getComputedStyle(test, ':before')
.getPropertyValue('content');
var color = window.getComputedStyle(test, ':before')
.getPropertyValue('color');
```
## StyleSheet對象
### 獲取樣式表
StyleSheet對象代表網頁的一張樣式表,它包括link節點加載的樣式表和style節點內嵌的樣式表。
document對象的styleSheets屬性,可以返回當前頁面的所有StyleSheet對象(即所有樣式表)。它是一個類似數組的對象。
```javascript
var sheets = document.styleSheets;
var sheet = document.styleSheets[0];
```
此外,link節點和style節點的sheet屬性,也可以獲取StyleSheet對象。
```javascript
// HTML代碼為
// <link id="linkElement" href="http://path/to/stylesheet">
// <style id="styleElement">
// body{font-size: 1.2 rem;}
// </style>
// 等同于document.styleSheets[0]
document.querySelector('#linkElement').sheet
// 等同于document.styleSheets[1]
document.querySelector('#styleElement').sheet
```
### 屬性
StyleSheet對象有以下屬性。
**(1)media屬性**
media屬性表示這個樣式表是用于屏幕(screen),還是用于打印(print),或兩者都適用(all)。該屬性只讀,默認值是screen。
```javascript
document.styleSheets[0].media.mediaText
// "all"
```
**(2)disabled屬性**
`disabled`屬性用于打開或關閉一張樣式表。
```javascript
document.querySelector('#linkElement').disabled = true;
// 或者
document.querySelector('#linkElement').disabled = 'disabled';
```
一旦樣式表設置了`disabled`屬性,這張樣式表就將失效。
注意,`disabled`屬性只能在JavaScript中設置,不能在HTML語句中設置。
**(3)href屬性**
href屬性是只讀屬性,返回StyleSheet對象連接的樣式表地址。對于內嵌的style節點,該屬性等于null。
```javascript
document.styleSheets[0].href
```
**(4)title屬性**
title屬性返回StyleSheet對象的title值。
**(5)type屬性**
type屬性返回StyleSheet對象的type值,通常是text/css。
```javascript
document.styleSheets[0].type // "text/css"
```
**(6)parentStyleSheet屬性**
CSS的@import命令允許在樣式表中加載其他樣式表。parentStyleSheet屬性返回包括了當前樣式表的那張樣式表。如果當前樣式表是頂層樣式表,則該屬性返回null。
```javascript
if (stylesheet.parentStyleSheet) {
sheet = stylesheet.parentStyleSheet;
} else {
sheet = stylesheet;
}
```
**(7)ownerNode屬性**
ownerNode屬性返回StyleSheet對象所在的DOM節點,通常是<link>或<style>。對于那些由其他樣式表引用的樣式表,該屬性為null。
```javascript
// HTML代碼為
// <link rel="StyleSheet" href="example.css" type="text/css" />
document.styleSheets[0].ownerNode // [object HTMLLinkElement]
```
**(8)cssRules屬性**
cssRules屬性指向一個類似數組的對象,里面每一個成員就是當前樣式表的一條CSS規則。使用該規則的cssText屬性,可以得到CSS規則對應的字符串。
```javascript
var sheet = document.querySelector('#styleElement').sheet;
sheet.cssRules[0].cssText
// "body { background-color: red; margin: 20px; }"
sheet.cssRules[1].cssText
// "p { line-height: 1.4em; color: blue; }"
```
每條CSS規則還有一個style屬性,指向一個對象,用來讀寫具體的CSS命令。
```javascript
styleSheet.cssRules[0].style.color = 'red';
styleSheet.cssRules[1].style.color = 'purple';
```
### insertRule(),deleteRule()
`insertRule`方法用于在當前樣式表的`cssRules`對象插入CSS規則,`deleteRule`方法用于刪除`cssRules`對象的CSS規則。
```javascript
var sheet = document.querySelector('#styleElement').sheet;
sheet.insertRule('#block { color:white }', 0);
sheet.insertRule('p { color:red }',1);
sheet.deleteRule(1);
```
`insertRule`方法的第一個參數是表示CSS規則的字符串,第二個參數是該規則在`cssRules`對象的插入位置。`deleteRule`方法的參數是該條規則在`cssRules`對象中的位置。
### 添加樣式表
添加樣式表有兩種方式。一種是添加一張內置樣式表,即在文檔中添加一個<style>節點。
```javascript
var style = document.createElement('style');
style.setAttribute('media', 'screen');
// 或者
style.setAttribute("media", "@media only screen and (max-width : 1024px)");
style.innerHTML = 'body{color:red}';
// 或者
sheet.insertRule("header { float: left; opacity: 0.8; }", 1);
document.head.appendChild(style);
```
另一種是添加外部樣式表,即在文檔中添加一個link節點,然后將href屬性指向外部樣式表的URL。
```javascript
var linkElm = document.createElement('link');
linkElm.setAttribute('rel', 'stylesheet');
linkElm.setAttribute('type', 'text/css');
linkElm.setAttribute('href', 'reset-min.css');
document.head.appendChild(linkElm);
```
## CSS規則
一條CSS規則包括兩個部分:CSS選擇器和樣式聲明。下面就是一條典型的CSS規則。
```css
.myClass {
background-color: yellow;
}
```
### CSSRule接口
CSS規則部署了CSSRule接口,它包括了以下屬性。
**(1)cssText**
cssText屬性返回當前規則的文本。
```javascript
// CSS代碼為
// body { background-color: darkblue; }
var stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].cssText
// body { background-color: darkblue; }
```
**(2)parentStyleSheet**
parentStyleSheet屬性返回定義當前規則的樣式表對象。
**(3)parentRule**
parentRule返回包含當前規則的那條CSS規則。最典型的情況,就是當前規則包含在一個@media代碼塊之中。如果當前規則是頂層規則,則該屬性返回null。
**(4)type**
type屬性返回有一個整數值,表示當前規則的類型。
最常見的類型有以下幾種。
- 1:樣式規則,部署了CSSStyleRule接口
- 3:輸入規則,部署了CSSImportRule接口
- 4:Media規則,部署了CSSMediaRule接口
- 5:字體規則,部署了CSSFontFaceRule接口
### CSSStyleRule接口
如果一條CSS規則是普通的樣式規則,那么除了CSSRule接口,它還部署了CSSStyleRule接口。
CSSRule接口有以下兩個屬性。
**(1)selectorText屬性**
selectorText屬性返回當前規則的選擇器。
```javascript
var stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].selectorText // ".myClass"
```
**(2)style屬性**
style屬性返回一個對象,代表當前規則的樣式聲明,也就是選擇器后面的大括號里面的部分。該對象部署了CSSStyleDeclaration接口,使用它的cssText屬性,可以返回所有樣式聲明,格式為字符串。
```javascript
document.styleSheets[0].cssRules[0].style.cssText
// "background-color: gray;font-size: 120%;"
```
### CSSMediaRule接口
如果一條CSS規則是@media代碼塊,那么它除了CSSRule接口,還部署了CSSMediaRule接口。
該接口主要提供一個media屬性,可以返回@media代碼塊的media規則。
### CSSStyleDeclaration對象
每一條CSS規則的樣式聲明部分(大括號內部的部分),都是一個CSSStyleDeclaration對象,主要包括三種情況。
- HTML元素的行內樣式(<elem style="...">)
- CSSStyleRule接口的style屬性
- window.getComputedStyle()的返回結果
每一條CSS屬性,都是CSSStyleDeclaration對象的屬性。不過,連詞號需要編程駱駝拼寫法。
```javascript
var styleObj = document.styleSheets[0].cssRules[1].style;
styleObj.color // "red";
styleObj.fontSize // "100%"
```
除了CSS屬性以外,CSSStyleDeclaration對象還包括以下屬性。
- cssText:當前規則的所有樣式聲明文本。該屬性可讀寫,即可用來設置當前規則。
- length:當前規則包含多少條聲明。
- parentRule:包含當前規則的那條規則,同CSSRule接口的parentRule屬性。
CSSStyleDeclaration對象包括以下方法。
**(1)getPropertyPriority()**
getPropertyPriority方法返回指定聲明的優先級,如果有的話,就是“important”,否則就是空字符串。
```javascript
var styleObj = document.styleSheets[0].cssRules[1].style;
styleObj.getPropertyPriority('color') // ""
```
**(2)getPropertyValue()**
getPropertyValue方法返回指定聲明的值。
```javascript
// CSS代碼為
// color:red;
var styleObj = document.styleSheets[0].cssRules[1].style;
styleObj.getPropertyValue('color') // "red"
```
**(3)item()**
item方法返回指定位置的屬性名。
```javascript
var styleObj = document.styleSheets[0].cssRules[1].style;
styleObj.item(0) // "color"
// 或者
styleObj[0] // "color"
```
**(4)removeProperty()**
removeProperty方法用于刪除一條CSS屬性,返回被刪除的值。
```javascript
// CSS代碼為
// color:red;
var styleObj = document.styleSheets[0].cssRules[1].style;
styleObj.removeProperty('color') // "red"
```
**(5)setProperty()**
setProperty方法用于設置指定的CSS屬性,沒有返回值。
```javascript
var styleObj = document.styleSheets[0].cssRules[1].style;
styleObj.setProperty('color', 'green', 'important');
```
下面是遍歷一條CSS規則所有屬性的例子。
```javascript
var styleObj = document.styleSheets[0].cssRules[0].style;
for (var i = styleObj.length - 1; i >= 0; i--) {
var nameString = styleObj[i];
styleObj.removeProperty(nameString);
}
```
上面刪除了一條CSS規則的所有屬性,更簡便的方法是設置cssText屬性為空字符串。
```javascript
styleObj.cssText = '';
```
## window.getComputedStyle()
`getComputedStyle`方法接受一個DOM節點對象作為參數,返回一個包含該節點最終樣式信息的對象。所謂“最終樣式信息”,指的是各種CSS規則疊加后的結果。
```javascript
var div = document.querySelector('div');
window.getComputedStyle(div).backgroundColor
```
getComputedStyle方法還可以接受第二個參數,表示指定節點的偽元素。
```javascript
var result = window.getComputedStyle(div, ':before');
```
getComputedStyle方法返回的是一個CSSStyleDeclaration對象。但是此時,這個對象是只讀的,也就是只能用來讀取樣式信息,不能用來設置。如果想設置樣式,應該使用Element節點的style屬性。
```javascript
var elem = document.getElementById("elem-container");
var hValue = window.getComputedStyle(elem,null).getPropertyValue("height");
```
## window.matchMedia()
### 基本用法
`window.matchMedia`方法用來檢查CSS的[`mediaQuery`](https://developer.mozilla.org/en-US/docs/DOM/Using_media_queries_from_code)語句。各種瀏覽器的最新版本(包括IE 10+)都支持該方法,對于不支持該方法的老式瀏覽器,可以使用第三方函數庫[matchMedia.js](https://github.com/paulirish/matchMedia.js/)。
CSS的`mediaQuery`語句有點像`if`語句,只要顯示媒介(包括瀏覽器和屏幕等)滿足`mediaQuery`語句設定的條件,就會執行區塊內部的語句。下面是`mediaQuery`語句的一個例子。
```css
@media all and (max-width: 700px) {
body {
background: #FF0;
}
}
```
上面的CSS代碼表示,該區塊對所有媒介(media)有效,且視口的最大寬度不得超過`700`像素。如果條件滿足,則`body`元素的背景設為#FF0。
需要注意的是,`mediaQuery`接受兩種寬度/高度的度量,一種是上例的“視口”的寬度/高度,還有一種是“設備”的寬度/高度,下面就是一個例子。
```css
@media all and (max-device-width: 700px) {
body {
background: #FF0;
}
}
```
視口的寬度/高度(width/height)使用`documentElement.clientWidth/clientHeight`來衡量,單位是CSS像素;設備的寬度/高度(device-width/device-height)使用`screen.width/height`來衡量,單位是設備硬件的像素。
`window.matchMedia`方法接受一個`mediaQuery`語句的字符串作為參數,返回一個[`MediaQueryList`](https://developer.mozilla.org/en-US/docs/DOM/MediaQueryList)對象。該對象有以下兩個屬性。
- `media`:返回所查詢的`mediaQuery`語句字符串。
- `matches`:返回一個布爾值,表示當前環境是否匹配查詢語句。
```javascript
var result = window.matchMedia('(min-width: 600px)');
result.media // (min-width: 600px)
result.matches // true
```
下面是另外一個例子,根據mediaQuery是否匹配當前環境,執行不同的JavaScript代碼。
```javascript
var result = window.matchMedia('(max-width: 700px)');
if (result.matches) {
console.log('頁面寬度小于等于700px');
} else {
console.log('頁面寬度大于700px');
}
```
下面的例子根據`mediaQuery`是否匹配當前環境,加載相應的CSS樣式表。
```javascript
var result = window.matchMedia("(max-width: 700px)");
if (result.matches){
var linkElm = document.createElement('link');
linkElm.setAttribute('rel', 'stylesheet');
linkElm.setAttribute('type', 'text/css');
linkElm.setAttribute('href', 'small.css');
document.head.appendChild(linkElm);
}
```
注意,如果`window.matchMedia`無法解析`mediaQuery`參數,返回的總是`false`,而不是報錯。
```javascript
window.matchMedia('bad string').matches
// false
```
### 監聽事件
window.matchMedia方法返回的MediaQueryList對象有兩個方法,用來監聽事件:addListener方法和removeListener方法。如果mediaQuery查詢結果發生變化,就調用指定的回調函數。
```javascript
var mql = window.matchMedia("(max-width: 700px)");
// 指定回調函數
mql.addListener(mqCallback);
// 撤銷回調函數
mql.removeListener(mqCallback);
function mqCallback(mql) {
if (mql.matches) {
// 寬度小于等于700像素
} else {
// 寬度大于700像素
}
}
```
上面代碼中,回調函數的參數是MediaQueryList對象。回調函數的調用可能存在兩種情況。一種是顯示寬度從700像素以上變為以下,另一種是從700像素以下變為以上,所以在回調函數內部要判斷一下當前的屏幕寬度。
## CSS事件
### transitionEnd事件
CSS的過渡效果(transition)結束后,觸發`transitionEnd`事件。
```javascript
el.addEventListener('transitionend', onTransitionEnd, false);
function onTransitionEnd() {
console.log('Transition end');
}
```
`transitionEnd`的事件對象具有以下屬性。
- `propertyName`:發生`transition`效果的CSS屬性名。
- `elapsedTime`:`transition`效果持續的秒數,不含`transition-delay`的時間。
- `pseudoElement`:如果`transition`效果發生在偽元素,會返回該偽元素的名稱,以“::”開頭。如果不發生在偽元素上,則返回一個空字符串。
實際使用`transitionend`事件時,可能需要添加瀏覽器前綴。
```javascript
el.addEventListener('webkitTransitionEnd', function () {
el.style.transition = 'none';
});
```
### animationstart事件,animationend事件,animationiteration事件
CSS動畫有以下三個事件。
- animationstart事件:動畫開始時觸發。
- animationend事件:動畫結束時觸發。
- animationiteration事件:開始新一輪動畫循環時觸發。如果animation-iteration-count屬性等于1,該事件不觸發,即只播放一輪的CSS動畫,不會觸發animationiteration事件。
```javascript
div.addEventListener('animationiteration', function() {
console.log('完成一次動畫');
});
```
這三個事件的事件對象,都有animationName屬性(返回產生過渡效果的CSS屬性名)和elapsedTime屬性(動畫已經運行的秒數)。對于animationstart事件,elapsedTime屬性等于0,除非animation-delay屬性等于負值。
```javascript
var el = document.getElementById("animation");
el.addEventListener("animationstart", listener, false);
el.addEventListener("animationend", listener, false);
el.addEventListener("animationiteration", listener, false);
function listener(e) {
var li = document.createElement("li");
switch(e.type) {
case "animationstart":
li.innerHTML = "Started: elapsed time is " + e.elapsedTime;
break;
case "animationend":
li.innerHTML = "Ended: elapsed time is " + e.elapsedTime;
break;
case "animationiteration":
li.innerHTML = "New loop started at time " + e.elapsedTime;
break;
}
document.getElementById("output").appendChild(li);
}
```
上面代碼的運行結果是下面的樣子。
```html
Started: elapsed time is 0
New loop started at time 3.01200008392334
New loop started at time 6.00600004196167
Ended: elapsed time is 9.234000205993652
```
animation-play-state屬性可以控制動畫的狀態(暫停/播放),該屬性需求加上瀏覽器前綴。
```javascript
element.style.webkitAnimationPlayState = "paused";
element.style.webkitAnimationPlayState = "running";
```
<h2 id="5.7">Mutation Observer</h2>
## 概述
Mutation Observer(變動觀察器)是監視DOM變動的接口。DOM發生任何變動,Mutation Observer會得到通知。
概念上,它很接近事件。可以理解為,當DOM發生變動,會觸發Mutation Observer事件。但是,它與事件有一個本質不同:事件是同步觸發,也就是說,當DOM發生變動,立刻會觸發相應的事件;Mutation Observer則是異步觸發,DOM發生變動以后,并不會馬上觸發,而是要等到當前所有DOM操作都結束后才觸發。
這樣設計是為了應付DOM變動頻繁的特點。舉例來說,如果在文檔中連續插入1000個段落(p元素),就會連續觸發1000個插入事件,執行每個事件的回調函數,這很可能造成瀏覽器的卡頓;而Mutation Observer完全不同,只在1000個段落都插入結束后才會觸發,而且只觸發一次。
Mutation Observer有以下特點:
- 它等待所有腳本任務完成后,才會運行,即采用異步方式。
- 它把DOM變動記錄封裝成一個數組進行處理,而不是一條條地個別處理DOM變動。
- 它既可以觀察發生在DOM的所有類型變動,也可以觀察某一類變動。
目前,Firefox(14+)、 Chrome(26+)、Opera(15+)、IE(11+)和Safari(6.1+)支持這個API。Safari 6.0和Chrome 18-25使用這個API的時候,需要加上WebKit前綴(WebKitMutationObserver)。可以使用下面的表達式,檢查當前瀏覽器是否支持這個API。
```javascript
var MutationObserver = window.MutationObserver
|| window.WebKitMutationObserver
|| window.MozMutationObserver;
var observeMutationSupport = !!MutationObserver;
```
## MutationObserver構造函數
首先,使用MutationObserver構造函數,新建一個觀察器實例,同時指定這個實例的回調函數。
```javascript
var observer = new MutationObserver(callback);
```
觀察器的回調函數會在每次DOM發生變動后調用。它接受兩個參數,第一個是變動數組(詳見后文),第二個是觀察器實例。
## Mutation Observer實例的方法
### observe()
observe方法指定所要觀察的DOM節點,以及所要觀察的特定變動。
```javascript
var article = document.querySelector('article');
var options = {
'childList': true,
'attributes':true
} ;
observer.observe(article, options);
```
上面代碼中,observe方法接受兩個參數,第一個是所要觀察的DOM元素是article,第二個是所要觀察的變動類型(子節點變動和屬性變動)。
觀察器所能觀察的DOM變動類型(即上面代碼的options對象),有以下幾種:
- **childList**:子節點的變動。
- **attributes**:屬性的變動。
- **characterData**:節點內容或節點文本的變動。
- **subtree**:所有后代節點的變動。
想要觀察哪一種變動類型,就在option對象中指定它的值為true。需要注意的是,不能單獨觀察subtree變動,必須同時指定childList、attributes和characterData中的一種或多種。
除了變動類型,options對象還可以設定以下屬性:
- **attributeOldValue**:類型為布爾值,表示觀察attributes變動時,是否需要記錄變動前的屬性值。
- **characterDataOldValue**:類型為布爾值,表示觀察characterData變動時,是否需要記錄變動前的值。
- **attributeFilter**:類型為數組,表示需要觀察的特定屬性(比如['class','src'])。
對一個節點添加觀察器,就像添加addEventListener方法一樣。多次添加同一個觀察器是無效的,回調函數依然只會觸發一次。但是,如果指定不同的options對象,就會被當作兩個不同的觀察器。
下面的例子觀察新增的子節點。
```javascript
var insertedNodes = [];
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for (var i = 0; i < mutation.addedNodes.length; i++)
insertedNodes.push(mutation.addedNodes[i]);
})
});
observer.observe(document, { childList: true });
console.log(insertedNodes);
```
### disconnect(),takeRecords()
disconnect方法用來停止觀察。再發生相應變動,就不再調用回調函數。
```javascript
observer.disconnect();
```
takeRecords方法用來清除變動記錄,即不再處理未處理的變動。該方法返回變動記錄的數組。
```javascript
observer.takeRecords();
```
### MutationRecord對象
DOM每次發生變化,就會生成一條變動記錄。這個變動記錄對應一個MutationRecord對象,該對象包含了與變動相關的所有信息。Mutation Observer處理的是一個個MutationRecord對象所組成的數組。
MutationRecord對象包含了DOM的相關信息,有如下屬性:
- **type**:觀察的變動類型(attribute、characterData或者childList)。
- **target**:發生變動的DOM節點。
- **addedNodes**:新增的DOM節點。
- **removedNodes**:刪除的DOM節點。
- **previousSibling**:前一個同級節點,如果沒有則返回null。
- **nextSibling**:下一個同級節點,如果沒有則返回null。
- **attributeName**:發生變動的屬性。如果設置了attributeFilter,則只返回預先指定的屬性。
- **oldValue**:變動前的值。這個屬性只對attribute和characterData變動有效,如果發生childList變動,則返回null。
## 應用示例
### 子元素的變動
下面的例子說明如何讀取變動記錄。
```javascript
var callback = function(records){
records.map(function(record){
console.log('Mutation type: ' + record.type);
console.log('Mutation target: ' + record.target);
});
};
var mo = new MutationObserver(callback);
var option = {
'childList': true,
'subtree': true
};
mo.observe(document.body, option);
```
上面代碼的觀察器,觀察body的所有下級節點(childList表示觀察子節點,subtree表示觀察后代節點)的變動。回調函數會在控制臺顯示所有變動的類型和目標節點。
### 屬性的變動
下面的例子說明如何追蹤屬性的變動。
```javascript
var callback = function(records){
records.map(function(record){
console.log('Previous attribute value: ' + record.oldValue);
});
};
var mo = new MutationObserver(callback);
var element = document.getElementById('#my_element');
var options = {
'attributes': true,
'attributeOldValue': true
}
mo.observe(element, options);
```
上面代碼先設定追蹤屬性變動('attributes': true),然后設定記錄變動前的值。實際發生變動時,會將變動前的值顯示在控制臺。
### 取代DOMContentLoaded事件
網頁加載的時候,DOM節點的生成會產生變動記錄,因此只要觀察DOM的變動,就能在第一時間觸發相關事件,因此也就沒有必要使用DOMContentLoaded事件。
```javascript
var observer = new MutationObserver(callback);
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
```
上面代碼中,監聽document.documentElement(即HTML節點)的子節點的變動,subtree屬性指定監聽還包括后代節點。因此,任意一個網頁元素一旦生成,就能立刻被監聽到。
下面的代碼,使用MutationObserver對象封裝一個監聽DOM生成的函數。
```javascript
(function(win){
'use strict';
var listeners = [];
var doc = win.document;
var MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
var observer;
function ready(selector, fn){
// 儲存選擇器和回調函數
listeners.push({
selector: selector,
fn: fn
});
if(!observer){
// 監聽document變化
observer = new MutationObserver(check);
observer.observe(doc.documentElement, {
childList: true,
subtree: true
});
}
// 檢查該節點是否已經在DOM中
check();
}
function check(){
// 檢查是否匹配已儲存的節點
for(var i = 0; i < listeners.length; i++){
var listener = listeners[i];
// 檢查指定節點是否有匹配
var elements = doc.querySelectorAll(listener.selector);
for(var j = 0; j < elements.length; j++){
var element = elements[j];
// 確保回調函數只會對該元素調用一次
if(!element.ready){
element.ready = true;
// 對該節點調用回調函數
listener.fn.call(element, element);
}
}
}
}
// 對外暴露ready
win.ready = ready;
})(this);
ready('.foo', function(element){
// ...
});
```