💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
<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 | 文档类型节点 | 文档的类型(比如&lt;!DOCTYPE html&gt;) Element | 元素节点 | HTML元素(比如&lt;body&gt;、&lt;a&gt;等) 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标签解释为文本,即&amp;lt;p&amp;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的&lt;base&gt;标签,改变该属性的值。 ```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子节点是&lt;HTML&gt;。 **(3)lastElementChild** lastElementChild属性返回当前节点的最后一个Element子节点,如果不存在任何Element子节点,则返回null。 ```javascript document.lastElementChild.nodeName // "HTML" ``` 上面代码中,document节点的最后一个Element子节点是&lt;HTML&gt;。 **(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文档,该节点就代表&lt;!DOCTYPE html&gt;。如果网页没有声明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节点,代表&lt;html lang="en"&gt;。 **(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属性。如果传入大写的标签名,会被转为小写。如果参数带有尖括号(即&lt;和&gt;)或者是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) // &lt;span&gt;Foo &amp; bar&lt;/span&gt; ``` 上面代码中,`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原来包含的节点被全部删除。 注意,如果文本节点中包含&amp;、小于号(&lt;)和大于号(%gt;),innerHTML属性会将它们转为实体形式&amp、&lt、&gt。 ```javascript // HTML代码如下 <p id="para"> 5 > 3 </p> document.getElementById('para').innerHTML // 5 &gt; 3 ``` 由于上面这个原因,导致在innerHTML插入&lt;script&gt;标签,不会被执行。 ```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请求的过程、&lt;img&gt;、&lt;audio&gt;、&lt;video&gt;、&lt;style&gt;、&lt;link&gt;加载外部资源的过程。下载和上传都会发生进度事件。 进度事件有以下几种。 - 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事件当&lt;input&gt;、&lt;textarea&gt;的值发生变化时触发。此外,打开contenteditable属性的元素,只要值发生变化,也会触发input事件。 input事件的一个特点,就是会连续触发,比如用户每次按下一次按键,就会触发一次input事件。 **(2)select事件** select事件当在&lt;input&gt;、&lt;textarea&gt;中选中文本时触发。 ```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事件当&lt;input&gt;、&lt;select&gt;、&lt;textarea&gt;的值发生变化时触发。它与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节点,通常是&lt;link&gt;或&lt;style&gt;。对于那些由其他样式表引用的样式表,该属性为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`对象中的位置。 ### 添加样式表 添加样式表有两种方式。一种是添加一张内置样式表,即在文档中添加一个&lt;style&gt;节点。 ```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元素的行内样式(&lt;elem style="..."&gt;) - 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){ // ... }); ```