JavaScript高级:深浅拷贝、异常处理、防抖及节流

JavaScript 高级 - 第4天

文章目录

深浅拷贝

在开发中,我经常需要复制一个对象,如果直接用赋值会有下面的问题:

html 复制代码
<script>
  const obj = {
    uname: 'pink',
    age: 18
  }
  const o = obj  // 把 obj 赋值给 o
  console.log(o) // {uname: 'pink', age: 18}
  o.age = 20  // 修改 o 中的 age
  console.log(o) // {uname: 'pink', age: 20}
  console.log(obj)  // {uname: 'pink', age: 20} obj 也被修改了 
</script>

这样和我们的意愿相悖,那么应该如何实现拷贝后双方互不干扰呢?

浅拷贝

首先浅拷贝和深拷贝只针对引用类型

浅拷贝:拷贝的是地址

常见方法:

  1. 拷贝对象:Object.assgin() 或 展开运算符 {...obj} 拷贝对象

    html 复制代码
    <script>
      const obj = {
        uname: 'pink',
        age: 18
      }
      // 展开运算符
      const o = { ...obj }
      console.log(o) // {uname: 'pink', age: 18}
      o.age = 20
      console.log(o) // {uname: 'pink', age: 20}
      console.log(obj)  // {uname: 'pink', age: 18} 不变
    </script>

    或:

    html 复制代码
    <script>
      const obj = {
        uname: 'pink',
        age: 18
      }
      // assign
      const o = {}
      Object.assign(o, obj)
      console.log(o) // {uname: 'pink', age: 18}
      o.age = 20
      console.log(o) // {uname: 'pink', age: 20}
      console.log(obj)  // {uname: 'pink', age: 18}
    </script>
  2. 拷贝数组:Array.prototype.concat() 或者 [...arr],可以参考上面的例子自行补充。

但这两个方法也有一定的问题,如果我们在对象中再套一个对象,你会发现这个问题的根本还是没有被解决:

html 复制代码
<script>
  const obj = {
    uname: 'pink',
    age: 18,
    family: {
      baby: '小pink'
    }
  }
  
  // 浅拷贝
  const o = {}
  Object.assign(o, obj)
  o.age = 20
  o.family.baby = '老pink'
  console.log(o) // { uname: 'pink', age: 20, family: {baby: '老pink'} }
  console.log(obj) // { uname: 'pink', age: 20, family: {baby: '老pink'} } family 中的 baby 也被修改了!
</script>

浅拷贝拷贝的是地址,当拷贝的是对象的时候,在栈中拷贝到的实际上是指向对象内容的地址,它们指向同一个对象内容。因此对对象中的对象进行修改时,在堆中的内容会被修改,即被拷贝者所指的内容被修改了:

所以,浅拷贝仅是表层的拷贝,无法做到多层拷贝。

如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)

深拷贝

首先浅拷贝和深拷贝只针对引用类型

深拷贝:拷贝的是对象,不是地址。

常见方法

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.stringify()实现
递归实现深拷贝

通过函数递归:

如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。

  • 简单理解:函数内部自己调用自己,这个函数就是递归函数。
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生"栈溢出 "错误(stack overflow),所以必须要加退出条件 return

我们在代码中来实现深拷贝(简易版):

html 复制代码
<body>
  <script>
    const obj = {
      uname: 'pink',
      age: 18,
      // 复杂数据类型
      hobby: ['乒乓球', '足球'],
      family: {
        baby: '小pink'
      }
    }
    
    const o = {}
    // 拷贝函数
    function deepCopy(newObj, oldObj) {
      for (let k in oldObj) {
        // 处理数组的问题  一定先写数组再写对象 不能颠倒 因为数组也属于对象
        if (oldObj[k] instanceof Array) {
          // 因为数组是引用类型 需要遍历来拷贝 因此也需要进行for循环 我们可以利用递归来实现
          // 声明空数组
          newObj[k] = []
          //  newObj[k] []
          //  oldObj[k] ['乒乓球', '足球']
          // 再次递归实现数组拷贝
          deepCopy(newObj[k], oldObj[k])
        } else if (oldObj[k] instanceof Object) {
          // 处理对象问题 和数组一个道理
          newObj[k] = {}
          deepCopy(newObj[k], oldObj[k])
        }
        else {
          // 简单数据类型
          //  k 属性名  oldObj[k] 属性值
          // newObj[k]  === o.uname  给新对象添加属性
          newObj[k] = oldObj[k]
        }
      }
    }
    deepCopy(o, obj) // 函数调用  两个参数 o 新对象  obj 旧对象
    console.log(o)
    o.age = 20
    o.hobby[0] = '篮球'
    o.family.baby = '老pink'
    console.log(obj)
  </script>
</body>
js库lodash里面cloneDeep内部实现了深拷贝

在开发中,我们不需要亲自写深拷贝函数,只需要引用lodash即可。详细安装和使用教程见官方网站。其中深拷贝指路:lodash.cloneDeep | Lodash中文文档 | Lodash中文网

语法规范:_.cloneDeep(obj),这是一个函数,有返回值。

使用步骤:

  • 先在自己的JavaScript代码前引用lodash库
  • 使用专门的语法调用函数,接收返回值
html 复制代码
<body>
  <!-- 先引用 -->
  <script src="./lodash.min.js"></script>
  <script>
    const obj = {
      uname: 'pink',
      age: 18,
      hobby: ['乒乓球', '足球'],
      family: {
        baby: '小pink'
      }
    }
    // 接收返回值
    const o = _.cloneDeep(obj)
    console.log(o)
    o.family.baby = '老pink'
    console.log(obj)  // 不受影响
  </script>
</body>
JSON序列化

我们可以先把对象转换为字符串:JSON.stringify(obj),此时我们拷贝的不再是引用类型了,而是简单数据类型。如果这时我们又把这个字符串转换为对象:JSON.parse(JSON.stringify(obj)),正好,这个新的对象和这个被拷贝的不是一个对象!

html 复制代码
<body>
  <script>
    const obj = {
      uname: 'pink',
      age: 18,
      hobby: ['乒乓球', '足球'],
      family: {
        baby: '小pink'
      }
    }
    
    // 把对象转换为 JSON 字符串
    // 再把字符串转换为对象 使用新的变量接收
    const o = JSON.parse(JSON.stringify(obj))
    console.log(o)
    o.family.baby = '123'
    console.log(obj)  // 不受影响
  </script>
</body>

异常处理

了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。

throw 抛异常

异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行。

html 复制代码
<script>
  function counter(x, y) {
    if(!x || !y) {
      throw new Error('参数不能为空!')  // 抛出异常信息
      // 或 throw '参数不能为空!'
    }
    return x + y
  }
  counter()  // 没有传递参数
</script>

如果没有传参,最后的报错:

总结:

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息

try ... catch 捕获异常

我们可以通过try/catch捕获错误信息(浏览器提供的错误信息),try表示试试,catch拦住,finally最后。

html 复制代码
<script>
   function foo() {
      try {
        // 可能发送错误的代码 要写到 try
        const p = document.querySelector('.p')
        p.style.color = 'red'
      } catch (error) {
        // 如果 try 代码段中执行有错误时,会执行 catch 代码段
        // 查看错误信息
        console.log(error.message)
        // catch 只是拦截错误 提供信息 但不会终止程序 需要手动中断
        // 终止代码继续执行
        return
        // 或配合 throw 使用 因为 throw 可以主动中断
      }
      finally {
        // 不管程序对不对 一定会执行 finally 里的代码
        alert('执行')
      }
      console.log('如果出现错误,我的语句不会执行')
   }
   foo()
</script>

总结:

  1. try...catch 用于捕获错误信息
  2. 将预估可能发生错误的代码写在 try 代码段中
  3. 如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
  4. finally不管是否有错误,都会执行

debugger

相当于断点调试,类似于在代码某一行设置断点 ,当代码运行后,会直接跳转到debugger处。

JavaScript 复制代码
function potentiallyBuggyCode() {
  debugger
  // 执行可能有漏洞的操作以进行检查、逐步调试等
}

处理 this

了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。

this 是 JavaScript 最具"魅惑"的知识点,不同的应用场合 this 的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this 默认的取值】情况进行归纳和总结。

this 指向-普通函数

普通函数 的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】,如下代码所示:

html 复制代码
<script>
  // 普通函数 谁调用 this就指向谁
  console.log(this)  // window
  function fn() {
    console.log(this)  // window    
  }
  window.fn()
  
  // 定时器
  window.setTimeout(function () {
    console.log(this) // window 
  }, 1000)
  
  // 事件监听
  document.querySelector('button').addEventListener('click', function () {
    console.log(this)  // 指向 button
  })
  
  // 对象
  const obj = {
    sayHi: function () {
      console.log(this)  // 指向 obj
    }
  }
  obj.sayHi()
</script>

注: 普通函数没有明确调用者时 this 值为 window严格模式 下没有调用者时 this 的值为 undefined。(严格模式要求必须先声明再调用/赋值)

this 指向-箭头函数

箭头函数 中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。

  • 箭头函数会默认我们绑定外层this的值,所以箭头函数中this的值和外层的this是一样的
  • 箭头函数中的this引用的就是最近的作用域中的this
  • 向外层作用域中,一层一层查找this,直到有this的定义
html 复制代码
<script>
    
  console.log(this) // 此处为 window
  // 箭头函数
  const sayHi = function() {
    console.log(this) // 该箭头函数中的 this 为函数声明环境中 this 一致
  }
  // 普通对象
  const user = {
    name: '小明',
    // 该箭头函数中的 this 为函数声明环境中 this 一致
    walk: () => {
      console.log(this)
    },
    
    sleep: function () {
      let str = 'hello'
      console.log(this)
      let fn = () => {
        console.log(str)
        console.log(this) // 该箭头函数中的 this 与 sleep 中的 this 一致
      }
      // 调用箭头函数
      fn();
    }
  }

  // 动态添加方法
  user.sayHi = sayHi
  
  // 函数调用
  user.sayHi()
  user.sleep()
  user.walk()
</script>

在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:

html 复制代码
<script>
  // DOM 节点
  const btn = document.querySelector('.btn')
  // 箭头函数 此时 this 指向了 window 而不是DOM节点
  btn.addEventListener('click', () => {
    console.log(this)
  })
  // 普通函数 此时 this 指向了 DOM 对象
  btn.addEventListener('click', function () {
    console.log(this)
  })
</script>

同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:

html 复制代码
<script>
  function Person() {
  }
  // 原型对象上添加了箭头函数
  Person.prototype.walk = () => {
    console.log('人都要走路...')
    console.log(this); // window
  }
  const p1 = new Person()
  p1.walk()
</script>

总结:

  1. 函数内不存在this,沿用上一级的
  2. 不适用于构造函数、原型函数、dom事件函数等
  3. 适用于需要使用上层的this的地方
  4. 使用正确的话,它会在很多地方带来方便,后面我们会大量使用,慢慢体会

改变 this 指向

以上归纳了普通函数和箭头函数中关于 this 默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向:

call()(了解)

使用 call 方法调用函数,同时指定函数中 this 的值。

语法:函数名.call(thisArg, arg1, arg2, ...)

其中,

  • thisArg:在函数运行时指定的this
  • arg1, arg2:传递到其他参数
  • 返回值就是函数的返回值,因为它就是调用函数

使用方法如下代码所示:

html 复制代码
<script>
  // 普通函数
  function sayHi() {
    console.log(this)  // window
  }

  let obj = {
    name: '小明',
    age: 18
  }

  // 调用函数并指定 this 的值
  sayHi.call(obj) // this 值为 obj

  // 如果函数有形参 可以在call中添加实参
  // 求和函数
  function counter(x, y) {
    return x + y;
  }

  // 调用 counter 函数,并传入参数
	counter.call(obj, 1, 2)  // 指向 obj 返回 3
</script>

总结:

  1. call 方法能够在调用函数的同时指定 this 的值
  2. 使用 call 方法调用函数时,第1个参数为 this 指定的值
  3. call 方法的其余参数会依次自动传入函数做为函数的参数
apply()

使用 apply 方法调用函数 ,同时指定函数中 this 的值.

语法:函数名.apply(thisArg, [argsArray])

  • thisArg:在函数运行时指定的this
  • argsArray:传递的值,必须包含在数组里
  • 返回值就是函数的返回值,因为它就是调用函数
  • 因此apply主要跟数组 有关系,比如使用Math.max()求数组最大值

使用方法如下代码所示:

html 复制代码
<script>
  // 普通函数
  function sayHi() {
    console.log(this) // { name: '小明', age: 18 }
  }

  let obj = {
    name: '小明',
    age: 18
  }

  // 调用函数并指定 this 的值
  sayHi.apply(obj) // this 值为 obj

  // 求和函数
  function counter(x, y) {
    return x + y
  }
  // 调用 apply 函数,并传入参数
  // 必须以数组方式传递数据
  counter.apply(obj, [5, 10])  // 返回 15
</script>

由于apply主要针对于数组参数,因此可以结合Math.max()求数组最大值:

javascript 复制代码
// 使用场景: 求数组最大值/最小值
// const max = Math.max(1, 2, 3)
const arr = [100, 44, 77]
const max = Math.max.apply(Math, arr)
const min = Math.min.apply(null, arr)
// 或使用展开运算符
// const max = Math.max(...arr)
console.log(max, min)  // 100 44

总结:

  1. apply 方法能够在调用函数的同时指定 this 的值
  2. 使用 apply 方法调用函数时,第1个参数为 this 指定的值
  3. apply 方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind()(重点)

bind 方法并不会调用函数 ,而是创建一个指定了 this 值的新函数。

语法:函数名.bind(thisArg, arg1, arg2, ...)

  • thisArg:在函数运行时指定的this
  • argsArray:传递的值,必须包含在数组里
  • 返回由指定的this值和初始化参数改造的原函数拷贝(新函数)
  • 因此当我们只是想改变this指向,并且不想调用这个函数的时候,可以使用bind,比如改变定时器内部的this指向。

使用方法如下代码所示:

html 复制代码
<script>
  // 普通函数
  function sayHi() {
    console.log(this)
  }
  let obj = {
    name: '小明',
    age: 18
  }
  // 调用 bind 指定 this 的值
  // bind 不会调用函数 但能改变 this 的指向
  // 返回值是一个函数 但是这个函数里面的 this 是更改过的
  const sayHello = sayHi.bind(obj)
  // 调用使用 bind 创建的新函数
  sayHello()  // { name: '小明', age: 18 }
</script>

在开发中,由于定时器是默认由window调用的,有时候我们需要改变定时器内部的this指向:

html 复制代码
<body>
  <button>发送短信</button>
  <script>
    // 需求,有一个按钮,点击里面就禁用,2秒钟之后开启
    document.querySelector('button').addEventListener('click', function () {
      // 禁用按钮
      this.disabled = true
      window.setTimeout(function () {
        // 在这个普通函数里面,我们要this由原来的window 改为 btn
        this.disabled = false
      }.bind(this), 2000)   // 这里的this 和 btn 一样
    })
  </script>
</body>

注:bind 方法创建新的函数,与原函数的唯一的变化是改变了 this 的值。

call apply bind 总结

相同点:

  • 都可以改变函数内部的this指向

区别点:

  • callapply都会调用函数,并且改变函数内部this指向
  • callapply传递的参数不一样,call传递参数arg1, arg2, ...形式,apply必须数组形式
  • bind不会调用函数,可以改变函数内部this指向

主要应用场景:

  • call调用函数并且可以传递参数
  • apply经常跟数组有关系,比如借助于数学对象实现数组最大值和最小值
  • bind不调用函数,但是还想改变this指向,比如改变定时器内部的this指向

防抖节流

防抖(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。防抖,即单位时间内,频繁触发事件,只执行最后一次

由图所示,当我们触发一次事件后,事件执行还没结束时,我们又再次触发该事件。防抖会取消掉没执行完的事件,重新开始准备本次触发的执行。总而言之,在事件未结束前若多次触发,防抖会选择重新开始执行最近一次的触发,而把之间的所有事件都取消掉。只要被打断,就需要重新来。

使用场景:

  • 搜索框输入,只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测

我们在例子中体会一下:

我们设计一个函数,当鼠标在盒子里移动时,计数器 i会加1:

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>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>

<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let i = 1  // 让这个变量++
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
      // 如果里面存在大量操作 dom 的情况,可能会卡顿
    }
    box.addEventListener('mousemove', mouseMove)

  </script>
</body>

</html>

但这会有个问题,因为鼠标移动太快,像素一改动i就会加1,这会导致多次进行 DOM 操作而导致卡顿。

由此,我们想要延时检测鼠标是否移动,只有在鼠标停顿后再移动才进行加1操作。我们可以通过防抖实现这个效果。

实现防抖只要有两种方式:

  1. lodash提供的防抖函数来处理
  2. 手写一个防抖函数来处理
lodash提供的防抖函数

我们先看看方式1,在lodash官网中搜索,找到防抖函数,这里指路:lodash.debounce | Lodash中文文档 | Lodash中文网

语法:_.debounce(func, [wait=0], [options=])

debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。 debounced(防抖动)函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。

options(选项) 对象决定如何调用 func 方法,options.leading 与|或 options.trailing 决定延迟前后如何触发(注:是 先调用后等待 还是 先等待后调用)。

func 调用时会传入最后一次提供给 debounced(防抖动)函数 的参数。 后续调用的 debounced(防抖动)函数返回是最后一次 func 调用的结果。

简单来说,wait就是延迟的毫秒数,func就是需要执行的函数。有点像定时器。

在上述例子中:

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>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>

<body>
  <div class="box"></div>
  <!-- 先引入 这里写你们存放库代码的位置即可 -->
  <script src="./lodash.min.js"></script>
  <script>
    const box = document.querySelector('.box')
    let i = 1  // 让这个变量++
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
      // 如果里面存在大量操作 dom 的情况,可能会卡顿
    }
    // 使用防抖函数 鼠标停止500ms后才调用函数
    box.addEventListener('mousemove', _.debounce(mouseMove, 500))

  </script>
</body>

</html>

注意,一定要先引用lodash库才能使用这个防抖函数。

现在我们处理后,可以看到效果好了很多,也减少了性能的消耗:

手写防抖函数

要求:鼠标在盒子上移动,鼠标停止500ms之后,里面的数字才会发生变化+1

核心思路 :防抖的核心就是利用定时器(setTimeout)来实现

  • 声明一个定时器变量
  • 当鼠标每次滑动都先判断是否有定时器 了,如果有定时器先清除以前的定时器
  • 如果没有定时器则开启 定时器,记得存到变量里面(方便关闭)
  • 定时器里面调用要执行的函数
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>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>

<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let i = 1
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
    }
    // 防抖函数 接受实参
    function debounce(fn, time) {
      // 1. 声明一个定时器变量
      let timer
      // 如果直接写一个定时器 在事件监听中由于debounce(mouseMove, 500)直接调用了函数 在页面一打开后会直接先执行debounce函数才进行事件监听 但此时debounce函数已执行完毕 默认有返回值 这时候再进行事件监听就不能执行debounce函数了
      // 因此这里我们需要设置返回值为函数 这个函数就是用来执行2 3 4 步骤的
      return function () {
        // 2. 判断是否有定时器
        if (timer) clearTimeout(timer)
        // 3. 开启定时器
        timer = setTimeout(function() {
          // 4. 调用要执行的函数
          fn()
        }, time)
      }
    }
    // 调用函数
    box.addEventListener('mousemove', debounce(mouseMove, 500))

  </script>
</body>

</html>

节流(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。简单来说,节流就是单位时间内,频繁触发事件,只执行一次

如图所示,当事件触发后开始执行,如果执行过程中事件又再次被触发,当前执行的事件仍然不会被取消,而是继续执行直到完毕后才能接受事件触发。期间被触发的任务会被取消。

使用场景:

  • 高频事件:鼠标移动mousemove
  • 页面尺寸缩放resize
  • 滚动条滚动scroll等等

我们还是以上面鼠标移动加1的例子来解释:

利用节流来处理鼠标滑过盒子显示文字,要求:鼠标在盒子上移动,不管移动多少次,每隔500ms才+1.

还是有两个方法实现节流:

  1. lodash提供的节流函数来处理
  2. 手写一个节流函数来处理
lodash提供的节流函数

指路:lodash.throttle | Lodash中文文档 | Lodash中文网

语法:_.throttle(func, [wait=0], [options=])

throttle 节流函数,在 wait 秒内最多执行 func 一次的函数。

该函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。

可以提供一个 options 对象决定如何调用 func 方法, options.leading 与|或 options.trailing 决定 wait 前后如何触发。

func 会传入最后一次传入的参数给这个函数。 随后调用的函数返回是最后一次 func 调用的结果。

简单来说,就是在 wait 毫秒内,函数func仅能执行一次。

我们在代码中演示一下:

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>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>

<body>
  <div class="box"></div>
  <!-- 先引入 -->
  <script src="./lodash.min.js"></script>
  <script>
    const box = document.querySelector('.box')
    let i = 1
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
    }
    // 使用节流函数 鼠标在500ms内仅调用一次函数
    box.addEventListener('mousemove', _.throttle(mouseMove, 500))

  </script>
</body>

</html>
手写节流函数

核心思想 :节流的核心就是利用定时器(setTimeout)来实现

  • 声明一个定时器变量
  • 当鼠标每次滑动都先判断是否有定时器 了,如果有定时器则不开启新定时器
  • 如果没有定时器则开启 定时器,记得存到变量里面
  • 定时器里面调用 要执行的函数,定时器里面要把定时器清空
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>Document</title>
  <style>
    .box {
      width: 500px;
      height: 500px;
      background-color: #ccc;
      color: #fff;
      text-align: center;
      font-size: 100px;
    }
  </style>
</head>

<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let i = 1
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
    }
    // 节流函数 鼠标在500ms内仅调用一次函数
    function throttle(fn, time) {
      // 1. 声明一个定时器变量
      let timer = null
      return function () {
        // 2. 判断是否有定时器
        if(!timer) {
          timer = setTimeout(function(){
          	// 3. 调用要执行的函数
            fn()
            // 4. 把定时器清空
            timer = null  // 在 setTimeout 中无法使用 clearTimeout(timer)来清除 因为定时器还在运作
          }, time)
        }
      }
    }
    // 函数调用
		box.addEventListener('mousemove', throttle(mouseMove, 500))
  </script>
</body>

</html>

最终效果实现:

总结

防抖和节流:

相关推荐
bing.shao2 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang
唐叔在学习2 小时前
30s让ai编写「跳过外链中转页」的油猴脚本
前端·javascript
酸菜土狗2 小时前
🔥 纯 JS 实现 SQL 字段智能解析工具类,前端也能玩转 SQL 解析
前端
wo不是黄蓉2 小时前
脚手架步骤流程
前端
liwulin05062 小时前
【PYTHON-YOLOV8N】关于YOLO的推理训练图片的尺寸
开发语言·python·yolo
我这一生如履薄冰~2 小时前
css属性pointer-events: none
前端·css
博客胡2 小时前
Python-fastAPI的学习与使用
学习·fastapi·ai编程
brzhang2 小时前
A2UI:但 Google 把它写成协议后,模型和交互的最后一公里被彻底补全
前端·后端·架构
API技术员2 小时前
item_get_app - 根据ID取商品详情原数据H5数据接口实战解析
javascript