(Suspended)Note for Professional JavaScript for Web Developers (3/4)
这本书拖了好久,战线拉太长了…JavaScript高级程序设计(第3版)笔记第三部分,本篇对应9~?
客户端检测
首先要指出的是,不到万不得已,不要使用客户端检测。应当先设计最通用的方案,再使用特定于浏览器的技术增强该方案。
1.能力检测
- 检测规则: 一是先检测达到某个目的的最常用属性,二是必须直接测试实际要用到的属性。例如要想获取特定ID的元素,就应该先检测
document.getElementById
再检测document.all
,同时你不能检测到了document.all
就认为它一定是IE进而使用其他IE特定的方法,你必须要用什么就检测什么。 - 更可靠的能力检测: 有时候直接通过属性访问并不能保证某个特性真的会按照适当的方式运行,例如你希望通过
object.sort
来检测该对象能否排序,而其实拥有sort属性时它也会返回true。因此我们应该尽量用typeof
操作符来做能力检测。在极品的IE中,有时typeof返回的函数是object而非function,甚至访问ActiveX对象直接用点操作符访问函数也会报错。在浏览器中测试对象的某个特性是否存在应该这样做:
1 | function isHostMethod(object, property) { |
2.怪癖检测
怪癖就是Bug,是个别浏览器独有的。只需要检测对代码有直接影响的怪癖,且最好在脚本一开始就执行这类检测。例如在IE8-中,某实例有与被标记为DontEnum的原型属性同名的属性,它也不会出现在for-in循环中。
1 | var hasDontEnumQuirk = function() { |
3.用户代理检测
由于各大浏览器厂商都习惯将navigator.userAgent进行伪装spoofing(防止被嗅探而拒绝访问),检测浏览器信息十分困难。
- 识别呈现引擎: 确切知道浏览器的名字和版本号不如确切知道它使用的是什么呈现引擎。我们主要要检测五大呈现引擎: IE, Gecko, WebKit, KHTML和Opera. 为了不在全局作用域添加多余的变量,我们采用模块增强模式来封装检测脚本,然后依照Opera - WebKit - KHTML - Gecko - IE的顺序来检测(谁最喜欢伪装成别人就先检测谁,即先检测的字符串通常会包含后面检测的字符串)
- 识别浏览器: 只知道呈现引擎并不能说明存在所需的JavaScript功能,例如同为WebKit引擎,Safari和Chrome却是不同的JavaScript引擎。
- 识别平台: 不同平台版本的浏览器可能会有不同的行为,主要识别Windows, MacOS, UNIX。
- 识别Windows版本: 可以这么做,但我看不懂为什么要提取Windows版本…
- 识别移动设备: 识别iOS、安卓、诺基亚N、WinMobile。
- 游戏设备: 任天堂Wii和PlayStation都有浏览器,也可以检测出来。
1 | var client = function() { |
DOM
DOM是针对HTML/XML文档的一个API,其中XML(Extensible Markup Language)是独立于软件和硬件的信息传输(存储)工具,焦点在于数据的内容;而HTML则是控制数据的显示,两者长得比较像。DOM描绘了一个层次化的节点树,能方便地添加、移除和修改页面的某一部分。
1.节点层次
DOM可以将任何HTML/XML文档描绘成多层次节点构成的有根树。
- Node类型: 可通过someNode.nodeType访问节点的类型(在IE中无效!),主要有元素节点(1)、属性节点(2)和文本节点(3)。
- nodeName和nodeValue: 根据节点类型不同,节点的这两个属性的取值也有不同。例如对于元素节点来说,nodeName就是标签名,nodeValue为null。
- 节点关系: 树中节点的关系可以挪用到DOM树上,可以通过节点的某些属性方便地访问与之相关联的其他节点。
-> childNodes: 每个节点都有一个childNodes属性返回NodeList对象。NodeList是一个类数组对象,但它是基于DOM结构动态执行查询的结果,DOM结构的变化会即时反映到NodeList对象上。可以通过手写函数将NodeList转换为Array,但需要在IE中做特殊处理。
-> firstChild/lastChild: 即childNodes[0]和childNodes[someNode.childNodes.length - 1]。
-> parentNode: 指向当前节点的父节点。
-> previousSibling/nextSibling: 前/后的同侪。
-> ownerDocument: 文档节点,对于HTML来说就是最外层的根节点<html>
.
1 | function convertToArray(nodes) { |
操作节点: 前面的节点关系指针都是只读的,不能直接赋值修改。DOM提供了其他操作节点的方法。
-> appendChild(): 为当前节点添加一个子节点,成为其lastChild,将新增节点返回。当这个子节点是当前文档中已经存在的节点时,该节点会被挪动到新的位置而不是复制,因为任何DOM节点都不能同时出现在文档中的多个位置上。someNode.appendChild(newNode)
-> insertBefore(): 为当前节点根据给定参照节点插入一个子节点,成为参照节点的previousSibling,将新增节点返回。当传入的参照节点为null,则等效于appendChild。someNode.insertBefore(newNode, someNode.firstChild);
-> replaceChild(): 将传入的节点替换掉给定的节点,原有的节点关系会被完整复制到新插入的节点,而被替换掉的节点返回后其实仍在文档中,只是没有了位置,可以通过appendChild等操作重新赋予位置。someNode.replaceChild(newNode, someNode.firstChild);
-> removeChild(): 删除子节点。someNode.removeChild(someNode.firstChild);
-> cloneNode(): 可浅复制(只复制节点本身)或深复制节点(复制节点以及整个子节点树,需传入参数true)。此时获得的节点虽说属于文档但没有与文档产生联系,需要通过appendChild等操作融入文档树。
-> normalize(): 整理文档树中的文本节点,清除空文本节点、合并相邻的文本节点。Document类型: 表示整个文档。在浏览器中,表示整个HTML页面的document对象是HTMLDocument类型的实例,而HTMLDocument是继承自Document类型的。同时document还是window对象的属性。
文档的子节点: 有两个子节点可以快速访问,一是document.documentElement(即HTML页面中的
<html>
元素),二是document.body(<body>
元素的引用)。其余的doctype, childNodes都会因浏览器不同而有所区别。文档信息: document对象还具有一些标准Document对象没有的属性,如
document.title
可获取页面的标题(但无法通过对其赋值的方式修改标题)、document.URL
获取完整URL(不可设置)、document.domain
获取域名部分(只能设置到更具体的子域名)、document.referer
获取是从什么URL链接到当前页面的(不可设置)。查找元素: 获取特定的某个或某组元素的引用。
-> getElementById(): 根据ID严格匹配查找,但在IE中有两个quirk,一是不区分大小写、二是将name属性和id属性等价了。
-> getElementsByTagName(): 根据标签名严格匹配,返回NodeList,在HTML文档中则是返回HTMLCollection对象,与NodeList相同可访问length属性、可用[index]或item(index)来访问其中的元素,不过HTMLCollection又新增了namedItem(“nameValue”)方法,可以根据集合中元素的name属性获取元素。
-> getElementsByName(): 是HTML文档独有的,根据name属性严格匹配,返回HTMLCollection,经常用在获取拥有相同name属性的一组单选按钮.特殊集合: document对象有一些特殊的HTMLCollection对象。
-> document.anchors: 获取所有带name的<a>
元素。
-> document.links: 获取所有带href的<a>
元素。
-> document.forms: 获取所有<form>
元素。
-> document.images: 获取所有<img>
元素。文档写入: 使用
write(str)
或writeln(str)
向文档中该段代码所在位置写入内容。若放在window.onload中执行,这是完全重写页面而不是在指定位置添加。Element类型: 元素节点,nodeType为1,nodeName/tagName为标签名(大小写不一定,需要用.toLowerCase转换一下再判断标签),nodeValue为null,parentNode可能为Document或Element。
HTMLElement: 继承自Element类型并添加了id, title, className, lang等属性。它具有一系列更具体的子类型,对应诸多HTML元素。需要指出的是,所有的特性attribute都是元素属性property,但二者在使用时具有一定区别。
访问特性attribute: 使用
getAttribute()
、setAttribute()
、removeAttribute()
对元素attribute进行操作。特性是不区分大小写的,而自定义的特性按照惯例是要加上data-
前缀加以辨识的。但需要指出的是,有两类特性,HTMLElement属性的值和通过getAttribute返回的值不同。style属性是对象、获取style特性则是对应的CSS文本;onclick这类的事件处理程序,在属性中返回一个JavaScript函数、而获取onclick特性则返回对应的代码文本。由于这样的差别,按照惯例我们通常更多使用属性访问,而getAttribute只用于自定义的特性。设置特性: 使用setAttribute不仅可以设置现有特性,还能创建自定义特性。通过属性的方式创建自定义特性无法通过getAttribute访问,所以不推荐。但在旧版IE中setAttribute对class、style等特性不起作用,因此按照惯例我们推荐通过属性来设置特性,而setAttribute只用于新增自定义特性。
attributes属性: 包含一个NamedNodeMap,与NodeList类似。可用
getNamedItem(name)
获取指定Name的节点,用removeNamedItem(name)
移除,用setNamedItem(node)
插入新节点,用item(pos)
获取特定位置的节点。这些方式并不方便,通常还是getAttribute
、removeAttribute
和setAttribute
。创建元素createElement: 根据传入的标签名来新建元素,返回该元素的引用。再调用appendChild添加到指定元素末尾。
1 | var myDiv = document.createElement("div"); |
- 元素子节点childNodes: 元素的childNodes属性中包含了它所有的子节点,可以是元素/文本节点/注释等,因此需要利用nodeType属性来判断是否是子元素。
1 | for (var i = 0, len = element.childNodes.length; i < len; i++) { |
- element.getElementsByTagName: 这里的element可以是document,也可以是一个元素节点,可通过给定标签名获得它包含的子孙元素节点。
- Text类型: 文本节点,nodeType为3,nodeName为”#text”,nodeValue为所包含的文本,parentNode为Element。可利用nodeValue对所插入的文本进行HTML编码,如
div.firstChild.nodeValue = "<strong>sth</strong>"
赋值时,会将字符串中的尖括号、引号等都进行编码,这样就省去了手动编码的麻烦。 - document.createTextNode(str): 传入字符串创建文本节点,必须将该节点添加到文档树中已经存在的节点中才会显示出来(appendChild)。
- element.normalize(): 若该元素含有多个文本节点,可用此函数合并相邻文本节点。
- TextNode.splitText(index): 将所给text节点一分为二,0 ~ index-1和index ~ *.
- Comment类型: 注释节点,nodeType为8,nodeName为”#comment”,nodeValue为注释的文本内容,parentNode为Document/Element。用法与前面的Text类似,可用
document.createComment
创建给定文本的注释。注意这些注释必须存在与html标签之间,否则无法访问到。
2.DOM操作技术
前面介绍了很多使用JavaScript动态生成HTML标签的方法,那么很自然地会想到动态插入JavaScript, CSS等内容会产生什么效果。
- 动态脚本: 类似于静态脚本,也有链接外部文件和直接插入代码两种动态生成脚本的方式。对于外部文件的方式,只有执行到appendChild才会真正下载外部文件。动态脚本会在全局作用域中执行,且这段脚本执行后,动态脚本立即可用。
1 | var script = document.createElement("script"); |
- 动态样式: CSS样式可用link链接外部样式表,也可直接用style嵌入样式代码。动态样式是页面加载完成后动态加载到页面中的。加载外部样式表是异步的,即加载样式与执行JavaScript代码的先后是不确定的。
1 | // 1.外部 |
- 操作表格: 为方便动态构建表格,DOM为
<table>
,<tbody>
,<tr>
等元素提供了属性和方法。
1 | var table = document.createElement("table"); |
- 使用NodeList: NodeList和NamedNodeMap和HTMLCollection三个集合都是动态的,总是在访问DOM文档时实时进行运行的查询,因此应当尽量将其length属性用变量存起来,避免重复多次访问,也防止length动态变化带来的意外死循环。
DOM Extensions
1.选择符API
诸多JavaScript库最常用的功能就是根据CSS选择符访问DOM元素,其中jQuery就是通过CSS选择符查询DOM文档取得元素的引用,绕开了传统的getElementById和getElementByTagName。
- element.querySelector: 接受一个CSS选择符,返回匹配元素的引用(若有多个则返回第一个)。若对document类型使用querySelector,则会在整个文档范围内查找匹配元素。
- element.querySelectorAll: 用法与上面类似,返回的是NodeList实例,可用方括号或.item(index)逐个访问其中元素。
- element.matchesSelector: 用于判断该元素是否符合传入的CSS选择符,但浏览器支持很差。
2.元素遍历API
为了弥补浏览器之间的差异(如IE不将元素间的空格作为文本节点)并保持DOM规范,Element Traversal API诞生,提供了如下方法。
- childElementCount: 子元素计数,不含文本/注释。
- firstElementChild: firstChild的元素版。
- lastELementChild: lastChild的元素版。
- previousElementSibling: previousSibling的元素版。
- nextElementSibling: nextSibling的元素版。
3.HTML5 API
- element.getElementsByClassName: 传入带有一或多个类名的字符串(以空格隔开),返回给定元素后代中匹配的元素组成的NodeList。由于class属性可用于添加样式又可帮助表示HTML元素的语义,class属性的使用愈发频繁,因此这个方法用得也蛮多。
- element.classList属性: 返回该元素所有的类名,存放于一个DOMTokenList实例,与NodeList一样可用方括号或item访问,此外还提供了
add(str)
,contains(str)
,remove(str)
,toggle(str)
对该元素的类名进行操作。以往要想遍历出所有类名可能需要对className属性的字符串进行split等操作,那样就复杂多了。 - document.activeElement属性: 获取DOM文档中当前获得了焦点的元素。
- document.readyState属性: 作为指示文档加载完成的指示器,有”loading”和”complete”两个取值。
- document.compatMode属性: 区分页面渲染是标准模式还是混杂模式,取值分别为”CSS1Compat”和”BackCompat”。
- document.head属性: 类似于
document.body
,方便直接访问页面中唯一的<head>
元素,但只有chrome和safari5支持,所以为了防止出问题应该这么写。
1 | var head = document.head || document.getElementsByTagName("head")[0]; |
- document.charset/defaultCharset: 获取当前字符集/默认字符集,前者还可通过赋值进行修改。
- 自定义数据属性: HTML5允许为元素添加非标准的属性,但必须添加前缀data-,强调这是与渲染无关的信息,可能是语义信息补充之类的。添加的自定义数据属性可通过dataset属性访问或赋值。例如对于
<div id="myDiv" data-appId="1234" data-myName="Bobo">
,可以这样访问:
1 | var div = document.getElementById("myDiv"); |
- 插入标记: DOM操作节点可以创建并插入文档,但对于大量标签的插入/替换,使用字符串的形式直接插入显然更直接划算。
-> innerHTML属性: 访问时返回的是调用元素内部所有子节点的HTML标记字符串。而写入时是直接将原本的子节点替换掉成字符串所解析成的DOM子树。不过需要注意大部分浏览器对于innerHTML插入的script并不会执行。
-> outerHTML属性: 访问时返回的是调用元素及其内部所有子节点的HTML标记字符串,注意与innerHTML相比包含了调用元素本身。对outerHTML赋值会将调用元素也给替换掉。
-> insertAdjacentHTML方法: 传入插入位置和要插入的HTML文本。第一个参数为”beforebegin”(作为当前元素的前一个同辈元素)、”afterend”(作为当前元素的后一个同辈元素)、”afterbegin”(作为当前元素的第一个子元素)和”beforeend”(作为当前元素的最后一个子元素),第二个参数为合法的HTML可解析字符串。
不过以上这些替换子节点的方法可能涉及内存占用问题,例如元素被替换后其绑定的事件处理程序却仍然存在于内存中。同时反复访问innerHTML/outerHTML属性也会降低性能,最好能够将访问次数尽可能限制。 - scrollIntoView方法: 对某元素调用这个方法,可以让它所在的容器滚动的时候,将该元素放在视窗顶部。
4.专有扩展
- children属性: 返回包含为元素类型的子节点的HTMLCollection实例,childNodes的元素版。
- contains方法: 用于判断某个节点是不是另一个节点的后代。
- 插入文本: 不同于innerHTML/outerHTML被纳入HTML5规范,innerText和outerText就没有。
-> innerText: 操作元素中的文本内容,也会完全改变该元素的DOM子树。还有DOM Level3规定的textContent属性。可以这样写:
1 | function getInnerText(element) { |
-> outerText属性: 在读取时outerText与innerText一样,在赋值时则是将当前元素整个替换为新的文本节点,包括它里面的子节点。
事件
1.事件流
- 事件冒泡: 事件开始时从最具体的元素接收,然后逐级向上传播到外层节点。
- 事件捕获: 与冒泡完全相反,不太具体的节点更早接收到事件,最具体的元素是最后接收到事件,目的在于在事件到达预定目标之前能够捕获它。
- DOM事件流: DOM Level2规定的事件流包括三个阶段: 事件捕获阶段、处于目标阶段和事件冒泡阶段。例如点击简单的只含有div的HTML页面来说,事件捕获阶段时事件只会从document到
<html>
到<body>
;在“处于目标”阶段,事件才会在<div>
上发生;最后冒泡阶段发生,事件一路传回文档。
2.事件处理程序
- HTML事件处理程序: 对于元素支持的事件如click, load, mouseover,都有对应的HTML特性如onclick, onload, onmouseover,通过这些特性可以指定事件处理程序。我们很常用的是在script中创建function然后在onclick中绑定到这个函数上,其实这里还有一些玄机。它会创建一个封装着元素属性的函数,这个函数中有一个局部变量event,这个事件对象可以直接访问对象的其他特性:
1 | <input type="button" value="Click Me" onclick="alert(event.type)"> |
同时这个动态创建的函数还会扩展作用域,不仅可访问该元素本身的成员,还可以访问document的成员。
- DOM Level0事件处理程序: 先获取要操作对象的引用,然后将一个函数复制给它的事件处理程序属性。函数中的this就指向当前元素。
1 | var btn = document.getElementById("myBtn"); |
- DOM Level2事件处理程序: 需要用两个方法来增/删事件处理程序: addEventListener和removeEventListener,他们都接收三个参数,分别是要处理的事件名、作为事件处理程序的函数和布尔值(true则在捕获阶段调用,false则在冒泡阶段调用。)。大多数情况下会选择将处理程序添加到事件流的冒泡阶段,这样可以最大程度地兼容各个浏览器。注意当add的时候是以匿名函数的形式传入的,则无法remove。
1 | var handler = function() { |
- IE事件处理程序: 支持与DOM2类似的方法,attachEvent和detachEvent,接受两个参数(与前面相比就是少了最后一个布尔值,因为只支持添加到冒泡阶段),还有就是第一个参数是事件处理程序的名称而不是要处理的事件名,即为”onclick”而非”click”。
- 跨浏览器方案: 为了保证大多数浏览器能正常运行,需要更关注冒泡阶段。我们将以DOM2 - IE - DOM0的顺序进行事件处理。
1 | var EventUtil = { |
3.事件对象
触发DOM上的某个事件时会产生一个事件对象event,包含了导致事件的元素、事件类型等相关信息。
- DOM事件对象: event对象会传入事件处理程序中,可用type属性确定被触发事件的类型、用target属性确定绑定到了哪个元素、用currentTarget属性确定是哪个元素触发了这个事件。要阻止特定事件的默认行为,可使用event.preventDefault方法,不过只有cancelable=true的事件才能这么阻止。要停止事件在DOM层次中传播,取消进一步的捕获/冒泡,可使用stopPropagation方法。
- IE中的事件对象: