手记

javascript中的DOM

DOM1

DOM1将HTML和XML文档形象地看作一个层次化的节点树,可以使用js来操作这个节点树,进而改变底层文档的外观和结构定义了一个Node接口,该接口将由DOM中的所有节点类型实现,每个节点都有nodeType属性,用于表示节点的类型.一共有12种:

Node.ELEMENT_NODE (1)
Node.ATTRIBUTE_NODE (2)
Node.TEXT_NODE (3)
Node.CDATA_SECTION_NODE (4)
Node.ENTITY_REFERENCE_NODE (5)
Node.ENTITY_NODE (6)
Node.PROCESSING_INSTRUCTION_NODE (7)
Node.COMMENT_NODE (8)
Node.DOCUMENT_NODE (9)
Node.DOCUMENT_TYPE_NODE (10)
Node.DOCUMENT_FRAGMENT_NODE (11)
Node.NOTATION_NODE (12)

以上属性可以通过Chrome控制台方便查看

我们可以使用someNode.nodeType==Node.xxx的方式来确定节点的类型,但是更兼容的写法是直接使用数字,因为IE没有公开Node的构造函数.

每个节点都有一个childNodes属性,其中保存者NodeList.在这里我们需要注意尽管NodeList具有length属性,但是他实际上不是数组.要想将NodeList转换为数组应该采用以下的函数:

'use strict'
let nodeList2Array = nodes => {
    let arr = null
    try {
        arr = Array.prototype.slice.call(nodes)
    } catch (e) {
        // IE8之前NodeList不是js对象而是COM对象
        arr = []
        for (let i = 0; i < nodes.length; i++)
            arr.push(nodes[i])
    }
    return arr
}

每个节点都有一个ownerDocument属性,该属性为文档节点.任何节点都可以通过someNode.ownerDocument直接访问文档节点而无需层层回溯.

Document类型

js通过Document类型表示文档.在浏览器中documentHTMLDocument(继承自Document)的一个实例,表示整个页面.因为documentwindow上的一个属性,因此可作为全局对象访问.其中document.documentElement可以取得整个html文档,而document.body则保存了对body的引用,这是两个比较常见的方法.

思考:为什么我们没有看到对document类型的节点进行DOM的增删操作?
答:document类型的节点是只读的,代表整个文档树,有且仅有一个子节点.

Tips:

除了这些,document还有一些关于http的东西例如:document.URI,document.referrer,document.domain.其中只有document.domain是可写的,需要注意的是新设置的值必须是父域.

//假设页面来自 p2p.wrox.com 域
document.domain = "wrox.com"; // 成功
document.domain = "nczonline.net"; // 出错

//假设页面来自于 p2p.wrox.com 域
document.domain = "wrox.com"; //松散的(成功)
document.domain = "p2p.wrox.com"; //紧绷的(出错!)

当页面中包含来自其他子域的框架或内嵌框架时,能够设置 document.domain 就非常方便了。由
于跨域安全限制,来自不同子域的页面无法通过JavaScript通信。而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。例如,假设有一个页面加载自www.wrox.com,其中包含一个内嵌框架,框架内的页面加载自p2p.wrox.com。由于document.domain字符串不一样,内外两个页面之间无法相互访问对方的JavaScript对象。但如果将这两个页面的document.domain值都设置为"wrox.com",它们之间就可以通信了。

取得DOM元素

document.getElementsByTagName()函数接收一个字符串参数,返回结果是HTMLCollection,该对象和NodeList非常像似,可以使用数组下标和item(index)方法来访问.除此之外它还提供了一个按名访问的方法namedItem(key):

<img src="" name="img">
<script>
    var images = document.getElementsByTagName('img')
    var img = images.namedItem('img')
    img = images['img']
</script>

总结起来,索引访问会调用item(index);按名访问会调用namedItem(key)方法.

当我们想要取得所有的元素的时候只需要传递*即可:

var elements = document.getElementsByTagName('*') // get all element

document上还有一些特殊的集合.document.forms取得所有表单;document.images取得所有图片;document.links取得所有的链接.

Element元素

为DOM元素添加自定义的属性会失败,例如:

var div = document.getElementById('demo')
div.aaa = 1
var t = div.getAttribute('aaa') // null

attributes属性

attributes属性中包含一个NamedNodeMap,通常仅用于序列化DOM结构.例如:

let outputAttributes = element => {
    let pairs = []

    for (i = 0; i < element.attributes.length; i++) {
        let attrName = element.attributes[i].nodeName
        let attrValue = element.attributes[i].nodeValue
        if (element.attributes[i].specified) 
            pairs.push(attrName + "=\"" + attrValue + "\"")
    }
    return pairs.join(" ")
}

IE7及更早的版本会返回HTML元素中所有可能的特性,包括没有指定的特性。换句话说,返回100多个特性的情况会很常见。每个特性节点都有一个名为specified的属性,这个属性的值如果为true,则意味着要么是在HTML中指定了相应特性,要么是通过setAttribute()方法设置了该特性。

Text节点

文本节点只可能是元素节点的子节点,一般来说文本节点只有一个,但是我们是可以将多个文本节点追加到一个元素节点的.

var div = document.createElement('div')
var textNode1 = document.createTextNode('content1')
var textNode2 = document.createTextNode('content2')

div.appendChild(textNode1)
div.appendChild(textNode2)
document.body.appendChild(div)

console.log(div.childNodes.length) // 2

DOM文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串,normalize()方法是专门解决此问题的.

再上面的程序中增加以下的代码:

div.normalize()
console.log(div.childNodes.length) // 1
console.log(div.firstChild.nodeValue) // content1content2

需要注意的是在浏览器渲染的时候永远不会出现兄弟文本节点.

相反地,还有一个splitText()方法用于分割文本节点.

var div = document.createElement('div')
var textNode =  document.createTextNode('content1 content2')
div.appendChild(textNode)
document.body.appendChild(div)

var newNode = div.firstChild.splitText(9)
console.log(div.firstChild.nodeValue) // content1
console.log(newNode.nodeValue) // content2
console.log(div.childNodes.length) // 2

分割文本节点是从文本节点中提取数据的一种常用DOM解析技术。

理解NodeList,NamedNodeMap和HTMLCollection

首先,这三个集合都是“动态的”,也就是说:每当文档结构发生变化时,它们都会得到更新。例如以下的代码将导致死循环.

var divs = document.getElementsByTagName('div')
for (var i = 0; i < divs.length; i++) {
    var div = document.createElement('div')
    document.body.appendChild(div)
}
console.log('done')

解决方法是先将divs.length缓存起来,即仅仅初始化一次,以上的for循环改为下面的:

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

为了性能的考虑,我们应该尽量少直接访问此类动态的实时性的集合,因为每次都会进行文档查询,最好将其缓存起来使用.这也就是为什么有些框架提倡尽量不要使用DOM操作的原因.

DOM拓展

Selector API

最常用的是querySelectorquerySelectorAll,前者返回匹配选择器的第一个DOM元素,后者返回匹配的NodeList.除此之外还有matchsSelector用于判断某个元素是否匹配特定选择器(各个浏览器有它自己的私有实现).我们可以自己编写以下的函数用于兼容.

function matchesSelector(element, selector) {
    if (element.matchesSelector) {
        return element.matchesSelector(selector);
    } else if (element.msMatchesSelector) {
        return element.msMatchesSelector(selector);
    } else if (element.mozMatchesSelector) {
        return element.mozMatchesSelector(selector);
    } else if (element.webkitMatchesSelector) {
        return element.webkitMatchesSelector(selector);
    } else {
        throw new Error("Not supported matches selector api");
    }
}

元素遍历

为了解决IE9及其之前版本由于空格的存在导致不会返回空白文本节点从而造成childNodes属性不一致的问题,提出了元素遍历api:

  • childElementCount :返回子元素(不包括文本节点和注释)的个数。
  • firstElementChild :指向第一个子元素; firstChild 的元素版。
  • lastElementChild :指向最后一个子元素; lastChild 的元素版。
  • previousElementSibling :指向前一个同辈元素; previousSibling 的元素版。
  • nextElementSibling :指向后一个同辈元素; nextSibling 的元素版。

使用上面的api不用考虑空白文本节点的问题了:

// 以前的写法
var child = element.firstChild;
while (child != element.lastChild) {
    if (child.nodeType == 1) { //检查是不是元素
        processChild(child); 
    }
    child = child.nextSibling;
}

// 现在的写法
var child = element.firstElementChild;
while (child != element.lastElementChild) {
    processChild(child); //已知其是元素
    child = child.nextElementSibling;
}

html5对DOM的扩充

  • 原生的通过类名获取元素的方法getElementsByClassName(),其中的参数可以传递多个类名,用空格分开.
  • 类名的集合classList提供了add,remove,contains,toggle(存在删除,没有添加)方便类的添加删除切换.避免了每次操作className这个单一的字符串.例如,一个有多个类,我们需要删除red这个类,方法可能是这样:
// 以前的做法
var classNames = div.className.split(/\s+/) // 类名拆分成数组
var pos = -1 // 保存待删除的类在数组中的索引
for(var i = 0;i < classNames.length;i++){
    if(classNames[i] === 'red'){
        pos = i
        break
    }
}
classNames.splice(i,1)
div.className = classNames.join(' ')

// 现在的做法
div.classList.remove('red')
  • 原生的获取当前焦点元素document.activeElement.当页面加载完成后默认的焦点元素是document,文档加载中的时候此值为null
  • HTMLDocument新增加了readyState属性表示页面的加载情况,取值有loadingcomplete.这样window.onload就可以使用document.readyState === 'complete'来代替了.
  • html5将标准模式和混杂模式纳入了标准.document.compatMode的取值:CSS1Compat(标准),BackCompat(混杂).
  • 可以使用document.head取得head的引用,在此标准之前可以使用document.getElementsByTagName('head')[0]兼容.
  • 可自定义data属性,通过dataset来取得KV映射.
  • 新的滚动api,scrollIntoView.

outerHTML

该属性和innerHTML略有区别.在读模式下会返回元素本身和它的子树;在写模式下会替换子树和它本身.举个栗子,有以下的DOM结构:

<div id="content">
    <p>This is a <strong>paragraph</strong> with a list following it.</p>
    <ul>
        <li>Item 1</li>
        <li>
            Item 2</li>
        <li>Item 3</li>
    </ul>
</div>
var oDiv = document.getElementById('content');
console.log(oDiv.outerHTML);

结果:

<div id="content">
    <p>This is a <strong>paragraph</strong> with a list following it.</p>
    <ul>
        <li>Item 1</li>
        <li>
            Item 2</li>
        <li>Item 3</li>
    </ul>
</div>

还是以上的DOM

oDiv.outerHTML = '<p>This is a paragraph.</p>'

DOM树将变为:

<p>This is a paragraph.</p>
4人推荐
随时随地看视频
慕课网APP