JavaScript通过Document类型表示文档,原型链的继承关系为:document.__proto__->HTMLDocument.prototype->Document.prototype->Node.prototype->EventTarget.prototype
Document节点具有下列特征:以下属性均为Node.prototype上的
- nodeType值为9
- nodeName值为"#document"
- nodeValue值为null
- parentNode值为null
- owerDocument值为null
- 其子节点可能是一个DocumentType(最多一个),Element(最多一个),ProcessingInstruction或Comment
Document类型可以表示HTML页面或其他基于XML的文档,最常见的应用还是作为document对象。通过这个文档对象可以取得与页面有关信息;操作页面外观;操作其底层结构。
目录
- 文档子节点
- 文档信息
- 查找元素
- 特殊集合
- DOM一致性检测
- 文档写入
文档的子节点:
介绍的大多都继承自Document.prototype
两个常用的子节点
1.document.documentElement:该属性继承自Document.prototype。始终指向HTML页面中的<html>元素
document.childNodes:该属性继承自Node.prototype。访问文档元素。
document.body:该属性继承自Document.prototype。指向<body>元素
2.子节点为DocumentType类型,通常将<!Doctype>标签看成是一个与文档其他部分不同的实体,可以通过document.doctype属性(该属性继承自Document.prototype,该属性的值是DocumentType的实例)访问,博客园这个编辑页面 document.doctype;// <!DOCTYPE html> 用的是HTML5标准的文档类型定义,告知HTML标签怎么使用要遵守HTML5文档类型规范,然后浏览器用标准模式去渲染页面。关于<!Doctype>的知识点可以戳浅谈HTML文档模式。
(1).浏览器对document.doctype节点的支持差别很大主要是<=IE9访问document.doctype为null;
(2).对位于外部注释节点的处理差异也很大,主要是>=IE9和Chrome只为位于<HTML>外部的第一条注释创建节点,FF却为所有<HTML>外的注释都创建,可通过document.childNodes可获取。
(3).对于<HTML>内部的注释,三大浏览器都认为是文档内的节点。当用document.childNodes可以获取到。
多数情况下用不着在document对象上调用appendChild,removeChild,replaceChild,因为文档类型(如果存在的话)是只读的,而且它只能有一个子元素。
文档信息
作为HTMLDocument的一个实例,document还继承了Document.prototype上的表现网页信息的属性,
1.document.title:包含着title元素中的文本,通过这个属性可以取得当前页面的标题,也可以修改当前页面的标题并反应在浏览器标题栏中。
我好奇给document.title设置值,这样不是给document对象自身添加值为字符串的属性么,怎么会直接影响到网页的标题变化(标题之所以变化估计是影响了DOM树中title节点的儿子文本节点的nodeValue的值)?然后控制台检测发现给document对象上添加title属性(document对象上之前不存在title属性,而是Document.prototype上存在title属性)添加不上去,所以就访问不到document上自身的title属性的各种特性。但是添加a属性却可以成功,难道是JS引擎进行了强制处理不让其添加到document对象上,然后通过document.getElementsByTagName(‘title‘)[0].firstChild.nodeValue来改变的网页标题。
if(document.title){ var titlestr=document.title; document.getElementsByTagName(‘title‘)[0].firstChild.nodeValue=titlestr; delete document.title; }
2.接下来介绍的三个属性都与对网页的请求有关,均继承自Document.prototype。它们是document.URL,document.domain,document.referrer。这里会牵扯到一点跨域的知识。
document.URL包含完整的URL(即地址栏显示的URL)
document.domain属性中包含页面的域名
document.referrer返回跳转的或打开当前页面的前一个那个页面URL,如果用户直接打开这个页面(不是通过页面跳转,而是通过地址栏键入网址或者书签等打开)就返回空字符串。当用户通过链接跳转(可以是点击某个链接(所有浏览器支持),也可以是window.opne(URL)或window.location.href=URL(IE5.5+除外))到当前页面,获取的referrer为上一个页面的URL。当我把这篇文章保存为书签后再通过书签访问获取到的document.referrer就为"",而上面图片之所以能获取到字符串是因为我是从草稿保存成功的那个页面的链接打开的这个页面,所以该字符串是链接所在的页面。document.referrer和刷新不刷新当前页面没任何关系。
所有这些信息都存在于请求的HTTP头部,只不过通过这些属性能让我们通过JavaScript访问它们而已。
(1).URL和domain属性是相互关联的 document.URL.match(document.domain);// ["cnblogs.com"] 。
这三个属性中,只有domain是可以设置的,但由于安全方面的限制,也并非可以给domain设置任何值,如果URL中包含一个子域名比如p2p.wrox.com,那么只能将domain设置成wrox.com。不能将这个属性设置为URL中不包含的域,例如
//假设页面来自p2p.wrox.com域 document.domain="wrox.com";// 成功 document.domain="nczonline.com";// 出错
(2).当页面中包含来自其他子域的框架或内嵌框架时,能够设置document.domain就非常方便,由于跨域安全的限制,来自不同子域的页面无法通过JavaScript通信,而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。例如假设有一个页面加载自www.wrox.com,其中包含一个内嵌框架,框架内的页面加载自p2p.wrox.com。由于document.domain字符串不一样,内外两个页面之间无法相互访问对方的JavaScript对象,但如果将这俩个页面的document.domain值都设置为"wrox.com",它们之间就可以通信了。
//index.html <!DOCTYPE html> <html> <head> <title></title> </head> <frameset> <frame src="1task-2.html"> </frame> </frameset> </html> //1task-2.html 企图访问index.html的document对象 ... <button onclick="console.log(window.parent.document)">点击</button> ...
但是Chrome下会报错:
这里突然想到平常用的WAMP服务器要求你必须把要用到的文件放在服务器的同一目录下,原来是为了保持window.domain相同啊,当两个框架能相互访问了后,他们能相互访问对方window对象上的所有属性和方法,不像之前虽然能得到对方的window对象但却无法访问window对象上的属性和方法。现在通过localhost:8080载入外部页面,由于框架和外部页面的域名相同,所以可以在外部页面访问框架的window.document对象。
(3).浏览器对domain属性还有一个限制,如果域名一开始就是松散的"loose",那么不能将它再设为紧绷的(tight),即在将document.domain设置为"wrox.com"后,就不能再将其设置回"p2p.wrox.com",否则将导致错误。
查找元素
取得特定的某个或某个组元素的引用,该操作可以使用document对象继承Document.prototype的几个方法来完成。
document.getElementById(id):如果找到相应元素返回该元素(该元素是HTML某元素Element类型实例),未找到返回null。这里的id必须与页面中元素的id特性(attribute)严格匹配,包括大小写。
注意(1).<=IE7可以不区分id的大小写。
(2).如果页面中多个元素的id的值相同,getElementById()只返回文档中第一次出现的元素
document.getElementsByTagName(tagname):返回的是一个包含0个或多个元素的HTMLCollection类型的实例集合,该集合是动态更新的。
可以使用item(index)或方括号语法来访问HTMLCollection实例集合的项。
使用namedItem(name)可以通过元素的name特性或方括号语法取得集合中的项。
//HTML <img src="a.jpg" name="myImg"> //JS document.getElementsByTagName(‘img‘).namedItem("myImg");//document.getElementsByTagName(‘img‘)["myImg"]
实际上在HTMLCollection实例集合上使用方括号传入数值或字符串形式的索引,在后台对数值索引就会调用item(),对字符串索引会调用namedItem()(其中<=IE7调用namedItem()返回null,而且 typeof document.getElementsByTagName(‘img‘).namedItem;// "object" )。
(3).取得文档中所有元素,document.getElementsByTagName(‘*‘) ,返回HTMLCollection类型实例集合,按照它们出现的先后顺序。第一项为<html>元素,第二项为<head>元素,采用深度优先遍历方式。
1.通过document.getElementsByTagNames(‘*‘)获得的集合不会包含注释节点无论注释节点在文档中的哪处,但要注意<=IE7document.getElementsByTagNames("*")会返回注释节点不管注释节点在文档哪。注意和上面的childNodes处理注释节点相区别。
2.虽然标准规定标签名要区分大小写,但为了最大限度地与既有HTML页面兼容,传给getElementsByTagName()的标签名师不需要区分大小写的,但对于XML页面而言(包括XHTML),getElementsByTagName()就会区分大小写。
(4).document.getElementsByName():继承自Document.prototype,返回带有给定name特性的所有元素的集合,该集合是NodeList的实例。最常使用的情况是取得单选按钮,为了确保发送给浏览器的值无误,所有单选按钮必须具有相同的name特性。
//HTML <fieldset> <legend>Which color do you prefer?</legend> <ul> <li><input type="radio" name="color" value="red" id="colorRed"><label for="colorRed">Red</label></li> <li><input type="radio" name="color" value="green" id="colorGreen"><label for="colorGreen">Green</label></li> <li><input type="radio" name="color" value="yellow" id="colorYellow"><label for="colorYellow">Yellow</label></li> </ul> </fieldset>
所有单选按钮的name特性值都是"color",但它们ID可以不同,ID的作用是在于将<label>标签应用到每个对应的单选按钮上,name特性确保三个值中只有一个被发送给浏览器。
这里注意一下高程三上P258页好像有误,通过document.getElementByName()获取的集合是NodeList类型实例并不是书上说的HTMLCollection实例,而且NodeList原型上也并没有namedItem()方法,所以只有通过item()方法获取了。
特殊集合
除了上述属性和方法,document还能从Document.prototype继承一些特殊的属性从而返回集合,这些集合也是HTMLCollection类型的实例即使动态变化,为访问文档常用的部分提供了快捷方式。
document.anchors:包含文档中所有带name特性的<a>元素
document.applets:包含文档中所有<applets>元素的集合,已废弃不推荐使用,推荐使用<object>元素代替,定义一个嵌入的对象。
document.forms:包含文档中所有<form>元素的集合。与document.getElementsByTagName("form")得到的结果相同。
document.images:包含文档中所有<img>元素的集合,与document.getElementsByTagName("img")得到结果相同。
document.links:包含文档中所有带href特性的<a>元素的集合。
DOM一致性检测
DOM分为多个级别,也包含多个部分,因此检测浏览器实现DOM的哪些部分十分重要,document.implementation继承自Document.prototype,提供了DOM相应的信息和功能,与浏览器对DOM的实现直接对应。document.implementation返回一个DOMImplementation类型的实例,原型链继承关系为document.implementation.__proto__->DOMImplementation.prototype->Object.prototype。
DOM1级只为document.implementation规定继承了一个方法为hasFeature(),这个方法两个参数,要检测DOM功能的名称和版本号。如果浏览器支持给定名称和版本的功能,该方法返回true。如果hasFeature()可用的话,那么DOMImplementation.prototype上的其他方法就可以为操作文档以外的内容提供一些服务了。例如document.implementation.createDocumentType方法它可以为实例管理的文档创建对应的DTD文档定义。
可以检测的值有"Core","XML","HTML"等等,可参考一系列模型名称。但返回true有时候也不一定意味着实现与规范一致,例如Safari2.x及更早版本会在没有完全实现某些DOM功能的情况下也返回true,更让人质疑的是如图这样都能返回true。。所以建议在多数情况下使用DOM的某些特殊功能之前最好除了检测hasFeature()之外,还同时检测使用能力检测。
文档写入
将输出流写入到网页中,这个能力体现在四个方法中:document.write(),document,writeln(),document.open(),document.close(),都继承自Document.prototype。
document.write():接收一个字符串参数,即要写入到输出流的文本。
document.writeln():接收一个字符串参数,也是写入到输出流的文本,但会在字符串的末尾添加一个换行符(\n)。在页面被加载的过程中可以使用这两个方法向页面动态的加入内容。
document.open():打开网页的输出流,每次加载页面浏览器默认打开。
document.close():关闭网页的输出流,每次文档渲染完毕浏览器默认关闭。
严格型的XHTML文档不支持文档写入,对于那些按照application/xml+xhtml内容类型提供的页面,这两个方法也同样无效。
在说document.write()之前先以一个例子说明一下浏览器的解析构建DOM树,渲染线程和JS执行线程是同步阻塞的:
- 浏览器是多线程的,比如有解析构建DOM的线程,JS执行线程,渲染线程,ajax线程,事件响应线程等
- 过程顺序大体是这样:解析构建DOM树线程->JS线程->渲染页面线程
当把script标签放在最后:
<head> <title></title> <meta charset="utf-8"> </head> <body> <h1>测试浏览器的渲染线程和js执行线程是同步还是异步</h1> <fieldset> <legend>Which color do you prefer?</legend> <ul> <li><input type="radio" name="color" value="red" id="colorRed"><label for="colorRed">Red</label></li> <li><input type="radio" name="color" value="green" id="colorGreen"><label for="colorGreen">Green</label></li> <li><input type="radio" name="color" value="yellow" id="colorYellow"><label for="colorYellow">Yellow</label></li> </ul> </fieldset> <script> var date=new Date().valueOf(),i=0; while(new Date().valueOf()-date<5000){ console.log(document.getElementsByTagName(‘ul‘)[0]); } console.log(‘循环完毕‘); </script> </body>
浏览器自上到下解析构建DOM树是在渲染之前,经测试发现页面是在加载了大约5s后(在这5s内控制台不断打印ul元素)才呈现表单内容,可见渲染线程和JS执行线程是同步阻塞的,需等到JS线程执行完才渲染DOM树。
当我把script标签放在前面改成这样:
<head> <title></title> <meta charset="utf-8"> <script> var date=new Date().valueOf(),i=0; while(new Date().valueOf()-date<5000){ console.log(document.getElementsByTagName(‘ul‘)[0]); } console.log(‘循环完毕‘); </script> </head> <body> <h1>测试浏览器的渲染线程和js执行线程是同步还是异步</h1> <fieldset> <legend>Which color do you prefer?</legend> <ul> <li><input type="radio" name="color" value="red" id="colorRed"><label for="colorRed">Red</label></li> <li><input type="radio" name="color" value="green" id="colorGreen"><label for="colorGreen">Green</label></li> <li><input type="radio" name="color" value="yellow" id="colorYellow"><label for="colorYellow">Yellow</label></li> </ul> </fieldset> </body>
当自上到下解析构建DOM树时遇到script标签后立马通知JS执行线程去执行,让解析构建DOM树的这个过程先暂停阻塞。
这里解释一下为什么我推测解析构建DOM树的过程是阻塞的:JS代码执行完控制台一直都是undefiend表明JS整个执行过程都没获取到DOM元素,因为我的代码每次都是 document.getElementsByTagName(‘ul‘)[0]让去重新获取ul元素,但总是获取不到。因为解析构建的过程肯定比我设置的5s快,如果说在JS执行的时候DOM树也同时在解析构建(宏观上的并行执行,微观上的并发执行),肯定有几次能获取到ul元素,但是控制台证明并没有,所以说解析构建DOM树的过程是阻塞的。
等到JS执行完毕后,继续解析构建DOM树,构建完后才是渲染线程来渲染页面。所以这也就是许多人推荐将<script>标签放在HTML代码最后面的原因。
document.write()的应用:
1.在页面加载过程中动态创建DOM元素插入元素
//HTML <body> <script type="text/javascript"> document.write("<strong>"+new Date().toString()+"</strong>"); </script> </body>
浏览器解析构建DOM树过程中遇到script标签暂停阻塞解析,执行document.write(),创建了一个<strong>元素并向当前文档流写入该元素。解析构建线程运行解析strong元素及后面的元素,解析完后渲染并关闭文档流。
2.动态包含外部资源,例如JavaScript文件等
//HTML <body> <script type="text/javascript"> document.write("<script type=\"text/javascript\" src=\"file.js\">"+"</script>"); </script> </body>
这看起来好像是没什么错,但字符串"</script>"将被解释为与外部开头的那个<script>标签相匹配,字符串被解释为脚本块的结束,它后面的代码将无法执行。结果会导致‘")‘字符串出现在页面中。
为避免这个问题,只需加入转义字符\即可。<\/script>或</script\>均可。
可以看到通过这种途径去加载外部资源是GET方式的。
3.注意如果在文档加载结束后在调用document.write()那么输出的内容将会重写整个页面。这和调用document.write()的时机很重要。
当把document.write()放在事件的回调函数中:
<h3>测试 document.write()</h3> <script type="text/javascript"> window.onload=function(){ document.write(‘test‘); console.log(document==dd);//这里验证两次的document对象是否一样 } </script> <h2>test</h2> <h3>test</h3> <h4>test</h4> <script> window.dd=document; </script>
等到页面完全加载完后再去执行函数,会重写整个页面。注意到没,被重写后的<!DOCTYPE html>,head,body等不见了,取而代之的是一个新的HTML结构但document对象还是原来的对象。这是因为在触发onload事件之前浏览器已经完成了DOM树的解析构建渲染,这时触发事件执行document.write()发现文档流已经关闭了就会默认重新调用window.open()打开一个新的输出流,而document.open()会清除已有文档内容,所以最终看到的就是新的文档内容。这样看来除非是在浏览器关闭文档之前调用document.write(),否则当前页面都会被清除。
参考:
《JavaScript高级程序设计》
JavaScript操作referrerJS中几种实用的跨域方法原理详解MDN document.implementationdocument.write()的用处