Javascript Dom操作

1.DOM 节点类型

DOM(文档对象模型)将 HTML 文档表示为一棵树,其中每个组成部分都是一个节点(Node)。常见的节点类型包括:

  • 元素节点(Element Node):如div、p 等标签。
  • 属性节点(Attribute Node):如 id="box"、title="..." 等(注意:在现代 DOM 标准中,属性通常不被视为独立的“子节点”,但可通过 attributes 访问)。
  • 文本节点(Text Node):元素内的文字内容,例如 "我是谁"。

⚠️ 补充说明:虽然“属性节点”在早期 DOM 规范中被定义为一种节点类型(nodeType === 2),但在当前 Web 开发实践中,属性一般通过元素的 getAttribute/setAttribute 等方法操作,而非直接操作属性节点。不过 getAttributeNode() 和 element.attributes 仍可访问它们。

节点的常用属性

属性 说明
nodeName 节点的名称(如元素名、#text、#comment 等)
nodeType 节点的类型(以数字常量表示)
nodeValue 节点的值(对元素节点恒为 null,对文本节点为文本内容)

nodeType 的常见取值

<script>
    //依次输出Node对象的属性和值
    for(var key in Node){
        console.log(key,Node[key])
        //只列出常用的
        //ELEMENT_NODE 1 (元素节点)
        //ATTRIBUTE_NODE 2 (属性节点)
        //TEXT_NODE 3 (文本性节点)
        //COMMENT_NODE 8 (注释节点)
        //DOCUMENT_NODE 9 (文档节点,即document)
        //DOCUMENT_TYPE_NODE 10 (文档类型节点,即<!DOCTYPE html>)
        //DOCUMENT_FRAGMENT_NODE 11 (文档片断节点,DocumentFragment)
    }
</script>

各个节点属性值示例:

<body>
    <div id="box" title="我在哪">我是谁</div>
</body>
<script>
    // 获取元素节点
    var box = document.querySelector('#box')
    // 元素节点的属性
    console.log(box.nodeName);//DIV
    console.log(box.nodeType); //1
    console.log(box.nodeValue); //一律为null
    
    // 根据属性名获取单个属性节点
    var attrTitle = box.getAttributeNode("title")
    // 属性节点的属性
    console.log(attrTitle.nodeName);//title
    console.log(attrTitle.nodeType); //2
    console.log(attrTitle.nodeValue); //"我在哪"
    //获取所有的属性节点
    var atttibues = box.attributes
    //获取特定的节点 下标和属性名都可以获取
    var idNode = atttibues.id //atttibues[0] id="box"
    console.log(idNode.nodeName,idNode.nodeType,idNode.nodeValue)//id 2 box

    // 获取box的文本节点(没有专门的方法用来获取文本节点,但是这个div的第一个子节点就是文本节点)
    var textNode = box.childNodes[0];
    // 文本节点的属性
    console.log(textNode.nodeName);//#text
    console.log(textNode.nodeType); //3
    console.log(textNode.nodeValue); //"我是谁"
</script>

✅ 关键提示:

  • 元素节点的 nodeValue 始终为 null;
  • 文本节点的内容存储在 nodeValue(或等效的 data 属性)中;
  • 属性节点虽存在,但日常开发中更推荐使用 element.getAttribute('attr') 而非操作属性节点本身。

2.Dom获取

通过选择器拿到的元素列表有2类:NodeList和HTMLCollection,区别如下:

特性 NodeList HTMLCollection
包含的节点类型 任意 Node(包括元素、文本、注释等) 仅 HTML 元素节点(Element)
是否动态(live) 通常静态(但有例外) 总是动态(live)
是否有数组方法(如 map, forEach) 有(现代浏览器支持 forEach 等) ❌ 没有(不是可迭代对象,除非手动转)
能否用 for...of 遍历 ✅ 可以(现代环境) ✅ 可以(现代浏览器也实现了 Symbol.iterator)
典型获取方式 querySelectorAll, childNodes getElementsBy* 系列

HTMLCollection

  • 只包含 Element 节点(比如 div, p),自动过滤掉文本、注释等。
  • 是“动态集合”(live collection):
  • 如果 DOM 发生变化(比如新增/删除匹配的元素),HTMLCollection 会自动更新。
  • 没有数组原型方法(如 map, filter),但可以用 Array.from() 或展开运算符转换。

NodeList

  • 可以包含任何类型的 Node(元素、文本节点、注释等)。
  • 分为两种:
    • 静态 NodeList:如 querySelectorAll() 返回的 —— 不会随 DOM 变化而更新。
    • 动态 NodeList:如 childNodes 返回的 —— 会随 DOM 更新(这是少数例外!)。

静态 vs 动态 示例

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Test</title>
    </head>
    <style>
        *{padding: 0;margin: 0;}
    </style>
    <body>
        <ul>
            <li class="test">1</li>
            <li class="test">2</li>
            <li class="test">3</li>
            <li class="test">4</li>
        </ul>
    </body>
</html>
<script>
    var lis = document.getElementsByTagName('li')
    var liByQuery = document.querySelectorAll('li')
    console.log(lis.length)  // 4
    console.log(liByQuery.length)  // 4

    // 动态添加li标签
    document.querySelector('ul').innerHTML += "<li class='test'>5</li>"
    console.log(lis.length) // 5 
    console.log(liByQuery.length) // 4 
</script>

遍历:NodeList,可以使用foreach()遍历,而HTMLCollection没有forEach方法,只能使用for循环遍历

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="initial-scale=2.0">
        <title>Test</title>
    </head>
    <body>
        <ul>
            <li class="test">1</li>
            <li class="test">2</li>
            <li class="test">3</li>
            <li class="test">4</li>
        </ul>
    </body>
</html>
<script>
    var li = document.getElementsByTagName('li')
    var li2 = document.getElementsByClassName('test')
    var li3 = document.querySelectorAll('li')
    var li4 = document.querySelector('ul').children
    console.log(li) // HTMLCollection(4) [li.test, li.test, li.test, li.test]
    console.log(li2) // HTMLCollection(4) [li.test, li.test, li.test, li.test]
    console.log(li3) // NodeList(4) [li.test, li.test, li.test, li.test]
    console.log(li4) // HTMLCollection(4) [li.test, li.test, li.test, li.test]
</script>

3.子元素 vs 子节点

属性 说明 返回类型
children 获取当前 DOM 元素的所有子元素(仅元素节点) HTMLCollection
firstElementChild 获取第一个子元素(等价于 children[0]) Element
lastElementChild 获取最后一个子元素(等价于 children[children.length - 1]) Element
childElementCount 返回子元素的数量(等价于 children.length) number
childNodes 获取当前 DOM 元素的所有子节点(包括元素节点、文本节点、注释节点等,不包含属性节点) NodeList
firstChild 获取第一个子节点(等价于 childNodes[0]) Node
lastChild 获取最后一个子节点(等价于 childNodes[childNodes.length - 1]) Node

💡 关键区别:

  • “子元素”仅指 标签 这类 HTML 元素节点;
  • “子节点”包含所有类型的节点,例如换行、空格产生的文本节点(#text)。

示例代码:

<body>
    <div id="app">
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
        </ul>
    </div>
</body>
<script>
    var ul = document.querySelector('ul')
    //子元素
    console.log(ul.children) //HTMLCollection(4) [li, li, li, li]
    //子元素数量
    console.log(ul.childElementCount) //4
    console.log(ul.children.length) //4
    //第一个子元素(等价)
    console.log(ul.children[0] === ul.firstElementChild) //true
    //最后一个子元素(等价)
    console.log(ul.children[3] === ul.lastElementChild) //true

    //子节点(把代码回车换行当作文本节点也算进去了)
    console.log(ul.childNodes) //NodeList(9) [text, li, text, li, text, li, text, li, text]
    //第一个子节点
    console.log(ul.firstChild) //#text
    //等价
    console.log(ul.childNodes[0] === ul.firstChild) //true
    console.log(ul.childNodes[8] === ul.lastChild) //true
</script>

📌 注意:
在 HTML 源码中,标签之间的换行符和空格会被解析为文本节点(Text Node),因此 childNodes 的长度通常比 children 大。若想避免干扰,可使用压缩后的 HTML(无多余空白),或始终优先使用 children / firstElementChild 等“仅元素”接口。

4.父元素与父节点

属性 / 方法 说明 调用者 备注
parentElement 获取当前 DOM 元素的父元素节点(仅限 Element 类型) 任意 Element 若父节点不是元素(如文档片段、文档根等),返回 null
parentNode 获取当前 DOM 节点的父节点(可以是任意 Node 类型) 任意 Node 总是返回实际的父节点(可能是 Element、Document、DocumentFragment 等)
closest(selector) 从当前元素向上遍历祖先树,返回第一个匹配指定 CSS 选择器的祖先元素 任意 Element 若未找到则返回 null;选择器参数不能为空

💡 关键区别:

  • parentNode 是所有节点都有的属性,返回的是直接父节点(类型不限);
  • parentElement 是 Element 接口的属性,只在父节点是元素时才返回它,否则返回 null。
  • 在普通 HTML 文档中,绝大多数元素的父节点本身就是元素(如 li 的父是 ul),所以两者通常相等;
  • 但在特殊场景(如操作 DocumentFragment、 的父是 document 等)下,二者结果可能不同。

示例代码:

<body>
    <div id="app">
       <div class="box">
            <ul>
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
            </ul>
       </div>
    </div>
</body>
<script>
    //父元素
    var ul = document.querySelector('ul')
    //第一个子元素
    var li = document.querySelector('ul > li')

    //等价
    console.log(ul === li.parentElement) //true
    console.log(ul === li.parentNode) //true

    //最近的祖先元素
    console.log(li.closest('.box')) //div.box
</script>

5.兄弟元素与兄弟节点

属性 说明 调用者
previousElementSibling 获取当前元素的前一个兄弟元素节点(仅限 Element 类型) 当前 DOM 元素
nextElementSibling 获取当前元素的后一个兄弟元素节点(仅限 Element 类型) 当前 DOM 元素
previousSibling 获取当前节点的前一个兄弟节点(可能是元素、文本、注释等) 当前 DOM 节点
nextSibling 获取当前节点的后一个兄弟节点(可能是元素、文本、注释等) 当前 DOM 节点

💡 关键区别:

  • 带 Element 的属性只返回元素节点,自动跳过文本、注释等;
  • 不带 Element 的属性返回所有类型的兄弟节点,包括 HTML 源码中的空白字符生成的文本节点(#text)

示例代码:

<body>
  <div id="app">
    <ul>
      <li id="li1">1</li>
      <li id="li2">2</li>
      <li id="li3">3</li>
      <li id="li4">4</li>
    </ul>
  </div>
</body>

<script>
  const li1 = document.querySelector('#li1');
  const li2 = document.querySelector('#li2');
  const li3 = document.querySelector('#li3');
  const li4 = document.querySelector('#li4');

  // —— 兄弟元素(仅元素节点) ——
  console.log(li2.previousElementSibling === li1); // true
  console.log(li2.nextElementSibling === li3);     // true

  // —— 兄弟节点(包含空白文本节点) ——
  console.log(li2.previousSibling); // #text(位于 <li1> 和 <li2> 之间的换行+缩进)
  console.log(li2.nextSibling);     // #text(位于 <li2> 和 <li3> 之间的换行+缩进)

  // 验证节点类型
  console.log(li2.previousSibling.nodeType === Node.TEXT_NODE); // true
  console.log(li2.previousSibling.nodeValue.trim() === '');    // true(内容为空白)
</script>

6.添加元素

DOM 提供了多种方式向文档中插入新节点,主要分为两类:作为子元素插入 和 作为兄弟节点插入

作为子元素插入

方法 说明
parent.appendChild(child) 将 child 作为 parent 的最后一个子节点插入
parent.insertBefore(newNode, referenceNode) 将 newNode 插入到 parent 中的 referenceNode 之前

示例代码

<body>
  <ul>
    <li>1</li>
    <li id="test">2</li>
    <li>3</li>
  </ul>
</body>
</html>

<script>
  const ul = document.querySelector('ul');

  // 创建新 <li> 并追加到末尾
  const li4 = document.createElement('li');
  li4.textContent = '4'; // 推荐使用 textContent 而非 innerHTML(防 XSS)
  ul.appendChild(li4);

  // 创建另一个 <li> 并插入到 #test 之前
  const liLife = document.createElement('li');
  liLife.textContent = 'life';
  const liTest = document.querySelector('#test');
  ul.insertBefore(liLife, liTest);
</script>

运行结果

<ul>
    <li>1</li>
    <li>life</li>
    <li id="test">2</li>
    <li>3</li>
    <li>4</li>
</ul>

作为兄弟节点插入(现代 API)

方法 说明
node.before(...nodes) 在 node 之前插入一个或多个节点(可为元素、文本等)
node.after(...nodes) 在 node 之后插入一个或多个节点

⚠️ IE 不支持,但现代浏览器(Chrome、Firefox、Safari、Edge)均已支持

示例代码

<body>
  <div id="app">
    <ul>
      <li id="li1">1</li>
      <li id="li2">2</li>
      <li id="li3">3</li>
    </ul>
  </div>
</body>

<script>
  // 在 #li1 前插入新 <li>
  const li0 = document.createElement('li');
  li0.id = 'li0';
  li0.textContent = '0';
  document.querySelector('#li1').before(li0);

  // 在 #li3 后插入新 <li>
  const li4 = document.createElement('li');
  li4.id = 'li4';
  li4.textContent = '4';
  document.querySelector('#li3').after(li4);

  // 也可以直接插入文本(虽然较少用)
  // li0.before('⚠️ '); // 会在 li0 前插入一个文本节点
</script>

运行结果

<ul>
  <li id="li0">0</li>
  <li id="li1">1</li>
  <li id="li2">2</li>
  <li id="li3">3</li>
  <li id="li4">4</li>
</ul>

7.移除与替换元素

DOM 提供了多种方式来移除或替换节点,分为两类操作:通过父节点操作子节点(传统方式)和节点自我操作(现代方式)

传统方式(通过父节点)

方法 说明
parent.removeChild(child) 从 parent 中移除指定的子节点 child
parent.replaceChild(newNode, oldNode) 用 newNode 替换 parent 中的 oldNode

✅ 这些方法兼容性极好,适用于所有浏览器(包括 IE)

现代方式(节点自我操作)

方法 说明
node.remove() 移除自身(无需通过父节点)
node.replaceWith(...nodes) 用一个或多个新节点替换自身

IE 不支持,但在 Chrome、Firefox、Safari、Edge 等现代浏览器中已广泛支持

示例代码

<body>
  <div id="app">
    <ul>
      <li id="li1">1</li>
      <li id="li2">2</li>
      <li id="li3">3</li>
    </ul>
  </div>
</body>

<script>
  const ul = document.querySelector('ul');
  const li1 = document.querySelector('#li1');
  const li2 = document.querySelector('#li2');
  const li3 = document.querySelector('#li3');

  // —— 移除元素 ——
  li1.remove();               // 方式1:自我移除(现代)
  ul.removeChild(li2);        // 方式2:通过父节点移除(传统)

  // —— 替换元素 ——
  const newLi = document.createElement('li');
  newLi.textContent = 'replaced li'; // 推荐使用 textContent 防 XSS

  ul.replaceChild(newLi, li3);       // 方式1:通过父节点替换(传统)
  // 或者(如果 li3 还存在):
  // li3.replaceWith(newLi);         // 方式2:自我替换(现代)
</script>

运行结果:(第一,第二个被移除,第三个被替换)

<ul>
  <li>replaced li</li>
</ul>

8.复制 DOM 元素

使用 element.cloneNode() 可复制一个 DOM 节点:

调用方式 说明
cloneNode(true) 深拷贝:复制元素本身、属性及其所有后代节点(包括子元素和文本)
cloneNode(false) 或 cloneNode() 浅拷贝:仅复制元素本身和属性,不包含任何子节点(如文本、子标签等)

坑爹的W3C不解释,查文档只推荐MDN

示例代码

<body>
  <div id="app">
    <ul>
      <li class="text-align">hello world</li>
    </ul>
  </div>
</body>

<script>
  const ul = document.querySelector('ul');
  const li = document.querySelector('li');

  // 两次浅拷贝(不带子节点)
  ul.appendChild(li.cloneNode());       // 等价于 cloneNode(false)
  ul.appendChild(li.cloneNode(false));

  // 一次深拷贝(带子节点,包括文本)
  ul.appendChild(li.cloneNode(true));
</script>

运行结果:(可以看出,第二第三都是浅拷贝而来,没有带上子节点)

<li class="text-align">hello world</li>  <!-- 原始节点 -->
<li class="text-align"></li>             <!-- 浅拷贝 1 -->
<li class="text-align"></li>             <!-- 浅拷贝 2 -->
<li class="text-align">hello world</li>  <!-- 深拷贝 -->

补充说明

  • cloneNode() 不会复制通过 addEventListener 添加的事件监听器;
  • 若元素有 id 属性,克隆后会产生重复 ID,需手动处理;

9.滚动到目标元素所在位置

有两种常用方式可让页面(或可滚动容器)自动滚动到某个元素的位置:

使用锚点链接(HTML 原生方式)

通过 a 标签的 href 属性指向目标元素的 id(格式为 #id),点击后浏览器会自动滚动到该元素

<body>
    <div>
        <!-- 使用a链接进行滚动 -->
        <a href="#box3">滚动到3</a>
    </div>
    <div class="box" id="box1">1</div>
    <div class="box" id="box2">2</div>
    <div class="box" id="box3">3</div>
</body>

✅ 优点:无需 JavaScript,语义清晰,支持浏览器历史记录(可后退);

使用 element.scrollIntoView()(JavaScript 方式)

调用元素的 scrollIntoView() 方法,可编程控制滚动行为

基础用法

<button>滚动到 #box3</button>
<div class="box" id="box1">1</div>
<div class="box" id="box2">2</div>
<div class="box" id="box3">3</div>

<script>
  const btn = document.querySelector('button');
  btn.onclick = function () {
    const box3 = document.querySelector('#box3');
    box3.scrollIntoView(); // 默认平滑滚动到视口顶部(行为因浏览器而异)
  };
</script>

高级用法(推荐)

scrollIntoView() 支持传入配置对象,精确控制滚动行为:

box3.scrollIntoView({
  behavior: 'smooth',  // 滚动动画:'auto'(默认,瞬间跳转)或 'smooth'(平滑滚动)
  block: 'start',      // 垂直对齐:'start' | 'center' | 'end' | 'nearest'
  inline: 'nearest'    // 水平对齐(对行内元素或横向滚动有用)
});

⚠️ 兼容性:behavior: 'smooth' 在 IE 中不支持,但现代浏览器(Chrome ≥61, Firefox ≥36, Safari ≥15.4)均已支持

10.获取与设置标签内容

DOM 提供了多种方式读取或修改元素的内容,它们的行为差异显著:

属性 说明 是否解析 HTML 是否包含隐藏内容 是否保留格式/空白
el.innerHTML 获取/设置元素内部的 HTML 字符串 ✅ 是(赋值时会渲染标签) ✅ 包含注释、隐藏元素等所有子节点 ✅ 保留原始 HTML 格式(包括换行、缩进)
el.outerHTML 获取/设置元素自身及其内部的完整 HTML 字符串 ✅ 是 ✅ 同 innerHTML,但包含当前标签本身 ✅ 保留完整结构
el.textContent 获取/设置元素内所有文本节点的拼接结果(纯文本) ❌ 否(赋值时 < 不会被解析为标签) ✅ 包含隐藏元素、注释中的文本(注释文本不可见,但 textContent 不包含注释内容!) ✅ 保留所有空白字符(包括换行、缩进)
el.innerText 获取元素内用户可见的文本内容 ❌ 否 ❌ 忽略 display: none、visibility: hidden 等不可见文本 ❌ 按视觉渲染结果处理空白(类似用户复制粘贴的效果)

代码示例

<body>
    <div class="box">
        <span style="display: none;">
            隐藏文本
        </span>
        <!-- 注释 -->
        <span>
            123
        </span>
        <span>
            456
        </span>
        <div>
            另一行
        </div>
    </div>
</body>
<script>
    var box = document.querySelector('.box')
    console.log('---innerHTML---')
    console.log(box.innerHTML)

    console.log('---textContent---')
    console.log(box.textContent)

    console.log('---innerText---')
    console.log(box.innerText)
</script>

posted @ 2019-09-23 00:04  ---空白---  阅读(1146)  评论(0)    收藏  举报