javascript进阶知识26 - DOM

196 阅读7分钟

DOM:Document Object Model 文档对象模型

节点层级

<html>
    <head>
        <title>Sample Page</title>
    </head>
    <body>
        <p>Hellow World!</p>
    </body>
</html>

对应的层级结构:

Document
    - Element html
        - Element head
            - Element title
                - Text Sample Page
        - Element body
            - Element p
                - Text Hello worlds!

其中,document结点表示每个文档的跟节点。根节点的唯一子节点是<html>元素,我们称之为文档元素。文档元素是文档最外层的元素,所有其他元素都存在于这个元素之内。每个文档只能有一个文档元素。

HTML中的每段标记都可以表示为这个树形结构中的一个节点。元素节点表示HTML元素,属性节点表示属性,文档节点表示文档类型,注释节点表示注释。DOM中一共有12种节点类型,这些类型都继承一种基本类型。

节点关系

每个节点都有一个childNodes属性,其中包含一个NodeList的实例。NodeList是一个类数组对象,用于存储可以按位置存取的有序节点。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            width: 300px;
            height: 300px;
            background-color: red;
        }
        .box>div {
            flex: 1;
            margin: 20px 0;
            border: 1px solid blue;
            background-color: #fff;
        }
    </style>
</head>
<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        let box = document.querySelector('.box');
        let nodes = box.childNodes;
        console.log(nodes);
    </script>
</body>
</html>

image.png

访问NodeList中的元素

  • 一种是使用中括号[]

    let firstChild = someNode.childNodes[0]
    
  • 一种是使用item()方法

    let secondChild = someNode.childNodes.item(1)
    
  • 使用length可以返回节点数量

    let count = someNode.childNodes.length;
    

每个节点都有一个parentNode属性,指向DOM树(层级结构)中的父元素。childNodes中的所有结点都有同一个父元素,因此他们的parentNode属性都指向同一个节点。此外,childNodes列表中的每个节点都是同一列表中其他节点的同胞节点。而使用previousSiblingnextSibling可以在这个列表的节点间导航。这个列表中的第一个节点的previousSibling属性是null, 最后一个节点的nextSibling属性也是null。

// html接的上面的
let box = document.querySelector('.box');
let nodes = box.childNodes;
let firstNode = nodes[0];
console.log(firstNode.previousSibling);  // nulllet lastNode = nodes[nodes.length - 1];
console.log(lastNode.nextSibling); //null

注:如果childNodes中只有一个节点,则它的previousSibling和nextSibling属性都是null。

此外,第一个节点和最后一个节点有专门的属性。

// html接的上面的
let box = document.querySelector('.box');
let firstNode = box.firstChild;
console.log('firstNode:', firstNode);  
​
let lastNode = box.lastChild;
console.log('lastNode:', lastNode); 

image.png

childNodes与children

但是我们发现这个childNodes有一些不好的地方,他只要在子元素中有任何换行、空格就会产生Text Element

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            width: 300px;
            height: 300px;
            background-color: red;
        }
​
        .box>div {
            flex: 1;
            margin: 20px 0;
            border: 1px solid blue;
            background-color: #fff;
        }
    </style>
</head><body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        console.log(box.childNodes);
    </script>
</body></html>

image.png

  • 0:text的data是'\n' ,指的是

image.png

  • 1:div是<div>1</div>

  • 2:text的data是'\n',指的是

image.png

.......

如果我们吧这个换行去掉:

<body>
    <div class="box"><div>1</div><div>2</div><div>3</div></div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        console.log(box.childNodes);
    </script>
</body>

image.png

我们发现它的子节点没有text了,length也为3了。

为了更高的获得标签Element,而不是text,我们一般不使用someNode.childNodes,而是使用someNode.children.

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            width: 300px;
            height: 300px;
            background-color: red;
        }
​
        .box>div {
            flex: 1;
            margin: 20px 0;
            border: 1px solid blue;
            background-color: #fff;
        }
    </style>
</head><body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        console.log(box.children);
    </script>
</body></html>

image.png

获取节点

老方法就不写了,基本没咋用过:

document.getElementById()
document.getElementByTagName()
document.get....
...

现在都是使用querySelector()来获取节点元素了:(返回的是一个标签对象)

let box1 = document.querySelector('#box'); //获取id为box的元素
let box2 = document.querySelector('.box'); //获取类名为box的第一个元素
let body = document.querySelector('body'); //获取<body>
let div1 = document.querySelector('#box div'); //获取类名为box下面的第一个div元素
let div2 = box2.querySelector('div'); // 返回的是box2下面的第一个div标签
let img = document.querySelector('img.button'); //取得类名为button的图片

要获取多个元素使用querySelectorAll():(返回的是一个伪数组)

document.querySelectorAll('div'); //获取所有的div标签
document.querySelectorAll('.box'); //获取所有类名包含box的标签

获取body可以使用:

let body = document.body;

获取html可以使用:

let html = document.documentElement;

获取head可以使用:

let head = document.head;

操纵节点

createElement()

用于创建元素节点。并返回一个Element对象:

//语法   document.createElement(tagName);
// tagName 字符串值,指明创建元素的类型
let div = document.createElement('div');
div.innerHTML = '4';
console.log(div);  // <div>4</div>

appendChild()

用于在childNodes或者children列表末尾添加结点。appendChild()方法返回新添加的节点。

// 接上面的代码
let box = document.querySelector('.box');
let div = document.createElement('div');
div.innerHTML = '4';
let newNode = box.appendChild(div);
console.log(newNode);   // <div>4</div>

如果把文档树中已存在的节点传给appendChild(),则这个节点会从之前的位置被转移到新位置。

<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        let firstNode = box.firstElementChild;
        let newNode = box.appendChild(firstNode);
        console.log(newNode);
    </script>
</body>

image.png

注意:someNode.firstElementChild,得到的是子节点中的第一个标签节点,会忽略其他节点。同理,还有someNode.lastElementChild,指向最后一个Element类型的子元素;previousElementSibling,指向前一个Element类型的同胞元素,nextElementSibling,指向后一个Element类型的同胞元素;childElementCount返回子元素中Element类型的元素数量。

insertBefore()

如果想要吧节点放到childNodes或者children列表中的特定位置而不是末尾,则可以调用insertBefore()方法。这个方法接收两个参数:要插入的节点和参照节点。调用这个方法后,要插入的节点会变成参照节点的前一个同胞节点,并被返回。如果参照节点是null,则insertBefore()和appendChild()效果相同。

<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        let nodes = box.children;
        let div = document.createElement('div');
        div.innerHTML = '4';
        let newNode = box.insertBefore(div,nodes[1]);  //nodes[1] 在这里就是 <div>2</div>  
        console.log(newNode);
    </script>
</body>

image.png

replaceChild()

replaceChild()方法接收两个参数:要插入的节点和要替换的节点。要替换的节点会被返回并从文档树中完全移除,要插入的节点取而代之。

<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        let nodes = box.children;
        let div = document.createElement('div');
        div.innerHTML = '4';
        let newNode = box.replaceChild(div,nodes[1]);  //nodes[1] 在这里就是 <div>2</div>  
        console.log(newNode);
    </script>
</body>

image.png

removeChild()

该方法接收一个参数,即将要移除的节点。

<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        let nodes = box.children;
        let node = box.removeChild(nodes[1]);  //nodes[1] 在这里就是 <div>2</div>  
        console.log(node);
    </script>
</body>

image.png

cloneNode()

克隆节点。会返回与调用它的节点一模一样的节点,此外,该方法接收一个参数:布尔值,表示是否深层复制。传入true时,会进行深层复制,即复制结点以及其整个子DOM树。如果传入false,则只会复制调用该方法的节点。

<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        let nodes = box.children;
        let newNode = nodes[1].cloneNode(true);
        box.insertBefore(newNode,nodes[0]);
        console.log(newNode);
    </script>
</body>

image.png

如果我们传入false:

<body>
    <div class="box">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>
    <script>
        // html接的上面的
        let box = document.querySelector('.box');
        let nodes = box.children;
        let newNode = nodes[1].cloneNode(false);
        box.insertBefore(newNode,nodes[0]);
        console.log(newNode);
    </script>
</body>

image.png

则只会复制

,而不会复制它里面的Text Node: 2.

注:这个方法只会复制其HTML属性,不会复制其JavaScript属性,比如事件处理程序。

parentNode()

返回元素的父节点。

normalize()

现在没啥用了吧,有了Children代替childNodes后。

标签属性

我们每一个标签上面就有很多属性,有些是自带的,有些是我们自定义的:

<img src="" alt="" index=“”>

它上面有自带的src和alt属性,也有我们自定义的index属性。

DOM上提供了3种方法对标签属性进行操作:

getAttribute()

获取标签的属性:

<body>
    <img src="https://img1.baidu.com/it/u=1149359842,3831784241&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400" alt="头像" index="1">
    <script>
        let img = document.querySelector('img');
        let src = img.getAttribute('src');
        console.log(src);  // 1.html:34 https://img1.baidu.com/it/u=1149359842,3831784241&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400
    </script>
</body>

removeAttribute()

去除标签的属性:

<body>
    <img src="https://img1.baidu.com/it/u=1149359842,3831784241&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400" alt="头像" index="1">
    <script>
        let img = document.querySelector('img');
        img.removeAttribute('index');
        console.log(img); 
        // <img src="https://img1.baidu.com/it/u=1149359842,3831784241&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400" alt="头像">
    </script>
</body>

setAttribute()

设置标签的属性:

<body>
    <img src="https://img1.baidu.com/it/u=1149359842,3831784241&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400" alt="头像" index="1">
    <script>
        let img = document.querySelector('img');
        img.setAttribute('from','百度');
        console.log(img);  
        //  <img src="https://img1.baidu.com/it/u=1149359842,3831784241&fm=253&fmt=auto&app=138&f=JPEG?w=400&h=400" alt="头像" from="百度">
    </script>
</body>

操作CSS类

如果标签的类名只有一个,那么可以很好的操作:

<body>
    <div class="box">
        1
    </div>
    <script>
        let box = document.querySelector('.box');
        // 获取类名
        let name = box.className;
        console.log(name);  // box
        box.className = 'box2';
        console.log(box);  // <div class="box2">1</div>
    </script>
</body>

如果有多个类名,那么使用className就很恶心了。

classList

HTML5新增的,是一个新的集合类型DOMTokenList的实例。属于DOM。

DOMTokenList增加了以下方法:

  • add(value),向类名列表中添加指定的字符串值value。如果已存在这个值,则什么也不做。
  • contains(value),返回布尔值,表示给定的value是否存在。
  • remove(value),从类名列表中删除指定的字符串值value。
  • toggle(value),如果类名列表中已经存在指定的value,则删除,如果不存在,则添加。
<body>
    <div class="box user disabled">
        1
    </div>
    <script>
        let box = document.querySelector('.box');
        // 删除user类
        box.classList.remove('user');
        // 添加current类
        box.classList.add('current');
        console.log(box);  // <div class="box disabled current">1</div>
    </script>
</body>

H5的其他扩展

readyState属性

有两个值:

  • loading,表示文档正在加载
  • complete,表示文档加载完成

自定义数据类型

H5允许给元素指定非标准的属性,但要使用前缀data-以便告诉浏览器,这些属性既不包含与渲染有关的信息,也不包含元素的语义信息。

<div id="myDiv" data-appId="12345" data-myname="Nicholas">
    1
</div>

定义了自定义数据属性后,可以通过元素的dataset属性来访问。dataset属性是一个DOMStringMap的实例,包含一组键值对映射。元素的每一个data-name属性在dataset中都可以通过data-后面的字符串作为健来访问(例如,属性data-myname、data-myName可以通过myname访问,但要注意data-my-name、data-My-Name要通过myName来访问)。

<body>
    <div id="myDiv" data-appId="12345" data-myname="Nicholas">
        1
    </div>
    <script>
       let div = document.querySelector('#myDiv');
       let appId = div.dataset.appid;
       let myName = div.dataset.myname;
​
       //设置自定义数据属性的值
       div.dataset.appid = 23456;
       div.dataset.myname = 'Michael';
​
       console.log(appId,myName); // 12345 Nicholas
       console.log(div);  // <div id="myDiv" data-appId="23456" data-myname="Michael">1</div>
    </script>
</body>

innerHTML、outerHTML、innerText、outerText

innerHTML

这个包括元素所有后代的HTML字符串,包括元素、注释、文本节点。而在写入innerHTML时,则会根据提供的字符串值以新的DOM子树替代元素中原来包含的所有结点。

这个搭配ES6新增的模板字符串(``) 很好用哦!

outerHTML

会返回调用它的元素(及所有后代元素)的HTML字符串。其实就是比innerHTML多一个它本身!

<body>
    <div id="myDiv" data-appId="12345" data-myname="Nicholas">
        <h2>标题</h2>
        <p>啦啦啦啦啦</p>
    </div>
    <script>
        let div = document.querySelector('#myDiv');
        console.log(div.outerHTML);
        /***
         * <div id="myDiv" data-appid="12345" data-myname="Nicholas">
         *      <h2>标题</h2>
         *      <p>啦啦啦啦啦</p>
         * </div> 
         */
        console.log(div.innerHTML);
        /**
         *  <h2>标题</h2>
         *  <p>啦啦啦啦啦</p>
        */
    </script>
</body>

注意,如果使用outerHTML设置HTML:

div.outerHTML = '<p>this is a paragraph</p>';

则等于:

let p = document.createElement('p');
p.appendChild(document.createTextNode('this is a paragraph'));
div.parentNode.replaceChild(p,div);

新的<p>元素会替换掉DOM树原先的<div>元素。

innerText

对应元素中包含的所有文本内容。无论文本在子树中的哪一个层级。

读值时,innerText会按照深度优先的顺序将子树中的所有文本节点拼接起来。

写值时,innerText会移除元素的所有后代并插入一个包含该值的文本节点。(不会解析html标签)

<body>
    <div id="myDiv" data-appId="12345" data-myname="Nicholas">
        <h2>标题</h2>
        <p>啦啦啦啦啦</p>
    </div>
    <script>
        let div = document.querySelector('#myDiv');
        console.log(div.innerText);
        /**
         *   标题
         *   啦啦啦啦啦
         */
        div.innerText = '<p>this is a paragraph</p>';
        console.log(div);
        // <div id="myDiv" data-appid="12345" data-myname="Nicholas"><p>this is a paragraph<p></div>
    </script>
</body>

outerText

与innerText属性类似,只不过作用范围包含调用它的节点。

读取时,与innerText返回相同的内容

写值时,outerText不止会移除后代节点,而是会替换整个元素。

div.outerText = '<p>this is a paragraph</p>'

就等同于:

let text = document.createTextNode('<p>this is a paragraph</p>');
div.parentNode.replaceChild(text,div);