JavaScript 进阶 - 第4天
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
- 拷贝数组:Array.prototype.concat() 或者 […arr]
如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
思考?
- 直接赋值和浅拷贝有什么区别?
a.直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址。
b.浅拷贝如果是一层对象,不互相影响,如果是多层对象,会相互影响。
- 浅拷贝怎么理解?
a.拷贝对象之后,如果对象中的属性是基本数据类型,修改拷贝之后的对象,不会影响原对象。
b.如果对象中的属性是引用数据类型,修改拷贝之后的对象,会影响原对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script> const obj = { uname: 'pink', age: 18, family: { baby: '小pink' } } const o = {} Object.assign(o, obj) o.age = 20 o.family.baby = '老pink' console.log(o) console.log(obj) </script>
|
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
用递归的方式实现一秒钟打印一次当前时间
1 2 3 4 5 6 7
| <script> function getTime() { document.querySelector('div').innerHTML = new Date().toLocaleString() setTimeout(getTime, 1000) } getTime() </script>
|
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <body> <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] = [] 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) console.log(o) o.age = 20 o.hobby[0] = '篮球' o.family.baby = '老pink' console.log(obj) console.log([1, 23] instanceof Object) </script> </body>
|
面试题怎么实现深拷贝
- 深拷贝拷贝出来的对象不会影响新对象,通过递归实现深拷贝
- 在普通拷贝时可以直接赋值,但是在拷贝时遇到数组,需要递归拷贝,再次调用这个递归函数就可以了
- 如果遇到对象,也需要递归拷贝,再次调用这个递归函数就可以了
注意:先拷贝数组,再拷贝对象,不能颠倒
js库lodash里面cloneDeep内部实现了深拷贝
lodash是一个js库,提供了很多js常用的方法,cloneDeep就是其中一个方法,用来实现深拷贝
lodash官网:https://www.lodashjs.com/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <body> <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> </body>
|
JSON序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <body> <script> const obj = { uname: 'pink', age: 18, hobby: ['乒乓球', '足球'], family: { baby: '小pink' } } const o = JSON.parse(JSON.stringify(obj)) console.log(o) o.family.baby = '123' console.log(obj) </script> </body>
|
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
throw
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script> function counter(x, y) {
if(!x || !y) { throw new Error('参数不能为空!') }
return x + y }
counter() </script>
|
总结:
throw 抛出异常信息,程序也会终止执行
throw 后面跟的是错误提示信息
Error 对象配合 throw 使用,能够设置更详细的错误信息
try … catch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <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>
|
总结:
try...catch 用于捕获错误信息
- 将预估可能发生错误的代码写在
try 代码段中
- 如果
try 代码段中出现错误后,会执行 catch 代码段,并截获到错误信息
finally 代码段中的代码不管 try 代码段中的代码是否出错,都会执行
debugger
相当于断点调试
处理this
了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。
this 是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this 的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this 默认的取值】情况进行归纳和总结。
普通函数
普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script> function sayHi() { console.log(this) } const sayHello = function () { console.log(this) } sayHi() window.sayHi()
const user = { name: '小明', walk: function () { console.log(this) } } user.sayHi = sayHi uesr.sayHello = sayHello user.sayHi() user.sayHello() </script>
|
普通函数的this指向问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <!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"> <title>Document</title> </head>
<body> <button>点击</button> <script> console.log(this) function fn() { console.log(this) } window.fn() window.setTimeout(function () { console.log(this) }, 1000) document.querySelector('button').addEventListener('click', function () { console.log(this) }) const obj = { sayHi: function () { console.log(this) } } obj.sayHi() </script> </body>
</html>
|
注: 普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <script> console.log(this) const sayHi = function() { console.log(this) } const user = { name: '小明', walk: () => { console.log(this) }, sleep: function () { let str = 'hello' console.log(this) let fn = () => { console.log(str) console.log(this) } fn(); } }
user.sayHi = sayHi user.sayHi() user.sleep() user.walk() </script>
|
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12
| <script> const btn = document.querySelector('.btn') btn.addEventListener('click', () => { console.log(this) }) btn.addEventListener('click', function () { console.log(this) }) </script>
|
同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
1 2 3 4 5 6 7 8 9 10 11
| <script> function Person() { } Person.prototype.walk = () => { console.log('人都要走路...') console.log(this); } const p1 = new Person() p1.walk() </script>
|
箭头函数总结
- 箭头函数内不存在this时,沿用上一级的
不适用
构造函数,原型函数,dom事件函数等等
适用
普通函数,对象方法函数,闭包函数,需要使用上一层this的地方等等
改变this指向
以上归纳了普通函数和箭头函数中关于 this 默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向:
call
使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <script> function sayHi() { console.log(this); }
let user = { name: '小明', age: 18 }
let student = { name: '小红', age: 16 }
sayHi.call(user); sayHi.call(student);
function counter(x, y) { return x + y; }
let result = counter.call(null, 5, 10); console.log(result); </script>
|
总结:
call 方法能够在调用函数的同时指定 this 的值
- 使用
call 方法调用函数时,第1个参数为 this 指定的值
call 方法的其余参数会依次自动传入函数做为函数的参数
apply
使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <script> function sayHi() { console.log(this) }
let user = { name: '小明', age: 18 }
let student = { name: '小红', age: 16 }
sayHi.apply(user) sayHi.apply(student)
function counter(x, y) { return x + y } let result = counter.apply(null, [5, 10]) console.log(result) </script>
|
总结:
apply 方法能够在调用函数的同时指定 this 的值
- 使用
apply 方法调用函数时,第1个参数为 this 指定的值
apply 方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind
bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数,使用方法如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script> function sayHi() { console.log(this) } let user = { name: '小明', age: 18 } let sayHello = sayHi.bind(user); sayHello() </script>
|
注:bind 方法创建新的函数,与原函数的唯一的变化是改变了 this 的值。
call
call 方法可以动态的指定函数中 this 的值,call 方法的第一个参数就是 this 的值,后面的参数是函数执行时的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <!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"> <title>Document</title> </head>
<body> <script> const obj = { uname: 'pink' } function fn(x, y) { console.log(this) console.log(x + y) } fn.call(obj, 1, 2) </script> </body>
</html>
|
apply
apply 方法与 call 方法的作用一致,只是传参方式不同,apply 方法的第一个参数也是 this 的值,第二个参数是一个数组,数组中的元素是函数执行时的参数。
语法:
1
| fn.apply(thisArg, [argsArray])
|
thisArg:必选项,函数执行时的 this 的值
argsArray:可选项,数组,函数执行时的参数,传递的参数必须包含在数组里
返回值:函数的返回值,因为是调用函数,所以返回值就是函数的返回值
因此apply主要跟数组有关系,比如使用Math.max求最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <!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"> <title>Document</title> </head>
<body> <script> const obj = { age: 18 } function fn(x, y) { console.log(this) console.log(x + y) } fn.apply(obj, [1, 2])
const arr = [100, 44, 77] const max = Math.max.apply(Math, arr) const min = Math.min.apply(null, arr) console.log(max, min) console.log(Math.max(...arr)) </script> </body>
</html>
|
bind(重点)
bind 方法与 call 方法的作用一致,只是 bind 方法不会立即调用函数,而是返回一个新的函数,新函数中的 this 值已经被绑定,后面的参数是函数执行时的参数。
语法:
1
| fn.bind(thisArg, agr1,agr2)
|
bind()方法不会调用函数,但是能改变this指向
thisArg:必选项,函数执行时的 this 的值
agr1,agr2:可选项,函数执行时的参数,传递的其他参数
返回由指定的this值和初始化参数改造的 原函数拷贝 (新函数)
因此如果我们只想改变this的指向,不想调用函数时,就可以使用bind,比如改变定时器中的this指向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <!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"> <title>Document</title> </head>
<body> <button>发送短信</button> <script> const obj = { age: 18 } function fn() { console.log(this) }
const fun = fn.bind(obj) fun()
document.querySelector('button').addEventListener('click', function () { this.disabled = true window.setTimeout(function () { this.disabled = false }.bind(this), 2000) }) </script> </body>
</html>
|
call apply bind 总结
- 相同点
都可以改变函数内部this指向
- 区别
a. call和apply会调用函数,并改变函数内部的this指向
b. call和apply传递的参数不同,call传递aru1,aru2,apply传递的参数必须是数组形式
c. bind不会调用函数,可以改变函数内部this的指向
- 应用场景
a. call调用函数并且可以传递参数
b. apply经常和数组有关系,比如借助于数学对象,求数组中的最大值
c. bind不调用函数,但是可以改变函数内部的this指向,比如改变定时器内部的this指向
防抖
- 防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间(例如:王者荣耀回城,只要被打断就需要重新执行)
应用场景
a. 搜索框输入,只需用户最后一次输入完,再发送请求。
b. 手机号,邮箱验证输入检测
手写防抖函数(实现鼠标经过盒子显示文字)
要求:鼠标停止500ms之后,里面的数字+1
核心思路:
防抖函数的核心是定时器(setTimeout)实现
a. 声明一个定时器变量
b. 当鼠标每次滑动都先判断定时器是否存在,如果存在就清除定时器
c. 如果没有定时器,就创建一个定时器,存到定时器变量中
d. 在定时器中执行函数,并且清除定时器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <!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"> <title>Document</title> <style> .box { width: 500px; height: 500px; background-color: #ccc; color: #fff; text-align: center; font-size: 100px; } </style> </head> <body> <div class="box"> </div> <script> const box = document.querySelector(".box"); let i = 1; function movemover(){ box.innerHTML = i++; } function debounce(fn,t){ let timer = null; return function(){ if(timer){ clearTimeout(timer) } timer = setTimeout(function(){ fn(); },t) } } box.addEventListener("mousemove",debounce(movemover,500)); </script> </body> </html>
|
节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数(单位时间内频繁触发,但是只执行一次)
使用场景:
a. 滚动条事件(scroll),加载更多或滚到底部监听
b. 鼠标移动(mousemove)
c. 页面尺寸缩放
手写节流函数(实现鼠标经过盒子显示文字)
要求:鼠标移动的时候,不管移动多快,每隔500ms,里面的数字+1
核心思路:
节流函数的核心是定时器(setTimeout)实现
a. 声明一个定时器变量
b. 当鼠标每次滑动都先判断定时器是否存在,如果存在就不开启新定时器
c. 如果没有定时器,就创建一个定时器,存到定时器变量中 把定时器存放到定时器变量中(在定时器里调用要执行的函数,还要记得把定时器清空)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <!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"> <title>Document</title> <style> .box { width: 500px; height: 500px; background-color: #ccc; color: #fff; text-align: center; font-size: 100px; } </style> </head> <body> <div class="box"> </div> <script> const box = document.querySelector(".box"); let i = 1; function movemover(){ 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(movemover,500)); </script> </body> </html>
|
清除定时器问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!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"> <title>Document</title> </head> <body> <script> let timer = null; timer = setTimeout(function(){ clearTimeout(timer); console.log(timer); },1000)
</script> </body> </html>
|
节流小案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| <!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" /> <title>Document</title> <style> .box { width: 500px; height: 500px; background-color: #ccc; color: #fff; text-align: center; font-size: 100px; } </style> </head>
<body> <div class="box"></div> <script> const box = document.querySelector('.box') let i = 1 function mouseMove() { box.innerHTML = ++i } function throttle(fn, t) { let startTime = 0 return function () { let now = Date.now() if (now - startTime >= t) { fn() startTime = now } } } box.addEventListener('mousemove', throttle(mouseMove, 500))
</script> </body>
</html>
|
防抖小案例(手写)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <!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" /> <title>Document</title> <style> .box { width: 500px; height: 500px; background-color: #ccc; color: #fff; text-align: center; font-size: 100px; } </style> </head>
<body> <div class="box"></div> <script> const box = document.querySelector('.box') let i = 1 function mouseMove() { box.innerHTML = ++i } function debounce(fn, t) { let timeId return function () { if (timeId) clearTimeout(timeId) timeId = setTimeout(function () { fn() }, t) } } box.addEventListener('mousemove', debounce(mouseMove, 200))
</script> </body>
</html>
|
lodash节流和防抖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <!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" /> <title>Document</title> <style> .box { width: 500px; height: 500px; background-color: #ccc; color: #fff; text-align: center; font-size: 100px; } </style> </head>
<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>
</html>
|
节流综合案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <!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>
|