深浅拷贝和异常处理与性能优化

深入this学习,知道如何判断this指向和改变this指向,知道在JS中如何处理异常,学习深浅拷贝,理解递归。

浅拷贝

开发中我们经常需要复制一个对象。如果直接用赋值,只要做修改就会影响原对象。

对象赋值出现的问题

浅拷贝和深拷贝只针对引用类型。浅拷贝:拷贝的是地址

浅拷贝对象

const obj = {
  uname: '欣标',
  age: 18
}

// 第一种方法 浅拷贝
const o = { ...obj }
console.log(o)   // {uname: '欣标', age: 18}
o.age = 20
console.log(o)   // {uname: '欣标', age: 20}
console.log(obj) // {uname: '欣标', age: 18}

// 第二种方法 浅拷贝
const o = {}
Object.assign(o, obj)
console.log(o)      // {uname: '欣标', age: 18}
o.age = 20
console.log(o)      // {uname: '欣标', age: 20}
console.log(obj)    // {uname: '欣标', age: 18}

浅拷贝数组

const arr = [1, 2, 3]

// 第一种方法 浅拷贝
const newArr = [...arr]
console.log(newArr)   // [1, 2, 3]
newArr[1] = 9
console.log(newArr)   // [1, 9, 3]
console.log(arr)      // [1, 2, 3]

// 第二种方法 浅拷贝
const newArr = arr.slice()
console.log(newArr) // [1, 2, 3]
newArr[1] = 9
console.log(newArr) // [1, 9, 3]
console.log(arr)    // [1, 2, 3]

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

深拷贝

浅拷贝和深拷贝只针对引用类型。深拷贝:拷贝的是对象,不是地址。

递归函数

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

  • 简单理解:函数内部自己调用自己, 这个函数就是递归函数
  • 递归函数的作用和循环效果类似
  • 由于递归很容易发生“栈溢出”错误stack overflow,所以必须要加退出条件return
// 递归函数:类似for循环自己调用自己
let i = 1
function fn() {
  console.log(`这是第${i}次`)
  if (i >= 6) {
    return
  }
  i++
  fn()
}
fn()

练习:递归函数模拟定时器

利用递归函数实现 setTimeout 模拟 setlnterval效果 Demo

  1. 页面每隔一秒输出当前的时间
  2. 输出当前时间可以使用:new Date().toLocalestring()
function getTime() {
  document.querySelector('div').innerHTML = new Date().toLocaleString()
  setTimeout(getTime, 1000)
}
getTime()

递归函数实现深拷贝

Demo

const obj = {
  uname: '欣标',
  age: 18,
  hobby: ['兵乓球', '足球'],
  sing: {
    count: 100,
    why: false
  }

}

const o = {}
function deepCopy(newObj, oldObj) {
  // 1. 简单数据 遍历对象
  for (let k in oldObj) {
    // 2. 处理对象数组的问题 判断属性值是不是一个数组
    if (oldObj[k] instanceof Array) {
      newObj[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)  //函数调用 两个参数。新矿泉 obj旧对泉

// 第一 旧 递归函数复制到新数组 与旧数组一样
console.log(obj)

// 第二 新 递归函数复制到新数组 与旧数组一样
console.log(o)

// 新赋值
o.age = 20
o.hobby[0] = '篮球'
o.sing.big = '13mm'

//  第三 旧 新数组赋值 更改数据
console.log(o)

// 第四 打印旧数组 和旧数组一样
console.log(obj)
const obj = {
  uname: '欣标',
  age: 18
}

const o = {}
function deepCopy(newObj, oldObj) {
  for (let k in oldObj) {
    // k 属性名; oldObj[k] 属性值
    // newObj[k] === o.uname
    newObj[k] = oldObj[k]
  }
}
deepCopy(o, obj)  //函数调用 两个参数。新矿泉 obj旧对泉
console.log(o)   // {uname: '欣标', age: 18}
o.age = 20
console.log(o)   // {uname: '欣标', age: 20}
console.log(obj) // {uname: '欣标', age: 18}

js库lodash深拷贝

js库lodash里面cloneDeep内部实现了深拷贝

Lodash深拷贝文档:https://www.tkcnn.com/lodash/language/cloneDeep.html

先引入:<script src="./lodash.min.js"></script>

const obj = {
  uname: '欣标',
  age: 18,
  hobby: ['兵乓球', '足球'],
  sing: {
    count: 100,
    why: false
  },
  say: function () {
    console.log('方法')
  }
}
// Lodash语法
const o = _.cloneDeep(obj)
console.log(o) // {uname: '欣标', age: 18, hobby: Array(2), sing: {…}, say: ƒ}
obj.say() // 方法

// 赋值
o.hobby[1] = '篮球'
o.sing.foot = '面包'
o.say = () => console.log('新方法')
o.son = () => console.log('儿子')

o.say() // 新方法
o.son() // 儿子

console.log(o)
console.log(obj)

利用jSON实现深拷贝

const obj = {
  uname: '欣标',
  age: 18,
  hobby: ['兵乓球', '足球'],
  sing: {
    count: 100,
    why: false
  },
  say: function () {
    console.log('方法')
  }
}
// 把对象转换为 JSON 字符串
const o = JSON.parse(JSON.stringify(obj))
console.log(o)

o.sing.foot = '面包'
console.log(obj)

异常处理

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

throw抛异常

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

  1. throw抛出异常信息,程序也会终止执行
  2. throw后面跟的是错误提示信息
  3. Error对象配合throw使用,能够设置更详细的错误信息
function fn(x, y) {
  if (!x || !y) {
    // throw '没有参数传递进来'
    throw new Error('没有参数传递进来')
  }
  return x + y
}

console.log(fn())  // exe.js:3 Uncaught 没有参数传递进来

try /catch 捕获异常

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

  1. try...catch用于捕获错误信息
  2. 将预估可能发生错误的代码写在try代码段中
  3. 如果try代码段中出现错误后,会执行catch代码段,并截获到错误信息
  4. finally不管是否有错误,都会执行
function fn() {
  try {
    // 可能发生错误的代码 要写 try
    const p = document.querySelector('.p')
    p.style.color = 'red'
  } catch (err) {
    // 拦截错误,提示浏览器提供的错误信息,但是不中断程序的执行
    console.log(err.message) //Cannot read properties of null (reading 'style')
    throw new Error('你看看选择器错误了吧')
    // 需要加return 中断程序
    // return
  }
  finally {
    // 不管你程序对不对,一定会执行的代码
    alert('弹出对话框')
  }
  console.log(11)
}
fn()
function fn() {
  try {
    // 可能发生错误的代码 要写 try
    const p = document.querySelector('.p')
    p.style.color = 'red'
  } catch (err) {
    // 拦截错误,提示浏览器提供的错误信息,但是不中断程序的执行
    console.log(err.message) //Cannot read properties of null (reading 'style')
    // 需要加return 中断程序
    // return
  }
}
fn()

err.message 中的err 可以自己起名字

debugger

debugger 是 JavaScript 中用于调试代码的一个功能。它允许开发者在代码执行过程中暂停程序,检查当前的执行状态,以及单步执行代码。这对于发现和修复代码中的错误非常有用。

function exampleFunction() {
    let x = 10;
    debugger; // 程序将在这里暂停
    x = x + 5;
    console.log(x);
}

exampleFunction();

this指向

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

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

普通函数this

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

普通函数没有明确调用者时this值为window,严格模式下没有调用者时this的值为undefined

console.log(this)  // window

function fn() {
  console.log(this) // window
}
fn()

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()

// 严格模式 了解
// 'use strict'
// function fn() {
//   console.log(this) // window
// }
// window.fn()

箭头函数this

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

  1. 箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的
  2. 箭头函数中的this引用的就是最近作用域中的this
  3. 向外层作用域中,一层一层查找this,直到有this的定义

注意情况一:

使用箭头函数前需要考虑函数中this的值,事件回调函数使用箭头函数时,this为全局的 window

因此DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数

注意情况二:

同样由于箭头函数this的原因,基于原型的面向对象也不推荐采用箭头函数

function Person() {

}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
  console.log('人都要走路..')
  console.log(this) // window
}
const p1 = new Person()
P1.walk()

改变this

JavaScript 中还允许指定函数中this的指向,有 3 个方法可以动态指定普通函数中this的指向。

call方法

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

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

function fn() {
  console.log(this) // window
}
fn()

// 调用函数 改变this指同
fn.call(obj) // {uname: 'xinbiao'}
// 也可以传形参和实参
fnn.call(obj, 1, 2)

apply方法

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

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

function fn(x, y) {
  console.log(this) // window
  console.log(x + y)
}
// fn()

// 1. 调用函数 改变this指同
// fn.apply(obj) // {uname: 'xinbiao'}
// fn.apply( this指向,数组参数)
fn.apply(obj, [1, 2])
// 2. 返回值 本身就是在调用函数,所以返回值就是函数的返回值


// 使用场景
// const max = Math.max(1, 2, 3)
const arr = [1, 2, 3]
const max = Math.max.apply(null, arr)
console.log(max) // 3

// 展开运算符
console.log(Math.max(...arr)) // 3

bind方法

bind()方法不会调用函数。但是能改变函数内部this指向

  • thisArg:在 fun 函数运行时指定的 this 值
  • arg1,arg2:传递的其他参数
  • 返回由指定的this值和初始化参数改造的 原函数拷贝 (新函数)
  • 只想改变this指向,并且不想调用这个函数时,可以使用bind,比如改变定时器内部的this指向.
const obj = {
  uname: 'xinbiao'
}

function fn() {
  console.log(this) // window
}

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

使用场景:有一个按钮,点击里面就禁用,2秒钟之后开启 Demo

const btn = document.querySelector('button')

btn.addEventListener('click', function () {
  // 禁用按钮
  this.disabled = true
  window.setTimeout(function () {
    // 这个普通函数里面,我们要this由原来的window 改为 btn
    this.disabled = false
  }.bind(this), 2000) // 这行的 this等于 btn
})

总结

相同点:都可以改变函数内部的this指向

区别点:

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

主要应用场景:

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

性能优化

debounce 防抖

防抖指的是单位时间内,频繁触发事件,只执行最后一次。举个栗子:王者荣耀回城,只要被打断就需要重新来。

使用场景:① 搜索框搜索输入。只需用户最后一次输入完,再发送请求;② 手机号、邮箱验证输入检测

练习防抖:鼠标滑过盒子显示文字

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

lodash 防抖文档: https://www.tkcnn.com/lodash/function/debounce.html

// 如果里面存在大量消耗性能的代码,比如dom操作,比如数据处理,可能造成卡顿
const box = document.querySelector('.box')
let i = 1
function mou() {
  box.innerHTML = i++
}

// 利用Lodash库实现防抖 500毫秒之后采取 +1
// 语法 _.debounce(fun, 500)
box.addEventListener('mousemove', _.debounce(mou, 550))

手写防抖函数

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

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

  1. 声明一个定时器变量
  2. 当鼠标每次滑动都先判断是否有定时器了,如果有定时器先清除以前的定时器
  3. 如果没有定时器则开启定时器,记得存到变量里面
  4. 在定时器里面调用要执行的函数
const box = document.querySelector('.box')
let i = 1
function mou() {
  box.innerHTML = i++
}

// 手写防抖函数
function debounce(fn, t) {
  let timer
  // return 返回一个匿名函数
  return function () {
    // 2,3,4
    // 判断定时器,如果有就删除
    if (timer) clearTimeout(timer)
    // 开启定时器
    timer = setTimeout(function () {
      fn() // 加小括号调用 fn函数
    }, t)
  }
}

// 因为 debounce()是带了小括号的,页面一打开就执行完了,所以要返回一个匿名函数
box.addEventListener('mousemove', debounce(mou, 500))
// debounce(mou, 500) // 调用函数
// debounce(mou, 500) // function () { 2,3,4 }

节流 throttle

节流是单位时间内,频繁触发事件,只执行一次。

举个栗子:① 王者荣耀技能冷却,期间无法继续释放技能;② 和平精英 98k换子弹期间不能射击。

使用场景:高频事件中的鼠标移动mousemove、页面尺寸缩放resize、滚动条滚动scroll等。

练习节流:鼠标滑过盒子显示文字

要求:鼠标在盒子上移动,不管移动多少次,每隔3000ms+ 1 Demo

lodash 节流文档: https://www.tkcnn.com/lodash/function/throttle.html

const box = document.querySelector('.box')
let i = 1
function mou() {
  box.innerHTML = i++
}

// Lodash库实现节流 3000毫秒之后采取 +1
box.addEventListener('mousemove', _.throttle(mou, 3000))

手写节流函数

要求:鼠标在盒子上移动,不管移动多少次,每隔500ms+ 1 Demo

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

  1. 声明一个定时器变量
  2. 当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新定时器
  3. 如果没有定时器则开启定时器,记得存到变量里面
    • 定时器里面调用执行的函数
    • 定时器里面要把定时器清空
const box = document.querySelector('.box')
let i = 1
function mou() {
  box.innerHTML = i++
}

// 手写一个节流函数
function throttle(fn, t) {
  let timer = null
  return function () {
    // 判断没有定时器就开启定时
    if (!timer) {
      timer = setTimeout(function () {
        fn()
        // 清空定时器
        timer = null
      }, t)
    }
  }
}
box.addEventListener('mousemove', throttle(mou, 3000))
let timer = null
timer = setTimeout(() => {
  clearTimeout(timer)
  console.log(timer) // 1
}, 1000)

案例:记录上次视频播放位置

页面打开,可以记录上一次的视频播放位置 Demo

分析两个事件:

ontimeupdate事件在视频或者音频当前的播放位置发送改变时触发

onloadeddata事件在当前帧的数据加载完成且还没有足够的数据播放视频或音频的下一帧时触发

注:谁需要节流?ontimeupdate 触发频次太高了,我们可以设定1秒钟触发一次

  1. ontimeupdate事件触发时,每隔1秒钟,就记录当前时间到本地存储
  2. 下次打开页面, onloadeddata事件触发,就从本地存储取出的时间播放,如果没有就默认为0s
  3. 获得当前时间video.currentTime
const video = document.querySelector('video')

video.ontimeupdate = _.throttle(() => {
  // console.log(video.currentTime) // 打印当前的时间
  // 把当前的时间存储到本地存储里
  localStorage.setItem('currentTime', video.currentTime)
}, 1000)

// 打开页面触发事件,取出本地存储的时间开始播放
video.onloadeddata = function () {
  // console.log(1111)
  video.currentTime = localStorage.getItem('currentTime') || 0
}

原创文章,作者:霍欣标,如若转载,请注明出处:https://www.bigengwu.cn/xue/158.html

霍欣标的头像霍欣标
上一篇 2024-09-18 21:38
下一篇 2024-09-29 04:05

相关推荐

博主人懒,应管局要求暂不开启站内私信和评论功能,如需帮助请发邮件。

邮箱账号:1969600480@qq.com