JavaScript标准教程之DOM

文档结构

<h1 id=”5″>DOM</h1>

对一个图片添加该效果,首先,我们需要一个具有宽高的容器。DOM
结构非常简单。

<h2 id=”5.1″>DOM节点</h2>

divclass=containerimgclass=tilt-effectsrc=

DOM的概念

DOM是文档对象模型(Document Object
Model)的简称,它的基本思想是把结构化文档(比如HTML和XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM
Tree)。所有的节点和最终的树状结构,都有规范的对外接口,以达到使用编程语言操作文档的目的(比如增删内容)。所以,DOM可以理解成文档(HTML文档、XML文档和SVG文档)的编程接口。

DOM有自己的国际标准,目前的通用版本是DOM
3,下一代版本DOM
4正在拟定中。本章介绍的就是JavaScript对DOM标准的实现和用法。

严格地说,DOM不属于JavaScript,但是操作DOM是JavaScript最常见的任务,而JavaScript也是最常用于DOM操作的语言。所以,DOM往往放在JavaScript里面介绍。

上面这段结构经过脚本处理之后,会被替换成下面的结构:

节点的概念

DOM的最小组成单位叫做节点(node),一个文档的树形结构(DOM树),就是由各种不同类型的节点组成。

对于HTML文档,节点主要有以下六种类型:Document节点、DocumentType节点、Element节点、Attribute节点、Text节点和DocumentFragment节点。

节点 名称 含义
Document 文档节点 整个文档(window.document)
DocumentType 文档类型节点 文档的类型(比如<!DOCTYPE html>)
Element 元素节点 HTML元素(比如<body>、<a>等)
Attribute 属性节点 HTML元素的属性(比如class="right")
Text 文本节点 HTML文档中出现的文本
DocumentFragment 文档碎片节点 文档的片段

浏览器原生提供一个Node对象,上表所有类型的节点都是Node对象派生出来的。也就是说,它们都继承了Node的属性和方法。

divclass=containerdivclass=tiltdivclass=tilt__backstyle=background-image:url();/divdivclass=tilt__frontstyle=opacity:0.7;-webkit-transform:perspective(1000px)translate3d(0px,0px,0px)rotate3d(1,1,1,0deg);transform:perspective(1000px)translate3d(0px,0px,0px)rotate3d(1,1,1,0deg);background-image:url();/divdivclass=tilt__frontstyle=opacity:0.7;-webkit-transform:perspective(1000px)translate3d(0px,0px,0px)rotate3d(1,1,1,0deg);transform:perspective(1000px)translate3d(0px,0px,0px)rotate3d(1,1,1,0deg);background-image:url();/div/div/div

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属性等于#documentnodeType属性等于9。

document.nodeName // "#document"
document.nodeType // 9

通常来说,使用nodeType属性确定一个节点的类型,比较方便。

document.querySelector('a').nodeType === 1
// true

document.querySelector('a').nodeType === Node.ELEMENT_NODE
// true

上面两种写法是等价的。

我们利用了filtfx.js这个插件对上面的图片进行处理,
来实现倾斜效果。我在原来的代码中加入了一些注释,来帮助我们理解。下面我们对该插件的核心代码进行分析。

ownerDocument,nextSibling,previousSibling,parentNode,parentElement

以下属性返回当前节点的相关节点。

(1)ownerDocument

ownerDocument属性返回当前节点所在的顶层文档对象,即document对象。

var d = p.ownerDocument;
d === document // true

document对象本身的ownerDocument属性,返回null。

(2)nextSibling

nextSibling属性返回紧跟在当前节点后面的第一个同级节点。如果当前节点后面没有同级节点,则返回null。注意,该属性还包括文本节点和评论节点。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格。

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。

// 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节点。

下面代码是如何从父节点移除指定节点。

if (node.parentNode) {
  node.parentNode.removeChild(node);
}

对于document节点和documentfragment节点,它们的父节点都是null。另外,对于那些生成后还没插入DOM树的节点,父节点也是null。

(5)parentElement

parentElement属性返回当前节点的父Element节点。如果当前节点没有父节点,或者父节点类型不是Element节点,则返回null。

if (node.parentElement) {
  node.parentElement.style.color = "red";
}

上面代码设置指定节点的父Element节点的CSS属性。

在IE浏览器中,只有Element节点才有该属性,其他浏览器则是所有类型的节点都有该属性。

functionTiltFx(el,options){this.el=el;this.options=extend({},this.options);extend(this.options,options);this._init();this._initEvents();}

textContent,nodeValue

以下属性返回当前节点的内容。

(1)textContent

textContent属性返回当前节点和它的所有后代节点的文本内容。

// HTML代码为
// <div id="divA">This is some text</div>

document.getElementById("divA").textContent
// This is some text

上面代码的textContent属性,自动忽略当前节点内部的HTML标签,返回所有文本内容。

该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有它原来的子节点。它还有一个好处,就是自动对HTML标签转义。这很适合用于用户提供的内容。

document.getElementById('foo').textContent = '<p>GoodBye!</p>';

上面代码在插入文本时,会将p标签解释为文本,即<p>,而不会当作标签处理。

对于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属性是无效的。

这是构造函数,如果我们的文档中,加入了上面的插件,那么插件会遍历文档中具有tilt-effetimg元素,来调用构造函数TiltFx()

childNodes,firstChild,lastChild

以下属性返回当前节点的子节点。

(1)childNodes

childNodes属性返回一个NodeList集合,成员包括当前节点的所有子节点。注意,除了HTML元素节点,该属性返回的还包括Text节点和Comment节点。如果当前节点不包括任何子节点,则返回一个空的NodeList集合。由于NodeList对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中。

var ulElementChildNodes = document.querySelector('ul').childNodes;

(2)firstChild

firstChild属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回null

<p id="para-01">First span</p>

<script type="text/javascript">
  console.log(
    document.getElementById('para-01').firstChild.nodeName
  ) // "span"
</script>

上面代码中,p元素的第一个子节点是span元素。

注意,firstChild返回的除了HTML元素子节点,还可能是文本节点或评论节点。

<p id="para-01">
  First span
</p>

<script type="text/javascript">
  console.log(
    document.getElementById('para-01').firstChild.nodeName
  ) // "#text"
</script>

上面代码中,p元素与span元素之间有空白字符,这导致firstChild返回的是文本节点。

(3)lastChild

lastChild属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回null。

functioninit(){//遍历所有拥有‘title-effect’类的img元素[].slice.call(document.querySelectorAll(img.tilt-effect)).forEach(function(img){newTiltFx(img,JSON.parse(img.getAttribute(data-tilt-options)));});}

baseURI

baseURI属性返回一个字符串,由当前网页的协议、域名和所在的目录组成,表示当前网页的绝对路径。如果无法取到这个值,则返回null。浏览器根据这个属性,计算网页上的相对路径的URL。该属性为只读。

通常情况下,该属性由当前网址的URL(即window.location属性)决定,但是可以使用HTML的<base>标签,改变该属性的值。

<base href="http://www.example.com/page.html">
<base target="_blank" href="http://www.example.com/page.html">

该属性不仅document对象有(document.baseURI),元素节点也有(element.baseURI)。通常情况下,它们的值是相同的。

TiltFx()具有一个原型属性,两个原型方法。原型属性配置了一些默认的参数用于调用:

Node节点的方法

/***默认参数*/TiltFx.prototype.options={extraImgs:2,//额外的辅助图片数量opacity:0.7,bgfixed:true,//底图是否固定movement:{//这是一些用于移动的参数perspective:1000,translateX:-10,translateY:-10,translateZ:20,rotateX:2,rotateY:2,rotateZ:0}}

appendChild(),hasChildNodes()

以下方法与子节点相关。

(1)appendChild()

appendChild方法接受一个节点对象作为参数,将其作为最后一个子节点,插入当前节点。

var p = document.createElement("p");
document.body.appendChild(p);

如果参数节点是文档中现有的其他节点,appendChild方法会将其从原来的位置,移动到新位置。

hasChildNodes方法返回一个布尔值,表示当前节点是否有子节点。

var foo = document.getElementById("foo");

if ( foo.hasChildNodes() ) {
  foo.removeChild( foo.childNodes[0] );
}

上面代码表示,如果foo节点有子节点,就移除第一个子节点。

(2)hasChildNodes()

hasChildNodes方法结合firstChild属性和nextSibling属性,可以遍历当前节点的所有后代节点。

function DOMComb (oParent, oCallback) {
  if (oParent.hasChildNodes()) {
    for (var oNode = oParent.firstChild; oNode; oNode = oNode.nextSibling) {
      DOMComb(oNode, oCallback);
    }
  }
  oCallback.call(oParent);
}

上面代码的DOMComb函数的第一个参数是某个指定的节点,第二个参数是回调函数。这个回调函数会依次作用于指定节点,以及指定节点的所有后代节点。

function printContent () {
  if (this.nodeValue) {
    console.log(this.nodeValue);
  }
}

DOMComb(document.body, printContent);

第一个原型方法是_init(),用于初始化DOM结点,生成我们的目标DOM结点:

cloneNode(),insertBefore(),removeChild(),replaceChild()

下面方法与节点操作有关。

(1)cloneNode()

cloneNode方法用于克隆一个节点。它接受一个布尔值作为参数,表示是否同时克隆子节点,默认是false,即不克隆子节点。

var cloneUL = document.querySelector('ul').cloneNode(true);

需要注意的是,克隆一个节点,会拷贝该节点的所有属性,但是会丧失addEventListener方法和on-属性(即node.onclick = fn),添加在这个节点上的事件回调函数。

克隆一个节点之后,DOM树有可能出现两个有相同ID属性(即id="xxx")的HTML元素,这时应该修改其中一个HTML元素的ID属性。

(2)insertBefore()

insertBefore方法用于将某个节点插入当前节点的指定位置。它接受两个参数,第一个参数是所要插入的节点,第二个参数是当前节点的一个子节点,新的节点将插在这个节点的前面。该方法返回被插入的新节点。

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属性。

parentElement.insertBefore(newElement, parentElement.firstChild);

上面代码中,如果当前节点没有任何子节点,parentElement.firstChild会返回null,则新节点会插在当前节点的最后,等于是第一个子节点。

由于不存在insertAfter方法,如果要插在当前节点的某个子节点后面,可以用insertBefore方法结合nextSibling属性模拟。

parentDiv.insertBefore(s1, s2.nextSibling);

上面代码可以将s1节点,插在s2节点的后面。如果s2是当前节点的最后一个子节点,则s2.nextSibling返回null,这时s1节点会插在当前节点的最后,变成当前节点的最后一个子节点,等于紧跟在s2的后面。

(3)removeChild()

removeChild方法接受一个子节点作为参数,用于从当前节点移除该节点。它返回被移除的节点。

var divA = document.getElementById('A');
divA.parentNode.removeChild(divA);

上面代码是如何移除一个指定节点。

下面是如何移除当前节点的所有子节点。

var element = document.getElementById("top");
while (element.firstChild) {
  element.removeChild(element.firstChild);
}

被移除的节点依然存在于内存之中,但是不再是DOM的一部分。所以,一个节点移除以后,依然可以使用它,比如插入到另一个节点。

(4)replaceChild()

replaceChild方法用于将一个新的节点,替换当前节点的某一个子节点。它接受两个参数,第一个参数是用来替换的新节点,第二个参数将要被替换走的子节点。它返回被替换走的那个节点。

replacedNode = parentNode.replaceChild(newChild, oldChild);

下面是一个例子。

var divA = document.getElementById('A');
var newSpan = document.createElement('span');
newSpan.textContent = 'Hello World!';
divA.parentNode.replaceChild(newSpan,divA);

上面代码是如何替换指定节点。

TiltFx.prototype._init=function(){this.tiltWrapper=document.createElement(div);this.tiltWrapper.className=tilt;//mainimageelement.this.tiltImgBack=document.createElement(div);this.tiltImgBack.className=tilt__back;this.tiltImgBack.style.backgroundImage=url(+this.el.src+);this.tiltWrapper.appendChild(this.tiltImgBack);//imageelementslimit.if(this.options.extraImgs1){this.options.extraImgs=1;}elseif(this.options.extraImgs5){this.options.extraImgs=5;}if(!this.options.movement.perspective){this.options.movement.perspective=0;}//addtheextraimageelements.this.imgElems=[];for(vari=0;ithis.options.extraImgs;++i){varel=document.createElement(div);el.className=tilt__front;el.style.backgroundImage=url(+this.el.src+);el.style.opacity=this.options.opacity;this.tiltWrapper.appendChild(el);this.imgElems.push(el);}if(!this.options.bgfixed){this.imgElems.push(this.tiltImgBack);++this.options.extraImgs;}//addittotheDOMandremoveoriginalimgelement.this.el.parentNode.insertBefore(this.tiltWrapper,this.el);this.el.parentNode.removeChild(this.el);//tiltWrapperproperties:width/height/left/topthis.view={width:this.tiltWrapper.offsetWidth,height:this.tiltWrapper.offsetHeight};};

contains(),compareDocumentPosition(),isEqualNode()

下面方法用于节点的互相比较。

(1)contains()

contains方法接受一个节点作为参数,返回一个布尔值,表示参数节点是否为当前节点的后代节点。

document.body.contains(node)

上面代码检查某个节点,是否包含在当前文档之中。

注意,如果将当前节点传入contains方法,会返回true。虽然从意义上说,一个节点不应该包含自身。

nodeA.contains(nodeA) // true

(2)compareDocumentPosition()

compareDocumentPosition方法的用法,与contains方法完全一致,返回一个7个比特位的二进制值,表示参数节点与当前节点的关系。

二进制值 数值 含义
000000 0 两个节点相同
000001 1 两个节点不在同一个文档(即有一个节点不在当前文档)
000010 2 参数节点在当前节点的前面
000100 4 参数节点在当前节点的后面
001000 8 参数节点包含当前节点
010000 16 当前节点包含参数节点
100000 32 浏览器的私有用途
// 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返回值的含义,定义在每一个比特位上,所以如果要检查某一种特定的含义,就需要使用比特位运算符。

var head = document.head;
var body = document.body;
if (head.compareDocumentPosition(body) & 4) {
  console.log("文档结构正确");
} else {
  console.log("<head> 不能在 <body> 前面");
}

上面代码中,compareDocumentPosition的返回值与4(又称掩码)进行与运算(&),得到一个布尔值,表示head是否在body前面。

在这个方法的基础上,可以部署一些特定的函数,检查节点的位置。

Node.prototype.before = function (arg) {
  return !!(this.compareDocumentPosition(arg) & 2)
}

nodeA.before(nodeB)

上面代码在Node对象上部署了一个before方法,返回一个布尔值,表示参数节点是否在当前节点的前面。

(3)isEqualNode()

isEqualNode方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。

var targetEl = document.getElementById("targetEl");
var firstDiv = document.getElementsByTagName("div")[0];

targetEl.isEqualNode(firstDiv)

另外一个原型方式是用于监听鼠标事件之类的:

normalize()

normailize方法用于清理当前节点内部的所有Text节点。它会去除空的文本节点,并且将毗邻的文本节点合并成一个。

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节点》章节,了解更多内容。

TiltFx.prototype._initEvents=function(){varself=this,moveOpts=self.options.movement;//mousemoveevent..this.tiltWrapper.addEventListener(mousemove,function(ev){requestAnimationFrame(function(){//mousepositionrelativetothedocument.varmousepos=getMousePos(ev),//documentscrolls.docScrolls={left:document.body.scrollLeft+document.documentElement.scrollLeft,top:document.body.scrollTop+document.documentElement.scrollTop},bounds=self.tiltWrapper.getBoundingClientRect(),//mousepositionrelativetothemainelement(tiltWrapper).relmousepos={x:mousepos.x-bounds.left-docScrolls.left,y:mousepos.y-bounds.top-docScrolls.top};//configurethemovementforeachimageelement.for(vari=0,len=self.imgElems.length;ilen;++i){varel=self.imgElems[i],rotX=moveOpts.rotateX?2*((i+1)*moveOpts.rotateX/self.options.extraImgs)/self.view.height*relmousepos.y-((i+1)*moveOpts.rotateX/self.options.extraImgs):0,rotY=moveOpts.rotateY?2*((i+1)*moveOpts.rotateY/self.options.extraImgs)/self.view.width*relmousepos.x-((i+1)*moveOpts.rotateY/self.options.extraImgs):0,rotZ=moveOpts.rotateZ?2*((i+1)*moveOpts.rotateZ/self.options.extraImgs)/self.view.width*relmousepos.x-((i+1)*moveOpts.rotateZ/self.options.extraImgs):0,transX=moveOpts.translateX?2*((i+1)*moveOpts.translateX/self.options.extraImgs)/self.view.width*relmousepos.x-((i+1)*moveOpts.translateX/self.options.extraImgs):0,transY=moveOpts.translateY?2*((i+1)*moveOpts.translateY/self.options.extraImgs)/self.view.height*relmousepos.y-((i+1)*moveOpts.translateY/self.options.extraImgs):0,transZ=moveOpts.translateZ?2*((i+1)*moveOpts.translateZ/self.options.extraImgs)/self.view.height*relmousepos.y-((i+1)*moveOpts.translateZ/self.options.extraImgs):0;el.style.WebkitTransform=perspective(+moveOpts.perspective+px)translate3d(+transX+px,+transY+px,+transZ+px)rotate3d(1,0,0,+rotX+deg)rotate3d(0,1,0,+rotY+deg)rotate3d(0,0,1,+rotZ+deg);el.style.transform=perspective(+moveOpts.perspective+px)translate3d(+transX+px,+transY+px,+transZ+px)rotate3d(1,0,0,+rotX+deg)rotate3d(0,1,0,+rotY+deg)rotate3d(0,0,1,+rotZ+deg);}});});//resetallwhenmouseleavesthemainwrapper.this.tiltWrapper.addEventListener(mouseleave,function(ev){setTimeout(function(){for(vari=0,len=self.imgElems.length;ilen;++i){varel=self.imgElems[i];el.style.WebkitTransform=perspective(+moveOpts.perspective+px)translate3d(0,0,0)rotate3d(1,1,1,0deg);el.style.transform=perspective(+moveOpts.perspective+px)translate3d(0,0,0)rotate3d(1,1,1,0deg);}},60);});//windowresizewindow.addEventListener(resize,throttle(function(ev){//recalculatetiltWrapperproperties:width/height/left/topself.view={width:self.tiltWrapper.offsetWidth,height:self.tiltWrapper.offsetHeight};},50));};

NodeList接口,HTMLCollection接口

节点对象都是单个节点,但是有时会需要一种数据结构,能够容纳多个节点。DOM提供两种接口,用于部署这种节点的集合:NodeList接口和HTMLCollection接口。

我们可以看到,监听mousemove的事件处理函数中的计算比较复杂,关键的部分就是在这里:

NodeList接口

有些属性和方法返回的是一组节点,比如Node.childNodes、document.querySelectorAll()。它们返回的都是一个部署了NodeList接口的对象。

NodeList接口有时返回一个动态集合,有时返回一个静态集合。所谓动态集合就是一个活的集合,DOM树删除或新增一个相关节点,都会立刻反映在NodeList接口之中。Node.childNodes返回的,就是一个动态集合。

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之类数组特有的方法。

// 数组的继承链
myArray --> Array.prototype --> Object.prototype --> null

// NodeList的继承链
myNodeList --> NodeList.prototype --> Object.prototype --> null

从上面的继承链可以看到,NodeList接口对象并不继承Array.prototype,因此不具有数组接口提供的方法。如果要在NodeList接口使用数组方法,可以将NodeList接口对象转为真正的数组。

var div_list = document.querySelectorAll('div');
var div_array = Array.prototype.slice.call(div_list);

也可以通过下面的方法调用。

var forEach = Array.prototype.forEach;

forEach.call(element.childNodes, function(child){
  child.parentNode.style.color = '#0F0';
});

上面代码让数组的forEach方法在NodeList接口对象上调用。

不过,遍历NodeList接口对象的首选方法,还是使用for循环。

for (var i = 0; i < myNodeList.length; ++i) {
  var item = myNodeList[i];
}

不要使用for…in循环去遍历NodeList接口对象,因为for…in循环会将非数字索引的length属性和下面要讲到的item方法,也遍历进去,而且不保证各个成员遍历的顺序。

ES6新增的for…of循环,也可以正确遍历NodeList接口对象。

var list = document.querySelectorAll( 'input[type=checkbox]' );
for (var item of list) {
  item.checked = true;
}

NodeList接口提供item方法,接受一个数字索引作为参数,返回该索引对应的成员。如果取不到成员,或者索引不合法,则返回null。

nodeItem = nodeList.item(index)

// 实例
var divs = document.getElementsByTagName("div");
var secondDiv = divs.item(1);

上面代码中,由于数字索引从零开始计数,所以取出第二个成员,要使用数字索引1。

所有类似数组的对象,都可以使用方括号运算符取出成员,所以一般情况下,都是使用下面的写法,而不使用item方法。

nodeItem = nodeList[index]
varel=self.imgElems[i],rotX=moveOpts.rotateX?2*((i+1)*moveOpts.rotateX/self.options.extraImgs)/self.view.height*relmousepos.y-((i+1)*moveOpts.rotateX/self.options.extraImgs):0,rotY=moveOpts.rotateY?2*((i+1)*moveOpts.rotateY/self.options.extraImgs)/self.view.width*relmousepos.x-((i+1)*moveOpts.rotateY/self.options.extraImgs):0,rotZ=moveOpts.rotateZ?2*((i+1)*moveOpts.rotateZ/self.options.extraImgs)/self.view.width*relmousepos.x-((i+1)*moveOpts.rotateZ/self.options.extraImgs):0,transX=moveOpts.translateX?2*((i+1)*moveOpts.translateX/self.options.extraImgs)/self.view.width*relmousepos.x-((i+1)*moveOpts.translateX/self.options.extraImgs):0,transY=moveOpts.translateY?2*((i+1)*moveOpts.translateY/self.options.extraImgs)/self.view.height*relmousepos.y-((i+1)*moveOpts.translateY/self.options.extraImgs):0,transZ=moveOpts.translateZ?2*((i+1)*moveOpts.translateZ/self.options.extraImgs)/self.view.height*relmousepos.y-((i+1)*moveOpts.translateZ/self.options.extraImgs):0;el.style.WebkitTransform=perspective(+moveOpts.perspective+px)translate3d(+transX+px,+transY+px,+transZ+px)rotate3d(1,0,0,+rotX+deg)rotate3d(0,1,0,+rotY+deg)rotate3d(0,0,1,+rotZ+deg);el.style.transform=perspective(+moveOpts.perspective+px)translate3d(+transX+px,+transY+px,+transZ+px)rotate3d(1,0,0,+rotX+deg)rotate3d(0,1,0,+rotY+deg)rotate3d(0,0,1,+rotZ+deg);

HTMLCollection接口

HTMLCollection接口与NodeList接口类似,也是节点的集合,但是集合成员都是Element节点。该接口都是动态集合,节点的变化会实时反映在集合中。document.links、docuement.forms、document.images等属性,返回的都是HTMLCollection接口对象。

部署了该接口的对象,具有length属性和数字索引,因此是一个类似于数组的对象。

item方法根据成员的位置参数(从0开始),返回该成员。如果取不到成员或数字索引不合法,则返回null。

var c = document.images;
var img1 = c.item(10);

// 等价于下面的写法
var img1 = c[1];

namedItem方法根据成员的ID属性或name属性,返回该成员。如果没有对应的成员,则返回null。

// HTML代码为
// <form id="myForm"></form>
var elem = document.forms.namedItem("myForm");
// 等价于下面的写法
var elem = document.forms["myForm"];

由于item方法和namedItem方法,都可以用方括号运算符代替,所以建议一律使用方括号运算符。

这里我们根据鼠标的位置,计算出了各个图层对应的偏移量和旋转角度,然后对它们进行变换即可。最后mouseleave之后,我们再把个个图层恢复到初始位置就行了。

ParentNode接口,ChildNode接口

不同的节点除了继承Node接口以外,还会继承其他接口。ParentNode接口用于获取当前节点的Element子节点,ChildNode接口用于处理当前节点的子节点(包含但不限于Element子节点)。

ParentNode接口

ParentNode接口用于获取Element子节点。Element节点、Document节点和DocumentFragment节点,部署了ParentNode接口。凡是这三类节点,都具有以下四个属性,用于获取Element子节点。

(1)children

children属性返回一个动态的HTMLCollection集合,由当前节点的所有Element子节点组成。

下面代码遍历指定节点的所有Element子节点。

if (el.children.length) {
  for (var i = 0; i < el.children.length; i++) {
    // ...
  }
}

(2)firstElementChild

firstElementChild属性返回当前节点的第一个Element子节点,如果不存在任何Element子节点,则返回null。

document.firstElementChild.nodeName
// "HTML"

上面代码中,document节点的第一个Element子节点是<HTML>。

(3)lastElementChild

lastElementChild属性返回当前节点的最后一个Element子节点,如果不存在任何Element子节点,则返回null。

document.lastElementChild.nodeName
// "HTML"

上面代码中,document节点的最后一个Element子节点是<HTML>。

(4)childElementCount

childElementCount属性返回当前节点的所有Element子节点的数目。

ChildNode接口

ChildNode接口用于处理子节点(包含但不限于Element子节点)。Element节点、DocumentType节点和CharacterData接口,部署了ChildNode接口。凡是这三类节点(接口),都可以使用下面四个方法。但是现实的情况是,除了第一个remove方法,目前没有浏览器支持后面三个方法。

(1)remove()

remove方法用于移除当前节点。

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.clientWidthdocument.documentElement.clientHeight,基本上与window.innerWidthwindow.innerHeight同义。只有一个区别,前者不将滚动条计算在内(很显然,滚动条和工具栏会减小视口大小),而后者包括了滚动条的高度和宽度。

(2)offsetWidth属性,offsetHeight属性

这两个属性返回html元素的宽度和高度,即网页的总宽度和总高度。

dataset属性

dataset属性用于操作HTML标签元素的data-*属性。下面是一个有data-*属性的div节点。

<div id="myDiv" data-id="myId"></div>

要读取data-id属性,可以从当前节点的dataset.id属性读取。

var id = document.getElementById("myDiv").dataset.id;

要设置data-id属性,可以直接对dataset.id赋值。如果该属性不存在,将会被新建。

document.getElementById('myDiv').dataset.id = 'hello';

删除一个data-*属性,可以直接使用delete命令。

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键遍历,以及遍历的优先级。

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对象的子节点。因此,可以采用链式写法来选择子节点。

document.getElementById('header').getElementsByClassName('a')

各大浏览器对这四个方法都支持良好,IE的情况如下:IE
6开始支持getElementsByTagName,IE
8开始支持querySelector和querySelectorAll,IE
9开始支持getElementsByClassName。

(2)elementFromPoint方法

该方法用于选择在指定坐标的最上层的Element对象。

document.elementFromPoint(50,50)

上面代码了选中在(50,50)这个坐标的最上层的那个HTML元素。

(3)HTML元素的属性相关方法

  • hasAttribute():返回一个布尔值,表示Element对象是否有该属性。
  • getAttribute()
  • setAttribute()
  • removeAttribute()

(4)matchesSelector方法

该方法返回一个布尔值,表示Element对象是否符合某个CSS选择器。

document.querySelector('li').matchesSelector('li:first-child')

这个方法需要加上浏览器前缀,需要写成mozMatchesSelector()、webkitMatchesSelector()、oMatchesSelector()、msMatchesSelector()。

(5)focus方法

focus方法用于将当前页面的焦点,转移到指定元素上。

document.getElementById('my-span').focus();

table元素

表格有一些特殊的DOM操作方法。

  • insertRow():在指定位置插入一个新行(tr)。
  • deleteRow():在指定位置删除一行(tr)。
  • insertCell():在指定位置插入一个单元格(td)。
  • deleteCell():在指定位置删除一个单元格(td)。
  • createCaption():插入标题。
  • deleteCaption():删除标题。
  • createTHead():插入表头。
  • deleteTHead():删除表头。

下面是使用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节点有不同的办法可以获取。

  • 对于正常的网页,直接使用documentwindow.document
  • 对于iframe载入的网页,使用iframe节点的contentDocument属性。
  • 对Ajax操作返回的文档,使用XMLHttpRequest对象的responseXML属性。
  • 对于某个节点包含的文档,使用该节点的ownerDocument属性。

上面这四种document节点,都部署了Document接口,因此有共同的属性和方法。当然,各自也有一些自己独特的属性和方法,比如HTML和XML文档的document节点就不一样。

document节点的属性

document节点有很多属性,用得比较多的是下面这些。

doctype,documentElement,defaultView,body,head,activeElement

以下属性指向文档内部的某个节点。

(1)doctype

对于HTML文档来说,document对象一般有两个子节点。第一个子节点是document.doctype,它是一个对象,包含了当前文档类型(Document
Type Declaration,简写DTD)信息。对于HTML5文档,该节点就代表<!DOCTYPE
html>。如果网页没有声明DTD,该属性返回null。

var doctype = document.doctype;

doctype // "<!DOCTYPE html>"
doctype.name // "html"

document.firstChild通常就返回这个节点。

(2)documentElement

document.documentElement属性,表示当前文档的根节点(root)。它通常是document节点的第二个子节点,紧跟在document.doctype节点后面。

对于HTML网页,该属性返回HTML节点,代表<html lang=”en”>。

(3)defaultView

defaultView属性,在浏览器中返回document对象所在的window对象,否则返回null。

var win = document.defaultView;

(4)body

body属性返回当前文档的body或frameset节点,如果不存在这样的节点,就返回null。这个属性是可写的,如果对其写入一个新的节点,会导致原有的所有子节点被移除。

(4)head

head属性返回当前文档的head节点。如果当前文档有多个head,则返回第一个。

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文档独有的。

document.documentURI === document.URL
// true

(2)domain

domain属性返回当前文档的域名。比如,某张网页的网址是
http://www.example.com/hello.html
domain属性就等于www.example.com。如果无法获取域名,该属性返回null

var badDomain = 'www.example.xxx';
if (document.domain === badDomain)
  window.close();

上面代码判断,如果当前域名等于指定域名,则关闭窗口。

二级域名的情况下,domain属性可以设置为对应的一级域名。比如,当前域名是sub.example.com,则domain属性可以设置为example.com。除此之外的写入,都是不可以的。

(3)lastModified

lastModified属性返回当前文档最后修改的时间戳,格式为字符串。

document.lastModified
// Tuesday, July 10, 2001 10:19:42

注意,lastModified属性的值是字符串,所以不能用来直接比较,两个文档谁的日期更新,需要用Date.parse方法转成时间戳格式,才能进行比较。

if (Date.parse(doc1.lastModified) > Date.parse(doc2.lastModified)) {
  // ...
}

(4)location

document.location属性返回一个只读的location对象,提供了当前文档的URL信息。

// 当前网址为 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赋值给这个属性,网页就会自动跳转到指定网址。

document.location = 'http://www.example.com';
// 等同于
document.location.href = 'http://www.example.com';

注意,采用上面的方法重置URL,跟用户点击链接跳转的效果是一样的。上一个网页依然将保存在浏览器历史之中,点击“后退”按钮就可以回到前一个网页。如果不希望用户看到前一个网页,可以使用location.replace方法,浏览器history对象就会用新的网址,取代当前网址,这样的话,“后退”按钮就不会回到当前网页了。

window.location.replace('http://www.example.com/otherpage.html');

location对象的search属性代表URL的查询字符串(包括?)。

// 查询字符串为 ?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属性等价。

document.location === window.location //true

历史上,IE曾经不允许对document.location赋值,为了保险起见,建议优先使用window.location。如果只是单纯地获取当前网址,建议使用document.URL,语义性更好。

(5)referrer

referrer属性返回一个字符串,表示当前文档的访问来源,如果是无法获取来源或是用户直接键入网址,而不是从其他网页点击,则返回一个空字符串。

(6)title

title属性返回当前文档的标题,该属性是可写的。

document.title = '新标题';

(7)characterSet

characterSet属性返回渲染当前文档的字符集,比如UTF-8、ISO-8859-1。

readyState,designMode

以下属性与文档行为有关。

(1)readyState

readyState属性返回当前文档的状态,共有三种可能的值,加载HTML代码阶段(尚未完成解析)是“loading”,加载外部资源阶段是“interactive”,全部加载完成是“complete”。

下面的代码用来检查网页是否加载成功。

// 基本检查
if (document.readyState === 'complete') {
  // ...
}

// 轮询检查
var interval = setInterval(function() {
  if (document.readyState === 'complete') {
    clearInterval(interval);
    // ...
  }
}, 100);

(2)designMode

designMode属性控制当前document是否可编辑。通常会打开iframe的designMode属性,将其变为一个所见即所得的编辑器。

iframe_node.contentDocument.designMode = "on";

implementation,compatMode

以下属性返回文档的环境信息。

(1)implementation

implementation属性返回一个对象,用来甄别当前环境部署了哪些DOM相关接口。implementation属性的hasFeature方法,可以判断当前环境是否部署了特定版本的特定接口。

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属性返回页面中所有表单。

var selectForm = document.forms[index];
var selectFormElement = document.forms[index].elements[index];

上面代码获取指定表单的指定元素。

(4)images

images属性返回页面所有图片元素(即img标签)。

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标签)。

var scripts = document.scripts;
if (scripts.length !== 0 ) {
  console.log("当前网页有脚本");
}

(6)styleSheets

styleSheets属性返回一个类似数组的对象,包含了当前网页的所有样式表。该属性提供了样式表操作的接口。然后,每张样式表对象的cssRules属性,返回该样式表的所有CSS规则。这又方便了操作具体的CSS规则。

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方法关闭,它所写入的内容就会追加在已有内容的后面。

// 页面显示“helloworld”
document.open();
document.write('hello');
document.write('world');
document.close();

如果页面已经渲染完成(DOMContentLoaded事件发生之后),再调用write方法,它会先调用open方法,擦除当前文档所有内容,然后再写入。

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>
<body>
hello
<script type="text/javascript">
  document.write("world")
</script>
</body>
</html>

在浏览器打开上面网页,将会显示“hello world”。

需要注意的是,虽然调用close方法之后,无法再用write方法写入内容,但这时当前页面的其他DOM节点还是会继续加载。

<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方法完全一致,除了会在输出内容的尾部添加换行符。

document.write(1);
document.write(2);
// 12

document.writeln(1);
document.writeln(2);
// 1
// 2
//

注意,writeln方法添加的是ASCII码的换行符,渲染成HTML网页时不起作用。

hasFocus()

document.hasFocus方法返回一个布尔值,表示当前文档之中是否有元素被激活或获得焦点。

focused = document.hasFocus();

注意,有焦点的文档必定被激活(active),反之不成立,激活的文档未必有焦点。比如如果用户点击按钮,从当前窗口跳出一个新窗口,该新窗口就是激活的,但是不拥有焦点。

querySelector(),getElementById(),querySelectorAll(),getElementsByTagName(),getElementsByClassName(),getElementsByName(),elementFromPoint()

以下方法用来选中当前文档中的元素。

(1)querySelector()

querySelector方法返回匹配指定的CSS选择器的元素节点。如果有多个节点满足匹配条件,则返回第一个匹配的节点。如果没有发现匹配的节点,则返回null

var el1 = document.querySelector('.myclass');
var el2 = document.querySelector('#myParent > [ng-click]');

querySelector方法无法选中CSS伪元素。

(2)getElementById()

getElementById方法返回匹配指定ID属性的元素节点。如果没有发现匹配的节点,则返回null。

var elem = document.getElementById("para1");

注意,在搜索匹配节点时,id属性是大小写敏感的。比如,如果某个节点的id属性是main,那么document.getElementById("Main")将返回null,而不是指定节点。

getElementById方法与querySelector方法都能获取元素节点,不同之处是querySelector方法的参数使用CSS选择器语法,getElementById方法的参数是HTML标签元素的id属性。

document.getElementById('myElement')
document.querySelector('#myElement')

上面代码中,两个方法都能选中id为myElement的元素,但是getElementById()比querySelector()效率高得多。

(3)querySelectorAll()

querySelectorAll方法返回匹配指定的CSS选择器的所有节点,返回的是NodeList类型的对象。NodeList对象不是动态集合,所以元素节点的变化无法实时反映在返回结果中。

elementList = document.querySelectorAll(selectors);

querySelectorAll方法的参数,可以是逗号分隔的多个CSS选择器,返回所有匹配其中一个选择器的元素。

var matches = document.querySelectorAll('div.note, div.alert');

上面代码返回class属性是note或alert的div元素。

querySelectorAll方法支持复杂的CSS选择器。

// 选中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对象上调用,也可以在任何元素节点上调用。

// document对象上调用
var elements = document.getElementsByClassName(names);
// 非document对象上调用
var elements = rootElement.getElementsByClassName(names);

getElementsByClassName方法的参数,可以是多个空格分隔的class名字,返回同时具有这些节点的元素。

document.getElementsByClassName('red test');

上面代码返回class同时具有red和test的元素。

(5)getElementsByTagName()

getElementsByTagName方法返回所有指定标签的元素(搜索范围包括本身)。返回值是一个HTMLCollection对象,也就是说,搜索结果是一个动态集合,任何元素的变化都会实时反映在返回的集合中。这个方法不仅可以在document对象上调用,也可以在任何元素节点上调用。

var paras = document.getElementsByTagName("p");

上面代码返回当前文档的所有p元素节点。

注意,getElementsByTagName方法会将参数转为小写后,再进行搜索。

(6)getElementsByName()

getElementsByName方法用于选择拥有name属性的HTML元素,比如form、img、frame、embed和object,返回一个NodeList格式的对象,不会实时反映元素的变化。

// 表单为 <form name="x"></form>
var forms = document.getElementsByName("x");
forms[0].tagName // "FORM"

注意,在IE浏览器使用这个方法,会将没有name属性、但有同名id属性的元素也返回,所以name和id属性最好设为不一样的值。

(7)elementFromPoint()

elementFromPoint方法返回位于页面指定位置的元素。

var element = document.elementFromPoint(x, y);

上面代码中,elementFromPoint方法的参数x和y,分别是相对于当前窗口左上角的横坐标和纵坐标,单位是CSS像素。elementFromPoint方法返回位于这个位置的DOM元素,如果该元素不可返回(比如文本框的滚动条),则返回它的父元素(比如文本框)。如果坐标值无意义(比如负值),则返回null。

createElement(),createTextNode(),createAttribute(),createDocumentFragment()

以下方法用于生成元素节点。

(1)createElement()

createElement方法用来生成HTML元素节点。

var element = document.createElement(tagName);
// 实例
var newDiv = document.createElement("div");

createElement方法的参数为元素的标签名,即元素节点的tagName属性。如果传入大写的标签名,会被转为小写。如果参数带有尖括号(即<和>)或者是null,会报错。

(2)createTextNode()

document.createTextNode方法用来生成文本节点,参数为所要生成的文本节点的内容。

var newDiv = document.createElement('div');
var newContent = document.createTextNode('Hello');
newDiv.appendChild(newContent);

上面代码新建一个div节点和一个文本节点,然后将文本节点插入div节点。

这个方法可以确保返回的节点,被浏览器当作txt文本渲染,而不是当作HTML代码渲染。因此,可以用来展示用户的输入,避免XSS攻击。

var div = document.createElement('div');
div.appendChild(document.createTextNode('Foo & bar'));
console.log(div.innerHTML)
// Foo & bar

上面代码中,createTextNode方法对大于号和小于号进行转义,从而保证即使用户输入的内容包含恶意代码,也能正确显示。

需要注意的是,该方法不对单引号和双引号转义,所以不能用来对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="'%20+%20escapeHtml(userWebsite)%20+%20'">Bob</a>';
var div = document.getElemenetById('target');
div.innerHtml = profileLink;
// <a href="" onmouseover="alert('derp')" "">Bob</a>

上面代码中,由于createTextNode方法不转义双引号,导致onmouseover方法被注入了代码。

(3)createAttribute()

document.createAttribute方法生成一个新的属性对象节点,并返回它。

attribute = document.createAttribute(name);

createAttribute方法的参数name,是属性的名称。

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对象。

var docFragment = document.createDocumentFragment();

DocumentFragment对象是一个存在于内存的DOM片段,但是不属于当前文档,常常用来生成较复杂的DOM结构,然后插入当前文档。这样做的好处在于,因为DocumentFragment不属于当前文档,对它的任何改动,都不会引发网页的重新渲染,比直接修改当前文档的DOM有更好的性能表现。

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方法使用,触发指定事件。

var event = document.createEvent(type);

createEvent方法的参数是事件类型,比如UIEvents、MouseEvents、MutationEvents、HTMLEvents。

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的子节点遍历器。

var nodeIterator = document.createNodeIterator(
  document.body,
  NodeFilter.SHOW_ELEMENT
);

上面代码返回body元素的遍历器。createNodeIterator方法的第一个参数为遍历器的根节点,第二个参数为所要遍历的节点类型,这里指定为元素节点。其他类型还有所有节点(NodeFilter.SHOW_ALL)、文本节点(NodeFilter.SHOW_TEXT)、评论节点(NodeFilter.SHOW_COMMENT)等。

所谓“遍历器”,在这里指可以用nextNode方法和previousNode方法依次遍历根节点的所有子节点。

var nodeIterator = document.createNodeIterator(document.body);
var pars = [];
var currentNode;

while (currentNode = nodeIterator.nextNode()) {
  pars.push(currentNode);
}

上面代码使用遍历器的nextNode方法,将根节点的所有子节点,按照从头部到尾部的顺序,读入一个数组。nextNode方法先返回遍历器的内部指针所在的节点,然后会将指针移向下一个节点。所有成员遍历完成后,返回null。previousNode方法则是先将指针移向上一个节点,然后返回该节点。

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方法的第一个参数,是所要遍历的根节点,第二个参数指定所要遍历的节点类型。

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方法将某个节点,从其原来所在的文档移除,插入当前文档,并返回插入后的新节点。

node = document.adoptNode(externalNode);

importNode方法从外部文档拷贝指定节点,插入当前文档。

var node = document.importNode(externalNode, deep);

(2)importNode()

importNode方法用于创造一个外部节点的拷贝,然后插入当前文档。它的第一个参数是外部节点,第二个参数是一个布尔值,表示对外部节点是深拷贝还是浅拷贝,默认是浅拷贝(false)。虽然第二个参数是可选的,但是建议总是保留这个参数,并设为true。

另外一个需要注意的地方是,importNode方法只是拷贝外部节点,这时该节点的父节点是null。下一步还必须将这个节点插入当前文档的DOM树。

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》部分。

// 添加事件监听函数
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代码。

<p id="para">Hello World</p>

获取attributes成员的代码如下。

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属性。

下面代码是遍历一个元素节点的所有属性。

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属性的值相等。

// 假定HTML代码如下
// Hello
var span = document.getElementById("span");
span.tagName // "SPAN"

innerHTML,outerHTML

以下属性返回元素节点的HTML内容。

(1)innerHTML

innerHTML属性返回该元素包含的HTML代码。该属性可读写,常用来设置某个节点的内容。

如果将该属性设为空,等于删除所有它包含的所有节点。

el.innerHTML = '';

上面代码等于将el节点变成了一个空节点,el原来包含的节点被全部删除。

注意,如果文本节点中包含&、小于号(<)和大于号(%gt;),innerHTML属性会将它们转为实体形式&amp、&lt、&gt。

// HTML代码如下 <p id="para"> 5 > 3 </p>
document.getElementById('para').innerHTML
// 5 > 3

由于上面这个原因,导致在innerHTML插入<script>标签,不会被执行。

var name = "<script>alert('haha')</script>";
el.innerHTML = name;

上面代码将脚本插入内容,脚本并不会执行。但是,innerHTML还是有安全风险的。

var name = "<img src=x onerror=alert(1)>";
el.innerHTML = name;

上面代码中,alert方法是会执行的。因此为了安全考虑,如果插入的是文本,最好用textContent属性代替innerHTML。

(2)outerHTML

outerHTML属性返回一个字符串,内容为指定元素的所有HTML代码,包括它自身和包含的所有子元素。

// 假定HTML代码如下
// <div id="d"><p>Hello</p></div>

d = document.getElementById("d");
dump(d.outerHTML);

// '<div id="d"><p>Hello</p></div>'

outerHTML属性是可读写的,对它进行赋值,等于替换掉当前元素。

// 假定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属性重新赋值,会抛出一个错误。

document.documentElement.outerHTML = "test";  // DOMException

children,childElementCount,firstElementChild,lastElementChild

以下属性与元素节点的子元素相关。

(1)children

children属性返回一个类似数组的动态对象(实时反映变化),包括当前元素节点的所有子元素。如果当前元素没有子元素,则返回的对象包含零个成员。

// 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。

// 假定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就是这个对象的一个成员。

<div class="one two three" id="myDiv"></div>

上面这个div元素的节点对象的className属性和classList属性,分别如下。

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的列表转为字符串。

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在添加和删除某个类时的写法。

// 添加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,则去除该属性。

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。

element.scrollHeight - element.scrollTop === element.clientHeight

如果滚动条没有滚动到内容底部,上面的表达式为false。这个特性结合onscroll事件,可以判断用户是否滚动到了指定元素的底部,比如是否滚动到了《使用须知》区块的底部。

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。该属性是可读写属性,设置该属性的值,会导致浏览器将指定元素自动滚动到相应位置。

document.querySelector('div').scrollTop = 150;

上面代码将div元素向下滚动150像素。

方法

hasAttribute(),getAttribute(),removeAttribute(),setAttribute()

以下方法与元素节点的属性相关。

(1)hasAttribute()

hasAttribute方法返回一个布尔值,表示当前元素节点是否包含指定的HTML属性。

var d = document.getElementById("div1");

if (d.hasAttribute("align")) {
  d.setAttribute("align", "center");
}

上面代码检查div节点是否含有align属性。如果有,则设置为“居中对齐”。

(2)getAttribute()

getAttribute方法返回当前元素节点的指定属性。如果指定属性不存在,则返回null

var div = document.getElementById('div1');
div.getAttribute('align') // "left"

(3)removeAttribute()

removeAttribute方法用于从当前元素节点移除属性。

// 原来的HTML代码
// <div id="div1" align="left" width="200px">
document.getElementById("div1").removeAttribute("align");
// 现在的HTML代码
// <div id="div1" width="200px">

(4)setAttribute()

setAttribute方法用于为当前元素节点新增属性,或编辑已存在的属性。

var d = document.getElementById('d1');
d.setAttribute('align', 'center');

该方法会将所有属性名,都当作小写处理。对于那些已存在的属性,该方法是编辑操作,否则就会新建属性。

下面是一个对img元素的src属性赋值的例子。

var myImage = document.querySelector('img');
myImage.setAttribute ('src', 'path/to/example.png');

大多数情况下,直接对属性赋值比使用该方法更好。

el.value = 'hello';
// or
el.setAttribute('value', 'hello');

querySelector(),querySelectorAll(),getElementsByClassName(),getElementsByTagName()

以下方法与获取当前元素节点的子元素相关。

(1)querySelector()

querySelector方法接受CSS选择器作为参数,返回父元素的第一个匹配的子元素。

var content = document.getElementById('content');
var el = content.querySelector('p');

上面代码返回content节点的第一个p元素。

注意,如果CSS选择器有多个组成部分,比如div p,querySelector方法会把父元素考虑在内。假定HTML代码如下。

<div id="outer">
  <p>Hello</p>
  <div id="inner">
    <p>World</p>
  </div>
</div>

那么,下面代码会选中第一个p元素。

var outer = document.getElementById('outer');
var el = outer.querySelector('div p');

(2)querySelectorAll()

querySelectorAll方法接受CSS选择器作为参数,返回一个NodeList对象,包含所有匹配的子元素。

var el = document.querySelector('#test');
var matches = el.querySelectorAll('div.highlighted > p');

在CSS选择器有多个组成部分时,querySelectorAll方法也是会把父元素本身考虑在内。

还是以上面的HTML代码为例,下面代码会同时选中两个p元素。

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代码如下。

<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方法的例子如下。

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选择器。

if (el.matches(".someClass")) {
  console.log("Match!");
}

该方法带有浏览器前缀,下面的函数可以兼容不同的浏览器,并且在浏览器不支持时,自行部署这个功能。

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》部分。

// 添加事件监听函数
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盒状模型的内容。

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方法返回一个类似数组的对象,里面是当前元素在页面上形成的所有矩形。每个矩形都有bottomheightleftrighttopwidth六个属性,表示它们相对于视口的四个坐标,以及本身的高度和宽度。

对于盒状元素(比如div和p),该方法返回的对象中只有该元素一个成员。对于行内元素(比如span、a、em),该方法返回的对象有多少个成员,取决于该元素在页面上占据多少行。

Hello World
Hello World
Hello World

上面代码是一个行内元素span,如果它在页面上占据三行,getClientRects方法返回的对象就有三个成员,如果它在页面上占据一行,getClientRects方法返回的对象就只有一个成员。

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树的指定位置。

element.insertAdjacentHTML(position, text);

该方法接受两个参数,第一个是指定位置,第二个是待解析的字符串。

指定位置共有四个。

  • beforebegin:在当前元素节点的前面。
  • afterbegin:在当前元素节点的里面,插在它的第一个子元素之前。
  • beforeend:在当前元素节点的里面,插在它的最后一个子元素之后。
  • afterend:在当前元素节点的后面。’

// 原来的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树删除。

var el = document.getElementById('div-01');
el.remove();

scrollIntoView()

scrollIntoView方法滚动当前元素,进入浏览器的可见区域。

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节点。

// 获取Text节点
var textNode = document.querySelector('p').firstChild;

// 创造Text节点
var textNode = document.createTextNode('Hi');
document.querySelector('div').appendChild(textNode);

浏览器原生提供一个Text构造函数。它返回一个Text节点。它的参数就是该Text节点的文本内容。

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节点的内容。

// 读取文本内容
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代码如下。

<p id="para">A <em>B</em> C</p>

这时,Text节点的wholeText属性和data属性,返回值相同。

var el = document.getElementById("para");
el.firstChild.wholeText // "A "
el.firstChild.data // "A "

但是,一旦移除em节点,wholeText属性与data属性就会有差异,因为这时其实P节点下面包含了两个毗邻的Text节点。

el.removeChild(para.childNodes[1]);
el.firstChild.wholeText // "A C"
el.firstChild.data // "A "

length

length属性返回当前Text节点的文本长度。

(new Text('Hello')).length // 5

nextElementSibling

nextElementSibling属性返回紧跟在当前Text节点后面的那个同级Element节点。如果取不到这样的节点,则返回null。

// 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节点中的开始位置,第二个参数为子字符串长度。

// 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节点。

// HTML代码为
// <p>Hello World</p>

document.querySelector('p').firstChild.remove()
// 现在页面代码为
// <p></p>

splitText(),normalize()

splitText方法将Text节点一分为二,变成两个毗邻的Text节点。它的参数就是分割位置(从零开始),分割到该位置的字符前结束。如果分割位置不存在,将报错。

分割后,该方法返回分割位置后方的字符串,而原Text节点变成只包含分割位置前方的字符串。

// 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方法可以实现逆操作,将它们合并。

p.childNodes.length // 2

// 将毗邻的两个Text节点合并
p.normalize();
p.childNodes.length // 1

DocumentFragment节点

DocumentFragment节点代表一个文档的片段,本身就是一个完整的DOM树形结构。它没有父节点,不属于当前文档,操作DocumentFragment节点,要比直接操作DOM树快得多。

它一般用于构建一个DOM结构,然后插入当前文档。document.createDocumentFragment方法,以及浏览器原生的DocumentFragment构造函数,可以创建一个空的DocumentFragment节点。然后再使用其他DOM方法,向其添加子节点。

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方法。

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等浏览器内置对象,也部署了这个接口。

该接口就是三个方法,addEventListenerremoveEventListener用于绑定和移除监听函数,dispatchEvent用于触发事件。

addEventListener()

addEventListener方法用于在当前节点或对象上,定义一个特定事件的监听函数。

target.addEventListener(type, listener[, useCapture]);

上面是使用格式,addEventListener方法接受三个参数。

  • type,事件名称,大小写不敏感。

  • listener,监听函数。指定事件发生时,会调用该监听函数。

  • useCapture,监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分)。该参数是一个布尔值,默认为false(表示监听函数只在冒泡阶段被触发)。老式浏览器规定该参数必写,较新版本的浏览器允许该参数可选。为了保持兼容,建议总是写上该参数。

下面是一个例子。

function hello(){
  console.log('Hello world');
}

var button = document.getElementById("btn");
button.addEventListener('click', hello, false);

上面代码中,addEventListener方法为button节点,绑定click事件的监听函数hello,该函数只在冒泡阶段触发。

可以使用addEventListener方法,为当前对象的同一个事件,添加多个监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener方法手动去除)。

function hello(){
  console.log('Hello world');
}

document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);

执行上面代码,点击文档只会输出一行“Hello world”。

如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。

function print(x) {
  console.log(x);
}

var el = document.getElementById("div1");
el.addEventListener("click", function(){print('Hello')}, false);

上面代码通过匿名函数,向监听函数print传递了一个参数。

removeEventListener()

removeEventListener方法用来移除addEventListener方法添加的事件监听函数。

div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);

removeEventListener方法的参数,与addEventListener方法完全一致。它对第一个参数“事件类型”,也是大小写不敏感。

注意,removeEventListener方法移除的监听函数,必须与对应的addEventListener方法的参数完全一致,而且在同一个元素节点,否则无效。

dispatchEvent()

dispatchEvent方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true

target.dispatchEvent(event)

dispatchEvent方法的参数是一个Event对象的实例。

para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);

上面代码在当前节点触发了click事件。

如果dispatchEvent方法的参数为空,或者不是一个有效的事件对象,将报错。

下面代码根据dispatchEvent方法的返回值,判断事件是否被取消了。

var canceled = !cb.dispatchEvent(event);
  if (canceled) {
    console.log('事件取消');
  } else {
    console.log('事件未取消');
  }
}

监听函数

监听函数(listener)是事件发生时,程序所要执行的函数。它是事件驱动编程模式的主要编程方式。

DOM提供三种方法,可以用来为事件绑定监听函数。

HTML标签的on-属性

HTML语言允许在元素标签的属性中,直接定义某些事件的监听代码。

<body onload="doSomething()">
<div onclick="console.log('触发事件')">

上面代码为body节点的load事件、div节点的click事件,指定了监听函数。

使用这个方法指定的监听函数,只会在冒泡阶段触发。

注意,使用这种方法时,on-属性的值是将会执行的代码,而不是“监听函数”。

<!-- 正确 -->
<body onload="doSomething()">

<!-- 错误 -->
<body onload="doSomething">

一旦指定的事件发生,on-属性的值是原样传入JavaScript引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。

另外,Element节点的setAttribute方法,其实设置的也是这种效果。

el.setAttribute('onclick', 'doSomething()');

Element节点的事件属性

Element节点有事件属性,可以定义监听函数。

window.onload = doSomething;

div.onclick = function(event){
  console.log('触发事件');
};

使用这个方法指定的监听函数,只会在冒泡阶段触发。

addEventListener方法

通过Element节点、document节点、window对象的addEventListener方法,也可以定义事件的监听函数。

window.addEventListener('load', doSomething, false);

addEventListener方法的详细介绍,参见本节EventTarget接口的部分。

在上面三种方法中,第一种“HTML标签的on-属性”,违反了HTML与JavaScript代码相分离的原则;第二种“Element节点的事件属性”的缺点是,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。因此,这两种方法都不推荐使用,除非是为了程序的兼容问题,因为所有浏览器都支持这两种方法。

addEventListener是推荐的指定监听函数的方法。它有如下优点:

  • 可以针对同一个事件,添加多个监听函数。

  • 能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发回监听函数。

  • 除了DOM节点,还可以部署在window、XMLHttpRequest等对象上面,等于统一了整个JavaScript的监听函数接口。

this对象的指向

实际编程中,监听函数内部的this对象,常常需要指向触发事件的那个Element节点。

addEventListener方法指定的监听函数,内部的this对象总是指向触发事件的那个节点。

// 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。这是因为监听函数被“拷贝”成了节点的一个属性,使用下面的写法,会看得更清楚。

para.onclick = hello;

如果将监听函数部署在Element节点的on-属性上面,this不会指向触发事件的元素节点。

<p id="para" onclick="hello()">Hello</p>
<!-- 或者使用JavaScript代码  -->
<script>
  pElement.setAttribute('onclick', 'hello()');
</script>

执行上面代码,点击p节点会输出doc。这是因为这里只是调用hello函数,而hello函数实际是在全局作用域执行,相当于下面的代码。

para.onclick = function(){
  hello();
}

一种解决方法是,不引入函数作用域,直接在on-属性写入所要执行的代码。因为on-属性是在当前节点上执行的。

<p id="para" onclick="console.log(id)">Hello</p>
<!-- 或者 -->
<p id="para" onclick="console.log(this.id)">Hello</p>

上面两行,最后输出的都是para。

总结一下,以下写法的this对象都指向Element节点。

// 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代码
element.onclick = function (){ doSomething() };
element.setAttribute('onclick', 'doSomething()');

// HTML代码
<element onclick="doSomething()">