JavaScript高级_04day


深浅拷贝

<script>
  const obj = {
    uname: 'pink',
    age: 18
  }
  const o = obj
  console.log(o)
  o.age = 20
  console.log(o)
  console.log(obj)
</script>

直接的复制会出现问题,如上把obj对象赋予o对象,其实给o对象的是obj的地址,当我们修改o后,obj也会跟着变,这是不行的。

浅拷贝

872

对象的浅拷贝可以用展开运算符或者assign方法,数组的浅拷贝也可以用展开运算符或者concat方法。

//const o = { ...obj }
//Object.assign(o, obj)
//数组同理

问题

浅拷贝还是有问题,如下

<script>
   const obj = {
     uname: 'pink',
     age: 18,
     family: {
       baby: '小pink'
     }
   }
   // 浅拷贝
   // const o = { ...obj }
   // console.log(o)
   // o.age = 20
   // console.log(o)
   // console.log(obj)
   const o = {}
   Object.assign(o, obj)
   o.age = 20
   o.family.baby = '老pink'
   console.log(o)  
   console.log(obj)  //这里照样会把小pink改成老pink
 </script>

当obj对象里面又有对象或者数组这些引用形数据,那么里面的数据,还是和直接复制一样被改变。

所以说浅拷贝,只能将外层的数据重新生成一个新地址去储存,内层数据还是同一个地址。

873

直接赋值一个对象,就是把对象的地址给另一个变量,自然一变就多变

浅拷贝是把老对象的数据赋予新对象,生成了新地址内存去存储,所以一般不会一变多变,但是当老对象里面还有对象(也就是里面还有引用型数据),那么在浅拷贝中这个’内对象‘只是和直接赋值一样把地址给了新对象,所以还是会一变多变。

深拷贝

874

深浅拷贝都只针对引用类型数据,因为简单数据类型直接赋值就好了,也用不到它们。

递归深拷贝

875

<script>
   let i = 1
   function fn() {
     console.log(`这是第${i}`)
     if (i >= 6) {
       return
     }
     i++
     fn()
   }
   fn()
 </script>

记住递归函数一般要写return,否则死递归,但也根据需求而定。

那么怎么通过递归函数来深拷贝呢?

<script>
     const o={}
     const obj ={
         age:20,
         gender:'man'
     }
     function deepCopy(newObj,oldObj){
         for (let k in oldObj){
             newObj[k] = oldObj[k]
         }
     }
     deepCopy(o,obj)
     o.gender='woman'
     console.log(o);
     console.log(obj);
 </script>

当利用函数,通过对象遍历来赋值,还是浅拷贝。

如果对象中还有复杂数据类型,还是无法深拷贝,怎么办呢?我们需要通过一些手段判断是复杂数据类型还是简单数据类型,简单的直接放行,复杂的继续遍历。

const obj = {
     uname: 'pink',
     age: 18,
     hobby: ['乒乓球', '足球'],
     family: {
       baby: '小pink'
     }
   }
   const o = {}
   // 拷贝函数
   function deepCopy(newObj, oldObj) {
     debugger
     for (let k in oldObj) {
       // 处理数组的问题  一定先写数组 在写 对象 不能颠倒
       if (oldObj[k] instanceof Array) {
         newObj[k] = []
         //  newObj[k] 接收 []  hobby
         //  oldObj[k]   ['乒乓球', '足球']
         deepCopy(newObj[k], oldObj[k])
       } else if (oldObj[k] instanceof Object) {
         newObj[k] = {}
         deepCopy(newObj[k], oldObj[k])
       }
       else {
         //  k  属性名 uname age    oldObj[k]  属性值  18
         // 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)
   console.log([1, 23] instanceof Object)
   // 复习
   // const obj = {
   //   uname: 'pink',
   //   age: 18,
   //   hobby: ['乒乓球', '足球']
   // }
   // function deepCopy({ }, oldObj) {
   //   // k 属性名  oldObj[k] 属性值
   //   for (let k in oldObj) {
   //     // 处理数组的问题   k 变量
   //     newObj[k] = oldObj[k]
   //     // o.uname = 'pink'
   //     // newObj.k  = 'pink'
   //   }
   // }
 </script>

1-使用递归函数

2-当普通数据拷贝直接放行,如果遇到复杂数据类型如对象和数组,则再次调用此函数

3-判断是否是复杂类型的条件时候,必须先写数组再写对象,因为数组也是对象,需要先把数组筛选出去。

利用lodash实现深拷贝

lodash是一个人名,是这个人写的一个js库,里面的cloneDeep方法实现了深拷贝。

<!-- 先引用 -->
  <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>

注意 _.cloneDeep(obj),前面有一个下划线

利用json实现深拷贝

上面的lodash库的cloneDeep其实就是帮助我们封装了一个完善的递归函数实现深拷贝的方法,很实用,但是我还得引入一个js库,也有些麻烦。

通过json的两个方法,我们可以直接实现深拷贝,先用json.stringify(obj)把对象转换为字符,再通过json.parse(字符串),把字符串转化为对象即可。

<script>
  const obj = {
    uname: 'pink',
    age: 18,
    hobby: ['乒乓球', '足球'],
    family: {
      baby: '小pink'
    }
  }
  // 把对象转换为 JSON 字符串
  // console.log(JSON.stringify(obj))
  const o = JSON.parse(JSON.stringify(obj))
  console.log(o)
  o.family.baby = '123'
  console.log(obj)
</script>

异常处理

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

throw抛出异常

876

这里需要自己判断哪里会出错,设置出错条件,然后自己抛出异常,从而提示自己。如上的没有传递进参数就是一个错误。

抛出错误后,程序会中断

try/catch捕获异常

1-try/catch是捕获浏览器中发现的错误信息,如果 发现错误则执行catch中的代码(一般catch中代码会写return中断函数,因为发生了错误再运行下去可能发生更多错误),没有错误就继续运行。

2-finally中的代码不管有没有发生错误都会执行。

3-catch(error)中有一个参数error,它随便你命名,里面包含了错误的信息,一般我们会在catch中打印出error.message

4-try中写可能发生错误的代码

877

debugger

1-debugger是程序员在调试代码的时候用到,当我们在代码中写一个debugger,在f12控制台中调试代码的时候,就会自动跳转到这里,不再需要我们自己设置断点。

2-debugger只是方便跳转到需要调试的代码断点处,并不会中断代码执行。

this处理

this指向

普通函数this指向

1-谁调用了这个函数,this就指向谁

<script>
   // 普通函数:  谁调用我,this就指向谁
   console.log(this)  // window 
   function fn() {
     console.log(this)  // window  因为当我们写fn()实际上完整写法是window.fn()
   }
   window.fn()
   window.setTimeout(function () {
     console.log(this) // window 因为当我们写setTimeout(回调函数,时间)实际上完整写法是                                 window.setTimeout(回调函数,时间)
   }, 1000)
   document.querySelector('button').addEventListener('click', function () {
     console.log(this)  // 指向 button,因为是button调用了这个函数
   })
   const obj = {
     sayHi: function () {
       console.log(this)  // 指向 obj 因为是obj.sayHi(),是 obj调用了这个函数
     }
   }
   obj.sayHi()
 </script>

2-如果写了’use srtict‘在严格模式下,我们写fn(),则会被认为没有调用者,输出undefined

只有我们写window.fn(),才会获取到window

878

箭头函数的指向

箭头函数本身是没有this的,会通过外层作用域一层一层的查找有this定义来给自己。

879

1-在监听事件中,不建议写箭头函数,如上如果用了箭头函数,那么这个事件执行程序就不能再用this了,因为这个thi是外一层的this,也就是window、

2-构造函数和原型对象的this都指向它们的实例

构造函数和原型对象中一般也不用箭头函数

880

如上在原型对象中,我添加了严格walk方法,但是我需要在方法内写代码用this指向实例p1的

如果你用了箭头函数,那么this指向window,就不能再使用this操作实例p1了

this改变

this的指向是可以改变的

call方法

881

 

call方法不常用,了解即可,它的第一个参数可以指定this值,剩余的参数就是正常的给函数传参的作用。

apply方法

apply的第二个参数必须是数组,但是传入函数接的时候,还是一个一个数字接。

882

<script>
  const obj = {
    age: 18
  }
  function fn(x, y) {
    console.log(this) // {age: 18}
    console.log(x + y)
  }
  // 1. 调用函数
  // 2. 改变this指向 
  //  fn.apply(this指向谁, 数组参数)
  fn.apply(obj, [1, 2])
  // 3. 返回值   本身就是在调用函数,所以返回值就是函数的返回值

  // 使用场景: 求数组最大值
  // const max = Math.max(1, 2, 3)
  // console.log(max)
  const arr = [100, 44, 77]
  const max = Math.max.apply(Math, arr)//虽然传的是数组里面有多个值,不是说接还是一个一个接收,为什么这里能全部接收到去判断谁最大? 因为max底层是有循环的,不需要我们管。
  const min = Math.min.apply(null, arr) //你不想指向就写null,但不能不写
  console.log(max, min)
  // 使用场景: 求数组最大值
  console.log(Math.max(...arr))
</script>

求数组最大值,可以用for循环去比较,也可以用展开运算符就是那三个点,也可以用今天学的apply

bind方法与应用

883

bind的参数传递和call一样,但是bind不会调用函数,只改变this指向

const obj = {
    age: 18
  }
  function fn() {
    console.log(this)
  }

  // 1. bind 不会调用函数 
  // 2. 能改变this指向
  // 3. 返回值是个函数,  但是这个函数里面的this是更改过的obj
  const fun = fn.bind(obj)
  // console.log(fun) 
  fun()

call和apply会调用函数,所以返回的就是函数体内设置的返回,但是bind不会调用函数,返回的是用一个原函数拷贝的新函数(新函数的this会改变成我们设置的)

bind的应用

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

884

节流

885

throttle掐死,勒死,节流阀。

什么叫节流,就是在一定事件内,不管触发多少次事件,都只执行一次执行函数。

如上的轮播图手动鼠标切换,如果没有节流,我们一秒钟点个七八次,轮播图直接乱了停不下来

但是如果加了节流,在1s内只能触发一次,那么在这1s内无论点多少次,也只切换一张图

那么怎么写节流呢?

其实就是把执行函数换成了一个节流函数,这个函数里面添加了条件来判断,如果时间到了才运行原来的执行函数。

<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let i = 1  // 让这个变量++
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
      // 如果里面存在大量操作 dom 的情况,可能会卡顿
    }
    // console.log(mouseMove)
    // 节流函数 throttle 
   
    function throttle(fn, t) {
      // 起始时间
      let startTime = 0
      return function () {
        
        // 得到当前的时间,这是时间戳
        let now = Date.now()
        // 判断如果大于等于 500 采取调用函数
        if (now - startTime >= t) {
          // 调用函数
          fn() 
          // 起始的时间 = 现在的时间   为了精准度,写在调用函数的下面 
          startTime = now
        }
      }
    }
    box.addEventListener('mousemove', throttle(mouseMove, 500))

    // throttle(mouseMove, 500) === function () { console.log(1) }


    // box.addEventListener('mousemove', function () {
    //   // 得到当前的时间
    //   let now = Date.now()
    //   // 判断如果大于等于 500 采取调用函数
    //   if (now - startTime >= t) {
    //     // 调用函数
    //     fn()
    //     // 起始的时间 = 现在的时间   写在调用函数的下面
    //     startTime = now
    //   }
    // })

  </script>
</body>

原先我有一个疑问,每次触发,代码中不是都将 let startTime = 0了嘛?岂不是每次都能符合条件触发?

1-这里运用到了闭包,因为内存泄漏,所以startTime是不会被回收的,所以startTome会被赋值为now

2-你不会被回收就不会,我不是每次都赋值为0了?这里是闭包,事件执行函数并不是throttle(),而是里面的内函数,也就是那个return后的函数。(事件触发后,throttle(mouseMove, 500)会立即执行一次,然后得到闭包中的return后的函数体作为返回值,然后执行这个函数体)

// throttle(mouseMove, 500) === function () { 代码 }

防抖debounce

886

防抖和节流差不多,差别就是比如设置500ms,如果我一直触发这个事件,那么节流会在500ms后再次执行函数

防抖的话,因为如果中n s内又触发事件,会重新计算函数执行事件,当一直触发这个事件,就永远不会再触发。

<body>
  <div class="box"></div>
  <script>
    const box = document.querySelector('.box')
    let i = 1  // 让这个变量++
    // 鼠标移动函数
    function mouseMove() {
      box.innerHTML = ++i
      // 如果里面存在大量操作 dom 的情况,可能会卡顿
    }
    // 防抖函数
    function debounce(fn, t) {
      let timeId
      return function () {
        // 如果有定时器就清除
        if (timeId) clearTimeout(timeId)
        // 开启定时器 200
        timeId = setTimeout(function () {
          fn()
        }, t)
      }
    }
    // box.addEventListener('mousemove', mouseMove)
    box.addEventListener('mousemove', debounce(mouseMove, 200))

  </script>
</body>

这个是案例黑马给的代码,使用定时器,事件触发判断有定时器就清除定时器,重新设置一个,没有定时器的话就再设置一个,定时器的回调函数就是mouseMove()

问题

我寻思直接在节流的上面加上一行代码

return function () {
      
      // 得到当前的时间,这是时间戳
      let now = Date.now()
      // 判断如果大于等于 500 采取调用函数
      if (now - startTime >= t) {
        // 调用函数
        fn() 
        // 起始的时间 = 现在的时间   为了精准度,写在调用函数的下面 
        startTime = now
      }
     startTime = now

如上,让条件不成立的时候,也把now赋予startTime,那么时间就重新计算不就完了。

我的方法和黑马方法的区别

黑马定时器的方法是触发事件后,指定时间内不再触发时间,再执行函数

我的方法是触发事件立即执行一次,指定时间内不再触发,再触发事件才会再执行函数

887

如上防抖一般使用场景是搜索框输入,肯定是触发事件后一段时间再发送请求,而不是用户刚打一个字母就里面请求,所以黑马的定时器方法 防抖更好,大家公认的也是这种。

lodash防抖节流

lodash是一个js库,里面提供了节流防抖方法

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

   // box.addEventListener('mousemove', mouseMove)
   // lodash 节流写法
   // box.addEventListener('mousemove', _.throttle(mouseMove, 500))
   // lodash 防抖的写法
   box.addEventListener('mousemove', _.debounce(mouseMove, 500))

 </script>

节流综合案例

888

当我们看视频到一个时间,如果刷新页面就会从头播放,怎么记录原先播放的时间点呢?

两个事件

ontimeupdate时间变化 ,当前视频播放位置发生改变时触发,触发较快,一秒钟好几次

onloadeddata数据加载因为视频比较特殊,还是用老的on来监听事件,而不是addEventListener

889

<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>
    // 1. 获取元素  要对视频进行操作
 
    const video = document.querySelector('video')
    video.ontimeupdate = _.throttle(() => {
      // console.log(video.currentTime) 获得当前的视频时间
      // 把当前的时间存储到本地存储
      localStorage.setItem('currentTime', video.currentTime)
    }, 1000)

    // 打开页面触发事件,就从本地存储里面取出记录的时间, 赋值给  video.currentTime
    video.onloadeddata = () => {
      // console.log(111)
      video.currentTime = localStorage.getItem('currentTime') || 0
    }

  </script>
</body>

video.currentTime可以设置视频当前播放时间。

作业

1.下列选项中关于深浅拷贝说法错误的是? (d) 分值1分

A:直接赋值对象的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址

B:浅拷贝时,对象属性值是简单数据类型直接拷贝值,如果属性值是引用数据类型则拷贝的是地址

C:深拷贝拷贝的是对象,不是地址,所以不会相互影响了

D:我们可以采取函数递归的方式完成浅拷贝

回答正确+1分

答案解析:

递归函数可以实现深拷贝

2.下列选项中关于函数递归说法错误的是? (c) 分值1分

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

B:递归函数的作用和循环效果类似,不断的自己调用自己

C:由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 break

D:我们可以利用递归函数实现 setTimeout 模拟 setInterval效果

回答正确+1分

答案解析:

退出选择return 退出

3.下列选项中可以完成深拷贝的是? (abc) 分值1分

A:通过递归函数实现深拷贝

B:利用js库lodash里面cloneDeep内部实现了深拷贝

C:通过JSON.stringify() 转换字符串,再利用 JSON.parse() 方法转换为对象可以完成深拷贝

D:可以采取bind方法完成深拷贝

回答正确+1分

4.下列选项中throw抛异常说法错误的是? (a) 分值1分

A:throw 抛出异常信息,但是我们必须加 return 来终止程序的往下执行

B: throw 后面跟的是错误提示信息

C:Error 对象配合 throw 使用,能够设置更详细的错误信息

D:Error 对象配合 throw 控制台显示的提示可以为红色更警示

回答正确+1分

答案解析:

throw 直接中断程序,不需要写return

5.下列选项中 try/catch捕获错误信息 说法错误的是? (a) 分值1分

A:将预估可能发生错误的代码写在 catch 代码段中

B:如果 try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息

C:我们需要给catch 添加 return 可以终止程序继续执行

D:finally 不管是否有错误,都会执行

回答正确+1分

答案解析:

预估错误的代码写到 try 里面

6.下列选项中 关于this指向说法错误的是? (d) 分值1分

A:普通函数this指向我们理解为谁调用 this 的值指向谁

B:定时器中的this 指向 window

C:箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this

D:箭头函数中没有this, 是沿用 window,简单说,箭头函数的this 指向window

回答正确+1分

答案解析:

箭头函数没有this,但是是从上一级作用域里面找,以此类推

7.下列选项中 可以改变this指向的方法是? (abc) 分值1分

A:call()

B:bind()

C:apply()

D:catch()

回答正确+1分

8.下列选项中说法错误的是? (d) 分值1分

A:call 和 apply 会调用函数, 并且改变函数内部this指向

B:bind 不会调用函数, 可以改变函数内部this指向,它也是主要用来改变this指向的

C:call 和 apply 传递的参数不一样, call 传递参数 aru1, aru2..形式 apply 必须数组形式[arg]

D: apply 主要使用场景是可以改变定时器的this指向,并且不调用函数

回答正确+1分

答案解析:

apply 会调用函数

9.下列选项中关于节流和防抖说法正确的是? (a bcd) 分值1分

A:节流: 就是指连续触发事件但是在 n 秒中只执行一次函数,比如可以利用节流实现 1s之内只能触发一次鼠标移动事件

B:防抖:如果在 n 秒内又触发了事件,则会重新计算函数执行时间

C:节流应用场景: 鼠标移动,页面尺寸发生变化,滚动条滚动等开销比较大的情况下

D:防抖应用场景:搜索框输入

回答正确+1分


文章作者: 瑾年
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 周东奇 !
免责声明: 本站所发布的一切内容,包括但不限于IT技术资源,网络攻防教程及相应程序等文章仅限用于学习和研究目的:不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。本站部分信息与工具来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如有侵权请邮件(jinnian770@gmail.com)与我们联系处理。
  目录