[JS]实用技巧

本节目标

了解JS使用技巧

  • 深浅拷贝
  • 异常处理
  • 处理this
  • 性能优化
  • 综合案例

深浅拷贝

开发中, 我们经常要复制一个对象, 如果直接赋值, 复制后的对象会依然会影响原对象

浅拷贝

拷贝对象之后, 里面的属性值是简单数据类型直接拷贝值, 如果属性值是引用数据类型拷贝的是地址

对象浅拷贝:

Object.assgin() / 展开运算符 { ...obj }

<script>
    // 对象浅拷贝
    const obj = { name: '张三', age: 18 }
    //  方式1
    const obj2 = { ...obj }
    obj2.name = '李四'
    // 方式2
    const obj3 = Object.assign({}, obj)
    obj3.name = '王五'
    // 打印
    console.log(obj2)
    console.log(obj3)
</script>

数组浅拷贝:

Array.prototype.concat() / [ ...arr ]

<script>
    // 对象浅拷贝
    const arr = ['a', 'b', 'c']
    //  方式1
    const arr1 = [...arr]
    arr1[0] = '1'
    // 方式2
    const arr2 = arr.concat()
    arr2[0] = '2'
    // 打印
    console.log(arr1)
    console.log(arr2)
</script>

手写浅拷贝:

<script>
    const obj = {
      name: '张三',
      age: 18
    }
    const o = {}

    function deepCopy(newObj, oldObj) {
      for (const k in oldObj) {
        // k->属性名  oldObj[k]->属性值
        // newObj[k] => o.name
        newObj[k] = oldObj[k]
      }
    }

    deepCopy(o, obj)
    console.log(o);
</script>

小结:

直接赋值和浅拷贝的区别

  1. 直接赋值, 只要是对象,就会相互影响, 因为是直接拷贝对象栈里面的地址
  2. 浅拷贝之后, 如果是单层对象, 不再互相影响, 如果是多层对象,还会相互影响

深拷贝

递归实现

1, 认识递归

如果一个函数在内部调用其自身, 这个函数就是递归函数

<script>
    let num = 1
    function fn() {
      console.log('打印' + num)
      // 结束递归
      if (num >= 6) return
      num++
      // 递归调用
      fn()
    }
    fn()
  </script>

注意

  • 递归函数的作用和循环类似
  • 由于递归很容易发生"栈溢出"错误, 所以必须加退出添加return

2 实现深拷贝

实现的只是简单版的深拷贝, 无法应对复杂情况, 比如对象互相引用的情况

<script>
    const obj = {
      name: '张三',
      age: 18,
      hobby: ['篮球', '足球', '棒球'],
      family: {
        f: '张大',
        m: '丽丽'
      }
    }
    const o = {}

    function deepCopy(newObj, oldObj) {
      for (const k in oldObj) {

        if (oldObj[k] instanceof Array) {
          // 处理数组类型数据
          // 确定键
          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.name
          newObj[k] = oldObj[k]
        }

      }
    }

    deepCopy(o, obj)
    o.age = 20
    o.hobby[0] = '乒乓球'
    o.family.f = '李冰'
    console.log(obj, '原数据')
    console.log(o, '新数据')
  </script>

小结: 实现深拷贝的步骤

  • 深拷贝的目标是拷贝后的对象不影响原对象
  • 实现深拷贝需要用到函数递归
  • 对于基本数据类型, 直接赋值即可
  • 对与引用数据类型, 再次调用递归函数
  • 要先判断Array类型, 再判断Object类型

lodash/cloneDeep

<script src='./lodash.min.js'></script>
<script>
    const obj = {
      name: '张三',
      age: 18,
      hobby: ['篮球', '足球', '棒球'],
      family: {
        f: '张大',
        m: '丽丽'
      }
    }
    const o = _.cloneDeep(obj)
    console.log(o)
</script>

JSON.stringify()

字符串是简单数据类型, 不存在引用关系, 就断开了与原数据的联系

<script>
    const obj = {
      name: '张三',
      age: 18,
      hobby: ['篮球', '足球', '棒球'],
      family: {
        f: '张大',
        m: '丽丽'
      }
    }
    // 把对象转成JSON字符串, 再JSON字符串转对象
    const o = JSON.parse(JSON.stringify(obj))
    console.log(o)
</script>

异常处理

异常处理是指预估代码执行过程中可能发生的错误, 然后最大程度的避免错误的发生, 提高程序的健壮性

抛出异常

<body>
  <script>
    function fn(x, y) {
      if (!x || !y) {
        throw new Error('参数不能为空!')
      }

      return x + y
    }
    console.log(fn())
</script>

小结

  • throw抛出异常信息, 程序中断执行
  • throw后面可以直接写错误信息
  • 建议搭配Error错误对象一起使用, 得到更详细的错误信息

捕获异常

可以通过try/catch捕获错误信息(;浏览器提供的错误信息)

try 试试 catch 拦住 finally 最后

<div class="p"></div>
<script>
    function fn() {
      try {
        // 可能出现错误的代码
        const p = document.querySelector('.p')
        p.style.color = 'red'

      } catch (error) {
        // 拦截错误, 提示浏览器提供的错误信息, 但不会中断程序执行
        // return error.message 可以通过 return 中短程序
        throw new Error('你看看, 出错了吧')

      } finally {
        // 不管是否出错, 都会执行
        console.log('无忧无虑的执行');
      }

      console.log('如果不中断, 就会执行');

    }
    fn()
</script>

小结

  • 将预估可能发生错误的代码放在try代码段中
  • 如果try代码段中出现错误, 会执行catch代码段, 并捕获到错误信息
  • finally不管是否存在错误, 都会执行的代码

debugger

可以通过debugger跳到断点调试页面

<script>
    const arr = [1, 2, 3, 4, 5]
    arr.map(item => {
      debugger
      console.log(item)
    })
</script>

处理this

普通函数

普通函数的调用方式决定了this的值, 谁调用this指向谁

<script>
    // 普通函数
    function sayHi() {
      console.log(this)
    }

    // 函数表达式
    const asyHello = function () {
      console.log(this)
    }

    // 对象
    const user = {
      sayHi: function () {
        console.log(this)
      }
    }

    // this指向函数的调用者
    sayHi() //window
    asyHello() //window
    user.sayHi() // user
  </script>
<script>
    'use strict'
    // 普通函数
    function sayHi() {
      console.log(this)
    }

    // 函数表达式
    const asyHello = function () {
      console.log(this)
    }

    // 对象
    const user = {
      sayHi: function () {
        console.log(this)
      }
    }

    // this指向函数的调用者
    sayHi() //undefiend
    asyHello() //undefiend
    user.sayHi() // user
  </script>

小结

  • 普通函数没有明确调用者时this指向window
  • 严格模式下, 普通函数没有调用者时this指向undefined

箭头函数

箭头函数不存在this, 沿用上一级作用域的this

<button class="btn">按钮</button>
  <script>
    // 箭头函数
    const sayHi = () => {
      console.log(this)
    }

    // 对象
    const user = {
      sayHi: () => {
        console.log(this)
      }
    }

    // dom事件
    const btn = document.querySelector('.btn')
    btn.addEventListener('click', () => {
      console.log(this)  // window
    })


    // 原型函数
    function Persion() {
    }
    Persion.prototype.walk = () => {
      console.log(this) // window
    }
    const p = new Persion()
    p.walk()


    sayHi() //window
    user.sayHi() // window
</script>

小结

  • 箭头函数会默认帮我们绑定外层this的值, 所以在箭头函数中this的值和外层的this一致
  • 箭头函数中的this引用的就是最近作用域的this
  • 向外层作用域中, 一层一层查找this, 直到有this的定义
  • 构造函数, 原型函数, dom事件等函数不推荐使用箭头函数, 因为会造成this丢失

改变指向

call()方法

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

语法: fun.call(this, 参数1, 参数2...)

<script>
    const obj = {
      name: 'pink'
    }
    function fn(x, y) {
      console.log(this)
      console.log(x, y);
    }
    // 调用函数, 并改变this指向
    fn.call(obj, 1, 2)
</script>

apply()方法

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

语法: fun.apply(this, [参数1, 参数2...])

<script>
    const obj = {
      name: 'pink'
    }
    function fn(x, y) {
      console.log(this)
      console.log(x, y);
    }
    // 调用函数, 并改变this指向
    fn.apply(obj, [1, 2])
</script>

场景: apply经常跟数组有关系

<script>
    const arr = [100, 200, 30]
    const max = Math.max.apply(null, arr)
    const min = Math.min.apply(null, arr)
    console.log(max, min);
</script>

bind()方法

bind()方法不会调用函数, 但是能改变函数内部this指向, 返回改造完成的新函数

语法: fun.bind(this, 参数1, 参数2...)

<script>
    const obj = {
      age: 18
    }
    function fn() {
      console.log(this)
    }
    const fun = fn.bind(obj)
    fun()
</script>
<button>发送短信</button>
  <script>
    //需求: 点击按钮, 禁用按钮2秒
    document.querySelector('button').addEventListener('click', function () {
      this.disabled = true
      setTimeout(function () {
        // this指向由window改为button
        this.disabled = false
      }.bind(this), 2000)
    })
</script>

性能优化

防抖

单位时间内, 频繁触发事件, 只执行最后一次

王者荣耀回城, 只要被打断就需要重新回城

实现思路

  1. 核心是利用setTimeout定时器实现
  2. 声明一个定时器变量
  3. 事件触发时, 判断是否存在定时器, 如果存在先清除
  4. 如果不存在, 则新开定时器, 存入变量
  5. 定时器里面调用函数

使用场景

  1. 搜索输入框: 用户最后一次输入完后, 再发起请求
  2. 手机号, 邮箱验证等输入检测
<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>
  <script src="./lodash.min.js"></script>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')

    let i = 1
    function mouseMove() {
      box.innerHTML = i++
    }

    // 需求1: 鼠标移动, 显示数字
    // box.addEventListener('mousemove', mouseMove)

    // 需求2: 减少触发频率,提高性能
    // box.addEventListener('mousemove', _.debounce(mouseMove, 300))

    // 需求3: 自定义防抖函数
    function debounce(fn, t) {
      // 1,声明一个定时器变量
      let timer = null
      return function () {
        // 2,事件触发时, 判断是否存在定时器, 如果存在先清除
        if (timer) clearTimeout(timer)
        // 3,如果不存在, 则新开定时器, 存入变量
        timer = setTimeout(function () {
          // 4,定时器里面调用函数
          fn()
        }, t)
      }
    }
    box.addEventListener('mousemove', debounce(mouseMove, 300))
  </script>
</body>

节流

单位时间内, 频繁触发事件, 执行性一次

王者荣耀技能冷却, 期间无法释放技能

实现思路

  1. 核心是利用setTimeout定时器实现
  2. 声明一个定时器变量
  3. 事件触发时, 判断是否存在定时器, 如果存在不处理
  4. 如果不存在, 则新开定时器, 存入变量
  5. 定时器里面调用函数, 把定时器清空
  6. 注意: 定时器内不能使用clearTimeout(), 而应该采用置空的方式清除定时器

使用场景

  1. 高频事件, 鼠标移动mousemove
  2. 页面尺寸缩放resize
  3. 滚动条滚动scroll

<!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>
  <script src="./lodash.min.js"></script>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')

    let i = 1
    function mouseMove() {
      box.innerHTML = i++
    }

    // 需求1: 鼠标移动, 显示数字
    // box.addEventListener('mousemove', mouseMove)

    // 需求2: 减少触发频率,提高性能
    // box.addEventListener('mousemove', _.throttle(mouseMove, 3000))

    // 需求3: 自定义节流函数
    function throttle(fn, t) {
      // 1, 声明一个定时器变量
      let timer = null
      return function () {
        // 2, 事件触发时, 判断是否存在定时器, 如果存在不处理
        if (!timer) {
          // 3,如果不存在, 则新开定时器, 存入变量
          timer = setTimeout(function () {
            // 4, 定时器里面调用函数, 清理定时器
            fn()
            timer = null
          }, t)
        }
      }
    }
    box.addEventListener('mousemove', throttle(mouseMove, 3000))
  </script>
</body>

</html>

综合案例

相关事件:

  1. ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发生改变时触发
  2. onloadendata 事件在当前帧的数据加载完成切且还没有足够的数据播放视频/音频时触发

还原视频播放位置

<!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" />
  <meta name="referrer" content="never" />
  <title>综合案例</title>
  <style>
    * {
      padding: 0;
      margin: 0;
      box-sizing: border-box;
    }

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

    .video video {
      width: 100%;
      padding: 20px 0;
    }

    .elevator {
      position: fixed;
      top: 280px;
      right: 20px;
      z-index: 999;
      background: #fff;
      border: 1px solid #e4e4e4;
      width: 60px;
    }

    .elevator a {
      display: block;
      padding: 10px;
      text-decoration: none;
      text-align: center;
      color: #999;
    }

    .elevator a.active {
      color: #1286ff;
    }

    .outline {
      padding-bottom: 300px;
    }
  </style>
</head>

<body>
  <div class="container">
    <div class="header">
      <a href="http://pip.itcast.cn">
        <img src="https://pip.itcast.cn/img/logo_v3.29b9ba72.png" alt="" />
      </a>
    </div>
    <div class="video">
      <video src="https://v.itheima.net/LapADhV6.mp4" controls></video>
    </div>
    <div class="elevator">
      <a href="javascript:;" data-ref="video">视频介绍</a>
      <a href="javascript:;" data-ref="intro">课程简介</a>
      <a href="javascript:;" data-ref="outline">评论列表</a>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <script>
    const video = document.querySelector('video')

    // 播放位置发生改变
    video.ontimeupdate = _.throttle(() => {
      localStorage.setItem('currentTime', video.currentTime)
    }, 5000)

    // 视频初始化完成
    video.onloadeddata = () => {
      video.currentTime = localStorage.getItem('currentTime') || 0
    }
  </script>
</body>

</html>
  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值