目录
练习:利用递归函数实现 setTimeout 模拟 setInterval 效果
一、深浅拷贝
结构相同对象,复制对象重新赋值
不能用直接赋值来拷贝对象:o = obj
这样他俩指向同一个地址一个变了,就都变了
深浅拷贝只针对引用数据类型
(一)浅拷贝
适用于单层的对象和数组,不适用于多层嵌套的数组对象拷贝
最外层拷贝数值,内层拷贝地址
1.浅拷贝的简单使用
拷贝对象:
Object.assign() / 展开运算符 (...obj) 拷贝对象
拷贝数组:
Array.prototype.concat() / [...arr]
使用两种浅拷贝方法
解决了直接赋值的问题,这样就变成拷贝值(最外层)而不是地址
第一种:...展开运算符
<body>
<script>
const obj = {
uname: '一个人',
age: 18
}
const o = { ...obj }
console.log(o)
o.age = 29
console.log(o)
console.log(obj)
</script>
</body>
第二种:Object.assign(被赋值的,赋值的)
<body>
<script>
const obj = {
uname: '一个人',
age: 18
}
const o = {}
Object.assign(o, obj)
console.log(o)
o.age = 29
console.log(o)
console.log(obj)
</script>
</body>
2.浅拷贝的问题
只拷贝最外层的值,如果有多层的话就变成拷贝地址了
如下只改变 拷贝出来的 o 对象的 family 的 num 属性,结果原本的 obj 中的 num 的值也变了,是因为有复杂多层对象时,里层拷贝的是地址,地址相同变一个都变了
<body>
<script>
const obj = {
uname: '一个人',
age: 18,
family: {
num: 1
}
}
const o = {}
Object.assign(o, obj)
console.log(o)
o.family.num = 100
console.log(o)
console.log(obj)
</script>
</body>
(二)深拷贝
直接拷贝对象,解决浅拷贝的问题,有三种方式实现深拷贝
1.通过递归实现深拷贝
递归函数:
函数内部调用自己就是递归函数
作用类似循环
容易发生栈溢出错误 必须添加退出条件
递归实现过程:
调用函数:直接看函数 deepCopy() 里卖两个实参 obj 和 o
函数内部:
for(k in oldobj) 循环递归 原本对象中的元素,因为是对象 k 代表的是对象中的属性名 而且 k 不是具体的属性名 不能用 oldobj.k 的形式 而应该用 [ ]
注意 oldobj[k] 和 newobj[k] 的含义不同 因为一个对象里面有东西,一个没东西 oldobj[k] 是指对象中具体的属性值 而 newobj[k] 指的是属性名
相当于 o.uname = obj.uname ('pink')
在外层时 直接进行赋值就行 newobj[k] = oldobj[k]
在内层时就得进行判断了,因为是数组也得进行递归赋值,所以直接用我们这个现成的函数递归实现就行
判断如果当前 oldobj[k] 属性值 是数组时 就再次调用函数,先初始化新数组 就是类似 上面的 o 得有个空的数组
当再次调用时oldobj[k] 变成了数组 ['足球', ‘篮球’] 此时 for(k in oldobj[k]) 递归中的 k 的意义变了,在数组中递归 k 是数组下标 然后分别遍历 足球 篮球 都不是数组 到 else 中进行赋值
就是newobj[0] = oldobj[0] ('足球')
newobj[1] = oldobj[1] ('篮球')
最后组成数组
递归实现完成
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['足球', '篮球']
}
const o = {}
function deepCopy(newobj, oldobj) {
for (k in oldobj) {
if (oldobj[k] instanceof Array) {
newobj[k] = []
deepCopy(newobj[k], oldobj[k])
} else {
newobj[k] = oldobj[k]
}
}
}
deepCopy(o, obj)
o.hobby[0] = '羽毛球'
console.log(o)
console.log(obj)
</script>
</body>
其他问题:
如果有内层还有对象的话
同理上面再加一个 else if 分支即可
但是注意!!!
Object 判断必须在 Array 下面因为数组也算对象 就会把 hobby 误认为是对象
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['足球', '篮球'],
family: {
money: 100
}
}
const o = {}
function deepCopy(newobj, oldobj) {
for (k in oldobj) {
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 {
newobj[k] = oldobj[k]
}
}
}
deepCopy(o, obj)
o.hobby[0] = '羽毛球'
o.family.money = 20
console.log(o)
console.log(obj)
</script>
</body>
2.利用 lodash 实现深拷贝
它是一个 js 库 里面有递归实现深拷贝的方法 可以直接下载进行调用
先引入再调用
_cloneDeep
<body>
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['足球', '篮球'],
family: {
money: 100
}
}
const o = _.cloneDeep(obj)
console.log(o)
</script>
</body>
3.利用 JSON 实现深拷贝
先把对象转换成字符串,然后再把字符串放到新的对象中转换回对象
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['足球', '篮球'],
family: {
money: 100
}
}
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
</script>
</body>
二、异常处理
(一)抛出异常 throw
预估代码中的可能发生的错误,最大程度避免错误导致程序无法继续进行
throw 会使程序中断,然后抛出异常
body>
<script>
function counter(x, y){
if (!x || !y){
throw new Error('参数不能为空')
}
return x + y
}
counter()
</script>
</body>
(二)捕获异常 try catch
我们可以通过 try / catch 来捕获浏览器提供的错误信息
try 尝试 把觉得可能发生错误的代码写进去
catch 拦住 是捕获 try 中的错误信息 不是自动中断可以使用 return 中断 catch 里面有参数 用message 就能得到浏览器的错误信息了
finally 最后 不管程序对不对 一定会执行的代码 catch 使用 return 中断了其它的代码 但是不拦截 finally
<body>
<p>123</p>
<script>
function fn(){
try {
const p = document.querySelector('p')
p.style.color = 'red'
} catch (err) {
console.log(err.message)
throw new Error('有点错误')
} finally {
alert('弹出对话框')
}
console.log(11)
}
fn()
</script>
</body>
(三)debugger
给程序员调试用的,在代码的一行加上 debugger 在浏览器中打开,然后 f12 一刷新 就直接能跳到debugger 的地方
<body>
<p>123</p>
<script>
function fn() {
debugger
try {
const p = document.querySelector('p')
p.style.color = 'red'
} catch (err) {
console.log(err.message)
throw new Error('有点错误')
} finally {
alert('弹出对话框')
}
console.log(11)
}
fn()
</script>
</body>
三、处理 this
(一)this 指向
1.普通函数 this 指向
谁调用指向谁 如果是严格模式 指向 undefined
下面 this 分别指向 window window window button obj
<body>
<button></button>
<script>
console.log(this)
function fn() {
console.log(this)
}
fn()
setTimeout(function () {
console.log(this)
}, 1000)
document.querySelector('button').addEventListener('click',function(){
console.log(this)
})
const obj = {
uname: function(){
console.log(this)
}
}
obj.uname()
</script>
</body>
2.箭头函数 this 指向
箭头函数其实不存在 this 箭头函数默认会去上层作用域找 this,一层一层向外找
对象没有 this
事件回调函数使用箭头函数时 this 是全局的 window
使用普通函数就是那个调用者
dom 事件中的回调函数不建议用 this
<body>
<button class="a"></button>
<button class="b"></button>
<script>
document.querySelector('.a').addEventListener('click', function () {
console.log(this)
})
document.querySelector('.b').addEventListener('click', () => {
console.log(this)
})
</script>
</body>
原型对象 构造函数 也不建议用 箭头函数
原型对象 和 构造函数中的 this 都指向 实例对象 用箭头函数就变成指向 window 就不能再添加属性了
(二)改变 this
1.call() 了解
调用函数 ,然后改变this 指向
函数.call(改变后的this指向,实参1,实参2)
参数是数字
<body>
<script>
const obj = {
uname: 'pink'
}
function fn(x, y) {
console.log(this)
console.log(x + y)
}
fn.call(obj, 1, 2)
</script>
</body>
2.apply()
和 call 类似 ,但第二个参数必须是数组
函数.apply(改变后的this指向,[必须是数组])
<body>
<script>
const obj = {
uname: 'pink'
}
function fn(x, y) {
console.log(this)
console.log(x + y)
}
fn.apply(obj, [1, 2])
</script>
</body>
没有 ... 展开运算符之前的求最值写法
函数里面把数组变成数值了,这样就可以利用这个特点求最大最小值了
Math.max 是一种方法可以用 apply
指向为 null 也行
<body>
<script>
const max = Math.max.apply(Math, [1, 2, 3])
console.log(max)
</script>
</body>
3. bind() 重点
bind() 不会调用函数,但是能改变 this 指向
bind 拷贝了原来的函数 但是 this 向变了
返回值是一个函数
<body>
<script>
const obj = {
age: 18
}
function fn() {
console.log(this)
}
const fun = fn.bind(obj)
console.log(fun)
</script>
</body>
四、防抖 debounce
(一)介绍
单位时间内,频繁触发事件,只执行最后一次
拿倒计时举例子,点击按钮倒计时十秒 但是还没倒计时结束的时候,又点击一次倒计时,则刚才的倒计时就不再进行了,而是执行最后一次新的倒计时
搜索框输入时,用户最后一次输入完再发送请求,不用输入一个字请求一遍,减少服务器请求压力,减少请求次数
手机号 邮箱验证同理
(二)利用 lodash 函数
语法:_.debounce(函数名,间隔时间)
<body>
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
const box = document.querySelector('.box')
let i = 1
function mouseMove(){
box.innerHTML = i++
}
box.addEventListener('mousemove',_.debounce(mouseMove, 500))
</script>
</body>
(三)手写函数防抖
防抖的核心是利用定时器 setTimeout 来实现
1.声明定时器变量
2.鼠标滑动时 先判断是否有定时器,有就先清除之前的定时器
3.没有就开启定时器,存到变量里面
4.定时器里调用要执行的函数
<body>
<div class="box"></div>
<script>
const div = document.querySelector('div')
let i = 0
function mouseMove() {
div.innerHTML = i++
}
function debounce(fn, t) {
let timer
return function () {
if (timer) clearTimeout(timer)
timer = setTimeout(function () {
fn()
}, t)
}
}
div.addEventListener('mousemove', debounce(mouseMove, 500))
</script>
</body>
五、节流 throttle
(一)介绍
单位时间内,频繁触发事件,只执行一次
还拿倒计时的例子举例 如果点击按钮触发了进行倒计时 10s 的事件 ,然后再反复点击这个按钮,和防抖不一样,节流不会执行新点击的,而是把最开始的倒计时事件执行结束,不执行后面的事件
长使用于高频事件中
鼠标移动 mousemove,界面尺寸缩放 resize,滚动条滚动 scroll 等
(二)利用 lodash 函数
语法:_.throttle(函数名,单位时间)
<body>
<div class="box"></div>
<script src="./lodash.min.js"></script>
<script>
const div = document.querySelector('div')
let i = 0
function mouseMove() {
div.innerHTML = i++
}
div.addEventListener('mousemove', _.throttle(mouseMove, 500))
</script>
</body>
(三)手写函数节流
节流的核心和防抖类似,也是利用定时器 setTimeout 来实现
1.声明定时器变量
2.鼠标滑动时 先判断是否有定时器,有就不开启新的定时器
3.没有定时器就开启新的定时器,存到变量里面
4.定时器里调用要执行的函数 定时器里要把定时器清空
在定时器里面是无法用 clearTimeout(timer) 清除定时器的 写法不科学 得用 timer = null 才行
<body>
<div class="box"></div>
<script>
const div = document.querySelector('div')
let i = 0
function mouseMove() {
div.innerHTML = i++
}
function throttle(fn, t) {
let timer
return function () {
if (!timer) {
timer = setTimeout(function () {
fn()
timer = null
}, t)
}
}
}
div.addEventListener('mousemove', throttle(mouseMove, 500))
</script>
</body>
六、记录视频播放时间
具体请看综合案例
(一)ontimeupdata
视频音频当前的播放位置发生改变时触发
触发频率很高,需要进行节流操作
(二)ondoadeddata
当前帧的数据加载 完成且还没有足够多的数据播放视频音频的下一帧触发
就是页面一打开触发
可以利用这个加载时间 把我们存储的视频上一次看到的时间,读取出来并且把视频的当前时间赋值,跳到我们上次看的地方
练习:利用递归函数实现 setTimeout 模拟 setInterval 效果
界面展示:
代码部分:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div></div>
<script>
function getTime() {
document.querySelector('div').innerHTML = new Date().toLocaleString()
setTimeout(getTime, 1000)
}
getTime()
</script>
</body>
</html>
练习:改变 this 指向 按钮 2s 后再次可用
界面展示:
代码部分:
<body>
<button>关闭</button>
<script>
document.querySelector('button').addEventListener('click', function () {
this.disabled = true
setTimeout(function () {
this.disabled = false
}.bind(this), 2000)
})
</script>
</body>
综合案例:记录视频播放时间 节流
界面展示:
代码部分:
<!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)
}, 1000)
video.onloadeddata = () => {
video.currentTime = localStorage.getItem('currentTime') || 0
}
</script>
</body>
</html>