# JavaScript DOM 样式 与 节点操作

TIP

在正式本节课内容之前,我们首先我们来回顾下,JS 这门课程需要学习的内容。

具体参考如下:

image-20221004185803144

前面

  • 我们学习了 ECMAScript 标准中规定的基本语法中的一部分,还有 JS 高级部分+ES6+Ajax+本地存储等内容,等学完 DOM 和 BOM 后再学。
  • 从今天开始,我们开始学习 DOM 和 BOM 相关的内容。
  • DOM 与 BOM 属于 Web APIs 相关内容,那什么是 Web APIs 呢 ?

# 一、API 与 Web API

TIP

要了解什么是 Web API,我们需要先了解什么是 API,再这个基础上,再来了解 Web APIs 更容易

# 1、API 应用程序接口

API

Application Programming Interface ,应用程序接口 。在编程中可以理解为一些预先定义好的函数,目的是提供应用程序与开发人员基于某软件或硬件得一访问一组例程的能力,而无须考虑其底层的源代码为何、或理解其内部工作机制的细节。

  • 例程: 是某个系统对外提供的功能接口或服务的集合
  • 接口: 站在现实角度,可以理解为两个物体的口子相连接,而无需关心内部实现
手机充电接口 实现充电
image-20221011220200355 不用关心手机内部如何实现
不用关心充电线如何制作
我们需要充电,只需要拿充电线插进充电接口,就可以充电了
这里的充电接口就是一个 API(应用程序接口)

简单理解:

API 是一个被封装好具有一定功能的函数,程序需要使用某种功能时,只需要调用这个函数,就能轻松实现想要完成的功能。

所以 API 被称为应用程序接口。

# 2、Web API Web 应用程序接口

Web API

Web Application Programming Interface 在前端可以理解为是浏览器提供的一套操作浏览器功能页面元素的 API,其中包括 DOM 和 BOM。

  • DOM(Document Object Model)文档对象模型, 是 JavaScript 操作网页的接口。它定义了访问 HTML 文档对象的一套属性、方法和事件。
  • BOM:Browser Object浏览器对象模型, 是 JavaScript 操作浏览器的接口,提供一系列与浏览器相关的信息
  • DOM 与 BOM 是 W3C 国际组织定义的一套 Web 标准接口。

  • W3C(万维网联盟)创建于 1994 年,是 Web 技术领域最具权威和影响力的国际中立性技术标准机构。

在我们接下来的学习中,我们主要学习 DOM 和 BOM 相关的 API。

因为 Web API 很多,所以我们称其为 Web APIs。

# 二、DOM 概况与获取元素

TIP

  • 深入浅出 DOM,节点(NODE),document 文档对象,访问元素节点的常用方法
  • getElementById(),getElementsByTagName(),getElementsByClassName(),querySelector(),querySelectorAll()
  • querySelectorAll 、getElementsByClassName()、getElementsByTagName() 的区别
  • 获取 body 与 HTML 元素
  • 获取页面中所有元素
  • onload 方法

# 1、什么是 DOM

TIP

DOM 全称 Document Object Model 文档对象模型。

  • Document 文档,表示的就是整个 HTML 网页文档
  • Object 对象 ,表示将网页中的每一个部分都转换为一个对象
  • Model 模型,表示对象之间的关系,这样方便我们获取对象。

DOM 是 JavaScript 操作网页的接口,那 JS 具体是如何操作 DOM 的呢 ?

DOM 最大的特点,就是将整个 HTML 文档抽象成一个 DOM 树,JS 可以通过操作 DOM 树来实现对 HTML 文档的添加、删除 、修改等操作

我们来下面这段简单的 HTML 代码

<!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>DOM文档结构树</title>
  </head>
  <body>
    <div>
      <div>我是文本节点</div>
      <img src="" alt="" />
      <h3></h3>
    </div>
    <p></p>
  </body>
</html>

抽象出来的 DOM 树,如下图

dom-16493959945942

注:

  • DOM 的最小组成单位叫做节点(node)
  • 根据 W3C 的 HTML DOM 标准,HTML 文档中的所有内容都是节点。
  • DOM 树就是由各种不同类型的节点组成。

接下来,我们来学习下,有哪些不同的节点类型

# 2、节点(node)

DOM 中的节点的类型有如下七种:

节点分类 描述
Document 文档节点 整个 DOM 树的顶层节点
DocumentType 文档类型节点 如 doctype 标签(<!DOCTYPE html>)
Element 元素节点 网页的各种 HTML 标,如:<p><div>
Attr 属性节点 元素的各种属性,如:title='标题'class='box'
Text 文本节点 标签之间或标签包含的文本
Comment 注释节点 网页中的注释
DocumentFragment 文档片段 文档片段,不存于 DOM 树上,是一种游离态,通常作为仓库来使用

注:

在实际开发中,用到最多的是

  • 文档节点
  • 元素节点
  • 属性节点
  • 文本节点
  • 文档片段
  • 元素、属性、文本节点

image-20221004205418474

# 3、document 文档对象

TIP

  • document 文档对象是HTMLDocument的实例,表示整个 HTML 页面(HTMLDocument 继承 Document)
  • document 是 window 对象的属性,因此是一个全局对象
  • 控制台 console 可以输入下面两行代码,就可以获得当前网页的文档对象
window.document; // 获取 文档节点对象
document; // 获取 文档节点对象
// 注意区分大小写, document 与 Document是两个不同的东西
// HTMLDocument 继承 Document

注:

  • document 对象是 DOM 中最重要的东西,几乎所有DOM 的功能都封装在了 document 对象中
  • 我们可以通过 document 对象,来访问元素节点。

# 4、访问元素节点的常用方法

TIP

  • 所谓 "访问" 元素节点,就是指 "得到"、"获取" 页面上的元素节点
  • 对节点进行操作,第一步就是要得到它
方法 功能
getElementById() 通过元素 id 名获取到元素
getElementsByTagName() 通过标签名获取元素,返回的是一个数组
getElementsByClassName() 通过 class 名获取元素,返回的是一个数组
querySelector() 通过选择器得到元素,只能得到第一个被找到的元素
querySelectorAll() 通过选择器得到元素,返回的是一个数组

# 5、getElementById()

TIP

  • document.getElementById() 的功能是,通过元素的id名来得到元素节点
  • 不管元素藏的位置有多深,都能通过 id 把它找到
<div id="box">我是一个盒子</div>
<p id="title">我是一个段落</p>

<script>
  var box = document.getElementById("box");
  var title = document.getElementById("title");
  console.log(box); // <div id="box">我是一个盒子</div>
  console.log(title); // <p id="title">我是一个段落</p>
  console.log(typeof box); // object
</script>

注意事项:

如果页面上有相同 id 的元素,则只能得到第一个 ,id 是唯一的。

<div id="box">我是1</div>
<div id="box">我是2</div>
<script>
  var box = document.getElementById("box");
  console.log(box); // <div id="box">我是1</div>
</script>

# 6、getElementsByTagName()

TIP

  • getElementsByTagName()方法的功能是通过标签名得到节点元素组成的数组
  • 所以我们可以通过遍历数组,批量操控每一元素节点
<div id="box1">
  <p>我是p段落标签</p>
</div>
<div id="box2">
  <p>我是p段落标签</p>
  <p>我是p段落标签</p>
  <h3>我是h3标签</h3>
</div>
<script>
  var pList = document.getElementsByTagName("p");
  console.log(pList); // HTMLCollection(3) [p, p, p]
</script>

HTMLCollection 对象,是一个类数组对象,他没有数组身上的方法。

  • 即使页面上只有一个指定标签名的节点,也将得到长度为 1 的数组
  • 如果没有找找到指定标签名的节点,则返回一个空数组
<div id="box1">
  <p>我是p段落标签</p>
</div>
<script>
  var pList = document.getElementsByTagName("p");
  var h3 = document.getElementsByTagName("h3");
  console.log(pList); // HTMLCollection [p]
  console.log(h3); // HTMLCollection []
</script>
  • 任何一个节点元素也可以调用 getElementsByTagName()方法,从而得到其内部的某种类的元素节点
<div id="box1">
  <p>我是段落</p>
</div>

<div id="box2">
  <p>我是段落</p>
  <p>我是段落</p>
</div>

<script>
  // 先得到box1
  var box1 = document.getElementById("box1");
  // 再得到box1中p标签的数组
  var ps_inbox1 = box1.getElementsByTagName("p");
  console.log(ps_inbox1); // HTMLCollection [p]
</script>

# 7、getElementsByClassName()

TIP

  • getElementsByClassName()方法的功能是通过class 类名得到节点数组
  • 如果只能获取一个元素,返回长度为 1 的数组,如果没有找到,则返回空数组
  • document 和节点元素都可以调用 getElementsByClassName()方法,从而得到其内部的某类名的元素节点
<div class="box box1">box1</div>
<div class="box box2">box2</div>
<div id="content">
  <div class="box box3">box3</div>
</div>

<script>
  // 获取所有class名中包含 box的元素
  var box = document.getElementsByClassName("box");
  console.log(box);
  // 获取id content的元素
  var content = document.getElementById("content");
  // 获取content中class名为box的元素
  var conBox = content.getElementsByClassName("box");
  console.log(conBox);
</script>

image-20221004214957069

# 8、querySelector()

TIP

通过CSS 选择器得到页面当中的元素,不过只能得到第一个被找到的元素

<div class="box">
  <p>我是p1</p>
  <p>我是p2</p>
</div>
<script>
  var p = document.querySelector(".box p");
  console.log(p); // <p>我是p1</p>
</script>

# 9、querySelectorAll()

TIP

  • 通过CSS 选择器得到页面当中的元素,返回被找到元素组成的数组
  • 如果只有一个符合要求的,也将得到长度为 1 的数组
  • 如果没有符合要求的,则返回一个空数组
<div class="box">
  <div class="title">
    <p>我是p</p>
    <p>我是p</p>
  </div>
</div>

<div class="title">
  <p>我是p</p>
  <p>我是p</p>
</div>
<script>
  var pList = document.querySelectorAll(".title p");
  console.log(pList); // NodeList(4) [p, p, p, p]
</script>

# 10、querySelectorAll 、getElementsByClassName()、getElementsByTagName() 的区别

TIP

  • getElementsByClassName()getElementsByTagName() 方法是可以动态获取元素,也就是当页面上增加或删除元素时,获取的元素个数可以改变
  • querySelectorAll() 是做不到的
<ul class="list">
  <li class="item">oldli</li>
  <li class="item">oldli</li>
</ul>
<script>
  // 获取ul元素
  var listEle = document.getElementsByClassName("list")[0];
  // 通过querySelectorAll方法获取元素
  var lis = document.querySelectorAll(".list li");
  // 没有利用循环生成li之前打印lis
  console.log(lis); //2

  // 通过循环,生成5个li标签,类名为liEle,内容为new li,生成之后,放在ul里面
  for (var i = 0; i < 3; i++) {
    var newli = document.createElement("li");
    newli.className = "item";
    newli.innerHTML = "new li";
    listEle.appendChild(newli);
  }
  // 利用循环生成li之后打印lis
  console.log(lis); //2
</script>

image-20221004221229416

<ul class="list">
  <li class="item">oldli</li>
  <li class="item">oldli</li>
</ul>
<script>
  // 获取ul元素
  var listEle = document.getElementsByClassName("list")[0];
  // 通过querySelectorAll方法获取元素
  // var lis = document.getElementsByTagName("li");
  var lis = listEle.getElementsByClassName("item");
  // 没有利用循环生成li之前打印lis
  console.log(lis); // 打印出2个元素

  // 通过循环,生成5个li标签,类名为liEle,内容为new li,生成之后,放在ul里面
  for (var i = 0; i < 3; i++) {
    var newli = document.createElement("li");
    newli.className = "item";
    newli.innerHTML = "new li";
    listEle.appendChild(newli);
  }
  // 利用循环生成li之后打印lis
  console.log(lis); // 打印出5个元素
</script>

image-20221004221251693

# 11、获取 body 与 HTML 元素

属性 说明
document.body 获取 body 元素
document.documentElement 获取 html 元素

image-20221004221601187

# 12、获取页面中所有元素

TIP

  • document.all 获取页面当中所有元素
  • document.getElementsByTagName(“*”) 获取页面当中所有元素

image-20221004221745770

以上方式几乎不用,只是当做了解即可

# 13、onload 方法

TIP

  • 浏览器在加载一个页面时,是按照自下而是上的顺序加载的。
  • 如果 JS 写在了 body 前面,那 JS 在获取页面元素时,页面上的元素标签还没有被加载出来,就会造成读取不到内容。

通常 JS 代码一定要写到</body>节点的前面,否则 JS 无法找到相应 HTML 节点

<script>
  var box1 = document.getElementById("box1");
  console.log(box1); // null  没有获取到元素
</script>
<div id="box1">box</div>
  • 如果 JS 代码写在 body 前面,也能正常执行,可以使用window.onload = function(){}事件,使页面加载完毕后,再执行指定的代码
<script>
  // 给window对象添加onload事件监听,onload表示页面都加载完毕了
  window.onload = function () {
    var box1 = document.getElementById("box1");
    console.log(box1);
  };
</script>
<body>
  <div id="box1">box</div>
</body>

# 三、操作元素属性

TIP

上面我们学习了如何获取一个元素,接下来我们来学习如何操作元素的属性,我们会从以下四个方面来展开讲解

# 1、符合标准的 w3c 属性

TIP

常见的符合标准的 w3c 属性有

  • id、alt、title
  • class、style 更改元素样式属性 比较特殊,我们放在后面单独来讲
  • src 、 href 修改图片地址和 a 标签链接地址
  • type、value、checked、selected、disabled 表单元素属性

符合标准的 w3c 属性,我们可以直接用 对象.属性名 的方式来访问

# 1.1、id、alt 、title

TIP

  • id 这个属性在实际中,我们肯定不会去更改他的值
  • alt 图片描述属性
  • title 提示属性,主要用在 a 标签上
<img src="./images/logo2.png" id="img" />
<a href="http://www.baidu.com" id="link">百度</a>

<script>
  var img = document.getElementById("img");
  var a = document.getElementById("link");

  img.id = "img1";
  img.alt = "美女图片";
  a.title = "去百度走一趟";
</script>

image-20221004233013398

# 1.2、src 和 href

TIP

  • src 属性,用来修改图片的地址
  • href 属性,用来修改超链接的地址
<img src="images/img1.png" alt="" id="img" />
<a href="http://www.baidu.com" title="百度" id="link">百度</a>

<script>
  var img = document.getElementById("img");
  var a = document.getElementById("link");

  img.src = "images/logo.png";
  a.href = "http://www.icodingedu.com";
  a.innerText = "艾编程";
  a.title = "艾编程";
</script>

image-20221004233401358

# 1.3、表单属性

TIP

  • type 表单类型
  • value 表单值
  • checked 单选和复选框选中状态
  • selected 下拉列表元素选中状态
  • disabled 元素是否被禁用
用户名:<input type="text" name="" id="userName" value="" /><br />
密码:<input type="text" name="" id="iphone" value="" />
<h3>姓别</h3>
<input type="radio" name="sex" id="" value="" /><input type="radio" name="sex" id="" value="" /><h3>喜欢的水果</h3>
<input type="checkbox" name="fruit" id="" /> 苹果
<input type="checkbox" name="fruit" id="" />梨子
<input type="checkbox" name="fruit" id="" />葡萄
<h3>选择所在城市</h3>
<select name="" id="city">
  <option value="湖南">湖南</option>
  <option value="深圳">深圳</option>
  <option value="上海">上海</option>
</select>
<h3></h3>
<input type="submit" id="submit" value="提交" />

<script>
  // 获取用户名和电话号码输入框
  var userName = document.getElementById("userName");
  var iphone = document.getElementById("iphone");
  // 写入用户名和密码,将电话号码隐藏
  userName.value = "清心";
  iphone.value = "1223333";
  iphone.type = "password";

  // 获取单选框
  var sex = document.getElementsByName("sex");
  // 选中第一个男
  sex[0].checked = true;
  // sex[1].checked = "checked";

  // 获取复选框
  var fruit = document.getElementsByName("fruit");
  for (var i = 0; i < fruit.length; i++) {
    fruit[i].checked = true;
  }

  // 获取下拉列表
  var city = document.getElementById("city");
  var ops = city.getElementsByTagName("option");
  ops[2].selected = true;

  // 提交按扭
  var submit = document.getElementById("submit");
  submit.disabled = true; // 禁用
  // submit.disabled = "disabled";
</script>

image-20221005000330123

# 2、自定义属性

TIP

  • 自定义属性:由我们自己定义在元素身上的属性
  • 自定义属性的目的:用来保存元素标签后期要用到的一些数据内容,一些简单数据存在自定义属性中,后期操作方便。
  • 修改自定义属性,可以通过下面方法来操作
方法 说明
setAttribute(key,value) 添加或修改属性值,key 表示属性名,value 表示属性值
getAttribute(key) 获取属性,key 表示要获取的属性名
<div id="box" abs="" title="我是提示"></div>
<script>
  var box = document.getElementById("box");
  console.log(box.abs); // undefined
  console.log(box.getAttribute("abs")); // 值
  console.log(box.getAttribute("title")); // 我是提示
  box.setAttribute("mycustom", "自定义属性值");
</script>

setAttribute(key,value)getAttribute(key)也是可以操作标准属性

# 3、Html5 中自定义属性规范

TIP

  • Html5 中规定自定义属性名以data-开头
  • 使用data-前缀自定义属性,可以解决属性混乱无管理的现状,区分自定义属性与标准属性

# 3.1、设置自定义属性的 2 种方式

  • 方式一: 可以直接在 HTML 标签上面书写
<h2 data-weather="sunny">今天是晴天</h2>
<!--
	data-weather 自定义属性名
	sunny 自定义属性值
-->

如果设置的自定义属性是多个单词组合的形式,需要用中横线-连接

<h2 data-birth-date="20230501">今天是我的生日</h2>
  • 方式二: 通过 JS 的dataset属性来设置
<h2>今天是我的生日</h2>
<script>
  var h2 = document.querySelector("h2");
  h2.dataset.birthDate = "20230501";
</script>

image-20221005002528628

# 3.2、获取自定义属性

TIP

  • 直接通过对象.dataset.属性名 (属性名书写格式:属性去掉 data-之后的单词,以驼峰命名)
  • 如 属性名为data-ab-cd-fg 则访问方式:对象.dataset.abCdFg
<img src="images/dog.png" data-animal-type="animal" />
<script>
  var img = document.getElementsByTagName("img");
  var animalType = img[0].dataset.animalType;
  console.log(animalType);
</script>

重要提示:

自定义属性也是可以通过setAttributgetAttribut方式操作的

# 四、操作元素样式

TIP

  • 操作元素样式的属性有stylecssTextclassName
  • 同时 HTML5 提供了classList对象,classList对象身上的方法,用来操作元素的 class 属性,简直完美。

# 1、style 属性

TIP

  • style 属性用来操作元素的行内样式,他只对行内样式有效
  • 获取行内样式的写法:
对象.style.属性名; // 属性名要采用驼峰形式书写
  • 修改或添加行内样式的写法
对象.style.属性名 = 属性值; // 属性名要采用驼峰形式书写

代码演示

<style>
    .box {
        width: 100px;
        height: 200px;
        background-color: hotpink;
        color: red;
    }
</style>

<div class="box"></div>
<script>
    var box = document.getElementsByClassName("box");
    box[0].style.width = "200px";
    box[0].style.backgroundColor = "red";
    box[0].style.borderTop = "15px solid blue";

    // 获取样式
    console.log(box[0].style.width); // 200px
    console.log(box[0].style.color); // 啥也没有

image-20221005011215905

注意事项:

  • 操作元素的 style 上的样式,这里的样多权重最高,可以覆盖外部和内嵌样式
  • 如果样式写在了 class 类中,没写在 style 属性中,则通过 对象.stylel.属性名 的方式,是获取不到的
  • 如果元素要改变的样式量特别多,这种方式就比较麻烦。

# 2、cssText 属性

TIP

  • cssText 的本质就是设置 HTML 元素的 style 属性值
  • 所以每设置一次 cssText 值,就会把之前的 style 属性中的样式全覆盖掉
<div
  class="box"
  style="width: 100px; height: 200px; background-color: red"
></div>
<script>
  var box = document.getElementsByClassName("box")[0];
  box.style.cssText = "color:blue"; // 覆盖了之前的style样式中的值
</script>

image-20221005012223993

cssText 主要用来合并多次对 CSS 样式的修改,改为一次性处理

# 3、className 属性

TIP

  • 我们在操作元素的 class 样式属性时,不能用 对象.属性 的方法操作,因为 class 是关键字
  • 我们可以通过 对象.className 的方式来操作
  • 注意事项: className 修改样式,如果是多个样式,样式之间要用空隔隔开
<style>
  .box {
    width: 100px;
    height: 100px;
    background-color: skyblue;
  }
  .box1 {
    border: 5px dashed tomato;
  }
</style>
<body>
  <div class="box"></div>
  <script>
    var box = document.getElementsByClassName("box");
    box[0].className = "box box1"; // box与box1之间要有空格
  </script>
</body>

image-20221004234312546

注:

  • className 操作 CSS 样式有一个很大的缺点,就是我每次更新 class 类名时,都要把所有的类名带上。
  • 如果我只想在元素原有的基础上继续添加新的类名、或删除某个类名,能不能不动原有的类名,就能实现。答案肯定是可以的。

# 4、操作 CSS 样式的优先方案

TIP

style、cssText、className 三种方式操作 CSS 样式时,性能消耗底到高的排序分别如下:

image-20221018150747724

接下来我们用这三种不同的方式来操作同一个 DOM 的 CSS 样式,来对比他们所消耗的时间

<style>
  .box {
    width: 100px;
    height: 100px;
    color: red;
    font-size: 20px;
  }
</style>
<div id="box">box</div>

<script>
  var box = document.getElementById("box");

  // style方式操作样式
  console.time("style");
  for (var i = 0; i < 10000; i++) {
    box.style.width = "100px";
    box.style.height = "100px";
    box.style.color = "red";
    box.style.fontSize = "20px";
  }
  console.timeEnd("style");

  // cssText方式操作样式
  console.time("cssText");
  for (var i = 0; i < 10000; i++) {
    box.style.cssText = "width:100px;height:100px;color:red;font-size:20px";
  }
  console.timeEnd("cssText");

  // className方式操作样式
  console.time("className");
  for (var i = 0; i < 10000; i++) {
    box.className = "box";
  }
  console.timeEnd("className");
</script>

image-20221018145523909

为什么会出现这么大的差异,本质是什么 ?

  • 通过 style 属性来操作 CSS 样式,会频繁的触发页面的重排重绘DOM 重新渲染
  • 通过 style 身上的 cssText 属性来操作 CSS 样式,是把多次对 DOM 的操作合并为一次性处理,减少了触发重排重绘DOM 的重新渲染)次数
  • 通过 className 属性,本质也是一样的,减少了对 DOM 的操作,多次操作合并为一次性处理,同时 className 中的样式,一开始就准备好了。

# 5、classList 对象

TIP

html5 为每一个元素新增了一个classList对象,classList对象保存着控制当前元素类名的各个方法和属性。

classList 对象身上相关的属性和方法如下表:

属性或方法 说明
length 返回类名的个数
add() 在原有的类名基础上添加一个类名,如果这些类已经存在于元素的属性中,那么它们将被忽略。
remove() 在原有的类名基础上 移出某一个类名,使删除不存在的类值也不会导致抛出异常
toggle() 如果有这个类名 则删除这个类名,返回 false,如果没有 则添加减去,返回 true
item() 根据索引 获取类名
contains() 判断元素是否包含某一个类名
replace( oldClass, newClass ) 用一个新类值替换已有的类值,替换成功返回 true,替换失败,返回 false
<div class="box box1 box2">盒子</div>
<script>
  var box = document.getElementsByClassName("box")[0];
  console.log(box.classList.length); // 3 类名个数
  box.classList.add("box3"); // 追加一个类名box3
  box.classList.remove("box2"); // 移除一个类名box2
  box.classList.toggle("box1"); // 有就移除box1
  box.classList.toggle("box1"); // 没有就添加box1
  console.log(box.classList.item(0)); // 索引为0的类名 box
  console.log(box.classList.contains("box")); // true
  console.log(box.classList.replace("box", "mybox")); // 用mybox 替换box
</script>

注:

  • 假设现在浏览器版本过低,不支持 classList 对象,那就需要我们手写相关方法来实现对 class 属性的操作。
  • 这里我们尝试手写:addremovetoggle三个方法。
  • 我们期望 html 元素可以直接调用这些方法,实现对 class 类名的操作。
  • 那就需要确认,我们手写的这些方法要加在那个构造函数(类)的原型上。因此我们了解 DOM 中各个类的关系。

# 6、DOM 中各类的继承关系图

image-20221101152645480

在控制台查看对应的继承关系

分别在控制台输入下代码,然一层一层点看,查看类的继承关系

document.__proto__;

var div = document.createElement("div");
div.__proto__;

var attr = document.createAttribute("id");
attr.__proto__;

var text = document.createTextNode("文本");
text.__proto__;

var frag = document.createDocumentFragment();
frag.__proto__;

var comment = document.createComment("我是一段注释");
comment.__proto__;

document.childNodes[0].__proto__;

image-20221101151654017

结论:

  • 我们希望 HTML 元素可以直接打点调用 addClass 等方法,实现对 class 属性的操作
  • 所以这些方法要写在 HTMLElement 的原型上。

# 7、手写 addClass 方法

TIP

该方法实现对元素添加对应的 class 类名,如果元素上没有对应 class 类名添加,有的话就不加

<div class="box box1 box2 box2"></div>

<script>
  /**
   * addClass 方法实现对元素添加对应的class类名,如果元素上没有对应class类名就添加,有就不加
   * @param name  class类名字符串
   */
  HTMLElement.prototype.addClass = function (name) {
    // 获取当前对象上的class类名
    var className = this.className;
    // 以空格分隔成一个数组
    var classArr = className.split(" ");
    // 判断传过来的类名在不在当前数组中,如果不存在,就添加
    if (!classArr.includes(name)) {
      classArr.push(name); // 将类名添加到数组中
    }
    var newClassName = classArr.join(" "); // 将数组元素以空格拼接成字符串
    this.className = newClassName;
  };
  var box = document.querySelector(".box");
  box.addClass("box4");
</script>

# 8、手写 removeClass 方法

TIP

该方法实现移除元素上对应的 Class 类名,如果有就移除,如果没有不做处理

<div class="box box1 box2 box2"></div>

<script>
  /**
   * removeClass 该方法实现移除元素上对应的Class类名,如果有就移除,如果没有不做处理
   * @param name  class类名字符串
   */
  HTMLElement.prototype.removeClass = function (name) {
    // 获取当前对象上的class类名
    var className = this.className;
    // 以空格分隔成一个数组
    var classArr = className.split(" ");
    // 判断传过来的类名在不在当前数组中,如果存在,找到对应下标然后删掉
    // 要考虑傻逼模式,就是他本来就出现了两个相同的类名
    var index = classArr.indexOf(name);
    while (index !== -1) {
      classArr.splice(index, 1);
      index = classArr.indexOf(name, index);
    }
    var newClassName = classArr.join(" "); // 将数组元素以空格拼接成字符串
    this.className = newClassName;
  };

  var box = document.querySelector(".box");
  box.removeClass("box2"); // 移除
  box.removeClass("box1"); // 移除
</script>

# 9、手写 toggleClass 方法

TIP

该方法实现自动判断是给元素添加还是删除对应的 Class 类名,如果元素存在对应 Class 类名就删除,否则就添加

<div class="box box1 box2 box2"></div>
<script>
  /**
   * toggleClass 该该方法实现自动判断是给元素添加还是删除对应的Class类名,如果元素存在对应Class类名就删除,否则就添加
   * @param name  class类名字符串
   */
  HTMLElement.prototype.toggleClass = function (name) {
    // 获取当前对象上的class类名
    var className = this.className;
    // 以空格分隔成一个数组
    var classArr = className.split(" ");
    // 判断传过来的类名在不在当前数组中,如果不在,就添加,存在,找到对应下标然后删掉
    var index = classArr.indexOf(name);
    if (index === -1) {
      // 没有就添加
      classArr.push(name);
    } else {
      // 存在,就删除
      // 要考虑傻逼模式,就是他本来就出现了两个相同的类名
      var _index = index;
      while (_index !== -1) {
        classArr.splice(_index, 1);
        _index = classArr.indexOf(name, _index);
      }
    }
    var newClassName = classArr.join(" "); // 将数组元素以空格拼接成字符串
    this.className = newClassName;

    // 处理返回值
    if (index === -1) return true;
    return false;
  };

  var box = document.querySelector(".box");
  box.toggleClass("box2");
  box.toggleClass("box1");
  box.toggleClass("box3");
</script>

注:

以上所有方法,都没有办法获取 class 类名或 id 中定义的 css 样式。接下来我们来学习一个方法,用来获取 class 类名中的 css 样式

# 10、getComputedStyle 方法

TIP

getComputedStyle() 方法,获取元素的计算样式,但不能修改样式。

语法

var style = window.getComputedStyle(element, [pseudoElt]);
  • element 用于获取计算样式的元素
  • pseudoElt 指定一个要匹配的伪元素的字符串。必须对普通元素省略(或null
  • 返回的style是一个实时的 CSSStyleDeclaration (css 样式声明)对象(它是一个 CSS 声明块,CSS 属性键值对的集合),当元素的样式更改时,它会自动更新本身。
<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: red;
  }
  .box1 {
    font-size: 20px;
    color: yellow;
    line-height: 100px;
  }
</style>

<div class="box box1" id="mybox">我是css盒子</div>
<script>
  var mybox = document.getElementById("mybox");
  var style = getComputedStyle(mybox, null);
  console.log(style);
  for (var i = 0; i < style.length; i++) {
    key = style[i];
    console.log(cs + "=" + style.getPropertyValue(cs));
  }
</script>

# 11、访问 CSS 属性值 3 种方式

// 访问方式一
// propName 属性名,正常书写
window.getComputedStyle(element, [pseudoElt]).getgetPropertyValue(propName);

// 访问方式二
// propName 属性名 要采用驼峰命名方式
window.getComputedStyle(element, [pseudoElt]).propName;

// 访问方式三
// propName 属性名,正常书写
window.getComputedStyle(element, [pseudoElt])[propName];
<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: red;
  }
  .box1 {
    font-size: 20px;
    color: yellow;
    line-height: 100px;
  }
</style>
<div class="box box1" id="mybox">我是css盒子</div>

<script>
  var mybox = document.getElementById("mybox");
  var style = getComputedStyle(mybox, null);
  console.log(style.height);
  console.log(style.backgroundColor);
  console.log(style.lineHeight);

  console.log(style.getPropertyValue("height"));
  console.log(style["background-color"]);
  console.log(style.getPropertyValue("line-height"));
</script>

image-20221005015822903

# 12、获取伪元素样式

<style>
  #mybox {
    width: 200px;
    height: 200px;
    background-color: red;
  }
  #mybox::after {
    content: "我是伪元素内容";
    color: yellow;
  }
</style>

<div id="mybox"></div>
<script>
  var mybox = document.getElementById("mybox");
  var style = getComputedStyle(mybox, "::after");
  console.log(style.color);
  console.log(style.content);
</script>

image-20221025142409799

# 五、获取元素尺寸

TIP

接下来我们将学习一组属性,这些属性可以获取页面中元素当前的实际尺寸大小,其中包括

  • 偏移尺寸
  • 客户端尺寸
  • 滚动尺寸
  • 确定元素尺寸

# 1、偏移尺寸

TIP

以下 5 个属性,都与元素的偏移尺寸有关 ,并且都是只读的

  • offsetWidth
  • offsetHeight
  • offsetParent
  • offsetLeft
  • offsetTop

接一来,我们就一起来学习吧!

# 1.1、offsetWidth 与 offsetHeight

以下属性为只读的,每次访问都会重新计算

属性 说明
offsetWidth 返回一个元素的布局宽度
标准盒模型下,包括:width、border、padding、滚动条宽
怪异盒模型下为:width
offsetHeight 返回一个元素的布局宽度
标准盒模型下,包括:height、border、padding、滚动条宽
怪异盒模型下为:height
<style>
  * {
    margin: 0;
    padding: 0;
  }
  .box {
    width: 200px;
    height: 200px;
    padding: 30px;
    border: 10px solid red;
    margin: 20px;
  }
  .box1 {
    width: 100px;
    height: 100px;
    padding: 30px;
    border: 10px solid blue;
    margin: 20px;
    /* box-sizing: border-box; */
  }
</style>

<div class="box">
  <div class="box1"></div>
</div>

<script>
  var box1 = document.querySelector(".box1");
  var _width = box1.offsetWidth;
  var _height = box1.offsetHeight;
  console.log(_width); // width+padding+border=100+60+20=180
  console.log(_height); //  height+padding+border=100+60+20=180

  // 如果box1更改为border-box,则最后输出结果为100 100
</script>

# 1.2、offsetParent

TIP

  • 此属 性为只读属性,每次访问都会重新计算
  • 返回离当前元素最近的定位祖先元素或最近的 table,td,th,tody 元素
  • 在 Webkit 中,如果当前元素为隐藏的(该元素或其祖先元素的 style.display 为 "none"),或者该元素的 style.position 被设为 "fixed",则该属性返回 null
<style>
  .box {
    position: absolute;
  }
  .box1 {
    position: relative;
  }
</style>

<div class="box">
  <!--绝对定位-->
  <div class="box1">
    <!--相对定位-->
    <div class="box2">
      <!--未定位-->
      <div class="box3"></div>
    </div>
  </div>
</div>

<script>
  // 获取box3元素
  var box3 = document.querySelector(".box3");
  // 获取离box3最近的定位祖先元素
  var parent = box3.offsetParent;
  console.log(parent); // div.box1
</script>

image-20221006155613890

<style>
  .box {
    position: absolute;
  }
  .box1 {
    /* position: relative;*/
  }
</style>

<div class="box">
  <!--绝对定位-->
  <div class="box1">
    <!--相对定位-->
    <div class="box2">
      <!--未定位-->
      <div class="box3"></div>
    </div>
  </div>
</div>

<script>
  // 获取box3元素
  var box3 = document.querySelector(".box3");
  // 获取离box3最近的定位祖先元素
  var parent = box3.offsetParent;
  console.log(parent); // div.box
</script>

image-20221006155724388

  • 在 Webkit 中,如果当前元素为隐藏的(该元素或其祖先元素的 style.display 为 "none"),或者该元素的 style.position 被设为 "fixed",则该属性返回 null
<style>
  .box {
    position: absolute;
  }
  .box1 {
    /* display: none; */
    position: relative;
  }
  .box3 {
    /* position: fixed; */
    display: none;
  }
</style>
<div class="box">
  <!--绝对定位-->
  <div class="box1">
    <!--相对定位-->
    <div class="box2">
      <!--未定位-->
      <div class="box3"></div>
    </div>
  </div>
</div>
<script>
  // 获取box3元素
  var box3 = document.querySelector(".box3");
  // 获取离box3最近的定位祖先元素
  var parent = box3.offsetParent;
  console.log(parent); // null
</script>

# 1.3、offsetLeft 和 offsetTop

以下两属性为只读属性,每次访问都会重新计算

属性 说明
offsetLeft 它返回当前元素(左边框)相对于其 offsetParent元素的左边框内壁的距离
offsetTop 它返回当前元素(上边框)相对于其 offsetParent元素的上边框内壁的距离
<style>
  * {
    margin: 0;
    padding: 0;
  }
  body {
    padding: 100px;
  }
  .box1 {
    margin: 50px;
    padding: 20px;
    border: 5px solid blue;
    width: 200px;
    height: 200px;
    position: relative;
  }
  .box2 {
    border: 2px solid red;
    height: 150px;
  }
  .box3 {
    width: 100px;
    height: 100px;
    position: absolute;
    border: 10px solid skyblue;
    margin-top: 50px;
    margin-left: 30px;
    left: 20px;
    top: 50px;
  }
</style>

<div class="box1">
  <div class="box2">
    <div class="box3"></div>
  </div>
</div>

<script>
  // 获取box3元素
  var box3 = document.querySelector(".box3");
  // 与离他最近的定位祖先元素左内边距的距离
  var _left = box3.offsetLeft; // left + margin - left = 20 + 30 = 50
  // 与离他最近的定位祖先元素上内边距的距离
  var _top = box3.offsetTop; //  top + margin - top = 50 + 50 = 100
  console.log(_left, _top);
</script>

image-20221006162439940

# 1.4、计算元素与页面偏移量

TIP

  • 如果要计算一个元素与页面的左偏移量
  • 则需要把当前元素的的offsetLeftoffsetTop分别与他的offsetParent 的offsetLeftoffsetTop相加,同时还要分别加上他们父元素border-left-widthborder-top-width
  • 再分别加上offsetParent 元素offsetParent元素的offsetLeftoffsetTop,还有父元素的border-left-widthborder-top-width,一层层相加,一直加到根元素

获取与页面左偏移量

  • while 循环版
function getElementLeft(el) {
  // 获取当前元素左偏移量
  var left = el.offsetLeft;
  // 获了当前元素的offsetParent
  var parent = el.offsetParent;
  // 如果 offsetParent 存在,则一直获取,计算他的offsetLeft值,如果不存在,则终止
  while (parent) {
    left += parent.offsetLeft; // 与每一轮元素的父元素与其定位父元素左边距离累加
    // 计算父元素左边框大小
    style = getComputedStyle(parent, null);
    border = parseInt(style.borderLeftWidth); // 过滤单位部分,只取数字部分
    // 把左边框累加进去
    left += border;
    parent = parent.offsetParent;
  }
  // 最终返回获取的left值
  return left;
}
  • 递归版
function getElementLeft(el) {
  // 获取当前元素左边距
  var left = el.offsetLeft;
  // 获了当前元素的offsetParent
  var parent = el.offsetParent;
  // 如果 offsetParent 存在,则一直获取,计算他的offsetLeft值,如果不存在,则终止
  if (parent) {
    // 计算父元素左边框大小
    style = getComputedStyle(parent, null);
    border = parseInt(style.borderLeftWidth); // 过滤单位部分,只取数字部分
    // 把左边框累加进去
    left += border;
    left += getElementLeft(parent);
  }
  return left;
}

获取与页面上偏移量

  • while 循环版
function getElementTop(el) {
  // 获取当前元素左偏移量
  var top = el.offsetTop;
  // 获了当前元素的offsetParent
  var parent = el.offsetParent;
  // 如果 offsetParent 存在,则一直获取,计算他的offsetLeft值,如果不存在,则终止
  while (parent) {
    top += parent.offsetTop;
    // 计算父元素左边框大小
    style = getComputedStyle(parent, null);
    border = parseInt(style.borderTopWidth); // 过滤单位部分,只取数字部分
    // 把左边框累加进去
    top += border;
    parent = parent.offsetParent;
  }
  // 最近返回获取的left值
  return top;
}
  • 递归版
function getElementTop(el) {
  // 获取当前元素上偏移量
  var top = el.offsetTop;
  // 获了当前元素的offsetParent
  var parent = el.offsetParent;
  // 如果 offsetParent 存在,则一直获取,计算他的offsetTop值,如果不存在,则终止
  if (parent) {
    // 计算父元素左边框大小
    style = getComputedStyle(parent, null);
    border = parseInt(style.borderTopWidth); // 过滤单位部分,只取数字部分
    // 把左边框累加进去
    top += border;
    top += getElementTop(parent);
  }
  return top;
}

# 1.5、总结

以下 5 个属性都与元素的偏移尺寸相关

属性 说明
offsetWidth 返回一个元素的布局宽度
标准盒模型下,包括:width、border、padding、滚动条宽
怪异盒模型下为:width
offsetHeight 返回一个元素的布局宽度
标准盒模型下,包括:height、border、padding、滚动条宽
怪异盒模型下为:height
offsetParent 返回离当前元素最近的定位祖先元素或最近的 table,td,th,tody 元素。
当前元素 display: none;时,offsetParent 返回 null
offsetLeft 它返回当前元素(左边框)相对于其 offsetParent元素的左内边距的距离
offsetTop 它返回当前元素(上边框)相对于其 offsetParent元素的顶部内边距的距离

图形结构

image-20221006174614064

重点强调

以上偏移尺寸属性都是只读的,每次访问都会重新计算。因此,应该尽量减少查询它们的次数。

我们可以把查询的值保存在变量中,供后面使用,这样就可以避免影响性能。

# 2、案例 1:求两元素中心点之间的距离

TIP

两个元素相对于同一个父元素定位,现在我们要求这两个元素中心点之间的距离。

我们通过下面这个图来分析,如何求两个元素中心点之间距离

image-20221013215324621

代码实现如下:

// 求两点之间的距离
// obj1与obj2分别表示上图中 box1与box2
function getDistance(obj1, obj2) {
  var a =
    obj1.offsetLeft +
    obj1.offsetWidth / 2 -
    (obj2.offsetLeft + obj2.offsetWidth / 2);
  var b =
    obj1.offsetTop +
    obj1.offsetHeight / 2 -
    (obj2.offsetTop + obj2.offsetHeight / 2);
  return Math.sqrt(a * a + b * b);
}

# 3、案例 2:找出与当前元素最近的一个元素

TIP

  • 我们需要找到所有元素与当前元素的距离,然后再从中找出距离最小的那个元素
  • 我们可以新建一个数组,用来保存每个元素每个元素与当前元素中心点的距离

数组结构如下

var elementArr = [
  {
    element: li1, // html元素
    distance: 30, // 与当前元素的最近距离
  },
  {
    element: li2,
    distance: 50,
  },
];
  • 最后找出数组中 distance 值最小的那一个对象中的 element 元素

代码实现如下:

<style>
  .container {
    width: 400px;
    height: 500px;
    background-color: skyblue;
    position: relative;
  }
  .box {
    width: 100px;
    height: 100px;
    background-color: khaki;
    margin: 5px;
    position: absolute;
  }
  .box1 {
    left: 0;
    top: 100px;
  }
  .box2 {
    left: 100px;
    top: 150px;
  }
  .box3 {
    left: 200px;
    top: 0px;
  }
  .box4 {
    left: 200px;
    top: 300px;
  }
</style>

<body>
  <div class="container">
    <div class="box box1">box1</div>
    <div class="box box2">box2</div>
    <div class="box box3">box3</div>
    <div class="box box4">box4</div>
  </div>

  <script>
    var conatiner = document.querySelector(".container");
    var box = document.querySelectorAll(".container .box");

    // 找出与obj元素距离最近的元素
    function findNearest(obj) {
      var elementArr = [];
      // 遍历每个元素,计算每个元素于obj的中心位置,然后保存到数组中
      for (var i = 0; i < box.length; i++) {
        // 当前被用来比较的obj,不用存到数组中,要排除
        if (box[i] !== obj) {
          // 计算两中心点距离
          var centerDistance = getDistance(box[i], obj);
          // 把这个元素和对应中心点距离保存到数组中
          var el = {};
          el.element = box[i];
          el.distance = centerDistance;
          elementArr.push(el);
        }
      }

      // for循环遍历elementArr数组,找出数组中距离最小的那个元素,然后返回
      var minElement = elementArr[0];
      for (var j = 0; j < elementArr.length; j++) {
        if (elementArr[j].distance < minElement.distance) {
          minElement = elementArr[j];
        }
      }
      return minElement.element; // 返回最小的距离的那个对象
    }

    console.log(findNearest(box[2]));

    // 计算两个元素中心点位置
    function getDistance(obj1, obj2) {
      var x =
        obj1.offsetLeft +
        obj1.offsetWidth / 2 -
        (obj2.offsetLeft + obj2.offsetWidth / 2);

      var y =
        obj1.offsetTop +
        obj1.offsetHeight / 2 -
        (obj2.offsetTop + obj2.offsetHeight / 2);

      return Math.sqrt(x * x + y * y);
    }
  </script>
</body>

# 4、客户端尺寸

以下两个属性为元素的客户端尺寸,属性为只读的,每次访问都会重新计算

属性 说明
clientWidth 表示元素的内容区宽,在标准盒模型下,包括 width + padding,不包括 border + margin + 滚动条
clientHeight 表示元素的内容区高,在标准盒模型下,包括 height + padding,不包括 border + margin + 滚动条
<style>
  * {
    margin: 0;
    padding: 0;
  }
  .box {
    width: 200px;
    height: 200px;
    padding: 30px;
    border: 10px solid red;
    margin: 20px;
  }
</style>

<div class="box">
  <div class="box1"></div>
</div>

<script>
  var box = document.querySelector(".box");
  var w = box.clientWidth; // width + padding = 200 + 60 = 260
  var h = box.clientHeight; // height + padding = 200 + 60 = 260
  console.log(w, h);
</script>

image-20221006173256016

# 5、滚动尺寸

TIP

滚动尺寸,提供了元素内容滚动相关的信息。有以下四个属性

  • scrollWidth
  • scrollHeight
  • scrollLeft
  • scrollTop

接下来,就让我们一起来学习吧!

# 5.1、scrollWidth 与 scrollHeight

属性 说明
scrollWidth 元素内容宽度的一种度量,包括由于 overflow 溢出而在屏幕上不可见的内容
如果没有水平滚动条,其它大小与 clientWidth 相同
scrollHeight 元素内容高度的度量,包括由于 overflow 溢出导致的视图中不可见内容。
如果没有垂直滚动条,其它大小与 clientHeight 相同
<style>
  .box {
    width: 200px;
    height: 100px;
    border: 2px solid red;
    padding: 50px;
    overflow: scroll;
  }
  .box1 {
    width: 800px;
    height: 800px;
    background-color: skyblue;
  }
</style>

<div class="box">
  <div class="box1">
    滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容
  </div>
</div>
<script>
  var box = document.querySelector(".box");
  var _w = box.scrollWidth;
  var _h = box.scrollHeight;
  console.log(_w, _h); // 900 900
</script>

image-20221006182524877

# 5.2、scrollLeft 和 scrollTop

属性 说明
scrollLeft 获取或设置一个元素的内容水平滚动的距离
如果元素没有产生水平方向滚动条,那 scrollLeft = 0;
设置scrollLeft的值小于 0,scrollLeft 被设为0
如果设置了超出这个容器可滚动的值,scrollLeft 会被设为最大值
scrollTop 获取或设置一个元素的内容垂直滚动的距离
如果元素没有产生垂直方向滚动条,那 scrollTop = 0;
设置scrollTop的值小于 0,scrollTop 被设为0
如果设置了超出这个容器可滚动的值,scrollTop 会被设为最大值
<style>
  .box {
    width: 200px;
    height: 100px;
    border: 2px solid red;
    padding: 50px;
    overflow: scroll;
    border: 50px solid khaki;
  }
  .box1 {
    width: 800px;
    height: 800px;
    background-color: skyblue;
  }
</style>

<div class="box">
  <div class="box1">
    滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容滚动的内容
  </div>
</div>

<script>
  var box = document.querySelector(".box");
  box.onscroll = function () {
    console.log(box.scrollTop); // 打印滚动条滚动的高度
    console.log(box.scrollLeft); // 打印滚动条滚动的宽度
  };

  // 滚动条,滚动到底部
  box.scrollTop = 718; // 800-100+17 要注意,不要多减了滚条条的高度
  // 滚动条,滚动到最右边
  box.scrollLeft = 618; // 800-200+17 要注意,不要多减了滚条条的宽度
</script>

image-20221006183703295

# 5.3、总结

以下属性

  • scrollWidthscrollHeight只读 属性
  • scrollLeftscrollTop可读可写 属性
属性 说明
scrollWidth 元素内容宽度的一种度量,包括由于 overflow 溢出而在屏幕上不可见的内容
如果没有水平滚动条,其它大小与 clientWidth 相同
scrollHeight 元素内容高度的度量,包括由于 overflow 溢出导致的视图中不可见内容。
如果没有垂直滚动条,其它大小与 clientHeight 相同
scrollLeft 获取或设置一个元素的内容水平滚动的距离
scrollTop 获取或设置一个元素的内容垂直滚动的距离
如果元素没有产生垂直方向滚动条,那 scrollTop = 0;
设置scrollTop的值小于 0,scrollTop 被设为0
如果设置了超出这个容器可滚动的值,scrollTop 会被设为最大值

# 6、确定元素尺寸

TIP

  • 浏览器在每个元素上都暴露了getBoundingClientRect()方法,返回一个 DOMRect 对象
  • 访对象提供了元素的大小及其相对于视口(可视区)的位置

相关属性如下:

属性 说明
left、x 元素左边框相对于可视区左边的距离
top、y 元素上边框框相对于可视区顶部的距离
right 元素右边框相对于可视区左边的距离
bottom 元素底边框相对于可视区顶部的距离
height 元素的高,包括 height + padding + border
width 元素的宽,包括 width + padding + border

image-20221006195302347

代码演示

<style>
  .box {
    width: 100px;
    height: 100px;
    border: 2px solid red;
    padding: 20px;
    border: 20px solid khaki;
  }
</style>

<div class="box"></div>

<script>
  var box = document.querySelector(".box");
  var domRect = box.getBoundingClientRect();
  console.log(domRect); // 打印 DOMRect对象
  // 遍历对象
  for (key in domRect) {
    if (typeof domRect[key] !== "function") {
      // 过滤掉方法,只留下属性
      console.log(key + ":" + domRect[key]);
    }
  }
</script>

image-20221006195832471

<style>
  .box {
    width: 100px;
    height: 100px;
    border: 2px solid red;
    padding: 20px;
    border: 20px solid khaki;
  }

  body {
    height: 3000px;
  }
</style>
<div class="box"></div>
<script>
  var box = document.querySelector(".box");
  // 滚动浏览器窗口
  window.onscroll = function () {
    var domRect = box.getBoundingClientRect();
    console.log(domRect);
  };
</script>

image-20221006200815944

# 7、案例 1:如何判断两个元素发生了碰撞(经典面试题)

TIP

如何判断两个元素是否发生碰撞

我们来看下这个图

image-20221011234853780

// 以下代码仅限两个元素是相对于同一个定位的父元素
// 如是不是相对于同一个父元素,就要计算他们都相对于浏览器的位置来计算
// 前面讲过如何获取一个元素相对于浏览器的位置,大家可以去自己调用下对应的方法
function isBump(obj1, obj2) {
  var L1 = obj1.offsetLeft;
  var R1 = L1 + obj1.offsetWidth;
  var T1 = obj1.offsetTop;
  var B1 = T1 + obj1.offsetHeight;
  var L2 = obj2.offsetLeft;
  var R2 = L2 + obj2.offsetWidth;
  var T2 = obj2.offsetTop;
  var B2 = T2 + obj2.offsetHeight;
  if (L2 > R1 || L1 > R2 || T2 > B1 || T1 > B2) {
    return false; // 未碰撞,返回false
  } else {
    return true; // 碰撞,返回true
  }
}

# 8、案例 2:判断元素是否在可视区(经典面试题)

TIP

判断一个元素是不是在可视区,只有元素全在可视区,才算是在可视区内

可以参考下图的分析

image-20221012010911657

// 判断元素是否是在可视区内,只有当元素全部在可视区内才算是在可视区内
function isElementInViewport(el) {
  var rect = el.getBoundingClientRect();
  var T1 = rect.top;
  var B1 = rect.bottom;
  var L1 = rect.left;
  var R1 = rect.right;
  return (
    T1 >= 0 &&
    B1 <= document.documentElement.clientHeight &&
    L1 >= 0 &&
    R1 <= document.documentElement.clientWidth
  );
}

// 测试,自行在页面加一个.box的div
var box = document.querySelector(".box");
window.onscroll = function () {
  console.log(isElementInViewport(box));
};

# 六、节点操作

TIP

接下来我们来学习与节点相关的操作,在开始学习 DOM 的时候,我们提到过 DOM 的节点类型有以下 7 种

节点分类 描述
Document 文档节点 整个 DOM 树的顶层节点
DocumentType 文档类型节点 如 doctype 标签(<!DOCTYPE html>)
Element 元素节点 网页的各种 HTML 标,如:<p><div>
Attr 属性节点 元素的各种属性,如:title='标题'class='box'
Text 文本节点 标签之间或标签包含的文本
Comment 注释节点 网页中的注释
DocumentFragment 文档片段 文档片段,不存于 DOM 树上,是一种游离态,通常作为仓库来使用

在实际开发中,用到最多的是

  • 文档节点
  • 元素节点
  • 属性节点
  • 文本节点
  • 文档片段

那我们如何检测节点的类型呢 ?其实每一个节点对象都有以下三个属性:

  • nodeName 节点名
  • nodeType 节点类型
  • nodeValue 节点值

我们来看下不同的节点类型,对应以上三个属性的值

节点类型 nodeName nodeType nodeVulue
文档节点 #document 9 null
元素节点 标签名 1 null
属性节点 属性名 2 属性值
文本节点 #text 3 文本内容
<div class="box" id="box"></div>
<script>
  // 获取元素节点 box
  var box = document.querySelector(".box");
  console.log("-----打印元素节点 相关信息----");
  console.log("节点名:" + box.nodeName);
  console.log("节点类型:" + box.nodeType);
  console.log("节点值:" + box.nodeValue);

  // 打印document 文档节点 相关信息
  console.log("-----打印document 文档节点 相关信息----");
  console.log("节点名:" + document.nodeName);
  console.log("节点类型:" + document.nodeType);
  console.log("节点值:" + document.nodeValue);
</script>

image-20221006204144152

# 1、节点关系

页面节点之间存在以下关系

关系 考虑所有节点
子节点 childNodes
父节点 parentNode
第一个子节点 firstChild
最后一个子节点 lastChild
前一个兄弟节点 previousSibling
后一个兄弟节点 nextSibling

我们以下面这个代码为例,来绘制元素节点之间的关系图

<div class="box">
  <h3 class="title">我是h3标签</h3>
  <p class="p1">我是p标签</p>
  <div class="item">我是item</div>
</div>

image-20221006213045829

<div class="box">
  <h3 class="title">我是h3标签</h3>
  <p class="p1">我是p标签</p>
  <div class="item">我是item</div>
</div>

<script>
  var box = document.querySelector(".box");
  console.log(box.parentNode); // body
  console.log(box.firstChild); // #text
  console.log(box.lastChild); // #text
  console.log(box.firstChild.nextElementSibling); // h3.title

  var p = document.querySelector(".p1");
  console.log(p.nextSibling); // #text
  console.log(p.previousSibling); // #text
</script>

image-20221006212802065

注意点:文本节点也属于节点

文本节点(即实是空白的文本)也属于节点。

而我们实际开发中,我们常常希望获取的是元素类型的节点,所以文本类型的节点给我们带来了很大的干扰。

# 2、 只考虑元素节点

TIP

  • 实际上在 DOM 中还提供了一些只考虑元素节点的属性,如下表
  • 我们把考虑所有节点的属性与只考虑元素节点的属性作如下对比
关系 考虑所有节点 只考虑元素节点
子节点 childNodes children
父节点 parentNode parentNode
第一个子节点 firstChild fristElementChild
最后一个子节点 lastChild lastElementChild
前一个兄弟节点 previousSibling previousElementSibling
后一个兄弟节点 nextSibling nextElementSibling

我们以下面这个代码为例,来绘制元素节点之间的关系图

<div class="box">
  <h3 class="title">我是h3标签</h3>
  <p class="p1">我是p标签</p>
  <div class="item">我是item</div>
</div>

image-20221006210620804

<div class="box">
  <h3 class="title">我是h3标签</h3>
  <p class="p1">我是p标签</p>
  <div class="item">我是item</div>
</div>

<script>
  var box = document.querySelector(".box");
  var _children = box.children;
  console.log(_children);
  console.log(_children[0]);
  console.log(box.parentNode); // body
  console.log(box.firstElementChild); // h3.title
  console.log(box.lastElementChild); // div.item
  console.log(box.firstElementChild.nextElementSibling); // p.p1

  var p = document.querySelector(".p1");
  console.log(p.nextElementSibling); // div.item
  console.log(p.previousElementSibling); // h3.title
</script>

image-20221006223929261

# 3、手写 children 方法

TIP

实现思路

  • 当前节点.childNodes获取所有子节点,然后遍历所有子节点,判断节点的类型 nodeType 是否为 1
  • 如果为 1,则把这个节点添加到数组中
  • 最后遍历完,把数组作为返回值返回。
HTMLElement.prototype._children = function () {
  // this 指向,谁调用_children 那this就是谁
  var nodes = this.childNodes;
  var elementArr = [];
  // 过滤节点类型,只留下元素类型节点
  for (var i = 0; i < nodes.length; i++) {
    if (nodes[i].nodeType === 1) {
      // 是元素类型节点,保存
      elementArr.push(nodes[i]);
    }
  }
  return elementArr;
};

# 4、手写 prevElementSibling 方法

TIP

实现思路

  • 当前节点.previousSibling
  • 如果返回值为null,说明没有上一个元素兄弟节点,直接将返回值null返回
  • 如果返回值不为 null,要判断节点类型是否为元素节点,即判断返回节点.nodeType === 1是否成立
  • 如果不成立,则继续用返回的节点.previousSibling,一直重复上面过程,至到返回节点.nodeType === 1成立或返回值为 null,就不再继续查找了。
HTMLElement.prototype._prevElementSibling = function () {
  // this,谁打点调用这个方法,this是谁
  var nextElement = this.previousSibling; // 找上一个兄弟节点
  // if(!nextElement) return null;
  // 如果返回的兄弟节点不为null 且节点类型不等于1,说明当前兄弟节点不是元素类型节点,需要继续向上查找。
  while (nextElement && nextElement.nodeType !== 1) {
    nextElement = nextElement.previousSibling;
    // if (!nextElement) return null;
  }
  return nextElement; // 如果上面为null,直接返回null,如果不是,返回对应元素节点
};

# 5、 修改节点内容 innerText 和 innerHTML

TIP

改变元素节点中的内容可以使用两个相关属性

  • innerHTML:更改元素的内容,更改的内容能以 HTML 语法的形式显示
  • innerText:更改的内容,更改的内容只能以纯文本的形式显示
<div class="box"></div>
<div class="box"></div>

<script>
  var box = document.getElementsByClassName("box");
  box[0].innerHTML = "<h3>我是h3标签</h3>";
  box[1].innerText = "<h3>我是h3标签</h3>";
</script>

image-20221004231923269

# 6、节点创建与移动

TIP

DOM 中提供了以下方法,用来创建节点,并将创建好的节点插入到页面当中

操作节点方法 作用
document.createElement('标签名') 用来创建一个指定的元素节点对象,并将创建好的对象作为返回值
document.createTextNode('文本内容') 用来创建一个文本节点对象,并将创建好的对象作为返回值。
父节点.appendChild('子节点对象') 用来向父节点的最后面添加一个新的子节点。
父节点.insertBefore('新节点','旧节点') 将新创建的"孤儿节点"插入到页面原有的节点的前面

# 6.1、document.createElement()

TIP

  • document.createElement()方法用于创建一个指定 tagName 的 HTML 元素
  • 在创建元素之前,会将传入的 tagName 转化为小写,即生成的标签名是小写名
  • 创建出来的节点是一个 “孤儿节点”,他并不在 DOM 树上,而是独立存在的。
  • 所以我们必须使用appendChild()insertBefore()方法将孤儿节点插入到 DOM 树上
var myDiv = document.createElement("div"); //  创建一个标签名为div的元素节点
console.log(myDiv); // <div></div>

# 6.2、document.createTextNode()

TIP

用来创建一个文本节点对象,并将创建好的对象作为返回值

var text = document.createTextNode(data);
  • text 是一个文本节点。
  • data 是一个字符串,包含了要放入文本节点的内容
var text = document.createTextNode("我是文本节点内容");
console.log(text); // "我是文本节点内容"

# 6.3、appendChild()

TIP

  • 方法将一个节点附加到指定父节点的子节点列表的末尾处
  • 如果某个节点已经拥有父节点,在被传递给此方法后,它首先会被移除,再被插入到新的位置
  • 这意味着,一个节点不可能同时出现在文档的不同位置

返回值:appendChild() 返回的是被附加的子元素

element.appendChild(child);

// element  将节点追节到的那个父节点
// child 要追加给父节点的节点(通通是一个元素节点)

代码演示

创建一个有内容的 p 标签,插入到页面的 div 标签中

<div class="box"></div>
<script>
  // 获取页面元素
  var box = document.getElementsByClassName("box");
  // 创建 p元素
  var pNode = document.createElement("p");
  // 创建文本节点
  var pTxt = document.createTextNode("我是p标签的文本");
  // 将文本节点插入到创建的p标签中,并打印返回值
  console.log(pNode.appendChild(pTxt)); // "我是p标签的文本"
  // 将创建的p元素,插入到页面div标签中
  box[0].appendChild(pNode);
</script>

# 6.4、insertBefore()

TIP

  • 方法在参考节点之前插入一个拥有指定父节点的子节点
  • 如果被插入节点已经有父节点,则会从当前位置移动到新插入位置

返回值:返回被插入的子节点

var insertedNode = parentNode.insertBefore(newNode, referenceNode);

// insertedNode 被插入节点(即 newNode)
// parentNode 新插入节点的父节点
// newNode 用于插入的节点
// referenceNode 参考节点,newNode将插入到这个节点前

如果 referenceNodenullnewNode 将被插入到子节点的末尾

代码演示

创建一个 li 标签,插入到 ul 标签的第 3 个子标签的前面

<ul>
  <li>第一个li</li>
  <li>第二个li</li>
  <li>第三个li</li>
  <li>第四个li</li>
</ul>

<script>
  // 获取页面元素
  var oul = document.getElementsByTagName("ul");
  // 创建li元素
  var li = document.createElement("li");
  // 创建文本节点
  var liTxt = document.createTextNode("我是新创建的li文本");
  // 将文本节点插入到创建的li标签中
  li.appendChild(liTxt);
  // 将新创建的li,插入到ul原来的第3个li标签前面
  oul[0].insertBefore(li, oul[0].children[2]);
</script>

image-20221006223617946

移动节点

<h3 id="title">我是标题</h3>
<div class="box">
  <p>我是p</p>
  我是box
</div>

<script>
  var h3 = document.getElementById("title");
  var box = document.querySelector(".box");
  var p = box.firstElementChild;
  // 如果第二个参数为null,则新节点将成为父元素的最后一个节点
  box.insertBefore(h3, null);
  // box.insertBefore(h3, p);
</script>

image-20221006230432229

# 7、案例 1:请动态创建出一个 15 行 10 列的表格

<style>
  td {
    width: 30px;
    height: 30px;
    border: 1px solid #000;
  }
</style>
<div id="table-list"></div>

<script>
  // 请动态创建一个15行10列的表格
  var mytable = document.getElementById("table-list");
  var table = document.createElement("table");
  for (var i = 0; i < 10; i++) {
    // 创建新的tr标签
    var tr = document.createElement("tr");
    for (var j = 0; j < 10; j++) {
      // 创建新的td标签
      var td = document.createElement("td");
      // 让tr追加td标签
      tr.appendChild(td);
    }
    // 让mytable追加tr标签
    table.appendChild(tr);
  }
  mytable.appendChild(table);
</script>

image-20221006224854598

# 8、案例 2:请制作九九乘法表

<style>
  table {
    background-image: linear-gradient(to right, pink, skyblue);
  }
  td {
    width: 120px;
    height: 35px;
    border: 1px solid #000;
    text-align: center;
  }
</style>
<table id="mytable"></table>

<script>
  var mytable = document.getElementById("mytable");

  for (var i = 1; i <= 9; i++) {
    // 创建了新的tr标签
    var tr = document.createElement("tr");
    for (var j = 1; j <= i; j++) {
      // 创建新的td标签
      var td = document.createElement("td");
      // 设置td内部的文字
      td.innerText = i + "*" + j + "=" + i * j;
      // 让tr追加td标签
      tr.appendChild(td);
    }
    // 让mytable追加tr标签
    mytable.appendChild(tr);
  }
</script>

image-20221006225221315

# 9、案例 3:创建电影座位号

根据需求,创建几行几列的电影座位号

image-20221012180858393

<style>
  body,
  ul,
  li {
    margin: 0;
    padding: 0;
    list-style: none;
  }
  #taopiao {
    background-color: #ddd;
    margin: 10px auto;
  }
  #taopiao ul li div {
    float: left;
    width: 60px;
    height: 25px;
    margin: 5px;
    text-align: center;
    line-height: 25px;
    border: 1px solid #ddd;
    cursor: pointer;
  }
  .selected {
    background-color: khaki;
  }
</style>
<body>
  <div id="taopiao"></div>

  <script>
    // 创建电影票座位
    function createSeats(el, rows, columns) {
      var ul = document.createElement("ul");
      var index = 0; // 座位号
      for (var i = 1; i <= rows; i++) {
        // 创建每一行li元素
        var li = document.createElement("li");
        for (var j = 1; j <= columns; j++) {
          var div = document.createElement("div");
          div.innerText = ++index;
          // 把div添加到li中
          li.appendChild(div);
        }
        ul.insertBefore(li, ul.children[0]);
      }
      // 动态计算ul的宽
      ul.style.width = (j - 1) * 72 + "px";
      // 动态设置ul水平居中
      ul.style.margin = "0px auto";
      // 将ul添加到页面元素中
      el.appendChild(ul);
    }

    var taopiao = document.getElementById("taopiao");
    createSeats(taopiao, 6, 6);
  </script>
</body>

# 10、innerHTML 与 createElement 的效率问题

TIP

  • innerHTML 更改元素的内容,更改的内容能以 HTML 语法的形式显示
  • createElement 用于动态创建 HTML 元素,然后结合 appendChild 将元素插入到页面中

这两种方式都可以动态创建 HTML 元素,那一种效率更高呢 ?我们通过一个案例来分析

案例

  • 在页面动态创建 100 个 li,插入到页面中
<ul class="list"></ul>
<script>
  var oUl = document.querySelector(".list");
  // innerHTML实现
  console.time("innerHTML");
  for (var i = 1; i <= 100; i++) {
    oUl.innerHTML += "<li>这是第" + i + "条新闻</li>";
  }
  console.timeEnd("innerHTML");

  // createElement实现
  console.time("createElement");
  for (var j = 1; j <= 100; j++) {
    var oLi = document.createElement("li");
    oLi.innerText = "这是第" + j + "条新闻";
    oUl.appendChild(oLi);
  }
  console.timeEnd("createElement");
</script>

image-20221025151126275

以上代码中

innerHTML 的执行效率明显比 createElement 的方式要低很多,原因在于

  • 每次迭代都要设置一次 innerHTML,在设置 innerHTML 前还要先读取 innerHTML。而且每次读取和插入都是把之前的所有节点读取出来。

所以我们最后是通过一个字符串来拼接所有内容,然后再一次性的插入到页面中。

  • 而 createElement 每次迭代,只是把新创建的元素插入到之前元素的后面。

优化版

<ul class="list"></ul>
<script>
  var oUl = document.querySelector(".list");
  console.time("innerHTML");
  var str = "";
  for (var i = 1; i <= 1000; i++) {
    str += "<li>这是第" + i + "条新闻</li>";
  }
  oUl.innerHTML += str;
  console.timeEnd("innerHTML");

  console.time("createElement");
  for (var j = 1; j <= 1000; j++) {
    var oLi = document.createElement("li");
    oLi.innerText = "这是第" + j + "条新闻";
    oUl.appendChild(oLi);
  }
  console.timeEnd("createElement");
</script>

image-20221025154016134

# 10.1、innerHTML 安全问题

TIP

如果页面中需要提供用户输入的信息,那建议不要使用 innerHTML。因为有可能会造成XSS攻击。

  • 所谓XSS攻击全称是'Cross Site Scripting'跨站脚本。
  • 是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

2015 年喜马拉雅就被爆出了对应的 XSS 漏洞,是因为用户在设置专辑名称时,服务器对关键字过滤不严格,可以将专辑名设置为一段 Javascript。

以下代码简单演示 innerHTML 造成的 XSS 攻击

请输入标题:<input type="text" name="" class="title" />
<button id="button">插入</button>
<div class="box"></div>

<script>
  var btn = document.getElementById("button");
  var title = document.querySelector(".title");
  var box = document.querySelector(".box");
  btn.onclick = function () {
    //如果用户输入内容为: <a href="" onclick='alert("攻击")'>用户标题</a>
    var value = title.value;
    box.innerHTML = value;
  };
</script>

GIF-2022-10-25-16-06-14

# 10.2、innerHTML、createElement 总结

说明
innerHTML 在操作时相对结构复杂,可读性不强,如果内容较少,可以使用,不过要注意把内容拼接成字符串,一次性插入到页面
同时,如果内容为用户输入时,要考虑安全问题
createElement 创建元素相对复杂些,但可读性强,如果内容较多,建议使用这种方式

# 11、删除节点

TIP

DOM 中删除一个子节点,返回删除的节点。

var oldChild = node.removeChild(child);
  • child 是要移除的那个子节点。
  • nodechild的父节点。
  • oldChild 保存对删除的子节点的引用。oldChild === child

注意: 节点不能主动删除自己,必须由父节点删除它

代码演示

<div class="box">
  <h3 class="title">我是标题</h3>
</div>
<script>
  var box = document.querySelector(".box");
  var h3 = document.querySelector(".title");

  // 删除h3标签,方法一:先找到父节点,然后删除其子节点
  // var _h3 = box.removeChild(h3);
  // console.log(_h3);

  // 删除h3标签,方法二:通过自身调用parentNode,将自身删除
  h3.parentNode.removeChild(h3);
</script>

删除一个元素的所有子节点

<ul class="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <li>7</li>
</ul>

<script>
  var oUl = document.querySelector(".list");
  // 如果有第一个子元素,就将他给删除
  // while (oUl.firstChild) {
  //   oUl.removeChild(oUl.firstChild);
  // }

  // 最简单的方法
  oUl.innerHTML = "";
</script>

# 12、节点的替换(修改)

TIP

  • replaceChild 方法用指定的节点替换当前节点的一个子节点,并返回被替换掉的节点
parentNode.replaceChild(newChild, oldChild);
  • newChild:用来替换 oldChild 的新节点。如果该节点已经存在于 DOM 树中,则它首先会被从原始位置删除。
  • oldChild: 被替换掉的原始节点。
<style>
  li {
    list-style: none;
  }
</style>
<ul>
  <li>第一个li</li>
  <li>第二个li</li>
  <li>第三个li</li>
  <li>第四个li</li>
</ul>

<script>
  // 获取页面元素
  var oul = document.getElementsByTagName("ul")[0];
  var firstli = oul.children[0];
  var lastli = oul.children[3];
  oul.replaceChild(lastli, firstli);

  // 创建新的元素来替换
  var oli = document.createElement("li");
  oli.innerHTML = "<p>我是新创建的</p>";
  oli.title = "我是新创建的";
  oul.replaceChild(oli, oul.children[1]);
</script>

image-20221006235056794

# 13、案例:交换两个元素在节点中的位置(阿里面试题)

实现思路

  • 假设要交换以下中的第 1 个 li第 4 个 li
<ul class="list">
  <li>第一个li</li>
  <li>第二个li</li>
  <li>第三个li</li>
  <li>第四个li</li>
</ul>
  • 我们可以先创建一个新的节点 newNode,插入到第 1 个 li 前面,用来占位(记录第 1 个 li 的位置)
<ul class="list">
  <!--新建节点newNode,插入到此占位-->
  <li>第一个li</li>
  <li>第二个li</li>
  <li>第三个li</li>
  <li>第四个li</li>
</ul>
  • 然后用第一个 li来替换第 4 个 li,在替换前,要先把第 4 个 li 保存起来,供后面使用
  • 最后用第 4 个 li来替换之前新创建的节点newNode

如果传过来的节点中,有一个不存在,则返回 false ,如果替换成功,则返回 true

<ul class="list">
  <li>第一个li</li>
  <li>第二个li</li>
  <li>第三个li</li>
  <li>第四个li</li>
</ul>
<script>
  function changeOfPosition(obj1, obj2) {
    // 如果传过来的节点,有一个不存在,就不做任何操作
    if (!obj1 || !obj2) return false;
    // 首先创建一个新节点
    var newNode = document.createElement("div");
    // 把新创建的节点插入到 obj1的前面
    obj1.parentNode.insertBefore(newNode, obj1);
    // 用obj1替换obj2
    obj1.parentNode.replaceChild(obj1, obj2);
    // 用obj2替换newNode
    obj1.parentNode.replaceChild(obj2, newNode);
    return true;
  }

  var liList = document.querySelectorAll(".list li");
  // 交换第1个和第3个元素位置
  changeOfPosition(liList[0], liList[3]);
</script>

注:

以上代码,交换的两个元素是兄弟关系如果两交换的元素分别属于不同的父级呢 ?

则上面代码是会报错的,主要问题在于调用insertBeforereplaceChild的父级元素到底是obj1还是obj2的父素来操作。

修改后代码如下:

<div class="box">box</div>
<ul class="list">
  <li>第一个li</li>
  <li>第二个li</li>
  <li>第三个li</li>
  <li>第四个li</li>
</ul>

<script>
  function changeOfPosition(obj1, obj2) {
    // 如果传过来的节点,有一个不存在,就不做任何操作
    if (!obj1 || !obj2) return false;
    // 首先创建一个新节点
    var newNode = document.createElement("div");
    // 把新创建的节点插入到 obj1的前面
    obj1.parentNode.insertBefore(newNode, obj1);
    // 用obj1替换obj2,返回obj2
    obj2.parentNode.replaceChild(obj1, obj2);
    // 用obj2替换newNode,为什么不用obj1的父级,而要用newNode
    // 在上面用obj1替换obj2了,所以obj1的父级此时变成了obj2的父级
    newNode.parentNode.replaceChild(obj2, newNode);
    return true;
  }

  var liList = document.querySelectorAll(".list li");
  var box = document.querySelector(".box");
  // 交换第1个和第3个元素位置
  changeOfPosition(box, liList[3]);
</script>

# 14、克隆节点

TIP

  • cloneNode 方法返回调用该方法的节点的一个副本。
  • 也就是隆节点,克隆出来的节点是 “孤儿节点”
var dupNode = node.cloneNode(deep);
  • node 将要被克隆的节点
  • dupNode 克隆生成的副本节点
  • deep 是否采用深度克隆 .可选参数
    • 如果为 true,则该节点的所有后代节点也都会被克隆
    • 如果为 false,则只克隆该节点本身

在早期规范中,deep 的默认值是 true,现在的新规范里,把默认值变成了 false

所以考虑兼容问题,最好在克隆时把这个参数带上。

<div class="box">
  <ul>
    <li>第一个li</li>
    <li>第二个li</li>
    <li>第三个li</li>
    <li>第四个li</li>
  </ul>
</div>

<script>
  // 获取页面元素
  var box = document.getElementsByClassName("box");
  // var box2 = box[0].cloneNode(); // 浅克隆
  var box2 = box[0].cloneNode(false); // 浅克隆
  console.log(box2);
  var box3 = box[0].cloneNode(true); // 深克隆
  console.log(box3);
  document.body.appendChild(box2);
  document.body.appendChild(box3);
</script>

image-20221006233434902

温馨提示:

  • 如果克隆的节点,设置了 id 名,则克隆后要修改 id 的名字,确保 id 的唯 一性
  • 克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件(比如onclick="alert(1)"),但不会拷贝那些使用addEventListener()方法或者node.onclick = fn这种用 JavaScript 动态绑定的事件。

# 15、DocumentFragment

TIP

DocumentFragment 文档片段接口,表示一个没有父对象的最小文档对象。

它被作为一个轻量版的 Document 使用,就像标准的 document 一样,存储由节点(nodes)组成的文档结构。与 document 相比,最大的区别是它不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会对性能产生影响。

  • 他具有真实 DOM 的一切方法和属性,所以我们可以像操作真实 DOM 一样来操作他
  • 你可以把他理解成虚拟节点对象,他的作用是充当其它要被添加到真实文档节点的仓库。而他自己永远不会被添加到真实的文档对中

MDN 官方参考:https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment (opens new window)

我们可能通过document.createDocumentFragment()方法来创建一个文档片段

<ul id="list"></ul>
<script>
  var oUl = document.getElementById("list");
  // 创建一个文档片段,此时还没有插入到真实DOM树中,其存在内存中
  var fragment = document.createDocumentFragment();

  // for循环创建5个li,然后插入到ul中
  for (var i = 0; i < 4; i++) {
    var oLi = document.createElement("li");
    oLi.innerText = "第" + i + "个li";
    // 将创建的DOM先放在文档片段中,这样就不会造成频繁的操作真实DOM
    fragment.appendChild(oLi);
  }
  // 一次性将创建的5个oLi插入到真实的DOM树中
  oUl.appendChild(fragment);
</script>

DocumentFragment 文档片段的主要功能:

可以将频繁的 DOM 操作理改为一次性 DOM 操作,从页减少了页面的重排和重绘(减少 DOM 渲染的次数)

# 七、重难点总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

重点

# 1、根据真实 HTML 代码,绘制 DOM 树

按照自己的理解和课程中所讲的绘制 DOM 树

# 2、访问节点的常用方法

方法 功能
getElementById() 通过元素 id 名获取到元素
getElementsByTagName() 通过标签名获取元素,返回的是一个数组
getElementsByClassName() 通过 class 名获取元素,返回的是一个数组
querySelector() 通过选择器得到元素,只能得到第一个被找到的元素
querySelectorAll() 通过选择器得到元素,返回的是一个数组

注:

getElementsByTagName()getElementsByClassName()方法,可以动态获取元素,如果页面中有新增或删除元素,获取元素的个数会自动变化到最新的。

# 3、如何访问和修改属性值

方式一:w3c 标准属性: 对象.属性名对象.属性名 = 属性值

box.id; // 获取属性
box.id = "box1"; // 修改属性值

方式二:自定义属性: 对象.getAttribute(key)对象.setAttribute(key,value)

box.setAttribute("data", "a"); // 设置自定义属性
box.getAttribute("data"); // 获取自定义属性值

方式三:HTML5 中自定义属性规范:对象.dataset.属性名(驼峰命名) 对象.dataset.属性名 = value

// 设置自定义属性 <div data-birth-date="20230501"></div>
box.dataset.birthDate = "20230501";
// 获取自义属性
box.dataset.birthDate;

# 4、操作元素样式的方法

方式 说明 三者性能
style 属性 通过操作 style 属性来给元素添加行内样式,每次添加是在原来基础上追加 最低
cssText 属性 通过操作 cssText 来给元素添加行内样式,每次添加会把之前的样式全覆盖
className 属性 通过操作 className 属性来更改元素的 class 属性值,从而引起样式的变化。className 没次操作也是直接覆盖之前的
  • classList 对象提供了很多方法,可以帮助我们很方便的操作元素的 class 属性。常见方法如下
属性或方法 说明
length 返回类名的个数
add() 在原有的类名基础上添加一个类名,如果这些类已经存在于元素的属性中,那么它们将被忽略。
remove() 在原有的类名基础上 移出某一个类名,使删除不存在的类值也不会导致抛出异常
toggle() 如果有这个类名 则删除这个类名,返回 false,如果没有 则添加减去,返回 true
item() 根据索引 获取类名
contains() 判断元素是否包含某一个类名
replace( oldClass, newClass ) 用一个新类值替换已有的类值,替换成功返回 true,替换失败,返回 false

# 5、获取元素相关尺寸

  • 重点掌握,同时要区分以下属性
属性 分类 说明
offsetWidth / offsetHeight 偏移尺寸 offsetWidth 返回一个元素的布局宽度
标准盒模型下,包括:width、border、padding、滚动条宽
怪异盒模型下为:width
clientWidth / clientHeight 客户端尺寸 clientWidth 表示元素的内容区宽,在标准盒模型下,包括width+padding,不包括border+margin+滚动条
scrollWidth / scrollHeight 滚动尺寸 scrollWidth 是元素内容宽度的一种度量,包括由于 overflow 溢出而在屏幕上不可见的内容
如果没有水平滚动条,其它大小与 clientWidth 相同
  • 常握以下属性
属性 分类 说明
offsetParent 偏移尺寸 返回离当前元素最近的定位祖先元素或最近的table,td,th,tody元素
offsetLeft 偏移尺寸 它返回当前元素(左边框)相对于其 offsetParent元素的左边框内壁的距离,相当于元素 left+margin-left
offsetTop 偏移尺寸 它返回当前元素(上边框)相对于其 offsetParent元素的上边框内壁的距离,相当于元素 top+margin-top
scrollLeft 滚动尺寸 获取或设置一个元素的内容水平滚动的距离
scrollTop 滚动尺寸 获取或设置一个元素的内容垂直滚动的距离

# 6、getBoundingClientRect()方法

TIP

  • 浏览器在每个元素上都暴露了getBoundingClientRect()方法,返回一个 DOMRect 对象
  • 访对象提供了元素的大小及其相对于视口(可视区)的位置

相关属性如下:

属性 说明
left、x 元素左边框相对于可视区左边的距离
top、y 元素上边框框相对于可视区顶部的距离
right 元素右边框相对于可视区左边的距离
bottom 元素底边框相对于可视区顶部的距离
height 元素的高,包括 height+padding+border
width 元素的宽,包括 width+padding+border

image-20221006195302347

# 7、节点操作

  • 节点类型,及不同节点对象身上 nodeName、nodeType、nodeValue 值
节点类型 nodeName nodeType nodeVulue
文档节点(Document) #document 9 null
元素节点(Element) 标签名 1 null
属性节点(Attr) 属性名 2 属性值
文本节点(Text) #text 3 文本内容
  • 理清各个节点之间的关系,通过节点之间关系来访问到对应节点
关系 考虑所有节点 只考虑元素节点
子节点 childNodes children
父节点 parentNode parentNode
第一个子节点 firstChild fristElementChild
最后一个子节点 lastChild lastElementChild
前一个兄弟节点 previousSibling previousElementSibling
后一个兄弟节点 nextSibling nextElementSibling
  • 节点的创建、移动、删除、替换、克隆
方法 说明
document.createElement('标签名') 用来创建一个指定的元素节点对象,并将创建好的对象作为返回值
document.createTextNode('文本内容') 用来创建一个文本节点对象,并将创建好的对象作为返回值。
父节点.appendChild('子节点对象') 用来向父节点的最后面添加一个新的子节点。
父节点.insertBefore('新节点','旧节点') 将新创建的"孤儿节点"插入到页面原有的节点的前面
父节点.removeChild(子节点) 将子节点从父节点中移除,返回值为移出的子节点
父节点.replace(newchild,oldchild) 用指定的节点替换当前父节点的一个子节点,并返回被替换掉的节点
节点.cloneNode(布尔值) cloneNode 方法返回调用该方法的节点的一个副本,如果参数为 true,表示深克隆,否则为浅克隆
  • innerHTML 与 createElement 效率问题
  • DocumentFragment 文档片段

# 8、难点

TIP

  • 手写 add、remove、toggle 方法,来操作元素的 class 属性
  • 手写 children、nextElementSibling 方法
  • 手写 getElementLeft 方法,计算元素与页面左边距离
  • 求两元素中心点之间距
  • 求出与当前元素最近的兄弟元素
  • 判断两个元素是否发生碰撞
  • 判断元素是否在可视区内
  • 交换两个元素在节点中位置
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X