Be JS PUA (2)

207 阅读16分钟

2022.10.18

一.案例 - 分页渲染

分析:
1. 打开页面
+ 根据 list 数组内的部分数据( 根据 第几页 和 一页显示多少条 )渲染 ul 内的 li 标签名
+ 根据数据的 总数 和 一页显示多少条 计算出一共多少页, 渲染指定标签内的文本
+ 根据 当前是第几页 决定 上一页 按钮是否具有 disable 类名
+ 根据 当前是第几页 决定 下一页 按钮是否具有 disable 类名
2. 点击下一页按钮
+ 当前页改变(+1)
+ 根据 list 数组内的部分数据( 根据 第几页 和 一页显示多少条 )渲染 ul 内的 li 标签名
+ 根据数据的 总数 和 一页显示多少条 计算出一共多少页, 渲染指定标签内的文本
+ 根据 当前是第几页 决定 上一页 按钮是否具有 disable 类名
+ 根据 当前是第几页 决定 下一页 按钮是否具有 disable 类名
3. 点击上一页按钮
+ 当前页改变(-1)
+ 根据 list 数组内的部分数据( 根据 第几页 和 一页显示多少条 )渲染 ul 内的 li 标签名
+ 根据数据的 总数 和 一页显示多少条 计算出一共多少页, 渲染指定标签内的文本
+ 根据 当前是第几页 决定 上一页 按钮是否具有 disable 类名
+ 根据 当前是第几页 决定 下一页 按钮是否具有 disable 类名
4. 切换 select 标签的内容的时候
+ 当前页改变(=1)
+ 改变一页显示多少条
+ 根据 list 数组内的部分数据( 根据 第几页 和 一页显示多少条 )渲染 ul 内的 li 标签名
+ 根据数据的 总数 和 一页显示多少条 计算出一共多少页, 渲染指定标签内的文本
+ 根据 当前是第几页 决定 上一页 按钮是否具有 disable 类名
+ 根据 当前是第几页 决定 下一页 按钮是否具有 disable 类名

分析:
1. 书写一个函数(bindHtml)
+ 根据 list 数组内的部分数据( 根据 第几页 和 一页显示多少条 )渲染 ul 内的 li 标签名
+ 根据数据的 总数 和 一页显示多少条 计算出一共多少页, 渲染指定标签内的文本
+ 根据 当前是第几页 决定 上一页 按钮是否具有 disable 类名
+ 根据 当前是第几页 决定 下一页 按钮是否具有 disable 类名
2. 打开页面
+ 执行一遍 bindHtml
3. 点击下一页
+ 改变当前页(+1)
+ 执行一遍 bindHtml
4. 点击上一页
+ 改变当前页(-1)
+ 执行一遍 bindHtml
5. 切换 select 标签的内容的时候
+ 改变一页显示多少条
+ 改变当前页(=1)
+ 执行一遍 bindHtml

// 一页显示 x个 可选 通过 叠加覆盖 进行实现


<div class="header">网页头部</div>
<div class="pagination">
<span class="prev">&lt;</span>
<span class="total">1 / 100</span>
<span class="next">&gt;</span>
<select>
  <option value="4">4</option>
  <option value="8">8</option>
  <option value="12">12</option>
  <option value="16">16</option>
</select>
</div>
<ul class="content">
<li>
  <img src="" alt="">
  <p>名称</p>
  <p>城市: xx</p>
  <p>地址: xx</p>
  <p>价格: xx</p>
  <p>时间: xx</p>
 </li>
</ul>
<div class="footer">网页底部</div>

<script src="dm_list.js"></script> //此时的 外连接 只是一个 存入数据 的数组 模拟服务器拿数据

<script>

// 0. 获取元素
// 0-1. 获取承载页面的大盒子
var contentBox = document.querySelector('ul')
// 0-2. 获取到总数文本盒子
var totalBox = document.querySelector('.total')
// 0-3. 获取到上一页按钮
var prevBtn = document.querySelector('.prev')
// 0-4. 获取到下一页按钮
var nextBtn = document.querySelector('.next')
// 0-5. 获取到 select 标签
var selectBox = document.querySelector('select')

// 0. 准备变量
// 0-1. 准备一个变量表示当前是第几页
var current = 1
// 0-2. 准备一个变量表示一页多少条
var pagesize = 4
// 0-3. 准备一个变量表示一共多少页
var total = 0

// 1. 准备一个渲染页面的函数
function bindHtml() {
  // 1-1. 根据 list 数组内的部分数据( 根据 第几页 和 一页显示多少条 )渲染 ul 内的 li 标签名
  // 1-1-1. 从 list 内获取出部分数据(详见解释1)
  var bindList = list.slice((current - 1) * pagesize, current * pagesize)
  // 1-1-2. 根据获取出来的部分数据渲染页面
  contentBox.innerHTML = bindList.reduce(function (prev, item) {
    return prev + `
      <li>
        <img src="${ item.pic }" alt="">
        <p>${ item.name }</p>
        <p>城市: ${ item.city }</p>
        <p>地址: ${ item.address }</p>
        <p>价格: ${ item.price }</p>
        <p>时间: ${ item.showTime }</p>
      </li>
    `
  }, '')

  // 1-2. 根据数据的 总数 和 一页显示多少条 计算出一共多少页, 渲染指定标签内的文本
  // 1-2-1. 计算出总页数(详见解释2)
  total = Math.ceil(list.length / pagesize)
  // 1-2-2. 利用计算出的总页数来渲染 total 标签的文本内容
  totalBox.innerText = `${ current } / ${ total }`

  // 1-3. 根据 当前是第几页 决定 上一页 按钮是否具有 disable 类名(详见解释3)
  prevBtn.className = current === 1 ? 'prev disable' : 'prev'

  // 1-4. 根据 当前是第几页 决定 下一页 按钮是否具有 disable 类名
  nextBtn.className = current === total ? 'next disable' : 'next'
}

// 2. 打开页面执行一遍渲染页面的函数
bindHtml()

// 3. 点击下一页按钮
// 3-1. 给下一页按钮添加点击事件
nextBtn.onclick = function () {
  // 判断 如果当前页就是最后一页, 什么也不需要做了
  if (current >= total) return

  // 3-2. 改变当前页
  current++

  // 3-2. 在执行一遍 bindHtml
  bindHtml()
}

// 4. 点击上一页按钮
// 4-1. 给上一页按钮添加点击事件
prevBtn.onclick = function () {
  // 判断, 当前页就是第一页, 什么也不需要做了
  if (current <= 1) return

  // 4-2. 改变当前页(-1)
  current--

  // 4-3. 在执行一遍 bindHtml
  bindHtml()
}

// 5. 切换 select 标签内容
// 注意: 事件是 onchange 事件
selectBox.onchange = function () {
  // 5-1. 拿到 select 标签改变成什么了, 改变一页显示多少条
  // 说明: select 标签的 value 值是什么 ? 你选中的那个 option 的 value(是一个字符串)
  pagesize = selectBox.value - 0

  // 5-2. 改变当前页
  current = 1

  // 5-3. 在执行一遍 bindHtml
  bindHtml()
}
</script>

二.获取元素尺寸

  • 指获取元素在文档流内的占地面积

    offset 一套
    + 语法:
    => 元素.offsetWidth
    => 元素.offsetHeight

    • 得到: 该元素 内容 + padding + border 区域的尺寸

client 一套
+ 语法:
=> 元素.clientWidth
=> 元素.clientHeight
+ 得到: 该元素 内容 + padding 区域的尺寸

三.获取元素偏移量

offsetParant
+ offset 一套偏移量的参考父级
+ 语法: 元素.offsetParent
+ 得到: 该元素的偏移量参考父元素
=> 该元素的定位父级

offset 一套
+ 语法:
=> 元素.offsetLeft
=> 元素.offsetTop
+ 得到: 该元素相对于 offsetParent 的偏移量

client 一套
+ 语法:
=> 元素.clientLeft
=> 元素.clientTop
+ 得到: 该元素(内容+padding区域) 和 border 左上角的距离
=> 也就是该元素 左边框 和 上边框 的宽度

四.获取可视窗口尺寸

BOM 级别的获取
+ 语法:
=> window.innerWidth
=> window.innerHeight
+ 特点: 获取到的是包含滚动条在内的尺寸

DOM 级别的获取
+ 语法:
=> document.documentElement.clientWidth
=> document.documentElement.clientHeight
+ 特点: 获取到的是不包含滚动条在内的尺寸

五.案例 - 瀑布流渲染页面

分析:
+ 打开页面
=> 展示部分数据
+ 达到合适的时机
=> 拿到下一页的部分数据
=> 叠加在已有数据的下面

说明:
+ 在做分页的时候, 我们是在展示数组的部分数据
+ 因为完整数组是在本地, 只需要对数组的内的数据进行提取就可以了
+ 在实现效果的时候, 使用一个 延时定时器 来模拟网络环境
+ 设置的延时时间设定在 2000 ~ 5000 ms 之间

//一页 8个 当 滚动条 向下滑动到达零界点 进行 loading 之后 出新新的内容 
 


<div class="header">网页头部</div>
 <ul class="content">
<!-- JS 渲染 -->
 </ul>
 <div class="loading">
<img src="./003.gif" alt="">
 </div>
 <div class="footer">网页底部</div>
 <script src="./dm_list.js"></script>
 
 
 
 <script>
// 0. 获取元素
// 0-1. 获取承载列表的 ul 盒子
var contentBox = document.querySelector('ul')
// 0-2. 获取 loading 盒子
var loadingBox = document.querySelector('.loading')


// 0. 准备一些变量
// 0-1. 准备一个变量表示当前页
var current = 1
// 0-2. 准备一个变量表示一页多少条
var pagesize = 8
// 0-3. 准备一个变量表示总页数
var total = Math.ceil(list.length / pagesize)
// 0-4. 准备一个变量当做开关
var flag = true


// 1. 书写一个渲染页面的函数
function bindHtml() {
  // 1-1. 根据 当前第几页 和 一页多少条 截取数组内的部分数据
  var bindList = list.slice((current - 1) * pagesize, current * pagesize)

  // 1-2. 根据截取出来的部分数据渲染页面
  // 使用的是 赋值符号(=) 设置 ul 内的文本内容
  // 当你将来渲染第二页的时候, 会把第一页的内容覆盖
  // 注意: 需要使用 += 符号来设置文本内容
  // 注意: 如果你使用的是 +=, 那么 ul 内本身就不能有内容
  contentBox.innerHTML += bindList.reduce(function (prev, item) {
    return prev + `
      <li>
        <img src="${ item.pic }" alt="">
        <p>${ item.name }</p>
        <p>城市: ${ item.city }</p>
        <p>地址: ${ item.address }</p>
        <p>价格: ${ item.price }</p>
        <p>时间: ${ item.showTime }</p>
      </li>
    `
  }, '')
}

// 2. 打开页面执行一遍
bindHtml()
// 3. 在一个合适的时机
// 3-1. 需要一个滚动事件

window.onscroll = function () {
  // 3-2. 一定不会加载下一页的情况
  // 只要没有下一页了, 绝对不会再次加载写一页了
  if (current >= total) return
  
  // 3-2. 找到一个合适的时机
  // 卷去的高度 + 可视窗口的高度 < ul上方偏移量 + ul的高度 表示未到达时机, 不需要加载下一页
  var scroll_top = document.documentElement.scrollTop || document.body.scrollTop
  var window_height = document.documentElement.clientHeight
  var ul_top = contentBox.offsetTop
  var ul_height = contentBox.offsetHeight
  if (scroll_top + window_height < ul_top + ul_height) return
  
  // 3-3. 判断如果开关是关闭的, 就不在加载下一页了
  if (flag === false) return
  
  // 3-4. 代码来到这里, 应该加载下一页了
  
  // 把开关关闭
  flag = false
  
  // 首先让 loading 标签出现
  loadingBox.style.display = 'flex'
  
  // 为了观察效果, 使用一个延时定时器模拟网络加载效果
  setTimeout(function () {
  
    // 加载下一页
    current++
    bindHtml()
    
    // 让 loading 效果消失
    loadingBox.style.display = 'none'
    
    // 代码执行到这里, 说明第二页的数据加载完毕了
    // loading 效果消失了, 所有的一切都准备好了, 可以加载下一页了
    flag = true
  }, Math.floor(Math.random() * 3000) + 2000)
}
</script>

2022.10.19

一.认识DOM节点

1.认识DOM节点

  • 节点(node): 组成页面的每一个环节(部分, 内容, 零件)
    + 页面中常见的节点
    => 元素节点: 页面上的标签
    -> html: 最大的元素节点, 也叫作 根元素节点
    => 属性节点: 书写在标签身上的属性, 不作为独立节点出现
    -> 只是用来描述标签的内容
    -> 和任何节点不产生父子关系
    => 文本节点: 一切文本内容(包含换行和空格)
    => 注释节点: 一切注释内容(包含换行和空格)

2.获取节点的方式

  • 获取元素节点

  • 获取非常规元素节点
    -> html: document.documentElement
    -> head: document.head
    -> body: document.body

  • 获取常规元素节点
    -> getElementById()
    -> getElementByClassName()
    -> getElementByTagName()
    -> querySelector()
    -> querySelectorAll()

  • 获取节点

  1. childNodes
    语法: 节点.childNodes
    得到: 该节点的所有子节点

  2. children
    语法: 节点.children
    得到: 该节点的所有子元素节点

  3. firstChild
    语法: 节点.firstChild
    得到: 该节点下的第一个子节点

  4. firstElementChild
    语法: 节点.firstElementChild
    得到: 该节点下的第一个子元素节点

  5. lastChild
    语法: 节点.lastChild
    得到: 该节点下的最后一个子节点

  6. lastElementChild
    语法: 节点.lastElementChild
    得到: 该节点下的最后一个子元素节点

  7. previousSibling
    语法: 节点.previousSibling
    得到: 该节点的上一个兄弟节点

  8. previousElementSibling
    语法: 节点.previousElementSibling
    得到: 该节点的上一个兄弟元素节点

  9. nextSibling
    语法: 节点.nextSibling
    得到: 该节点的下一个兄弟节点

  10. nextElementSibling
    语法: 节点.nextElementSibling
    得到: 该节点的下一个兄弟元素节点

  11. parentNode
    语法: 节点.parentNode
    得到: 该节点的父节点(大部分都是元素节点)

  12. parentElement
    语法: 节点.parentElement
    得到: 该节点的父元素节点

  13. attributes
    语法: 节点.attributes
    得到: 该节点的所有属性节点

3.创建节点

  • 使用 js 的方式创建出一个 节点来
  1. 创建元素节点

    • 语法: document.createElement('标签名')
    • 返回值: 一个被创建出来的元素节点
    • 注意: 标签名可以是自定义标签名
  2. 创建文本节点

    • 语法: document.createTextNode('文本内容')
    • 返回值: 一个创建好的文本节点

4.插入节点

  • 把一个节点插入到另一个节点内作为子节点存在
  1. appendChild()

    • 语法: 父节点.appendChild(子节点)
    • 作用: 把该 子节点 插入到 父节点 内部, 排列在最后一个的位置
  2. insertBefore()

    • 语法: 父节点.insertBefore(要插入的子节点, 谁的前面)
    • 作用: 把该 子节点 插入到 父节点 内部, 排列在某一个已经存在的节点前面

5.删除节点

  • 指把某一个节点删除
  1. removeChild()

    • 语法: 父节点.removeChild(子节点)
    • 作用: 把该 子节点 从 父节点 内移除
  2. remove()

    • 语法: 节点.remove()
    • 作用: 把该节点删除

6.替换节点

  1. replaceChild()
    • 语法: 父节点.replaceChild(换上节点, 换下节点)
    • 作用: 在 父节点 内, 使用 换上节点 替换掉 换下节点

7.克隆节点

  • 指: 把已知节点复制一份一模一样的

cloneNode
+ 语法: 节点.cloneNode(参数)
+ 参数:
=> 默认是 false, 表示不克隆后代节点
=> 选填是 true, 表示克隆后代节点

二.认识事件

  • 注意: 所有 JS 的原生事件内没有大写字母
    + 我们对常见的事件进行了一些分类
    => 鼠标事件
    => 键盘事件
    => 浏览器事件
    => 表单事件
    => 触摸事件
    => 拖拽事件
    => 其他事件

1.鼠标事件

  1. click 鼠标左键单击
    ele.onclick = function () { console.log('鼠标左键单击') }

  2. dblclick 鼠标左键双击
    ele.ondblclick = function () { console.log('鼠标左键双击') }

  3. contextmenu 鼠标右键单击
    ele.oncontextmenu = function () { console.log('鼠标右键单击') }

  4. mousedown 鼠标按键按下
    ele.onmousedown = function () { console.log('鼠标按键按下') }

  5. mouseup 鼠标按键抬起
    ele.onmouseup = function () { console.log('鼠标按键抬起') }

  6. mousemove 鼠标移动事件
    随着移动实时触发, 大概每秒 60 次捕获
    ele.onmousemove = function () { console.log('鼠标移动') }

  7. mouseover 鼠标移入事件
    ele.onmouseover = function () { console.log('鼠标移入') }

  8. mouseout 鼠标移出事件
    ele.onmouseout = function () { console.log('鼠标移出') }

  9. mouseenter 鼠标移入(键入)事件
    ele.onmouseenter = function () { console.log('鼠标移入了') }

  10. mouseleave 鼠标移出(离开)事件
    ele.onmouseleave = function () { console.log('鼠标离开了') }

  • 注意:
    => mouseover 和 mouseout 一套事件, 在移入移出子元素的时候也会触发
    => mouseenter 和 mouseleave 一套事件, 在移入移出子元素的时候不会触发

2.键盘事件

  1. keydown 键盘按下事件 inp.onkeydown = function () { console.log('键盘按下了') }

  2. keyup 键盘抬起事件 inp.onkeyup = function () { console.log('键盘抬起了') }

  3. keypress 键盘键入事件 注意: 按下的按键可以真正键入内容 注意: 键入的内容需要和按下的内容一致 注意: 回车键也可以触发 inp.onkeypress = function () { console.log('键盘键入内容') }

键盘事件
+ 注意: 键盘事件一般不用于捕获输入内容
+ 注意: 键盘事件可以给所有元素绑定, 但不是所有元素都能触发
=> 键盘事件一般绑定给 window / document / 可聚焦元素

3.表单事件

  1. focus 聚焦
    inp.onfocus = function () { console.log('聚焦') }

  2. blur 失焦
    inp.onblur = function () { console.log('失焦') }

  3. input 输入
    只要表单内的内容改变, 随着改变实时触发 inp.oninput = function () { console.log('正在输入内容') }

  4. change 改变
    表单的真实内容发生变化的时候才会触发 当你失焦以后, 如果和聚焦时的内容不一样, 才叫做真实内容改变 inp.onchange = function () { console.log('表单改变了') }

  5. reset 重置
    注意: 表单的重置行为是 form 标签才有
    注意: 该事件需要绑定给 form 标签, 通过点击 reset 按钮来触发
    formEle.onreset = function () { console.log('表单重置了') }

  6. submit 提交
    注意: 表单的提交行为是 form 标签才有
    注意: 该事件需要绑定给 form 标签, 通过点击 submit 按钮来触发
    formEle.onsubmit = function () { console.log('表单提交了') }

表单事件
+ 依赖表单行为触发的事件
+ 注意:
=> input 事件常用在 input / textarea 等元素上
=> change 事件常用在 checkbox / radio / select 等元素上

4.触摸事件

触摸事件
+ 指在移动端等可触摸屏幕设置上使用的事件

  1. touchstart 触摸开始(手指接触到屏幕的瞬间)
    ele.ontouchstart = function () { console.log('触摸开始') }

  2. touchmove 触摸移动(手指在屏幕上移动的时候)
    ele.ontouchmove = function () { console.log('触摸移动') }

  3. touchend 触摸接触(手指离开屏幕的瞬间)
    ele.ontouchend = function () { console.log('触摸结束') }

5.拖拽事件

拖拽事件

  • 注意: 一般元素是不允许被拖拽的, 如果你想让该元素触发拖拽事件, 需要开启可拖拽属性
    => 给元素添加 draggable="true" 表示开启拖拽

  • 整套的拖拽事件和两种元素关联
    => 拖拽元素: 你真的拖着他走的元素
    => 拖放元素: 你要松手的元素

拖拽元素身上的事件
1. dragstart 拖拽开始
注意: 鼠标在该元素上按下, 并且有移动的动作出现的瞬间
eleDiv.ondragstart = function () { console.log('你要拖拽') }

  1. drag 拖拽过程
    eleDiv.ondrag = function () { console.log('正在拖拽') }

  2. dragend 拖拽结束
    eleDiv.ondragend = function () { console.log('拖拽结束了') }

拖放元素身上的事件
4. dragenter 拖拽元素进入拖放元素区域的时候触发
eleP.ondragenter = function () { console.log('进入拖放元素了') }

  1. dragleave 拖拽元素离开拖放元素区域的时候触发
    eleP.ondragleave = function () { console.log('离开拖放元素了') }

  2. ondragover 拖拽元素在拖放元素内移动的时候触发
    eleP.ondragover = function () {
    console.log('拖拽元素在拖放元素内移动')
    return false
    }

  3. drop 拖拽元素在拖放元素范围内结束拖拽的时候触发
    注意: 需要在 dragover 事件内进行阻止默认行为
    eleP.ondrop = function () { console.log('在拖放元素内放手') }

6.其他事件

其他事件
+ 指一些不好分类的事件

  1. selectstart 框选开始
    document.onselectstart = function () {
    console.log('你竟然想复制内容')
    return false
    }

  2. transitionend 过渡结束
    document.querySelector('p').ontransitionend = function () { console.log('过渡结束了')
    }

  3. unload 你要关闭页面的时候
    window.onunload = function () { window.localStorage.setItem('aaa', 100) }

7.案例 - 九宫格交换

  <style>
* {
  margin: 0;
  padding: 0;
  list-style: none;
}

ul {
  width: 640px;
  height: 640px;
  border: 10px solid orange;
  border-radius: 15px;
  margin: 50px auto;
  position: relative;
}

ul > li {
  width: 200px;
  height: 200px;
  font-size: 30px;
  font-weight: 700;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  color: #fff;

  transition: all 1s linear;
}
</style>
 </head>
 <body>


<ul>
<li draggable="true" style="left: 10px; top: 10px; background-color: pink;">1</li>
<li draggable="true" style="left: 220px; top: 10px; background-color: skyblue;">2</li>
<li draggable="true" style="left: 430px; top: 10px; background-color: orange;">3</li>
<li draggable="true" style="left: 10px; top: 220px; background-color: purple;">4</li>
<li draggable="true" style="left: 220px; top: 220px; background-color: cyan;">5</li>
<li draggable="true" style="left: 430px; top: 220px; background-color: green;">6</li>
<li draggable="true" style="left: 10px; top: 430px; background-color: yellow;">7</li>
<li draggable="true" style="left: 220px; top: 430px; background-color: red;">8</li>
<li draggable="true" style="left: 430px; top: 430px; background-color: black;">9</li>
</ul>


<script>
/*
  九宫格交换

  需求:
    + 拖拽哪一个元素, 在哪一个元素上松手, 就让两个元素交换位置
*/

// 0. 获取元素
var lis = document.querySelectorAll('ul > li')

// 0. 准备变量
var startEle = null

// 1. 开始绑定事件
// 每一个元素都有可能是被拖拽元素, 每一个元素也都有可能是拖放元素
// 每一个元素需要绑定一个 拖拽开始事件(目的记录下 开始元素)
// 每一个元素需要绑定一个 松手事件(目的记录下 结束元素)
// 每一个元素需要绑定一个 dragover 事件(目的, 阻止默认行为, 好让松手事件触发)
lis.forEach(function (item) {
  // 1-1. 每一个元素绑定一个 dragstart 事件
  item.addEventListener('dragstart', function () {
    // 只有在这里才能拿到开始元素
    // 我们需要把这个开始元素记录下来
    // 给 startEle 赋值
    startEle = item
  })

  // 1-2. 每一个元素绑定一个 dragover 事件
  item.ondragover = function () { return false }

  // 1-3. 每一个元素绑定一个 drop 事件
  item.ondrop = function () {
    // console.log('松手了')
    // 让 开始元素 和 结束元素 交换位置
    // console.log('结束元素 : ', item)
    // console.log('开始元素 : ', startEle)
    // 交换 item 和 startEle 的位置

    // 需要准备一个第三方变量临时接受一下
    var tmpLeft = startEle.offsetLeft
    var tmpTop = startEle.offsetTop

    // 把当前交换的两个元素的层级提高
    item.style.zIndex = 999
    startEle.style.zIndex = 999

    // 把 item 的值赋值给 startEle
    startEle.style.left = item.offsetLeft + 'px'
    startEle.style.top = item.offsetTop + 'px'

    // 把 临时变量 记录的内容赋值给 item
    item.style.left = tmpLeft + 'px'
    item.style.top = tmpTop + 'px'
  }

  // 1-4. 每一个元素绑定一个 transitionend 事件
  item.addEventListener('transitionend', function () {
    item.style.zIndex = 1
  })
})
</script>
</body>


2022.10.20

一.认识事件对象

事件对象

  • 概念: 当事件触发的时候, 由浏览器提供的一个对象数据结构, 用于记录本次事件的所有相关信息
  • 私人: 一个描述本次事件所有信息的对象数据结构

如何获取事件对象

  • 在事件处理函数内直接书写一个形参

  • 会在事件触发的时候, 有浏览器自动传递实参

  • 该实参就是 事件对象

var ele = document.querySelector('div')
  // 1.绑定事件
ele.onclick = function (abc) {
  // 该形参会在该事件触发的时候, 由浏览器传递实参
  console.log(abc)
}

二.事件对象内鼠标相关信息

事件对象内鼠标相关信息

光标的坐标位置

  1. client 一套
    + 语法:<br>
      => 事件对象.clientX<br>
      => 事件对象.clientY<br>
    + 得到: 光标相对于浏览器可视窗口左上角的坐标位置

  2. page 一套
    + 语法:<br>
      => 事件对象.pageX<br>
      => 事件对象.pageY<br>
    + 得到: 光标相对于文档流左上角的坐标位置

  3. offset 一套
    + 语法:<br>
      => 事件对象.offsetX<br>
      => 事件对象.offsetY<br>
    + 得到: 光标相对 准确触发事件的元素 左上角的坐标位置

认识 JS 内的两大家族

  1. offset 家族

    • offsetWidth 和 offsetHeight
      => 语法:
      -> 元素.offsetWidth
      -> 元素.offsetHeight
      => 得到: 该元素 内容 + padding + border 区域的尺寸

    • offsetLeft 和 offsetTop => 语法:
      -> 元素.offsetLeft
      -> 元素.offsetTop
      => 得到: 该元素相对于 定位父级 左边和上边的距离

    • offsetX 和 offsetY => 语法:
      -> 事件对象.offsetX
      -> 事件对象.offsetY
      => 得到: 光标相对于 准确触发事件的元素 左上角的坐标位置

    • offsetParent
      => 语法: 元素.offsetParent
      => 得到: 该元素的 定位父级, 也就是 offset 偏移量的参考元素

  2. client 家族

    • clientWidth 和 clientHeight
      => 语法:
      -> 元素.clientWidth
      -> 元素.clientHeight
      => 得到: 该元素 内容 + padding 区域的尺寸

    • clientLeft 和 clientTop
      => 语法:
      -> 元素.clientLeft
      -> 元素.clientTop
      => 得到: 该元素 (内容+padding) 区域和 border 左上角的距离
      -> 其实就是 左边框 和 上边框 的宽度

    • clientX 和 clientY
      => 语法:
      -> 事件对象.clientX
      -> 事件对象.clientY
      => 得到: 光标相对于 浏览器可视窗口左上角 的坐标位置

三.案例 - 列表鼠标跟随

 <style>
* {
  margin: 0;
  padding: 0;
}

ul {
  margin: 50px;
}

li {
  list-style: none;
  width: 600px;
  height: 40px;
  margin-bottom: 30px;
  border: 1px solid #333;
  position: relative;
}

li > p {
  width: 300px;
  height: 180px;
  background-color: pink;
  position: absolute;
  left: 800px;
  top: 300px;
  display: none;

  pointer-events: none;

  z-index: 999;
}
  </style>
  
  <ul>
<li>我是标题1
  <p style="background-color: pink;">我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1我是解释内容1</p>
</li>
<li>我是标题2
  <p style="background-color: skyblue;">我是解释内容2</p>
</li>
<li>我是标题3
  <p style="background-color: orange;">我是解释内容3我是解释内容3我是解释内容3我是解释内容3我是解释内容3我是解释内容3我是解释内容3我是解释内容3</p>
</li>
  </ul>
  
  
  <script>
/*
  列表鼠标跟随

  需求:
    + 光标在哪一个 li 内移动的时候, 哪一个 li 对应的 p 标签跟随光标移动

  问题1: 什么时候出现效果 ?
    => 移入 li 的时候: mouseover
    => 移出 li 的时候: mouseout
    => 在 li 内移动的时候: mousemove
  问题2: 在什么范围内 ?
    => li 身上
  问题3: 什么效果 ?
    => 移入 li 的时候: 该 li 对应的 p 显示(display: block)
    => 移出 li 的时候: 该 li 对应的 p 隐藏(display: none)
    => 在 li 内移动的时候: 实时获取坐标点, 赋值给 li 对应的 p 的 left 和 top 样式

  bug: 从左向右移动的时候, 会出现抖动问题 ?
    => 原因: 当你向右移动的时候, 光标会出现在 p 标签身上, 导致 li 和 光标之间有一个 p 标签出现
    => 解决:
      -> 穿透

*/

// 0. 获取元素
var lis = document.querySelectorAll('ul > li')


// 1. 给每一个 li 绑定事件
lis.forEach(function (item) {
  item.onmouseover = overHandler
  item.onmouseout = outHandler
  item.onmousemove = moveHandler
})

// 1-1. 移入的事件处理函数
function overHandler() {
  // 当前移入的这个 li 的对应的 p 出现
  // 问题: 你移入的是哪一个 li ? this
  // 问题: 对应的 p 是谁 ? 你移入的这个 li 的子元素
  // console.log(this.firstElementChild)

  // 对应的 p 出现
  this.firstElementChild.style.display = 'block'
}

// 1-2. 移出的事件处理函数
function outHandler() {
  // 当前移出的这个 li 的对应的 p 隐藏
  this.firstElementChild.style.display = 'none'
}

// 1-3. 移动的事件处理函数
function moveHandler(e) {
  // 当前移动的这个 li 的对应的 p 跟随
  // 拿到光标的坐标点
  // 问题: 哪一组坐标点 ?
  //   因为 p 是根据 li 来进行定位的
  //   应该拿到一组相对于 li 左上角的坐标点
  var x = e.offsetX + 8
  var y = e.offsetY + 8

  // 给对应 p 赋值
  this.firstElementChild.style.left = x + 'px'
  this.firstElementChild.style.top = y + 'px'
}
  </script>
  

四.案例 - 拖拽元素

拖拽案例(1)

 <style>
* {
  margin: 0;
  padding: 0;
}

div {
  width: 200px;
  height: 200px;
  background-color: skyblue;
  position: absolute;
}
  </style>
  
  <div></div>

  <script>
/*
  案例 - 拖拽

  需求:
    + 鼠标按下的时候, 可以跟着走
    + 随着鼠标移动, 让 div 跟随移动即可
    + 鼠标抬起的时候, 不能跟着走

  问题1: 什么时候有效果 ?
    + 鼠标按下
    + 鼠标抬起
    + 鼠标移动
  问题2: 在什么范围内有效果 ?
    + div 上按下
    + div 上抬起
    + document 内移动
  问题3: 什么效果 ?
*/

// 0. 获取元素
var ele = document.querySelector('div')

// 0. 准备一个开关
var flag = false

// 绑定事件
ele.addEventListener('mousedown', function () {
  // 开启开关
  flag = true
})

ele.addEventListener('mouseup', function () {
  // 关闭开关
  flag = false
})

document.addEventListener('mousemove', function (e) {
  // 函数内的代码, 不能随便执行
  // 鼠标按下以后, 才可执行
  // 一旦鼠标抬起以后, 代码不能执行
  if (flag === false) return

  // 实时获取坐标点
  // 为了让光标显示在盒子的中心位置, 让盒子向左上移动
  var x = e.clientX - ele.offsetWidth / 2
  var y = e.clientY - ele.offsetHeight / 2

  // 给 div 的 left 和 top 赋值
  ele.style.left = x + 'px'
  ele.style.top = y + 'px'
})
  </script>
  

拖拽案例(2)

   <style>
* {
  margin: 0;
  padding: 0;
}

div {
  width: 200px;
  height: 200px;
  background-color: skyblue;
  position: absolute;
}
  </style>
  
  <div></div>

  <script>
/*
  案例 - 拖拽

  需求:
    + 鼠标按下的时候, 可以跟着走
    + 随着鼠标移动, 让 div 跟随移动即可
    + 鼠标抬起的时候, 不能跟着走

  需求:
    + 光标 和 被移动元素 的相对位置不变
    + 核心: 随着 mousemove 事件, 实时拿到 光标的 移动距离
      => 求出光标终点 - 起点的差值
      => 光标的起点: 按下的瞬间
      => 光标的终点: 移动的实时
    + 赋值: 给 div 盒子的 left 和 top 赋值为 初始位置 + 本次移动距离
      => 初始位置: 按下的瞬间

*/

// 0. 获取元素
var ele = document.querySelector('div')

// 0. 准备一个开关
var flag = false
// 准备变量表示开始坐标点
var startX = 0, startY = 0
// 准备变量表示初始位置
var startLeft = 0, startTop = 0

// 绑定事件
ele.addEventListener('mousedown', function (e) {
  // 开启开关
  flag = true

  // 记录下按下时光标的坐标点
  startX = e.clientX
  startY = e.clientY

  // 记录下盒子的初始位置
  startLeft = ele.offsetLeft
  startTop = ele.offsetTop
})

ele.addEventListener('mouseup', function () {
  // 关闭开关
  flag = false
})

document.addEventListener('mousemove', function (e) {
  // 函数内的代码, 不能随便执行
  // 鼠标按下以后, 才可执行
  // 一旦鼠标抬起以后, 代码不能执行
  if (flag === false) return

  // 计算出光标的移动距离
  var x = e.clientX - startX + startLeft
  var y = e.clientY - startY + startTop

  // 给 div 赋值
  ele.style.left = x + 'px'
  ele.style.top = y + 'px'
})
  </script>
  
  

拖拽案例(3)边界值判断

 <style>
* {
  margin: 0;
  padding: 0;
}

div {
  width: 200px;
  height: 200px;
  background-color: skyblue;
  position: absolute;
}
  </style>
  
  <div></div>

  <script>
/*
  案例 - 拖拽

  需求:
    + 鼠标按下的时候, 可以跟着走
    + 随着鼠标移动, 让 div 跟随移动即可
    + 鼠标抬起的时候, 不能跟着走

  需求:
    + 光标 和 被移动元素 的相对位置不变
    + 核心: 随着 mousemove 事件, 实时拿到 光标的 移动距离
      => 求出光标终点 - 起点的差值
      => 光标的起点: 按下的瞬间
      => 光标的终点: 移动的实时
    + 赋值: 给 div 盒子的 left 和 top 赋值为 初始位置 + 本次移动距离
      => 初始位置: 按下的瞬间

  需求:
    + 边界值判断, 不允许出界
    + 判断一下 x 和 y 的值

*/

// 0. 获取元素
var ele = document.querySelector('div')

// 0. 准备一个开关
var flag = false
// 准备变量表示开始坐标点
var startX = 0, startY = 0
// 准备变量表示初始位置
var startLeft = 0, startTop = 0

// 绑定事件
ele.addEventListener('mousedown', function (e) {
  // 开启开关
  flag = true

  // 记录下按下时光标的坐标点
  startX = e.clientX
  startY = e.clientY

  // 记录下盒子的初始位置
  startLeft = ele.offsetLeft
  startTop = ele.offsetTop
})

ele.addEventListener('mouseup', function () {
  // 关闭开关
  flag = false
})

document.addEventListener('mousemove', function (e) {
  // 函数内的代码, 不能随便执行
  // 鼠标按下以后, 才可执行
  // 一旦鼠标抬起以后, 代码不能执行
  if (flag === false) return

  // 计算出光标的移动距离
  var x = e.clientX - startX + startLeft
  var y = e.clientY - startY + startTop

  // 在 拿到 和 使用 之间, 对 x 和 y 进行判断
  // 把 x 和 y 当做定位位置使用
  // 左边: x 最小是 0
  if (x <= 50) x = 0
  // 上边: y 最小是 0
  if (y <= 50) y = 0
  // 右边: x 最大是 窗口宽度 - 盒子宽度
  if (x >= document.documentElement.clientWidth - ele.offsetWidth - 50) x = document.documentElement.clientWidth - ele.offsetWidth
  // 下边: y 最大是 窗口高度 - 盒子高度
  if (y >= document.documentElement.clientHeight - ele.offsetHeight - 50) y = document.documentElement.clientHeight - ele.offsetHeight

  // 给 div 赋值
  ele.style.left = x + 'px'
  ele.style.top = y + 'px'
})
  </script>
  
  

五.事件对象内键盘相关信息

  事件对象内键盘相关信息
    + 按下的是哪一个按键
    + 按下的是否是组合按键

  按下的按键:
    + 语法: 事件对象.keyCode

  按下的按键(标准浏览器内才有):
    + 语法: 事件对象.key

  按下的是否是组合按键
    + 事件对象内有四个成员
      => shiftKey
      => ctrlKey
      => altKey
      => metaKey(win: win, mac: command)
    + 默认值是 false
      => 当按下的时候, 该值是 true

var inp = document.querySelector('input')

inp.onkeydown = function (e) {

  // 按下的是哪一个按键
  // console.log(e.keyCode)
  // if (e.keyCode === 27) {
  //   inp.value = ''
  // }

  // 按下的按键
  // console.log(e.key)

  // 判断是组合按键
  if (e.shiftKey === true && e.ctrlKey === true && e.altKey === true && e.keyCode === 65) {
    console.log('你按下的是 shift + ctrl + alt + a')
  }
}
 
 

六.案例 - 打字游戏

  <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
* {
  margin: 0;
  padding: 0;
}

.map {
  width: 800px;
  height: 400px;
  border: 3px solid orange;
  margin: 50px auto;
  position: relative;
  overflow: hidden;
}

.map > .word {
  width: 20px;
  height: 20px;
  background-color: skyblue;
  border-radius: 50%;
  line-height: 20px;
  text-align: center;
  position: absolute;
  top: -20px;
}

button {
  display: block;
  width: 200px;
  height: 30px;
  background-color: lightcoral;
  margin: 30px auto;
  cursor: pointer;
}
  </style>
</head>
<body>

  <button>开始游戏</button>

  <div class="map"></div>

  <div class="score">
目前的成绩是 : <h2></h2>
  </div>

  <script src="./utils.js"></script>
  <script>
/*
  案例 - 打字游戏
*/

// 0. 获取元素
var map = document.querySelector('.map')
var scoreBox = document.querySelector('.score > h2')
var startBtn = document.querySelector('button')

// 0. 准备变量
var str = 'abcdefghijklmnopqrstuvwxyz'
// 准备变量表示总定时器返回值
var t = 0
// 准备一个变量表示成绩
var score = 0

// 1. 准备一个函数
//    能生成一个 "字", 并且放在指定(top: -20px; left: 横向随机)的位置
function createWord() {
  // 1-1. 创建一个 div 标签
  var word = document.createElement('div')
  // 1-2. 设置 字 的样式
  word.className = 'word'
  // 1-3. 设置 字 的 left 值
  word.style.left = randomNum(0, map.offsetWidth - 20) + 'px'
  // 1-4. 设置 字 里面的 文本内容
  word.innerText = str[ randomNum(0, str.length - 1) ]
  // 设置随机背景颜色
  // word.style.backgroundColor = randomColor()
  // 1-5. 把这个 字 插入到 页面内
  map.appendChild(word)
  // 1-6. 开启一个定时器让他落下来
  var timer = setInterval(function () {
    // 拿到目前的偏移量 + 固定值
    var direction = word.offsetTop + 10
    // 再次赋值给 字
    word.style.top = direction + 'px'
    // 走到一个指定位置得 停下来
    if (direction >= map.offsetHeight) {
      // 让 map 内的所有正在下落的字停下来
      // 拿到 map 内所有的子元素, 拿到每一个的自定义属性 timer, 依次去关闭
      var words = [ ...map.children ]
      words.forEach(function (item) {
        clearInterval(item.dataset.timer - 0)
      })
      // 关闭总定时器
      clearInterval(t)

      // 提示结束游戏
      if (window.confirm('游戏结束, 是否重新开始游戏')) {
        // 代码执行到这里, 说明用户点击的是确定
        window.location.reload()
      }
    }
  }, 100)
  // 1-7. 把每一个字的定时器返回值, 以自定义属性的形式书写在标签身上
  word.dataset.timer = timer
}

// 2. 每间隔一段时间, 下落一个 字
function create() {
  t = setInterval(function () {
    createWord()
  }, 500)
}

// 3. 准备一个函数, 绑定键盘事件
function bindEvent() {
  document.addEventListener('keydown', function (e) {
    // 3-1. 每一次按下键盘, 拿到本次按下的内容
    var key = e.key

    // 3-2. 找到 map 的所有子元素内, 是否有一样的
    var words = [ ...map.children ]
    var info = words.find(function (item) { return item.innerText === key })

    // info 要么是一个 元素
    // info 要么是一个 undefined
    // 如果 !info 是一个 true, 说明 info 本身是一个 false
    // info 只有是 undefined 的时候, 本身才是 false

    // 判断如果没有, 什么也不用做
    if (!info) return

    // 如果有东西, 把当前这个定时器关掉
    clearInterval(info.dataset.timer - 0)

    // 把 info 移除页面
    info.remove()

    // 成绩 ++
    score++

    // 把成绩写入到标签内
    scoreBox.innerText = score
  })
}

// 4. 点击按钮开始游戏
startBtn.onclick = function () {
  create()
  bindEvent()

  // 一旦开始游戏, 让该按钮 隐藏或者禁用
  startBtn.disabled = true
}
  </script>
 
 

2022.10.22

一.事件传播

概念: 当行为发生的时候, 会按照 结构父级 的顺序, 依次向上传递该 事件行为

window先接收传给document传给html传给 事件目标

问题1: 如果 inner 元素身上没有绑定 click 事件, 点击 inner 的时候, center 级后续的元素是否还会触发事件 ?
+ 会触发, 传递的是事件行为

问题2: 如果给中间某一层没有 click 事件, 点击 inner 的时候, 是否还会继续传递到 window ?
+ 会, 传递的是事件行为

问题3: 把 inner 定位到 outer 的外面, 点击 inner 的时候, 是否还会传递到 center 和 outer ?
+ 会, 因为事件传播是按照结构父级的顺序传播

问题4: 当 p 定位到 center 内部的时候, 点击 p 的时候, 是否会传递到 center 和 outer 身上 ?
+ 不会, 因为事件传播是按照结构父级的顺序传播

二.事件流机制

事件流机制
+ 指: 事件的 捕获 目标 和 冒泡
+ 事件捕获: 在事件流中, 从 window 到 事件目标 的传递过程
+ 事件目标: 准确触发事件的元素
+ 事件冒泡: 在事件流中, 从 事件目标 到 window 的传递过程

事件流
+ 在 IE 低版本浏览器内, 只能在 冒泡阶段 触发事件, 不能在 捕获阶段 触发事件
+ 在标准浏览器中, 默认在 冒泡阶段 触发事件, 可选在 捕获阶段 触发事件

如何在捕获阶段触发事件(了解)
+ 必须是 DOM 2级 事件
+ 事件源.addEventListener('事件类型', 事件处理函数, 冒泡还是捕获)
=> 默认值是 false, 表示冒泡
=> 选填值是 true, 表示捕获

window > document >html >body > div > p
现在点击p p就是 事件目标
window-> document -> html -> body ->div ->p的过程是事件捕获
p -> div ->body ->html ->document->window 是事件冒泡

三.阻止事件传播

阻止事件传播
+ 语法:
=> 标准浏览器: 事件对象.stopPropagation()
=> IE 低版本: 事件对象.cancelBubble = true

案例-网页换肤

  需求:
    1. 点击 更换皮肤 按钮, ul 出现
    2. 点击 ul 内的 span, 让 ul 隐藏
    3. 点击 ul 内的每一个 img, 更换 body 的 backgroundImage 样式(ul 不能消失)
    4. 点击 网页其他 空白位置, 让 ul 隐藏

 <style>
* {
  margin: 0;
  padding: 0;
  list-style: none;
}

img {
  width: 100%;
  height: 100%;
  display: block;
  cursor: pointer;
}

ul {
  width: 900px;
  height: 430px;
  border: 5px solid #333;
  border-radius: 15px;
  background-color: #fff;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  display: none;
}

ul > li {
  width: 280px;
  height: 200px;
}

ul > span {
  font-size: 30px;
  font-weight: 700;
  position: absolute;
  right: 30px;
  top: 10px;
  cursor: pointer;
}

button {
  padding: 5px 15px;
  font-size: 20px;
}
</style>

<button>更换皮肤</button>

<ul class="box">
<span>X</span>
<li><img draggable="false" src="./imgs/001.webp" alt=""></li>
<li><img draggable="false" src="./imgs/002.webp" alt=""></li>
<li><img draggable="false" src="./imgs/003.webp" alt=""></li>
<button>上一页</button>
<button>下一页</button>
</ul>

  <script>

// 0. 获取元素
var btn = document.querySelector('button')
var box = document.querySelector('.box')
var closeBtn = box.querySelector('span')
var imgs = box.querySelectorAll('img')


//  1. 点击 更换皮肤 按钮, ul 出现
btn.onclick = function (e) {
  // 阻止事件传播
  e.stopPropagation()

  // 让 ul 出现
  box.style.display = 'flex'
}

// 2. 点击 ul 内的 span, 让 ul 隐藏
closeBtn.onclick = function () {
  // 让 ul 隐藏
  box.style.display = 'none'
}

// 3. 点击 ul 内的每一个 img, 更换 body 的 backgroundImage 样式(ul 不能消失)
imgs.forEach(function (item) {

  // 给每一个 img 标签绑定事件
  item.onclick = function () {
    // 拿到当前点击的这个 img 标签的 src 属性, 赋值给 body 的 backgroundImage
    var link = this.src

    document.body.style.backgroundImage = `url(${ link })`
  }

})

// 4. 点击 网页其他 空白位置, 让 ul 隐藏
document.onclick = function () {
  // 让 ul 隐藏
  box.style.display = 'none'
}

// 5. 在 ul 的 click 事件内, 阻止事件传播
box.onclick = function (e) {
  e.stopPropagation()
}

 </script>
 

四.事件委托

事件委托
+ 利用事件传播的机制, 来实现某些效果
+ 指: 把子元素的事件委托给父元素来绑定, 在父元素的事件内, 通过事件目标来判断你准确触发事件的元素

如何拿到本次事件的事件目标
+ 语法: 事件对象.target
+事件对象.target.tagName 拿到的标签名是全大写

循环绑定事件
+ 缺点1: 对于后期动态操作的元素不够友好
+ 缺点2: 大量的 DOM 操作

事件委托
+ 优点1: 对于后期动态操作的元素足够友好
+ 优点2: 减少 DOM 操作

五.阻止默认行为

默认行为
    + 不需要你进行事件绑定, 天生自带的一些行为
    + a 标签的点击行为
    + form 标签的表单提交
    + 鼠标右键的单击行为
    + ...

  阻止默认行为
    + 注意: 需要在同类型事件内进行阻止
    + 标准浏览器: 事件对象.preventDefault()
    + IE 低版本: 事件对象.returnValue = false
    + 通用: return false
    
    

六.案例 - 本地模拟购物车

css部分
* {
margin: 0;
padding: 0;
}

.container {
width: 1200px;
margin: 0 auto;
}

.header, .footer {
background-color: skyblue;
color: #fff;
font-weight: 700;
font-size: 50px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
}

  .footer {
height: 300px;
  }

  .content {
margin: 10px auto;
}

.content > .empty {
height: 600px;
display: flex;
justify-content: center;
align-items: center;
}

.content > .full {
display: flex;
flex-direction: column;
font-size: 20px;
}

.content > .full > .top,
.content > .full > .bottom {
height: 40px;
background-color: orange;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-sizing: border-box;
}

.content > .full > .top {
justify-content: flex-start;
}

.content > .full > .top > input {
width: 30px;
height: 30px;
margin: 0 15px;
}

.content > .full > .bottom > .btns > button {
font-size: 20px;
padding: 0 10px;
cursor: pointer;
}

.content > .full > .bottom > .totalPrice > span {
font-weight: 700;
font-size: 24px;
color: red;
}

.content > .full > .center {
padding-top: 10px;
}

.content > .full > .center > li {
width: 100%;
height: 100px;
margin-bottom: 10px;
border: 1px solid #333;
box-sizing: border-box;
list-style: none;
display: flex;
}

.content > .full > .center > li > div {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
border-right: 1px solid #333;
}

.content > .full > .center > li > div:last-child {
border: none;
}

.content > .full > .center > li > .select,
.content > .full > .center > li > .show,
.content > .full > .center > li > .destory {
width: 100px;
}

.content > .full > .center > li > .price,
.content > .full > .center > li > .number,
.content > .full > .center > li > .subPrice {
width: 200px;
}

.content > .full > .center > li > .title {
flex: 1;
box-sizing: border-box;
padding: 5px;
justify-content: flex-start;
align-items: flex-start;
}

.content > .full > .center > li > .price > span,
.content > .full > .center > li > .subPrice > span {
color: red;
font-size: 22px;
font-weight: 700;
}

.content > .full > .center > li > .destory > button {
padding: 5px 10px;
background-color: red;
color: #fff;
border: none;
outline: none;
cursor: pointer;
}

.content > .full > .center > li > .number > button {
width: 30px;
height: 30px;
cursor: pointer;
}

.content > .full > .center > li > .number > input {
width: 50px;
border: none;
outline: none;
text-align: center;
font-size: 20px;
}

.content > .full > .center > li > .show > img {
width: 100%;
height: 100%;
display: block;
}

html部分

<div class="header container">啥都有 · 购物车</div>
<div class="content container">
<!-- 空购物车的状态 -->
<!-- <div class="empty">
  <img src="https://img2.baidu.com/it/u=2075058702,1217821591&fm=253&app=138&size=w931&n=0&f=PNG&fmt=auto?sec=1666458000&t=76fe109491811f29b7b6b9ad44301195" alt="">
</div> -->
<div class="full">
  <div class="top">
    全选 : <input type="checkbox">
  </div>
  <ul class="center">
    <li>
      <div class="select">
        <input type="checkbox">
      </div>
      <div class="show">
        <img src="https://img0.baidu.com/it/u=3414885164,1584682578&fm=253&fmt=auto&app=120&f=JPEG?w=200&h=200" alt="">
      </div>
      <div class="title">
        商品介绍
      </div>
      <div class="price">
        <span>¥ 100.00</span>
      </div>
      <div class="number">
        <button>-</button>
        <input type="text" value="1">
        <button>+</button>
      </div>
      <div class="subPrice">
        <span>¥ 100.00</span>
      </div>
      <div class="destory">
        <button>删除</button>
      </div>
    </li>
  </ul>
  <div class="bottom">
    <div class="total">
      总计 : 0 件商品
    </div>
    <div class="btns">
      <button>清空购物车</button>
      <button>去支付</button>
      <button>删除所有已选中商品</button>
    </div>
    <div class="totalPrice">
      总价 : ¥ <span>100.00</span>
    </div>
  </div>
</div>
</div>

 <div class="footer container">网页底部</div>
 
JS部分
// 0. 准备数据
var cart = [{ id: 1, title: '我是一个手机', price: 100, buy: 2, number: 16, is_select: true, pic:  'https://img0.baidu.com/it/u=3414885164,1584682578&fm=253&fmt=auto&app=120&f=JPEG?w=200&h=200' },{ id: 2, title: '我是一个电脑', price: 33.56, buy: 1, number: 13, is_select: false, pic: 'https://img0.baidu.com/it/u=52910157,628543061&fm=253&fmt=auto&app=120&f=JPEG?w=200&h=200' },{ id: 3, title: '我是一个二级', price: 78.92, buy: 3, number: 8, is_select: true, pic: 'https://img1.baidu.com/it/u=486689227,4095944776&fm=253&fmt=auto&app=120&f=JPEG?w=200&h=200' } ]

// cart = []

// 0. 获取元素
var contentBox = document.querySelector('.content')

// 1. 渲染页面
bindHtml()
function bindHtml() {
  // 如果 cart.length 不为 0, 说明购物车有内容, 需要渲染的 full 标签
  // 如果 cart.length 为 0, 说明购物车没有内容, 需要渲染的 empty 标签

  if (!cart.length) {
    // 说明是空的把
    // 代码能执行到这里, 说明 !cart.length 是一个 true
    // 说明 cart.length 本身是 false
    contentBox.innerHTML = `
      <div class="empty">
        <img src="https://img2.baidu.com/it/u=2075058702,1217821591&fm=253&app=138&size=w931&n=0&f=PNG&fmt=auto?sec=1666458000&t=76fe109491811f29b7b6b9ad44301195" alt="">
      </div>
    `
    return
  }

  // 代码能执行到这里, 说明 cart.length 不为 0
  // 全选按钮是否选中
  //   如果所有数据中 is_select 都是 true, 那么有 checked
  //   只要任何一条数据的 is_select 是 false, 那么没有 checked
  // 总件数
  //   统计所有 is_select 为 true 的数据的 buy 的 和
  // 总价格
  //   统计所有 is_select 为 true 的数据的 price 的 和
  var totalBuy = 0, totalPrice = 0, totalSelect = 0
  cart.forEach(function (item) {
    // 什么情况下才 叠加, is_select 为 true
    if (item.is_select) {
      totalSelect++ // 叠加有多少种选中的
      totalBuy += item.buy // 统计总购买数量
      totalPrice += item.price * item.buy // 统计总购买价格
    }
  })

  var str = `
    <div class="full">
      <div class="top">
        全选 : <input class="toggle-all" type="checkbox" ${ cart.length === totalSelect ? 'checked' : '' }>
      </div>
      <ul class="center">
  `

  // 根据 cart 来决定渲染多少个 li
  cart.forEach(function (item) {
    str += `
      <li>
        <div class="select">
          <input data-id="${ item.id }" class="toggle" type="checkbox" ${ item.is_select ? 'checked' : '' }>
        </div>
        <div class="show">
          <img src="${ item.pic }" alt="">
        </div>
        <div class="title">
          ${ item.title }
        </div>
        <div class="price">
          <span>¥ ${ item.price.toFixed(2) }</span>
        </div>
        <div class="number">
          <button data-id="${ item.id }" class="sub">-</button>
          <input type="text" value="${ item.buy }">
          <button data-id="${ item.id }" class="add">+</button>
        </div>
        <div class="subPrice">
          <span>¥ ${ (item.price * item.buy).toFixed(2) }</span>
        </div>
        <div class="destory">
          <button data-id="${ item.id }" class="del">删除</button>
        </div>
      </li>
    `
  })

  str += `
      </ul>
      <div class="bottom">
        <div class="total">
          总计 : ${ totalBuy } 件商品
        </div>
        <div class="btns">
          <button class="clear">清空购物车</button>
          <button class="pay" ${ totalSelect === 0 ? 'disabled' : '' }>去支付</button>
          <button class="clear-completed" ${ totalSelect === 0 ? 'disabled' : '' }>删除所有已选中商品</button>
        </div>
        <div class="totalPrice">
          总价 : ¥ <span>${ totalPrice.toFixed(2) }</span>
        </div>
      </div>
    </div>
  `

  contentBox.innerHTML = str
}

// 2. 绑定事件
// 发现:
//   所有需要操作的事件都是 click 行为
//   并且有共同的父级结构, contentBox
//   所有元素 click 的时候都会 冒泡 传递到 contentBox 身上
//   事件委托
contentBox.addEventListener('click', function (e) {

  // 2-1. 判断点击的是 全选
  if (e.target.className === 'toggle-all') {
    // 我自己的状态是什么, 就需要给 cart 中所有数据的 is_select 设置为什么
    var state = e.target.checked
    // 遍历 cart 赋值
    cart.forEach(function (item) { item.is_select = state })
    // 重新渲染页面
    bindHtml()
  }

  // 2-2. 判断点击的是 清空购物车
  if (e.target.className === 'clear') {
    // 把 cart 内的所有内容都删除
    cart.length = 0
    // 重新渲染页面
    bindHtml()
  }

  // 2-3. 判断点击的是 去支付
  if (e.target.className === 'pay') {
    window.location.href = 'https://www.jd.com'
  }

  // 2-4. 判断点击的是 删除所有已选中商品
  if (e.target.className === 'clear-completed') {
    // 把 cart 内所有 is_select 为 true 的删除掉
    // 把 cart 内所有 is_select 为 false 的留下
    // 利用 filter 方法
    cart = cart.filter(function (item) { return item.is_select === false })
    // 重新渲染页面
    bindHtml()
  }

  // 2-5. 判断点击的是 每一项的 选中按钮
  if (e.target.className === 'toggle') {
    // 问题: 你要修改哪一条数据 ?
    //   在你渲染页面的时候, 把 该数据的 id 渲染在 标签身上
    //   点击的时候, 拿到 你点击的这个元素 身上一个叫做 data-id 的自定义属性的值, 就知道修改哪一个数据了 ?
    var id = e.target.dataset.id - 0
    // 从 cart 数组内找到 id 对应的那一条数据
    var info = cart.find(function (item) { return item.id === id })
    // 修改数据
    info.is_select = !info.is_select
    // 从新渲染页面
    bindHtml()
  }

  // 2-6. 判断点击的是 每一项的 删除按钮
  if (e.target.className === 'del') {
    // 拿到要删除的数据的 id
    var id = e.target.dataset.id - 0
    // 从 cart 内删除 id 对应的数据, 其实就是把 id 不一样的留下
    cart = cart.filter(function (item) { return item.id !== id })
    // 从新渲染页面
    bindHtml()
  }

  // 2-7. 判断点击的是 每一项的 增加按钮
  if (e.target.className === 'add') {
    // 拿到要修改的数据的 id
    var id = e.target.dataset.id - 0
    // 从 cart 内找到对应的数据
    var info = cart.find(function (item) { return item.id === id })
    // 判断是不是真的要 +
    if (info.buy >= info.number) return
    // 购买数量 ++
    info.buy++
    // 从新渲染页面
    bindHtml()
  }

  // 2-8. 判断点击的是 每一项的 减少按钮
  if (e.target.className === 'sub') {
    // 拿到要修改的数据的 id
    var id = e.target.dataset.id - 0
    // 从 cart 内找到 id 对应的数据
    var info = cart.find(function (item) { return item.id === id })
    // 判断是否真的要 减少
    if (info.buy <= 1) return
    // 操作购买数量减少
    info.buy--
    // 从新渲染页面
    bindHtml()
  }
})

2022.10.24

一.this指向

 this 指向
    + 是一个使用在作用域内的关键字
    + 要么使用在全局作用域
      => 比较少使用在全局
      => 使用在全局的时候, this 指向 window
    + 要么使用在私有作用域(函数内)
      => 变化比较大, 用处比较多

  函数内的 this 指向(熟读并背诵全文)
    + 核心: 不管函数怎么定义, 不管函数在哪定义, 只看函数的调用方式(箭头函数除外)
    + 总结:
      1. 基本调用(普通调用)
        => 函数名()
        => this 指向 window
      2. 对象调用
        => 对象名.函数名()
        => this 指向 点前面的内容(点前面是谁就是谁)
      3. 定时器调用
        => setTimeout(函数, 数字)
        => setInterval(函数, 数字)
        => this 指向 window
      4. 事件处理函数调用
        => 事件源.on事件类型 = 函数
        => 事件源.addEventListener('事件类型', 函数)
        => this 指向 事件源(绑定在谁身上的事件)
      5. 未完待续...
      
      

二.强行改变this指向

 强行改变 this 指向
    + 不管你本身指向哪里, 我让你指向哪里就得指向哪里

  1. call
    => 语法: 直接连接在函数名后面使用
      -> 函数名.call()
      -> 对象名.函数名.call()
    => 参数:
      -> 第一个参数: 该函数内的 this 指向
      -> 第二个参数开始: 依次给该函数进行形参赋值
    => 特点: 立即调用函数

  2. apply
    => 语法: 直接连接在函数名后面使用
      -> 函数名.apply()
      -> 对象名.函数名.apply()
    => 参数:
      -> 第一个参数: 该函数内的 this 指向
      -> 第二个参数: 是一个数组或者伪数组都行, 按照顺序依次给该函数进行形参赋值
    => 特点: 立即调用函数

  3. bind
    => 语法: 直接连接在函数名后面使用
      -> 函数名.bind()
      -> 对象名.函数名.bind()
    => 参数:
      -> 第一个参数: 该函数内的 this 指向
      -> 第二个参数开始: 依次给该函数进行形参赋值
    => 特点: 不会立即调用函数
      -> 而是返回一个一模一样的新函数
      -> **一个锁死了 this 指向的新函数**

  通常你需要立即调用的函数, 使用 call 和 apply 来改变 this
  你需要不是立即调用的函数(定时器处理函数, 事件处理函数), 使用 bind 来改变 this
  
  

三.认识ES6

认识 ES6
+ ECMAScript 语法的发展过程中的一个版本
+ 官方: ES2015
+ 一些语法上的更新和迭代

1.ES6 定义变量

+ 在 ES6 新增了两个定义变量的关键字
      => let      变量
      => const    常量

  let/const 和 var 的区别
    1. 预解析
      => var 有预解析, 可以先使用后定义
      => let/const 没有预解析, 必须先定义后使用
    2. 重复变量名
      => var 可以定义重复变量名
      => let 和 const 不允许重复变量名
    3. 块级作用域
      => var 没有块级作用域, 只有函数能限制其使用范围
      => let/const 有块级作用域, 任何一个可以执行代码的大括号, 都能限制其使用范围

  let 和 const 的区别
    4. 修改值的问题
      => let 定义的变量值可以修改
      => const 定义的常量值不可以被修改
    5. 初始化赋值的问题
      => let 可以在初始化的时候不赋值
      => const 初始化的时候必须赋值, 并且一经赋值不能修改

  变量命名规范
    + 定义的过程中, 能用 常量 尽量使用 常量
    + 常量的定义过程中, 字母全大写
    

2.ES6 的箭头函数

ES6 的箭头函数
    + ES6 语法中对 函数表达式(赋值式函数) 的简写形式
      => var fn = function () {}
      => setTimeout(function () {}, 1000)
      => xxx.onclick = function () {}
      => xxx.forEach(function () {})
      => ...
    + 语法: () => {}
      ()   书写形参的位置
      =>   箭头函数的标志
      {}   书写函数体的位置

  箭头函数的特点:
    1. 可以省略 小括号 不写
      => 当你的形参只有一个的时候, 可以省略小括号不写
    2. 可以省略 大括号 不写
      => 当你的代码只有一句话的时候, 可以省略大括号不写
      => 并且该函数会自动把这一句话的结果当做该函数的返回值
    3. 箭头函数没有 this
      => 箭头函数内的 this 就是外部作用域的 this
      => 私人: 你把该函数删除, this 是谁, 加上箭头函数 this 就是谁
    4. 箭头函数没有 arguments
    5. 箭头函数不能当做构造函数使用
    

3.ES6 的函数参数默认值

/*
  ES6 的函数参数默认值
    + 给函数的形参设置一个默认值, 当你没有传递实参的时候, 直接使用默认值
    + 如果你传递了实参, 那么就使用你传递的
    + 实现: 在定义函数的时候, 直接给形参使用 赋值符号(=) 赋值即可
*/

// 函数参数默认值
function fn(a = 10, b = 20) {
  // 当你没有给形参 a 赋值的时候, a 就是 10
  // 当你没有给形参 b 赋值的时候, b 就是 20
  console.log('a : ', a)
  console.log('b : ', b)
}

fn()
fn(100)
fn(1000, 2000)

4.ES6 的模板字符串

 /*
  ES6 的模板字符串
    + 在 ES6 语法中, 以 反引号(``) 来定义字符串
    + 特点1: 可以换行书写
    + 特点2: 可以直接解析变量, 当你需要解析变量的时候, 可以书写 ${ 变量 }
*/

5.ES6 的解构赋值

  ES6 的解构赋值
    + 目的: 快速从数组或者对象中获取成员
    + 解构数组
    + 解构对象
    
    /*
  解构数组
    + 语法: var 解构 = 数组
           var [ 变量1, 变量2, ... ] = 数组
    + 按照索引, 分别对该变量进行赋值
    + 多维数组解构
      => 数组怎么书写, 解构怎么书写, 数据换成变量
*/

// 解构数组
var arr = [ 100, 200, 300, 400 ]
var a = arr[0]
var b = arr[1]
var c = arr[2]
var d = arr[3]
// 尝试解构
var [ a, b, c ] = arr
console.log(a, b, c)
// 多维数组解构
var arr = [ 100, 200, [ 300, 400, [ 500, 600, [ 700, 800, [ 900 ] ] ] ] ]
var [ a, b, [ c, d, [ e, f, [ g, h, [ i ] ] ] ] ] = arr
console.log(e, i)



/*
  解构对象
    + 语法: var 解构 = 对象
           var { 同名key, ... } = 对象
    + 得到: 按照 同名key 去定义变量并且获取对象中的成员
  解构对象时候起一个别名
    + 语法: var { 同名key: 别名 } = 对象
  解构多维对象
    + 对象怎么写, 解构怎么写, 把值去掉即可
*/

// 解构对象
var obj = { name: 'Jack', age: 18, gender: '男' }
var name = obj.name
var age = obj.age
var gender = obj.gender
console.log(name, age, gender)
// 尝试解构
// 等价于 var age = obj.age
// 定义了一个叫做 age 的变量, 获取的是 obj 内一个叫做 age 的成员的值
var { gender, age, name, classRoom } = obj
console.log(age, name, gender, classRoom)
// 别名
var n = obj.name
console.log(n)
// 定义了一个叫做 n 的变量, 获取的值是 obj 内叫做 n 的 key 的值
var { n } = obj
// 定义了一个叫做 n 的变量, 获取的值是 obj 内叫做 name 的 key 的值
// 等价于 var n = obj.name
var { name: n } = obj
console.log(n)
var { age: a } = obj
console.log(a)
console.log(age)
var { name: n } = obj
console.log(n)
console.log(name)

// 解构多维对象
var obj = {
  name: 'Jack',
  age: 18,
  info: {
    weight: 180,
    height: 180,
    desc: {
      message: '你好世界',
      city: {
        address: '北京'
      }
    }
  },
  hobby: [ '足球', '篮球', '羽毛球' ]
}
// 尝试解构
var {
  name,
  age,
  info: {
    weight,
    height,
    desc: {
      message,
      city: {
        address: ad
      }
    }
  },
  hobby: [ a, b, c ]
} = obj
console.log(ad)

6.ES6 的对象简写语法

 /*
  ES6 的对象简写语法
    + ES6 语法内对于书写对象的简写形式

  1. 当 key 和 value 一模一样的时候, 并且 value 是一个变量的时候
    => 可以省略一个不写
  2. 当 value 值是一个 函数, 并且不是箭头函数的时候
    => 可以省略 function 关键字和 冒号(:) 不写
*/
// 简写1:
// var day = 12
// var hours = 13
// var minutes = 14
// var seconds = 15
// var obj = {
//   day, // 等原与 day: day
//   hours,
//   minutes,
//   seconds,
//   week: 'week'
// }
// console.log(obj)


// 简写2:
// var obj = {
//   f1: function () {}, // 这样的函数可以简写
//   f2: () => {
//     console.log(this)
//   }, // 这个函数不能在简写了
//   f3 () {
//     console.log(this)
//   }
// }

// obj.f3()
// obj.f2()

7.ES6 的扩展运算符 (...)

 + 含义1: 展开(数据结构内, 函数的实参)
 + 含义2: 合并(函数的形参, 解构位置)
 
 // 展开
// var a1 = [ 100, 200 ]
// var a2 = [ 300, 400 ]
// var arr = [ ...a1, '随便', ...a2 ]
// console.log(arr)

// var o1 = { name: 'Jack' }
// var o2 = { name: 'Rose', age: 18 }
// var obj = {
//   ...o1,
//   ...o2,
//   name: '张三'
// }
// console.log(obj)

// function fn(a, b, c) {
//   console.log(a, b, c)
// }
// var arr = [ 100, 200, 300 ]
// fn(...arr)


// 合并
// 在形参中使用 合并 的时候
// 该运算符必须要写在最后一个形参位置, 而且只能使用一次
// function fn(a, c, ...b) {
//   // 把第一个实参赋值给了 a
//   // 把从第二个实参开始到末尾的所有实参, 放在一个数组内给到 b
//   console.log(a)
//   console.log(c)
//   console.log(b)
// }
// fn(100, 200, 300, 400, 500, 600)


// var arr = [ 100, 200, 300, 400, 500, 600 ]
// 在解构中使用 合并 的时候
// 该运算符必须要写在最后一个形参位置, 而且只能使用一次
// var [ a, c, ...b ] = arr
// console.log(a)
// console.log(c)
// console.log(b)

8.ES6 的模块化开发

 /*
  ES6 的模块化开发
    + 指: 把一个完整逻辑, 按照功能拆开成为一部分一部分的内容, 在合并起来

  小问题:
    + 按照什么来区分的模块 ? 文件
      => 每一个 js 文件叫做一个 独立的文件模块
    + 当按照模块化语法开始书写代码的时候
      => 每一个 js 文件都相当于一个 独立的 模块作用域(文件作用域)
      => 写在 a.js 文件内的内容, 不能在 b.js 文件内使用的
      => 问题: 多个文件之间的内容不互通

  导入 / 导出
    + 目的: 就是为了 a.js 文件内书写的内容可以在 b.js 文件内使用

  导出
    + 在当前文件内进行导出的时候
    + 把该文件内的内容向外暴露
    + 语法1:
      => export default 数据
      => 在一个文件内只能使用一次, 而且只能导出一个数据
    + 语法2:
      => export 定义变量 = 值
      => 在一个文件内可以使用多次

  导入
    + 当你导入另一个 js 文件的时候, 接受一下该文件导出的内容
    + 语法1:
      => import 变量名 from '文件地址'
      => 变量 接受的就是 该 文件地址 内导出的内容
    + 语法2:
      => import { 变量, ... } from '文件地址'
      => 按照该文件地址内导出的内容去接受内容

  注意: 如果你想使用模块化语法
    + script 标签必须有一个 type 属性值为 module
    + 当前页面必须在 服务器 上打开, 本地打开不能使用(live server)
    + 导出语法1 对应 导入语法1
    + 导出语法2 对应 导入语法2
    +合并导入语法:
      import *as 变量名 from '文件地址'
      import 变量名 ,{变量名,....} from '文件地址'
*/

2022.10.26

一.认识正则表达式

认识正则表达式
    + 正则也是 JS 内的一种数据类型, 是一个复杂数据类型
    + 叫做 Regular Expression
    + 作用: 自己书写一个规则, 来 验证字符串是否符合规则 或者 从一段字符串内获取一段满足要求的字符串片段

  创建正则表达式
    1. 字面量方式创建
      => 语法: var reg = /符号/
    2. 内置构造函数方式创建
      => 语法: var reg = new RegExp('符号')

  两种创建方式的区别
   1. 语法书写
    => 字面量: var reg = /元字符/标识符
    => 内置构造函数: var reg = new RegExp('元字符', '标识符')

  2. 字符串拼接
    => 字面量不接受拼接字符串
    => 内置构造函数接受拼接字符串(因为第一个参数就是以字符串的形式传入正则符号)

  3. 书写基础元字符
    => 字面量直接书写基础元字符即可, \s\d\w
    => 内置构造函数方式需要书写双斜线 \\s\\d\\w
    

二.正则表达式的常用方法

1. 匹配
    + 作用: 验证字符串是否符合规则
    + 语法: 正则.test(字符串)
    + 返回值: 一个布尔值
      => true, 说明该字符串符合正则规则
      => false, 说明该字符串不符合正则规则

  2. 捕获
    + 作用: 从完整字符串内捕获出一段符合规则的字符串片段
     + exec 捕获
    + 作用: 从完整字符串内捕获出符合要求的字符串片段
    + 语法: 正则.exec(字符串)
    + 返回值:
      => 字符串内没有符合正则规则的字符串片段, 此时返回值就是 null
      => 字符串内有符合正则规则的字符串片段
        -> 返回值必然是一个数组
        -> [0] 位置就是找到的第一个满足要求的字符串片段
        -> 只能找到第一个满足要求的字符串片段
        -> 每一次的检测, 都是从 [0] 开始向后检索
        -> 标识符 g 的作用
          + 如果正则没有全局标识符 g, 那么每一次的检测都是从 [0] 开始检索
          + 如果正则有全局标识符 g, 那么第二次检索是从第一次的结束位置开始向后检索, 直到找不到为止, 返回 null, 再下一次检索又从 [0] 开始检索
        -> () 的作用
          + 如果没有 (), 那么 [1] 开始不会有内容
          + 如果有 (), 那么从 [1] 开始依次是每一个小括号内的内容


  扩展
    + 匹配但不捕获
      => 当你指向要 小括号的 整体作用, 不需要单独捕获的时候
      => 书写 (?:)
    + 起名小分组
      => (?<变量名>)
      => 会在返回值的 groups 内单独添加一个该成员
      

三.基本元字符

正则的组成
    + 符号 和 文本
    + 符号:
      => 元字符(基础元字符 / 边界符 / 限定符 / ...)
      => 标识符

  基本元字符
    + \d      表示 一位 数字(0-9)
    + \D      表示 一位 非数字
    + \s      表示 一位 空白内容(空格 / 制表符)
    + \S      表示 一位 非空白内容
    + \w      表示 一位 数字字母下划线(0-9a-zA-Z_) 任意一个就行
    + \W      表示 一位 非数字字母下划线内容
    + .       表示 非换行 的任意内容
    + \       转义符
      => 把有意义的符号转换成没有意义的文本
      => 把没有意义的文本转换成有意义的符号
      

四.边界符

  /*
  边界符

  1. ^      表示开头
  2. $      表示结尾

  注意: 当 ^ 和 $ 一起使用的时候, 表示 从 开头 到 结尾
    => 当你书写正则表达式的时候
    => 如果没有 ^ 和 $ 表示的意义是 "包含"
    => 当你 ^ 和 $ 一起使用的时候, 表示的意义是 "只能"
*/


// 含义: 表示 包含 一位数字
// let reg = /\d/


// 1. ^
// 含义: 表示 以 一位数字 开头
// let reg = /^\d/

// 2. $
// 含义: 表示 以 一位数字 结尾
// let reg = /\d$/


// 含义: 表示从开头到结尾只能有一位数字组成
let reg = /^\d$/

console.log(reg.test('123'))
console.log(reg.test('1a3'))
console.log(reg.test('13'))
console.log(reg.test('3'))

五.限定符

 限定符
    + 作用: 限定出现次数使用
    + 注意: 一个限定符只能描述前面一个符号的出现次数

  1. *      表示 0 ~ 多次
  2. +      表示 1 ~ 多次
  3. ?      表示 0 ~ 1 次
  4. {n}    表示限定 n 次
  5. {n,}   表示限定 n ~ 多次
    => {0,} 等价于 *
    => {1,} 等价于 +
  6. {n,m}  表示限定 n ~ m 次
    => {0,1} 等价于 ?
    

六.特殊符号

 特殊符号

  1. ()
    + 含义1: 单独捕获(欠着)
    + 含义2: 一个整体

  2. |
    + 含义: 或
    + 注意: 或 的边界, 要么是 下一个 |, 要么是 () 边界, 要么是正则边界

  3. []
    + 含义: 包含
    + 注意: 一个 [] 必定只占一个字符位置, 表示任意一个都行

  4. [^]
    + 含义: 非
    + 注意: 一个 [^] 必定只占一个字符位置, 表示任意一个都不行

  5. -
    + 含义: 至 到
    + 一般和 [] 还有 [^] 连用, 表示 unicode 编码连续的
      => [0-9]  等价于  \d
      => [^0-9] 等价于  \D
      => [a-zA-Z0-9_] 等价于 \w
      => [^a-zA-Z0-9_] 等价于 \W
    + 特殊: [\u4e00-\u9fa5] 表示所有中文内容
*/


// 1. ()
// {2} 修饰的是 () 这个整体内容
// let reg = /^(abc){2}$/
// console.log(reg.test('abcabc'))
// console.log(reg.test('abcc'))


// 2. |
// let reg = /^12(a|b)34$/
// console.log(reg.test('12a34'))
// console.log(reg.test('12b34'))
// console.log(reg.test('12c34'))
// console.log(reg.test('1234'))

// 小练习
// let reg = /^(a|b|c)$/
// console.log(reg.test('a'))
// console.log(reg.test('b'))
// console.log(reg.test('c'))
// console.log(reg.test('c1'))
// console.log(reg.test('aa'))

// let reg = /^a|b|c$/
// console.log(reg.test('aaaa'))
// console.log(reg.test('abbb'))
// console.log(reg.test('abbbc'))
// console.log(reg.test('55666b1234'))
// console.log(reg.test('da123123123c'))


// {2} 修饰的是 () 这个整体
// 只要 整体 出现两次即可, 没有要求两次必须一样
// 整体 可以是 'abc' 也可以是 'def'
// let reg = /^(abc|def){2}$/
// console.log(reg.test('abcabc'))
// console.log(reg.test('defdef'))
// console.log(reg.test('abcdef'))
// console.log(reg.test('defabc'))


// 3. []
// 表示该字符串只能由一个字符组成, 这个字符可以是 a 或者 b 或者 c 或者 d
// let reg = /^[abcd]$/
// console.log(reg.test('a'))
// console.log(reg.test('b'))
// console.log(reg.test('c'))
// console.log(reg.test('d'))
// console.log(reg.test('e'))
// console.log(reg.test('aa'))
// let reg = /^[100]$/
// console.log(reg.test('1'))
// console.log(reg.test('0'))
// console.log(reg.test('100'))


// 4. [^]
// 表示该字符串只能由一个字符组成, 只要不是 a 不是 b 不是 c 不是 d 都可以
// let reg = /^[^abcd]$/
// console.log(reg.test('ef'))
// console.log(reg.test('e'))
// console.log(reg.test('f'))


// 5. -
let reg = /^[a-z]$/
console.log(reg.test('a'))
console.log(reg.test('m'))
console.log(reg.test('x'))

七.重复符号

  /*
  重复符号
    + \1 \2 \3 \4 \5 \6 \7 \8 \9
    + 意义: 表示重复一个第 n 个小括号一模一样的内容
    + 注意: 这个数字不是多少遍, 是第几个小括号
*/


// 表示把 () 内容重复两遍
// let reg = /^(abc|def){2}$/

// 表示在 \1 位置出现一个和 () 位置出现的内容一模一样的才行
// let reg = /^(abc|def)\1$/
// console.log(reg.test('abcabc'))
// console.log(reg.test('defdef'))
// console.log(reg.test('abcdef'))
// console.log(reg.test('defabc'))



// let str = '<span></span>'
// 使用转义符(\) 把有意义的(/) 转换成没有意义的'/' 文本
// let reg = /^<(span|p)>.*<\/\1>$/
// console.log(reg.test(str))


// let reg = /^(ab|cd)(11|22)\2\1$/
// console.log(reg.test('cd1111cd'))
// console.log(reg.test('cd2222cd'))
// console.log(reg.test('ab1111ab'))
// console.log(reg.test('ab2222ab'))

八.标识符

 /*
  标识符
    + 书写在正则的外面, 用来修饰整个正则表达式使用的

  1. i
    + 表示 忽略大小写

  2. g
    + 表示 若正则无标识符g 第一次从索引0开始检测
    有全局 g 第二次从第一次的结束位置开始向后检索直到找不到
*/

// let reg = /^[abc]$/

// i 表示该正则在检测字符串的时候, 忽略大小写不计
let reg = /^[abc]$/i


console.log(reg.test('a'))
console.log(reg.test('b'))
console.log(reg.test('c'))
console.log(reg.test('A'))

// 全局标识符 g 的作用
let reg = /\d{3}/g
console.log(reg.exec('aaa123bbb456ccc789ddd012eeee'))
console.log(reg.exec('aaa123bbb456ccc789ddd012eeee'))
console.log(reg.exec('aaa123bbb456ccc789ddd012eeee'))
console.log(reg.exec('aaa123bbb456ccc789ddd012eeee'))
console.log(reg.exec('aaa123bbb456ccc789ddd012eeee'))
console.log(reg.exec('aaa123bbb456ccc789ddd012eeee'))

// 小括号的作用
let reg = /(\d{2})(\d{2})(\d{2})(?<year>\d{4})(?<month>\d{2})(?<date>\d{2})\d{2}(\d)(?:\d|x)/
let res = reg.exec('你好, 我是前端小灰狼, 我的身份证号是 11010820050223001x, ^_^')
console.log(res)

九.正则的两大特性

  /*
  正则的两大特性

  1. 懒惰性
    + 每次检索的时候, 都会从 [0] 开始对字符串进行检索
    + 解决: 加一个全局标识符 g

  2. 贪婪性
    + 当我们使用限定符的时候, 会尽可能多的去捕获内容
    + 贪婪限定符
      => +
      => *
      => ?
      => {n,}
      => {n,m}
    + 解决: 使用非贪婪限定符
      => +?
      => *?
      => ??
      => {n,}?
      => {n,m}?
      => 当你使用的是非贪婪限定符的时候, 会尽可能少的去捕获内容
*/

十.字符串常用方法(以下方法可以和正则表达式合作)

1. replace()
      + 语法:
        => 字符串.replace(换下字符, 换上字符)
        => 字符串.replace(正则表达式, 换上字符)
      + 返回值: 替换好的字符串
        => 当你使用 字符串片段 或者 没有全局标识符g 的正则表达式的时候, 只能替换一个
        => 当你使用的正则表达式有全局标识符 g 的时候, 有多少替换多少

    2. search()
      + 语法:
        => 字符串.search(字符串片段)
        => 字符串.search(正则表达式)
      + 返回值:
        => 如果字符串内有满足要求的字符串片段, 那么返回的就是字符串片段开始的索引位置
        => 如果字符串内没有满足要求的字符串片段, 那么就是 -1

    3. match()
      + 语法:
        => 字符串.match(字符串片段)
        => 字符串.match(正则表达式)
      + 返回值:
        => 如果你传递的参数是 字符串片段 或者 没有全局标识符g的正则表达式 那么返回值和 exec 一模一样
        => 如果传递的参数是 带有全局标识符g的正则表达式, 那么返回值是一个数组, 字符串内有多少满足要求的捕获多少
*/


// 1. replace
// let str = 'aaa123bbb123ccc789ddd012eee'
// let r1 = str.replace('123', '**')
// console.log(r1)
// let r2 = str.replace(/\d{3}/, '**')
// console.log(r2)
// let r3 = str.replace(/\d{3}/g, '**')
// console.log(r3)

// let str = 'sdHHuyNNfgMMuyNNksMMadHHfbHHvhNNjk'
// let list = [ 'HH', 'NN', 'MM' ]
// let reg = new RegExp(`(${ list.join('|') })`, 'g')
// let res = str.replace(reg, '**')
// console.log(res)

// 2. search
// let str = '你好 世界 123 hello world'
// let r1 = str.search('123')
// let r2 = str.search(/\d{3}/)
// console.log(r2)

// 3. match
// let str = 'aaa123bbb123ccc789ddd012eee'
// let r1 = str.match('123')
// console.log(r1)
// let r2 = str.match(/\d{3}/)
// console.log(r2)
// let r3 = str.match(/\d{3}/g)
// console.log(r3)

十一.正则的预查(断言)

/*
  正则的预查(断言)
    + 了解
    + 两种
      => 正向预查
        -> 正向肯定预查  (?=内容)
          + 示例: /windows(?=\d+)/
          + 捕获 windows, 后面紧跟着有 1 ~ 多个数字的 windows
        -> 正向否定预查  (?!内容)
          + 示例: /windows(?!\d+)/
          + 捕获 windows, 后面紧跟着不是 1 ~ 多个数字的 windows
      => 负向预查
        -> 负向肯定预查  (?<=内容)
          + 示例: /(?<=\d+)windows/
          + 捕获 windows, 前面紧挨着是 1 ~ 多个数字的 windows
        -> 负向否定预查  (?<!内容)
          + 示例: /(?<!\d+)windows/
          + 捕获 windows, 前面紧挨着不是 1 ~ 多个数字的 windows

  ?         表示 贪婪限定符 0 ~ 1??        表示 非贪婪限定符 0 ~ 1 次
  (?:)      表示 匹配但不捕获
  (?<变量>)  表示 单独起名小分组
  (?=内容)   正向肯定预查
  (?!内容)   正向否定预查
  (?<=内容)  负向肯定预查
  (?<!内容)  负向否定预查

  ^         开头
  [^]       非
*/

十二.案例-歌词条的滚动

1666702685011.png

1666702720189.png

<head>
 <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
  margin: 0;
  padding: 0;
}

audio {
  display: block;
  margin: 20px auto;
}

.lrc-box {
  width: 300px;
  height: 200px;
  margin: 0 auto;
  border: 1px solid #333;
  overflow: hidden;

  position: relative;
}

.lrc-box > ul {
  width: 100%;
  position: absolute;
  left: 0;
  top: 100px;

  transition: top .3s linear;
}

.lrc-box > ul > li {
  list-style: none;
  height: 20px;
  line-height: 20px;
  text-align: center;

  color: rgba(0, 0, 0, .5);
}

.lrc-box > ul > li.active {
  background-color: skyblue;
  color: rgba(255, 255, 255, 1);
}


</style>
</head>
<body>

<audio src="./data/丑八怪.mp3" controls></audio>

<div class="lrc-box">
<ul>
  <li class="active">1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
  <li>7</li>
  <li>8</li>
  <li>9</li>
  <li>10</li>
</ul>
</div>


<script src="./data/lrc.js"></script>
<script>
/*
  案例 - 歌词滚动
    + 准备: 装备一个 lrc 歌词
*/

// 0. 获取元素
const ulBox = document.querySelector('ul')
const audioEle = document.querySelector('audio')

// 0. 准备变量
// 专门用来存储每一句歌词的时间
const timeArr = []
// 准备一个正则表达式
const reg = /\[(?<time>\d{2}:\d{2})\.\d{2}\](?<lrc>.+)/g
// 准备一个变量接受 html 格式字符串
let lrcStr = ''

// 1. 对 lrc 这个 字符串进行分割
//   把里面的 时间 提取出来, 把 歌词 渲染在页面上
//   能从 lrc 内捕获多少内容出来
//     只要捕获一次不是 null, 就需要再次捕获
let res = reg.exec(lrc)
while (res !== null) {
  // 当前的 res 捕获到内容了
  // console.log(res[1], ' --- ', res[2])

  // 把所有的时间放在 timeArr 内存起来
  timeArr.push(res[1])
  // 把所有的歌词组装成一个 字符串
  lrcStr += `<li>${ res[2] }</li>`

  res = reg.exec(lrc)
}

// 代码执行到这里, 表示 res 为 null 了, 表示全部捕获完毕
// 2. 把 歌词 插入到 ul 内
ulBox.innerHTML = lrcStr

// 3. 随着音乐的播放, 让 ul 进行 top 值的改变
// 3-1. 给 audio 标签绑定一个 timeupdate 事件
//      随着视音频的播放而触发(1s 3 ~ 4 次)
audioEle.ontimeupdate = function () {
  // 3-2. 拿到当前播放的时间
  //     语法: 视音频元素.currentTime
  //     得到的就是该视音频当前的时间(单位是 s)
  const current = Math.round(audioEle.currentTime)

  // 3-3. 对拿到的时间 进行 格式化, 格式化成 '00:00'
  const minutes = parseInt(current / 60)
  const seconds = current % 60
  const time = `${ minutes >= 10 ? minutes : '0' + minutes }:${ seconds >= 10 ? seconds : '0' + seconds }`

  

  // 3-4. 去到 timeArr 内查找时间对应的那一个内容
  //      因为 timeArr 的 索引, 和 li 的索引是一一对应的
  const index = timeArr.findIndex(item => item === time)
  if (index === -1) return

  // 3-5. 根据 index 的值调整 ul 的定位
  // index === 0,  [0] 的 li 显示, ul 的 top 值是 100
  // index === 1,  [1] 的 li 显示, ul 的 top 值是 100 -= 20
  // index === 2,  [2] 的 li 显示, ul 的 top 值是 100 -= 40
  // index === 3,  [3] 的 li 显示, ul 的 top 值是 100 -= 60
  // index === x,  [x] 的 li 显示, ul 的 top 值是 100 -= 20 * x
  ulBox.style.top = 100 - index * 20 + 'px'

  // 3-5. 只能给 [index] 对应的 li 添加 active 类名
  // 如何拿到所有的 li: ul 内的所有子元素
  // 拿到一个节点的所有子元素: 节点.children
  for (let i = 0; i < ulBox.children.length; i++) {
    ulBox.children[i].classList.remove('active')
  }
  ulBox.children[index].classList.add('active')
}
</script>

补充:
3-1. 给 audio 标签绑定一个 timeupdate 事件
随着视音频的播放而触发(1s 3 ~ 4 次)
audioEle.ontimeupdate = function () {
3-2. 拿到当前播放的时间
语法: 视音频元素.currentTime
得到的就是该视音频当前的时间(单位是 s)
const current = Math.round(audioEle.currentTime)

  先清除所有 再加上
  for (let i = 0; i < ulBox.children.length; i++) {
    ulBox.children[i].classList.remove('active')
  }
  ulBox.children[index].classList.add('active')