深入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
- 页面每隔一秒输出当前的时间
- 输出当前时间可以使用:
new Date().toLocalestring()
function getTime() {
document.querySelector('div').innerHTML = new Date().toLocaleString()
setTimeout(getTime, 1000)
}
getTime()
递归函数实现深拷贝
万物皆对象,先把数组排查完毕后再刷选对象
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抛异常
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行。
throw
抛出异常信息,程序也会终止执行throw
后面跟的是错误提示信息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
最后
-
try...catch
用于捕获错误信息 - 将预估可能发生错误的代码写在
try
代码段中 - 如果
try
代码段中出现错误后,会执行catch
代码段,并截获到错误信息 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
!
- 箭头函数会默认帮我们绑定外层
this
的值,所以在箭头函数中this
的值和外层的this
是一样的 - 箭头函数中的
this
引用的就是最近作用域中的this
- 向外层作用域中,一层一层查找
this
,直到有this
的定义
构造函数,原型函数,字面量对象中函数,dom事件函数不使用箭头函数
注意情况一:
使用箭头函数前需要考虑函数中
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()
求数组的最大值
null
是不改变this
指向,但是不能省略
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指向
区别点:
call
和apply
会调用函数, 并且改变函数内部this
指向call
和apply
传递的参数不一样,call
传递参数aru1,aru2..
形式apply
必须数组形式[arg]bind
不会调用函数, 可以改变函数内部this
指向主要应用场景:
call
调用函数并且可以传递参数apply
经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值bind
不调用函数,但是还想改变this指向. 比如改变定时器内部的this
指向
性能优化
debounce 防抖
防抖指的是单位时间内,频繁触发事件,只执行最后一次。举个栗子:王者荣耀回城,只要被打断就需要重新来。
使用场景:① 搜索框搜索输入。只需用户最后一次输入完,再发送请求;② 手机号、邮箱验证输入检测
练习防抖:鼠标滑过盒子显示文字
要求:鼠标在盒子上移动,鼠标停止500ms
之后,里面的数字就会变化+1
Demo
① lodash
提供的防抖来处理
② 手写一个防抖函数来处理
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
来实现
- 声明一个定时器变量
- 当鼠标每次滑动都先判断是否有定时器了,如果有定时器先清除以前的定时器
- 如果没有定时器则开启定时器,记得存到变量里面
- 在定时器里面调用要执行的函数
函数调用只执行一次,那添加事件多次执行这个函数该怎么办呢?那就让被调用函数里面return
返回一个匿名函数
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
提供的节流来处理
② 手写一个节流函数来处理
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
来实现
- 声明一个定时器变量
- 当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新定时器
- 如果没有定时器则开启定时器,记得存到变量里面
- 定时器里面调用执行的函数
- 定时器里面要把定时器清空
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))
setTimeout
中是无法删除定时器的,因为定时器还在运作,所以使用timer =null
而不是cLearTimeout(timer)
let timer = null
timer = setTimeout(() => {
clearTimeout(timer)
console.log(timer) // 1
}, 1000)
案例:记录上次视频播放位置
页面打开,可以记录上一次的视频播放位置 Demo
分析两个事件:
①
ontimeupdate
事件在视频或者音频当前的播放位置发送改变时触发②
onloadeddata
事件在当前帧的数据加载完成且还没有足够的数据播放视频或音频的下一帧时触发注:谁需要节流?
ontimeupdate
触发频次太高了,我们可以设定1秒钟触发一次
ontimeupdate
事件触发时,每隔1
秒钟,就记录当前时间到本地存储- 下次打开页面,
onloadeddata
事件触发,就从本地存储取出的时间播放,如果没有就默认为0s
- 获得当前时间
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