DOM探索之基础详解

DOM为document   object   model三个单词的缩写,直译过来即文档对象模型。

DOM的地位

我们知道,一个网页是由html来搭建结构的,通过css来定义网页的样式,而javascript赋予了页面的行为,通过它我们可以与页面进行交互,实现页面的动画效果等等。那javascript究竟通过什么来实现的呢?通过ECMAScript这个标准,我们可以编写程序让浏览器来解析,利用ECMAScript,我们可以通过BOM对象(即browser object model)来操作浏览器窗口、浏览器导航对象、屏幕分辨率、浏览器历史、cookie等等。但这个通过BOM来实现的交互远远不够。要实现页面的动态交互和效果,操作html才是核心。那如何操作html呢?对,就是DOM,简单的说,DOM给我们提供了用程序来动态控制html的接口,也就是早期的DHTMl的概念。因此,DOM处在javascript赋予html具备动态交互和效果的能力的核心地位上。

首先我们通过一个案例认识一下DOM在实际开发中是如何应用的;其次来了解什么是DOM,以及与DOM相关的基础概念;然后重新认识一下html与xml的两种文档类型以及文档的各种节点类型;接着来详细的实现DOMReady;接着了解如何判断元素节点的类型以及什么是元素节点的继承层次;最后了解各种元素节点的分类和规则。

在深入学习DOM之前,先做一个热身,做一个小案例感受一下DOM在web特效开发中是如何应用的。

DOM操作案例——滑动门特效

需求:把鼠标放到想要展示的图片上,当前图片伸展打开,其他的图片堆叠关闭。

实现:搭建结构——定义样式——着手编写交互效果代码(js+DOM)

效果图


(1).搭建结构:index.html

<!doctype html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8">
		<title>sliding doors</title>
		<link rel="stylesheet" href="styles/slidingdoors.css" />
		<script src="scripts/slidingdoors.js"></script>
	</head>
	<body>
		<div id='container'>
			<img src="images/door1.png" alt="1080P神器" title="1080P神器" />
			<img src="images/door2.png" alt="5.5寸四核" title="5.5寸四核" />
			<img src="images/door3.png" alt="四核5寸" title="四核5寸" />
			<img src="images/door4.png" alt="5.7寸机皇" title="5.7寸机皇" />
		</div>
	</body>
</html>

(2).定义样式:slidingdoors.css

* {
	margin:0;
	padding:0;
}
#container {
	height: 477px;
	margin: 0 auto;
	border-right: 1px solid #ccc;
	border-bottom: 1px solid #ccc;
	overflow: hidden;
	position: relative;
}

#container img {
	position: absolute;
	display: block;
	left: 0;
	border-left: 1px solid #ccc;
}

(3).编写交互效果:

第一步先定义页面加载完毕再进行DOM操作的方法。

window.οnlοad=function(){

};

window.onload方法表示当页面所有的元素都加载完毕,并且所有要请求的资源也加载完毕才触发执行function这个匿名函数里边的具体内容。这里的onload就是DOM范畴的其中一部分内容——事件。我们要实现和页面进行交互,就要通过js来操作页面元素或者说操作DOM,而要想操作DOM,就必须先找到DOM元素的节点,也就是要找到操作的对象

我们先来获得div这个容器对象,再通过获得的div对象,获得四张图片的对象集合。

//获得容器对象

var box=document.getElementById("container");

//获得图片nodeList对象集合

var imgs=box.getElementsByTagName("img");

先看效果图,当前状态是第一张图片打开,我们来看四张图片的具体位置,第一张图片的位置距离容器左侧为0;第二张图片的位置距离容器左侧为一张图片的宽度;第三张图片的位置距离容器左侧为一张图片的宽度+1个堆叠的宽度;第四张图片的位置距离容器左侧为一张图片的宽度+2个堆叠的宽度;以此类推。

返回代码,根据刚才直观的查看,想要设置我们四张图片的位置就需要知道单张图片的宽度和需要定义我们堆叠的宽度

//获得单张图片的宽度(这里的每张图片大小一致,所以可以取得任意一张图片获得其宽度,这里取第一张图片imgs[0]的宽度)

var imgWidth=imgs[0].offsetWidth;

//设置掩藏门体露出的宽度

var exposeWidth=160;   //这个值可按需求自定义

我们返回到效果,我们要设置每张图片的具体位置,我们有必要将我们的总盒子的宽度设置为他应该的值,这样的话,我们的盒子才正好的将我们所有的图片包含在内。那么我们盒子的总宽度应该等于我们单张打开的图片的宽度+我们剩余的图片的堆叠的宽度的和。

回到代码

//设置容器总宽度

var boxWidth=imgWidth+(imgs.length-1)*exposeWidth;

box.style.width=boxWidth+'px';

写到这,我们可以先看看效果。目前我们四张图片还是堆叠在一起,但容器的宽度已经设置好了,下面我们就让图片归位。

回到代码,我们来设置每道门的初始位置,这里通过for循环一并为所有的图片来进行定位。由于第一道门距离容器左侧为0,我们无需设置它的位置,因此我们循环的累加变量初始值为1。

//设置每道门的初始位置

function setImgsPos(){

for(var i=1,len=imgs.length;i<len;i++){

    imgs[i].style.left=imgWidth+exposeWidth*(i-1)+'px';

}

}

setImgsPos();         //需要调用执行一下

现在我们来浏览器中看一下效果,此时四张图片已经归位,下面我们来实现门体的滑动。鼠标滑过以后,每个应该滑动的门体滑动的距离是多少呢?我们仔细观察不难看出,这个距离就是我们图片的宽度减去我们堆叠露出的宽度。

回到代码,我们首先计算每道门打开时应移动的距离

//计算每道门打开时应移动的距离

var translate=imgWidth-exposeWidth;

//为每道门绑定事件

for(var i=0,len=imgs.length;i<len;i++){

    //使用立即调用的函数表达式,为了获得不同的i值

    (function(i){

        imgs[i].οnmοuseοver=function(){

            //先将每道门复位

            setImgsPos();

        };

    })(i);

}

下面我们来实现开门滑动的效果,回到浏览器观察一下,当我们所有的图片都归位以后,当我们鼠标滑到我们想打开的门的时候,比如说第三张,我们只需要将第二张和第三张图片向左滑动即可,其他图片的滑动规律类似。比如我们放到第四张,那么我们需要将第二张、第三张、第四张图片都向左滑动,这就是我们图片滑动的规律。根据得出的规律,我们要实现打开门,只需要处理第二张到当前图片之间所有的图片即可。

回到代码

//为每道门绑定事件

for(var i=0,len=imgs.length;i<len;i++){

    //使用立即调用的函数表达式,为了获得不同的i值

    (function(i){

        imgs[i].οnmοuseοver=function(){

            //先将每道门复位

            setImgsPos();

            //打开门

            for(var j=1;j<=i;j++){

                imgs[j].style.left=parseInt(imgs[j].style.left,10)-translate+'px';

            }

        };

    })(i);

}

到此,这个案例的js代码和DOM操作的代码就编写完毕了。

完整的js代码:slidingdoors.js

window.onload = function() {
	//容器对象
	var box = document.getElementById('container');

	//获得图片NodeList对象集合
	var imgs = box.getElementsByTagName('img');

	//单张图片的宽度
	var imgWidth = imgs[0].offsetWidth;

	//设置掩藏门体露出的宽度
	var exposeWidth = 160;

	//设置容器总宽度
	var boxWidth = imgWidth + (imgs.length - 1) * exposeWidth;
	box.style.width = boxWidth + 'px';

	//设置每道门的初始位置
	function setImgsPos() {
		for (var i = 1, len = imgs.length; i < len; i++) {
			imgs[i].style.left = imgWidth + exposeWidth * (i - 1) + 'px';
		}
	}
	setImgsPos();

	//计算每道门打开时应移动的距离
	var translate = imgWidth - exposeWidth;

	//为每道门绑定事件
	for (var i = 0, len = imgs.length; i < len; i++) {
		//使用立即调用的函数表答式,为了获得不同的i值
		(function(i) {
			imgs[i].onmouseover = function() {
				//先将每道门复位
				setImgsPos();
				//打开门
				for (var j = 1; j <= i; j++) {
					imgs[j].style.left = parseInt(imgs[j].style.left, 10) - translate + 'px';
				}
			};
		})(i);
	}
};	

认识DOM

DOM(文档对象模型)是针对xml经过扩展用于html的应用程序编程接口,我们又叫API。DOM把整个页面映射为一个多层的节点结构,html或xml页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。

DOM级别

(1).DOM1级:由两个模块组成,即DOM  Core(DOM核心)和DOM HTML。其中DOM Core规定的是如何映射基于xml的文档结构,以便简化对文档中任何部分的访问和操作;DOM HTML模块则在DOM Core的基础上加以扩展,添加了针对html的对象和方法。

其实DOM并不是针对javascript,很多别的语言也都实现了DOM,不过在web浏览器中基于ECMAScript实现的DOM的确已经成为javascript这门语言的一个重要组成部分。如果说DOM1级的目标主要是映射文档的结构,那么DOM2级的目标就要宽泛很多了。

(2).DOM2级:由四个模块组成,即DOM Views(DOM 视图)、DOM Events(DOM事件)、DOM Style、DOM Traversal and Range(DOM遍历和范围),DOM Views定义了跟踪不同文档视图的接口,比如跟踪应用css之前和应用css之后的文档视图;DOM Events定义了事件和事件处理的接口;DOM Style定义了基于css为元素应用样式的接口;DOM Traversal and Range定义了遍历和操作文档树的接口。

(3).DOM3级:进一步扩展了DOM,引入了以统一方式加载和保存文档的方法,它在DOM Load And Save这个模块中定义;同时新增了验证文档的方法,是在DOM Validation这个模块中定义的。

我们在阅读DOM标准的时候,经常会看到DOM0级这样的字眼,实际上DOM0级这个标准是不存在的。所谓DOM0级只是DOM历史座标系中的一个参照点而已,具体地说DOM0级就是指IE4.0和Netscape navigator4.0最初支持的那个DHTML。

Web浏览器对DOM的支持
浏览器 DOM兼容性
 Netscape Navigator 1.~4.x                       -
 Netscape 6+(Mozilla 0.6.0+)
1级、2级(几乎全部)、3级(部分)
IE2~IE4.x
-
IE5
1级(最小限度)
IE5.5~IE8
1级(几乎全部)
IE9+
1级、2级、3级
Opera 1~6
-
Opera7~8.x
1级(几乎全部)、2级(部分)
Opera9~9.9
1级、2级(几乎全部)、3级(部分)
Opera 10+
1级、2级、3级(部分)
Safari 1.0.x
1级
Safari 2+
1级、2级(部分)
Chrome 1+
1级、2级(部分)
Firefox 1+
1级、2级(几乎全部)、3级(部分)


DOM文档类型


我们说DOM文档对象模型是从文档中抽象出来的,DOM操作的对象也是文档,因此我们有必要了解一下文档的类型。文档随着历史的发展演变为多种类型,如下



DOM节点类型

DOM1级定义了一个Node接口,这个Node接口在javascript中是作为Node类型来实现的。除了IE以外,其他所有浏览器都可以访问这个类型。每个节点都有一个nodeType属性,用于表明节点的类型。节点类型通过定义数值常量和字符常量两种方式来表示,IE只支持数值常量。节点类型一共有12种,这里介绍常用的7种类型。如下图:



(1).Element(元素节点):是组成文档树的重要部分,它表示了html、xml文档中的元素。通常元素因为有子元素、文本节点或者两者的结合,元素节点是唯一能够拥有属性的节点类型。

(2).Attr(属性节点):代表了元素中的属性,因为属性实际上是附属于元素的,因此属性节点不能被看做是元素的子节点。因而在DOM中属性没有被认为是文档树的一部分。换句话说,属性节点其实被看做是包含它的元素节点的一部分,它并不作为单独的一个节点在文档树中出现。

(3).Text(文本节点):是只包含文本内容的节点,在xml中称为字符数据,它可以由更多的信息组成,也可以只包含空白。在文档树中元素的文本内容和属性的文本内容都是由文本节点来表示的。

(4).Comment(注释节点):表示注释的内容

(5)5.Document(文档节点) :是文档树的根节点,它是文档中其他所有节点的父节点。要注意的是,文档节点并不是html、xml文档的根元素,因为在xml文档中,处理指令、注释等内容可以出现在根元素之外,所以我们在构造DOM树的时候,根元素并不适合作为根节点,因此就有了文档节点,而根元素是作为文档节点的子节点出现的。

(6).DocumentType(文档类型节点):每一个Document都有一个DocumentType属性,它的值或者是null,或者是DocumentType对象。比如声明文档类型时<!doctype html>就是文档类型节点。

(7).DocumentFragment(文档片段节点):是轻量级的或最小的Document对象,它表示文档的一部分或者是一段,不属于文档树。不过它有一种特殊的行为,该行为使得它非常有用。比如:当请求把一个DocumentFragment节点插入到文档的时候,插入的不是DocumentFragment自身,而是它的所有的子孙节点。这使得DocumentFragment成了有用的占位符,暂时存放那些一次插入文档的节点,同时它还有利于实现文档的剪切、复制和粘贴等操作。实例代码如下。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
	<title>DocumentFragment文档片段节点</title>
</head>
<body>
	<ul id="list-node"></ul>
	<script>
		var frag = document.createDocumentFragment();
		for (var i = 0; i < 10; i++) {
			var li = document.createElement("li");
			li.innerHTML = "List item" + i;
			frag.appendChild(li);
		}
		document.getElementById("list-node").appendChild(frag);
	</script>
</body>
</html>
这段代码在浏览器查看元素时就会发现,页面中并没有插入DocumentFragment节点自身,它只作为一个临时占位符存在。

(1).DOM nodeType

通过DOM节点类型,我们可知,可以通过某个节点的nodeType属性来获得节点的类型,节点的类型可以是数值常量或者字符常量。示例代码如下:

<!DOCTYPE html>
<html>
<head lang="en">
	<meta charset="UTF-8">
	<title>nodeType</title>
</head>
<body>
	<div id="container">这是一个元素节点</div>
	<script>
		var divNode = document.getElementById('container');
		/*IE中只支持数值常量,其他浏览器数值常量和字符常量都支持,因此可以直接用数值常量判断,
		 *这里为了比较两种写法,便都写在了这里
		*/
		if (divNode.nodeType == Node.ELEMENT_NODE || divNode.nodeType == 1) {
			alert("Node is an element.");
		}		
	</script>
</body>
</html>


(2).DOM nodeName和nodeValue

先看示例代码

<!DOCTYPE html>
<html>
<head lang="en">
	<meta charset="UTF-8">
	<title>nodeName,nodeValue</title>
</head>
<body>
	<!--nodeName,nodeValue实验-->
	<div id="container">这是一个元素节点</div>
	<script>
		var divNode = document.getElementById('container');
		console.log(divNode.nodeName + "/" + divNode.nodeValue);   
		//结果:    DIV/null
		var attrNode = divNode.attributes[0];
		console.log(attrNode.nodeName + "/" + attrNode.nodeValue);    
		//结果:   id/container
		var textNode = divNode.childNodes[0];
		console.log(textNode.nodeName + "/" + textNode.nodeValue);    
		//结果:   #text/这是一个元素节点
		var commentNode = document.body.childNodes[1];
		//表示取第二个注释节点,因为body下面的第一个注释节点为空白符。
		console.log(commentNode.nodeName + "/" +commentNode.nodeValue);
		//结果:  #comment/nodeName,nodeValue实验
		console.log(document.doctype.nodeName + "/" + document.doctype.nodeValue); 
		//结果: html/null
	    var frag = document.createDocumentFragment();
	    console.log(frag.nodeName + "/" + frag.nodeValue);  
	    //结果: #document-fragment/null
	</script>
</body>
</html>

根据实验,得出以下汇总表格。

节点中的nodeName和nodeValue


domReady

(1).什么是domReady?

html是一种标记语言,它告诉我们这个页面有什么内容,但行为交互是需要通过DOM操作来实现的。我们不要以为有两个尖括号就以为它是一个DOM了,html标签要通过浏览器解析才会变成DOM节点,当我们向地址栏传入一个url的时候,我们开始加载页面,就能看到内容,在这期间就有一个DOM节点构建的过程。节点是以树的形式组织的,当页面上所有的html都转换为节点以后,就叫做DOM树构建完毕,简称为domReady。

(2).那么浏览器是如何将html标签解析变成DOM节点的呢?

实际上浏览器是通过渲染引擎来实现的。渲染引擎的职责就是把请求的内容显示到浏览器屏幕上。默认情况下渲染引擎可以显示html、xml文档及图片。通过插件(浏览器扩展)它可以显示其他类型的文档,比如我们安装pdf viewer插件,我们就可以显示pdf文档。这里专注渲染引擎的主要用途,即是将css格式化的html和图片在浏览器上进行显示。

(3).浏览器渲染引擎的基本渲染流程

渲染引擎首先通过网络获得所请求文档的内容,通常以8k分块的方法来完成

浏览器渲染引擎的基本渲染流程



上图就是html渲染的基本过程,但这并不包含解析过程中浏览器加载外部资源,比如图片、脚本、iframe等的一些过程。说白了,上面的4步仅仅是html结构的渲染过程。而外部资源的加载在html结构的渲染过程中是贯彻始终的,即便绘制DOM节点已经完成,而外部资源仍然可能正在加载或者尚未加载。

Webkit主要渲染流程


更多关于浏览器内部工作原理的文章,请阅读:http://kb.cnblogs.com/page/129756/

(4).domReady的实现策略

上面的各个代码实例中,并没有考虑domReady,程序也能正常运行,因为我们把javascript代码写在了body元素最后的位置。因为浏览器是从上到下,从左向右渲染元素的,这样实例中的js代码一定在domReady之后去执行的。那为什么还要用domReady呢?事实上,我们在编写大型项目的时候,js文件往往非常多,而且之间会相互调用,大多数都是外部引用的,不把js代码直接写在页面上。这样的话,如果有个domReady这个方法,我们想用它就调用,不管逻辑代码写在哪里,都是等到domReady之后去执行的。

在起初的滑动门特效实例中,用到了window.onload方法,表示当页面所有的元素都加载完毕,并且所有要请求的资源也加载完毕才触发执行function这个匿名函数里边的具体内容。这样肯定保证了代码在domReady之后执行。使用window.onload方法在文档外部资源不多的情况下不会有什么问题,但是当页面中有大量远程图片或要请求的远程资源时,我们需要让js在点击每张图片时,进行相应的操作,如果此时外部资源还没有加载完毕,点击图片是不会有任何反应的,大大降低了用户体验。那既然window.onload方法不可行,又该怎么做呢?
你肯定想到了jquery中的$(document).ready(function(){})方法了,其实jquery中的domReady应该和window.onload的实现原理是大同小异的。为了解决window.onload的短板,w3c 新增了一个 DOMContentLoaded 事件。
参考jquery中domReady的实现原理,来看一下javascript中domReady的实现策略。
在页面的DOM树创建完成后(也就是HTML解析第一步完成)即触发,而无需等待其他资源的加载。即domReady实现策略:
1.支持DOMContentLoaded事件的,就使用DOMContentLoaded事件。
2.不支持的就用来自Diego Perini发现的著名Hack兼容。兼容原理大概就是通过IE中的document,documentElement。doScroll('left')来判断DOM树是否创建完毕。
javascript实现domReady,【domReady.js】
function myReady(fn){
    //对于现代浏览器,对DOMContentLoaded事件的处理采用标准的事件绑定方式
    if ( document.addEventListener ) {
        document.addEventListener("DOMContentLoaded", fn, false);
    } else {
        IEContentLoaded(fn);
    }
    //IE模拟DOMContentLoaded
    function IEContentLoaded (fn) {
        var d = window.document;
        var done = false;

        //只执行一次用户的回调函数init()
        var init = function () {
            if (!done) {
                done = true;
                fn();
            }
        };
        (function () {
            try {
                // DOM树未创建完之前调用doScroll会抛出错误
                d.documentElement.doScroll('left');
            } catch (e) {
                //延迟再试一次~
                setTimeout(arguments.callee, 50);
                return;
            }
            // 没有错误就表示DOM树创建完毕,然后立马执行用户回调
            init();
        })();
        //监听document的加载状态
        d.onreadystatechange = function() {
            // 如果用户是在domReady之后绑定的函数,就立马执行
            if (d.readyState == 'complete') {
                d.onreadystatechange = null;
                init();
            }
        }
    }
}
在页面中引入donReady.js文件,引用myReady(回调函数)方法即可。
下面通过一个案例,来比较domReady与window.onload实现的不同,很明显,onload事件是要在所有请求都完成之后才执行,而domReady利用hack技术,在加载完dom树之后就能执行,所以domReady比onload执行时间更早,建议采用domReady。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <title>domReady与window.onload</title>
    <script src="domReady.js"></script>
  </head>
  <body>
    <div id="showMsg"></div>
    <div>
      <img src="1.jpg" />
      <img src="2.jpg" />
      <img src="3.jpg" />
      <img src="4.jpg" />
      <img src="5.jpg" />
      <img src="6.jpg" />
    </div>
    <script>
      var d = document;
      var msgBox = d.getElementById("showMsg");
      var imgs = d.getElementsByTagName("img");
      var time1 = null, time2 = null;
      myReady(function(){
          msgBox.innerHTML += "dom已加载!<br>";
          time1 = new Date().getTime();
          msgBox.innerHTML += "时间戳:" + time1 + "<br>";
      });
      window.onload = function(){
          msgBox.innerHTML += "onload已加载!<br>";
          time2 = new Date().getTime();
          msgBox.innerHTML += "时间戳:" + time2 + "<br>";
          msgBox.innerHTML +="domReady比onload快:" + (time2 - time1) + "ms<br>";
      };
    </script>
  </body>
</html>
执行结果



如何判断元素节点类型

设计元素类型的判定,这里给出有4个方法:
(1).  isElement  :判定某个节点是否为元素节点
(2).  isHTML     :判定某个节点是否为html文档的元素节点
(3).  isXML       : 判定某个节点是否为xml文档的元素节点
(4).  contains   :用来判定两个节点的包含关系

(1).元素节点的判定:isElement  

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isElement</title>
</head>
<body>
	<div id="test">aaa</div>
	<!--这是一个注释节点-->
	<script>
		var isElement = function (el){
			return !!el && el.nodeType === 1;
		}
		var a = {            //随意定义一个变量,设置nodeType为1
		   nodeType: 1
		}
		console.log(isElement(document.getElementById("test"))); 
		//结果:  true
		console.log(isElement(document.getElementById("test").nextSibling));//这里的nextSibling属性查找下一个相邻节点,即注释节点
		//结果:  false
		console.log(isElement(a));
		//结果:  true
	</script>
</body>
</html>
!!一般用来将后面的表达式转换为布尔型的数据(boolean).
因为javascript是弱类型的语言(变量没有固定的数据类型)所以有时需要强制转换为相应的类型,类似的如: 
a=parseInt("1234");
a="1234"+0 //转换为数字 
b=1234+"" //转换为字符串 
c=someObject.toString() //将对象转换为字符串 
其中第1种、第4种为显式转换,2、3为隐式转换.
布尔型的转换,javascript约定和c类似,规则为 :
false、undefinded、null、0、"" 为 false ;
true、1、"somestring"、[Object] 为 true .
!!el表示判断el是否存在,存在为true,反之为false.
注意:上面的代码定义了一个变量a,将它的nodeType的值设为1,由于元素节点的节点类型的数值常量为1,所以这里在打印的的时候,会将a认为是元素节点,所以打印true。这种结果明显不是我们想要的,即使这种情况很少出现。下面给出解决方案
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isElement</title>
</head>
<body>
	<div id="test">aaa</div>
	<!--这是一个注释节点-->
	<script>
		var testDiv = document.createElement('div');
		var isElement = function (obj) {
		    if (obj && obj.nodeType === 1) {//先过滤最简单的
		        if( window.Node && (obj instanceof Node )){ //如果是IE9,则判定其是否Node的实例
		            return true; //由于obj可能是来自另一个文档对象,因此不能轻易返回false
		        }
		        try {//最后以这种效率非常差但肯定可行的方案进行判定
		            testDiv.appendChild(obj);
		            testDiv.removeChild(obj);
		        } catch (e) {
		            return false;
		        }
		        return true;
		    }
		    return false;
		}
		var a = {
		   nodeType: 1
		}
		console.log(isElement(document.getElementById("test")));
		//结果:  true
		console.log(isElement(document.getElementById("test").nextSibling));
		//结果:  false
		console.log(isElement(a));
		//结果:  false
	</script>
</body>
</html>
这样,在判断a是否是元素节点时,结果就是false了。


(2).HTML文档元素节点的判定和XML文档元素节点的判定:isHTML     and      isXML  

我们可以简单的将所有的元素节点化为两类:一类是HTML,一类是XML。不过从严格意义上来说,HTML只是XML的一个子集,它拥有更多的特性,而XML在矢量绘图的处理上又派生出了两大类:SVG和VML。那么按照这种方法,我们可以简单的认为如果不是HTML,就是XML的元素节点了。而HTML是比较容易识别的,因为它有更多的特性。比如说,XML是没有className的,或者我们通过一个元素的ownerDocument得到它的文档对象,XML是没有document.getElementById()和document.getElementsByTagName()这些方法的.此外,最大的区别是HTML元素的nodeName总是大写的,当你使用createElement()方法创建HTML元素的时候,无论你传入的字母是大写还是小写,最后得到的都是大写。
接下来就看看各大类库是怎么实现HTML和XML文档的元素节点的判定的。

(1).Sizzle, jQuery自带的选择器引擎,判断是否是XML文档

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isXML</title>
</head>
<body>
	<script>
		//Sizzle, jQuery自带的选择器引擎
		var isXML = function(elem) {
		    var documentElement = elem && (elem.ownerDocument || elem).documentElement;
		    return documentElement ? documentElement.nodeName !== "HTML" : false;
		};
		console.log(isXML(document.getElementById("test")));

		//但这样不严谨,因为XML的根节点,也可能是HTML标签,比如这样创建一个XML文档
		try {
		    var doc = document.implementation.createDocument(null, 'HTML', null);
		    console.log(doc.documentElement);
		    console.log(isXML(doc));
		} catch (e) {
		    console.log("不支持creatDocument方法");
		}
	</script>
</body>
</html>

(2).mootools的slick选择器引擎的源码,判断是否是XML文档

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isXML</title>
</head>
<body>
	<script>
		//我们看看mootools的slick选择器引擎的源码:
		var isXML = function(document) {
		    return (!!document.xmlVersion) || (!!document.xml) || (toString.call(document) == '[object XMLDocument]')
		            || (document.nodeType == 9 && document.documentElement.nodeName != 'HTML');
		};

		//精简版
		var isXML = window.HTMLDocument ? function(doc) {
		    return !(doc instanceof HTMLDocument);
		} : function(doc) {
		    return "selectNodes" in doc;
		}
	</script>
</body>
</html>

不过,这些方法都只是规范,javascript对象是可以随意添加的,属性法很容易被攻破,最好是使用功能法。功能法的实现代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isXML</title>
</head>
<body>
	<script>
		var isXML = function(doc) {
		    return doc.createElement("p").nodeName !== doc.createElement("P").nodeName;
		}
		
	</script>
</body>
</html>

我们知道,无论是HTML文档,还是XML文档都支持createELement()方法,我们判定创建的元素的nodeName是区分大小写的还是不区分大小写的,我们就知道是XML还是HTML文档,这个方法是目前给出的最严谨的函数了。

判断是不是HTML文档的方法如下:
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isHTML</title>
</head>
<body>
	<script>
		var isHTML = function(doc) {
		    return doc.createElement("p").nodeName === doc.createElement("P").nodeName;
		}
		console.log(isHTML(document));
	</script>
</body>
</html>
有了以上判断XML和HTML文档的方法,我们就可以实现一个元素节点属于HTML还是XML文档的方法了,实现代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>isHTMLElement</title>
</head>
<body>
	<script>
		var testDiv = document.createElement('div');
		var isElement = function (obj) {
		    if (obj && obj.nodeType === 1) {//先过滤最简单的
		        if( window.Node && (obj instanceof Node )){ //如果是IE9,则判定其是否Node的实例
		            return true; //由于obj可能是来自另一个文档对象,因此不能轻易返回false
		        }
		        try {//最后以这种效率非常差但肯定可行的方案进行判定
		            testDiv.appendChild(obj);
		            testDiv.removeChild(obj);
		        } catch (e) {
		            return false;
		        }
		        return true;
		    }
		    return false;
		}
		var isHTML = function(doc) {
		    return doc.createElement("p").nodeName === doc.createElement("P").nodeName;
		}
		var isHTMLElement = function(el){
		   if(isElement){
		      return isHTML(el.ownerDocument);
		   }
		   return false;
		}
		console.log(isHTMLElement(testDiv));
	</script>
</body>
</html>

(3).判断节点的包含关系

节点关系不仅仅指元素节点的关系,document文档节点也包含在内。在最新的浏览器中,所有的节点都已经装备了contains()方法,
元素之间的包含关系,用自带的contains方法,只有两个都是元素节点,才能兼容各个浏览器,否则ie浏览器有的版本是不支持的,可以采用hack技术,自己写一个contains方法去兼容。
元素之间的包含关系:contains()方法
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>contains</title>
</head>
<body>
	<div id="p-node">
		<div id="c-node">子节点内容</div>
	</div>
	<script>
		var pNode = document.getElementById("p-node");
		var cNode = document.getElementById("c-node").childNodes[0];
		alert(pNode.contains(cNode));    //true
	</script>
</body>
</html>
兼容各浏览器的contains()方法
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>contains</title>
</head>
<body>
	<div id="p-node">
		<div id="c-node">子节点内容</div>
	</div>
	<script>
		//兼容的contains方法
		function fixContains(a, b) {
		    try {
		        while ((b = b.parentNode)){
		            if (b === a){
		                return true;
		            }
		        }
		        return false;
		    } catch (e) {
		        return false;
		    }
		}
		var pNode = document.getElementById("p-node");
		var cNode = document.getElementById("c-node").childNodes[0];
		alert(fixContains(pNode, cNode));        //true
		alert(fixContains(document, cNode));     //true
	</script>
</body>
</html>

继承层次与嵌套规则

(1).DOM节点继承层次
DOM节点是一个非常复杂的东西,对它的每一个属性的访问,不走运的话,就可能会向上溯寻到N多个原型链,因此DOM操作是个非常耗性能的操作。风头正盛的react为了解决这个问题,提出了虚拟DOM的概念,合并和屏蔽了很多无效的DOM操作,效果非常惊人。接下来看看DOM节点究竟是如何继承的。
(1).例如使用document.createElement("p")创建p元素,其实document.createElement("p")是HTMLParagraphElement的一个实例,而HTMLParagraphElement的父类是HTMLElement,HTMLElement的父类是Element,Element的父类是Node,Node的父类是EventTarget,
EventTarget的父类是Function,Function的父类是Object。
创建一个p元素一共溯寻了7层原型链
(2).例如使用document.createTextNode("xxx")创建文本节点,其实document.createTextNode("xxx")是Text的一个实例,而Text的父类是CharactorData,CharactorData的父类是Node,Node的父类是EventTarget,EventTarget的父类是Function,Function的父类是Object。创建一个文本节点一共溯寻了6层原型链

因此,所有节点的继承层次都不简单,但相比较而言,元素节点是更可怕的。从HTML1升级到HTML3.2,再升级到HTML4.1,再到HTML5,除了不断地增加新类型、新的嵌套规则以外,每个元素也不断的添加新属性。
下面看一个例子:创建一个p元素,打印它第一层原型的固有的属性的名字,通过Object的getOwnPropertyNames()获取当前元素的一些属性,这些属性都是他的原始属性,不包含用户自定义的属性。
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>DOM inheritance hierarchy</title>
</head>
<body>
	<script>
		console.log(Object.getOwnPropertyNames(document.createElement("p").__proto__));
		//访问p元素上一层原型控制台打印:  ["align","constructor"]
		console.log(Object.getOwnPropertyNames(document.createElement("p").__proto__.__proto__));
		/*访问p元素上一层原型的再上一层原型,控制台会打印很多属性,感兴趣的伙伴可以自己贴代码到控制台看一下,它要比访*问第一层原型的属性多得多。这也就是说,每往上一层,原型链就为它添加一些属性。
		*/
	</script>
</body>
</html>



下面看一个空的div元素,并且没有插入到DOM里边,看它有多少自有属性(不包括原型链继承来的属性)

空的DIV元素的自有属性



在新的HTML规范中,许多元素的固有属性(比如value)都放到了原型链当中,数量就更加庞大了。因此,未来的发展方向是尽量使用现成的框架来实现,比如MVVM框架,将所有的DOM操作都转交给框架内部做精细处理,这些实现方案当然就包括了虚拟DOM的技术了。但是在使用MVVM框架之前,掌握底层知识是非常重要的,明白为什么这样做,为什么不这样做的目的。这也是为什么要理解DOM节点继承层次的目的。

(2).HTML嵌套规则

HTML存在许多种类型的标签,有的标签下面只允许特定的标签存在,这就叫HTML嵌套规则。
不按HTML嵌套规则写,浏览器就不会正确解析,会将不符合嵌套规则的节点放到目标节点的下面,或者变成纯文本。
关于HTML嵌套规则,一定要掌握块状元素和内联元素的区别。
块状元素:一般是其他元素的容器,可容纳内联元素和其他块状元素,块状元素排斥其他元素与其位于同一行,宽度(width)高度(height)起作用。常见块状元素为div和p
内联元素:内联元素只能容纳文本或者其他内联元素,它允许其他内联元素与其位于同一行,但宽度(width)高度(height)不起作用。常见内联元素为a.
块状元素与内联元素嵌套规则:
(1).块元素可以包含内联元素或某些块元素,但内联元素却不能包含块元素,它只能包含其他的内联元素
例:
<div><h1></h1><p></p></div>
<a href="#"><span></span></a>
(2).块级元素不能放在<p>里面
例:<p><ol><li></li></ol></p><p><div></div></p>
(3).有几个特殊的块级元素提倡只能包含内联元素,不能再包含块级元素,这几个特殊的标签是
h1、h2、 h3、h4、 h5、 h6、 p 、dt
(4).li标签可以包含div标签
例:
<li><div></div></li>
(5).块级元素与块级元素并列,内联元素与内联元素并列
例:
<div><h2></h2><p></p></div>
<div><a href="#"></a><span></span></div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章