深浅拷贝
<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也会跟着变,这是不行的。
浅拷贝
对象的浅拷贝可以用展开运算符或者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对象里面又有对象或者数组这些引用形数据,那么里面的数据,还是和直接复制一样被改变。
所以说浅拷贝,只能将外层的数据重新生成一个新地址去储存,内层数据还是同一个地址。
直接赋值一个对象,就是把对象的地址给另一个变量,自然一变就多变
浅拷贝是把老对象的数据赋予新对象,生成了新地址内存去存储,所以一般不会一变多变,但是当老对象里面还有对象(也就是里面还有引用型数据),那么在浅拷贝中这个’内对象‘只是和直接赋值一样把地址给了新对象,所以还是会一变多变。
深拷贝
深浅拷贝都只针对引用类型数据,因为简单数据类型直接赋值就好了,也用不到它们。
递归深拷贝
<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抛出异常
这里需要自己判断哪里会出错,设置出错条件,然后自己抛出异常,从而提示自己。如上的没有传递进参数就是一个错误。
抛出错误后,程序会中断
try/catch捕获异常
1-try/catch是捕获浏览器中发现的错误信息,如果 发现错误则执行catch中的代码(一般catch中代码会写return中断函数,因为发生了错误再运行下去可能发生更多错误),没有错误就继续运行。
2-finally中的代码不管有没有发生错误都会执行。
3-catch(error)中有一个参数error,它随便你命名,里面包含了错误的信息,一般我们会在catch中打印出error.message
4-try中写可能发生错误的代码
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
箭头函数的指向
箭头函数本身是没有this的,会通过外层作用域一层一层的查找有this定义来给自己。
1-在监听事件中,不建议写箭头函数,如上如果用了箭头函数,那么这个事件执行程序就不能再用this了,因为这个thi是外一层的this,也就是window、
2-构造函数和原型对象的this都指向它们的实例
构造函数和原型对象中一般也不用箭头函数
如上在原型对象中,我添加了严格walk方法,但是我需要在方法内写代码用this指向实例p1的
如果你用了箭头函数,那么this指向window,就不能再使用this操作实例p1了
this改变
this的指向是可以改变的
call方法
call方法不常用,了解即可,它的第一个参数可以指定this值,剩余的参数就是正常的给函数传参的作用。
apply方法
apply的第二个参数必须是数组,但是传入函数接的时候,还是一个一个数字接。
<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方法与应用
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 一样
})
节流
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
防抖和节流差不多,差别就是比如设置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,那么时间就重新计算不就完了。
我的方法和黑马方法的区别
黑马定时器的方法是触发事件后,指定时间内不再触发时间,再执行函数
我的方法是触发事件立即执行一次,指定时间内不再触发,再触发事件才会再执行函数
如上防抖一般使用场景是搜索框输入,肯定是触发事件后一段时间再发送请求,而不是用户刚打一个字母就里面请求,所以黑马的定时器方法 防抖更好,大家公认的也是这种。
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>
节流综合案例
当我们看视频到一个时间,如果刷新页面就会从头播放,怎么记录原先播放的时间点呢?
两个事件
ontimeupdate时间变化 ,当前视频播放位置发生改变时触发,触发较快,一秒钟好几次
onloadeddata数据加载因为视频比较特殊,还是用老的on来监听事件,而不是addEventListener
<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分