DOM系列-节点

244 阅读10分钟

DOM属于Web API的一部分,Web API中定义了很多对象,通过这些对象可以完成对网页的各种操作(添加/删除元素、发送请求,操作浏览器等等)。

DOM全称Document Object Model(文档对象模型),文档就是HTML网页文档,对象表示将网页中的每一个部分都转化为了一个对象,这样就可以通过面向对象的方式去操作网页,想要操作哪个元素就获取哪个元素的对象,然后通过调用其方法或属性完成各种操作。模型是表示对象之间的关系,比如父子元素、祖先后代,兄弟元素等,明确关系后我们便可以通过任意一个对象去获取其它的对象。

  • 通过浏览器提供的方法可以操作XML和HTML元素,但是无法操作CSS样式表。
  • 我们所看见的改变样式其实是因为DOM操作了标签的style属性,增加了内联样式。

DOM对象是一种宿主对象(Host Object),由浏览器(宿主)提供,而不是ECMAScript。

JavaScript三种对象

本地对象 Native Object

  1. Object Function Array
  2. String Number Boolean(包装类)
  3. Error EvalError SyntaxError RangeError ReferenceError TypeError URIError
  4. Date RegExp

内置对象 Built-in Object

Global对象,也就是全局对象,它具有一些内置方法和属性。

  • 方法:isNaN() parseInt() Number() decodeURI encodeURI()
  • 属性:Infinity NaN undefined

Math对象

宿主对象 Host Object

宿主对象是指执行JS脚本的环境提供的对象,比如浏览器对象,不同浏览器或版本会涉及到兼容性问题。

浏览器对象具体分为:

  • window(BOM)对象,它是全局对象的一个属性,指向全局对象本身。
  • document(DOM)对象 它是BOM的一部分,它是有w3c标准的。

DOM结构

DOM结构就是HTML结构。

下面这张图是DOM结构树,暂时可以理解为Node函数是DOM树的顶点。

DOM结构.jpg

console.log(document.__proto__ === HTMLDocument.prototype); // true
console.log(HTMLDocument.prototype.__proto__ === Document.prototype); // true

节点

之前提到在DOM标准下,网页中的每一个部分都可以转换为对象,这些对象有一个共同的称呼叫做节点。节点本质上是从HTML中选出指定部分经过构造函数实例化(new HTMLParagraphElement())形成的,所以说DOM节点都是对象。

元素节点只是节点的一种,元素节点也叫DOM元素。

节点具体有以下几种类型,并且它们有对应的代表值(nodeType)用于区分类型:

  • 元素节点 - 1 HTML文档中的HTML标签
  • 属性节点 - 2 元素的属性
  • 文本节点 text - 3 HTML标签中的文本内容
  • 注释节点 comment - 8
  • 文档节点 document - 9 整个HTML文档
  • DocumentFragment - 11

获取节点

要使用DOM操作网页,我们需要浏览器至少先给我们一个对象,才能去完成各种操作。而document对象就是我们可以直接使用的一个对象,它是一个全局变量代表整个文档,整个网页。注意document是window对象得一个属性,window对象代表整个浏览器窗口。

document可以使用原型链中的诸多属性和方法,这些属性和方法就是DOM操作的基石。

  • getElementById()方法 通过元素的id属性获取一个元素节点。

  • getElementsByName()方法 通过元素的name属性获取一组实时更新的元素节点集合。

但要注意的是有些方法在Element.prototype中也有,意味着有些方法,元素节点对象也可以调用,比如以下四种方法。

  • getElementsByTagName()方法 通过元素标签名获取一组实时更新的元素节点集合,可以配合*选出所有元素。

  • getElementsByClassName()方法 通过元素类名获取一组实时更新的元素节点集合。(IE8及以下不支持)

  • querySelector()方法 获取第一个元素(多个同名元素)节点。(IE6及以下不支持)

  • querySelctorAll()方法 获取一组元素节点集合。(IE6及以下不支持)

query系列方法

query系列方法是通过CSS选择器来查询元素节点对象,可用性很强。比如以下代码中直接用父子选择器选出想要的元素。

<div>
    <h1>
        <p>123</p>
    </h1>
</div>
<div>
    <p>456</p>
</div>

<script>
    var div1 = document.querySelector('div > p');
    console.log(div1); // <p>456</p>
</script>

query系列方法的性能不如get系列,而且不会实时更新节点集合,无法进行一些操作比如remove

  • 当我们使用get系列方法,然后对获取的元素集合进行remove操作时,会发现节点集合更新了。
<div>123</div>
<div>456</div>
<div>789</div>

<script>
    var divs = document.getElementsByTagName('div');
    console.log(divs); // [div, div, div]
    divs[0].remove();
    console.log(divs); // [div, div]
</script>
  • 而当我们使用query系列方法,然后对获取的元素集合进行remove操作时,节点集合没有更新。
<div>123</div>
<div>456</div>
<div>789</div>

<script>
    var divs = document.querySelectorAll('div');
    console.log(divs); // [div, div, div]
    divs[0].remove();
    console.log(divs); // [div, div, div]
</script>

body和head元素节点

bodyhead也是元素节点,并且它们是HTMLDocument.prototype中的属性,但是不能直接访问。可以通过其实例对象document进行访问,所以就可以通过以下方法访问。

var body = document.body;
var head = document.head;

documentElement也是Document.prototype中的属性,但是不能直接访问,会报错。不过可以通过其实例对象进行访问,获取的是html根元素节点。

var html = document.documentElement;

document.title 获取的是title元素节点的文本内容。

节点实例化

当我们通过上面那些方法获取节点的时候,实际上中间还有一个构造函数实例化的过程(它是一个底层的内部原理),先是获取到元素,然后进行构造函数实例化。

比如以下代码的内部执行过程,是先获取到div元素,然后经过HTMLDivElement()实例化生成一个div对象放在内存中,最后放入DOM结构中形成一个div节点。

<div></div>

<script>
    var div = document.getElementsByTagName('div')[0];
</script>

内部原理可以大概理解为如下:

function getElementsByTagName(element){
    // 1. 从HTML中将元素选出来
    // 2. 将元素进行 new HTMLDivElement(){} -> DOM对象
}

节点属性

nodeName属性 返回当前节点的节点名称,是只读属性。

  • 元素节点的nodeName是大写形式,文档节点和文本节点的nodeName是#document#text
  • toLocaleLowerCase()方法 将字符串变为小写。
  • toUpperCase()方法 将字符串变为大写。
var div = document.getElementsByTagName('div')[0];
console.log(div.nodeName);
// 'DIV'
var nodeName = div.nodeName.toLocaleLowerCase();
console.log(nodeName);
// 'div'

nodeValue属性 返回或设置当前节点的值,是可写属性。

  • 元素节点没有nodeValue属性。但是元素一般都有其它属性,比如classidvalue等,这些属性是可以直接访问的,毕竟元素也是对象,对象的属性可以直接通过点.语法就可以访问。要注意因为class是关键字,所以需要通过className属性访问class
<button id="aaa" class="btn" value="ccc">点我</button>
<script>
    var btn = document.getElementsByTagName("button")[0];

    addEvent(btn, 'click', function(){
        console.log(btn.id);        // aa
        console.log(btn.className); // btn
        console.log(btn.value);     // ccc
        console.log(btn.innerHTML); // 点我
    });
</script>
  • 属性、文本、注释节点有nodeValue属性,它们返回的分别是属性值、文本内容、注释内容。
  • 获取属性节点可以使用getAttributeNode()方法(不常用)。
var demo = document.getElementsByTagName('div')[0];
var demoid = demo.getAttributeNode('id');
console.log(demoid.nodeValue); // cc

nodeType属性 返回的是该节点的类型,是只读属性。可用于封装遍历子元素节点的方法。

var div = document.getElementsByTagName('div')[0];
function elementChildren(node){
    var arr = {
        'length': 0,
        'push': Array.prototype.push,
        'splice': Array.prototype.splice
    },
    len = node.childNodes.length;
    for(var i = 0; i < len; i++){
        var childItem = node.childNodes[i];
        if(childItem.nodeType === 1){
            // 可以用属性添加的方式,注意这种方式要手动增加length值。
            // arr[arr['length']] = childItem;
            // arr['length']++;
            
            // 也可以用继承的push方法,这种方式会自动更新length值。
            arr.push(childItem);
        }
    }
    return arr;
}

console.log(elementChildren(div));

attributes属性 返回该元素所有属性节点的一个实时集合*(该属性不常用)。

<div id="demo" class="box ">

</div>

<script>
    var div = document.getElementsByTagName('div')[0];

    console.log(div.attributes); // NamedNodeMap {0: id, 1: class, id: id, class: class, length: 2}
    console.log(div.attributes[0]); // id="demo"
</script>

parentNode属性 返回指定节点的父节点。

<ul>
    <li>
        <h2>我是h2标题</h2>
        <a href="">我是超链接</a>
        <p>我是段落</p>
    </li>
</ul>

<script>
    var a = document.getElementsByTagName('a')[0];
    console.log(a.parentNode); // <li>...</li>
</script>

childNodes属性 返回指定节点的所有类型的子节点的集合(实时更新)。IE8及以下版本不会把空白文本当作子节点。

<ul>
    <li>
        <!-- 我是注释 -->
        <a href="">我是超链接</a>
        <p>我是段落标签</p>
        <h2>我是标题标签</h2>
    </li>
</ul>

<script>
    var li = document.getElementsByTagName('li')[0];
    console.log(li.childNodes.length); // 9
    console.log(li.childNodes); // [text, comment, text, a, text, p, text, h2, text]
</script>

firstChild属性和lastChild属性 分别返回指定节点的第一个子节点,最后一个子节点。

nextSibling属性 previousSibling属性 分别返回指定节点的下一个兄弟节点,上一个兄弟节点。

元素节点属性

parentElement属性 返回指定节点的父元素节点(IE9及以下不支持)。

children属性 返回指定节点的所有子元素节点(IE7及以下不支持)。

childElementCount属性 返回指定节点的所有子元素节点的长度(IE9及以下不支持),相当于children.length

firstElementChildlastElementChild属性 分别返回指定节点的第一个子元素节点和最后一个子元素节点 (IE9及以下不支持)。

nextElementSiblingpreviousElementSibling属性 分别返回指定节点的下一个兄弟元素节点,上一个兄弟元素节点(IE9及以下不支持)。

innerHTML属性 用于获取或修改元素内部的HTML代码。

innerText属性 用于获取或修改元素内部的文本内容(会考虑CSS样式)。

textContent属性 用于获取或修改元素内部的文本内容(不考虑CSS样式)。

节点方法

节点中的方法基本就用于节点的增删改查,注意之前提到的innerHTML属性也可以实现节点的增删改查,但是它有一个弊端,会更新所有的节点,意味着之前如果绑定了处理函数,则更新后会失效。所以一般都会配合以下方法使用。

创建和添加节点

document.createElement('div')方法 创建元素节点,注意此时的元素节点不在DOM中,是在内存中。

var div = document.createElement('div');
div.innerHTML = 123;
document.body.appendChild(div);

document.createTextNode('hellworld')方法 创建文本节点。

var text = document.createTextNode('helloworld');
document.body.appendChild(text);

document.createComment('我是注释')方法 创建注释节点。

var comment = document.createComment('我是注释');
document.body.appendChild(comment);

document.createDocumentFragment()方法 创建一个文档片段。该文档片段存在于内存中,而不是在DOM树中。可用于创建列表类元素时存储,优化系统。一般命名时首字母是o,o表示object,是一个对象。

var oUl = document.getElementById('list');
var oFrag = document.createDocumentFragment('div');

for(var i = 0; i < 10000; i++){
    var oli = document.createElement('li');
    oli.innerHTML = i + '这是第' + i + '个项目';
    oli.className = 'list-item';
    oFrag.appendChild(oli);
}
oUl.appendChild(oFrag);

Node.appendChild(child)方法 在指定父节点中的末端插入新的子节点,底层原理是对节点进行剪切然后粘贴到指定父节点中的末端,该方法存在于Node.prototype中。

Node.insertBefore(newchild, originchild) 在指定父节点的指定子节点之前插入新的子节点。

var div = document.getElementsByTagName('div')[0];
var p = document.getElementsByTagName('p')[0];
var a = document.createElement('a');
a.href = '';
div.insertBefore(a, p);

Node.insertAdjacentElement(position, element)方法 可以向元素的任意位置添加元素。第一个参数表示相对于该元素的位置,第二个参数是要插入的元素。第一个参数必须是以下字符串之一:

  • beforebegin: 在该元素本身的前面。
  • afterbegin: 只在该元素当中,在该元素第一个子元素前面。
  • beforeend: 只在该元素当中,在该元素最后一个子元素后面。
  • afterend: 在该元素本身的后面。

Node.inserAdjacentHTML()方法 可以向元素的任意位置添加DOM结构。

删除和替换节点

Node.removeChild(child) 方法 指定父节点删除子节点。删除的节点只是从DOM中移除,依旧存在于内存中,并未销毁。

Node.remove() 方法 删除当前节点,节点完全销毁。

Node.replaceChild(new, origin)方法 节点替换。

<div>
    <h1>我是h1</h1>
    <h4>我是h4</h4>
</div>

<script>
    var div = document.getElementsByTagName('div')[0],
        h1 = document.getElementsByTagName('h1')[0],
        h2 = document.getElementsByTagName('h4')[0];

    div.replaceChild(h2, h1);
</script>

Node.replaceWith(new)方法 节点替换。

Node.hasChildNodes()方法 返回一个布尔值,表明当前节点是否包含有子节点。

元素节点的方法

Element.setAttribute(name, value)方法 设置指定元素属性。

var div = document.getElementsByTagName('div')[0];
div.setAttribute('id', 'box');

Element.getAttribute()方法 获取指定元素的属性。

var div = document.getElementsByTagName('div')[0];
var attr = div.getAttribute('class');

Element.removeAttribute()方法 删除指定元素的属性。

HTML5

data-*属性 元素设置自定义属性

<p data-name="唐" data-age="18">helloworld</p>
<script>
    var p = document.getElementsByTagName('p')[0];
</script>
  • 获取自定义属性可以用dataset或者getAttribute()
// p.getAttribute('data-name');
// p.dataset.name;