深浅拷贝

在开发中经常需要复制,就会出现这种情况

image.png

两个对象都指向同一个地址,所以修改一个的制,另一个也会变化

浅拷贝

首先浅拷贝和深拷贝只针对引用类型

浅拷贝:拷贝的是地址

常见方法

  1. 拷贝对象:Object.assgin() / 展开运算符 {...obj} 拷贝对象
  2. 拷贝数组:Array.prototype.concat() 或者 [...arr]

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

深拷贝

首先浅拷贝和深拷贝只针对引用类型

深拷贝:拷贝的是对象,不是地址

常见方法

  1. 通过递归实现深拷贝
  2. lodash/cloneDeep
  3. 通过JSON.stringify()实现

通过递归实现深拷贝

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
const obj = {
uname: 'kun',
age: 18,
hobby: ['唱', '跳', 'rap', '篮球'],
family: {
baby: 'ikun'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 处理数组的问题  一定先写数组 在写 对象 不能颠倒
if (oldObj[k] instanceof Array) {
newObj[k] = []
//  newObj[k] 接收 []  hobby
//  oldObj[k]   ['唱', '跳', 'rap', '篮球']
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[4] = '中分'
o.family.baby = '小黑子'
console.log(obj) // 不会改变
console.log([1, 23] instanceof Object)

image.png

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

1
2
3
4
5
6
7
<script src="js/lodash.min.js"></script>
<script>
const o = _.cloneDeep(obj)
o.family.baby = '小黑子'
console.log(o)
console.log(obj)
</script>

image.png

通过JSON.stringify()实现

  • JSON.stringify(obj):将对象obj序列化成一个JSON字符串。
  • JSON.parse(...):将上一步得到的JSON字符串反序列化成一个新的JavaScript对象。
1
2
3
4
5
const o = JSON.parse(JSON.stringify(obj))
o.uname = '坤'
o.family.baby = '小黑子'
console.log(o)
console.log(obj)

image.png

异常处理

throw抛异常

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

1
2
3
4
5
6
7
8
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}
return x + y
}
counter()

image.png

总结

  1. throw 抛出异常信息,程序也会终止执行
  2. throw 后面跟的是错误提示信息
  3. Error 对象配合 throw 使用,能够设置更详细的错误信息

try/catch捕获错误信息

我们可以通过try/catch捕获错误信息(浏览器提供的错误信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo() {
try {
// 查找 DOM 节点
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息
console.log(error.message)
// 终止代码继续执行
return
}
finally {
alert('执行')
}
console.log('如果出现错误,我的语句不会执行')
}
foo()

image.png

总结

  1. try...catch用于捕获错误信息
  2. 将预估可能发生错误的代码写在try代码段中
  3. 如果try代码段中出现错误后,会执行catch代码段,并截获到错误信息
  4. finally不管是否有错误,都会执行

debugger

直接在代码中加入debugger,可以直接设置断点

image.png

处理this

this指向

普通函数

普通函数的调用方式决定this的值,就是谁调用的this就指向谁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function sayHi() {
console.log(this);
}
const sayHello = function () {
console.log(this)
}
sayHi() // window
sayHello() // window
const user = {
name: 'ikun',
word: function() {
console.log(this)
}
}
user.word() // user
user.sayHi = sayHi
user.sayHi() // user

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

箭头函数

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

  1. 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
  2. 箭头函数中的this引用的就是最近作用域中的this
  3. 向外层作用域中,一层一层查找this,直到有this的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const sayHi = () => {
console.log(this);
}
const user = {
name: 'ikun',
word: () => {
console.log(this)
},
hobby: function() {
let love = ['唱', '跳', 'rap', '篮球']
console.log(this) // user
let fn = () => {
console.log(this) // user 与hobby中的this一致
}
fn();
}
}
user.sayHi = sayHi
user.sayHi() // window 与函数声明环境中this值一致,也就是
user.hobby() // user user 两个都是hobby的this值
user.word() // window

ChatGPT的解释

  • user.sayHi(): 在这里,sayHi是一个箭头函数,它在全局上下文中被定义。因此,当user.sayHi()被调用时,this指向箭头函数定义时的上下文,即全局对象(在浏览器中是window,在Node.js中可能是globalglobalThis)。注意,即使sayHi被作为user对象的方法调用,由于它是箭头函数,这不会影响this的值。
  • user.hobby(): hobby是使用function关键字定义的传统函数,并作为user对象的方法被调用。因此,在hobby函数内部,this指向调用它的对象,即user对象。同理,内部的箭头函数fn捕获了hobby函数中的this,所以在fnthis也指向user对象。
  • user.word(): word是一个箭头函数,在user对象的定义中捕获了它定义时的this。由于user对象字面量不形成单独的作用域,word函数捕获的this是在包含user定义的外部作用域中的this,也就是全局对象。

特殊情况

在开发中使用箭头函数前需要考虑函数中 this 的值,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数

1
2
3
4
5
6
7
8
9
10
// DOM 节点
const btn = document.querySelector('.btn')
// 箭头函数 此时 this 指向了 window
btn.addEventListener('click', () => {
console.log(this)
})
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener('click', function () {
console.log(this)
})

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

1
2
3
4
5
6
7
8
9
function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log('人都要走路...')
console.log(this); // window
}
const p1 = new Person()
p1.walk()

总结

  1. 函数内不存在this,沿用上一级的
  2. 不适用于构造函数、原型函数、DOM事件函数等
  3. 适用于需要使用上层this的地方

改变this

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

  1. call()
  2. apply()
  3. bind()

call()

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

语法fn.call(thisArg, arg1, arg2, ...)

  • thisArg:在fn函数运行时指定的this值
  • arg1arg2:传递的其他参数
  • 返回值就是函数的返回值,因为他就是调用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.call(user); // this 值为 user
sayHi.call(student); // this 值为 student
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result); // 15

总结

  1. call() 方法能够在调用函数的同时指定 this 的值
  2. 使用 call() 方法调用函数时,第1个参数为 this 指定的值
  3. call() 方法的其余参数会依次自动传入函数做为函数的参数

apply()

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

语法fn.apply(thisArg, [argsArray])

  • thisArg:在fn函数运行时指定的 this
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数
  • 因此 apply() 主要跟数组有关系,比如使用 Math.max() 求数组的最大值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function sayHi() {
console.log(this);
}
let user = {
name: '小明',
age: 18
}
sayHi.apply(user); // this 值为 user
// 可以用作调用函数
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10]);
console.log(result); // 15
// 求最大值的用法
const arr = [2, 5, 8, 1, 4]
console.log(Math.max.apply(null, arr)) // 8
console.log(Math.max(...arr)) // 8

总结:

  1. apply() 方法与call() 方法几乎一致,只是第二个参数是否为数组的区别

bind()

bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数

语法fn.bind(thisArg, arg1, arg2, ...)

  • thisArg:在 fn 函数运行时指定的 this
  • arg1arg2:传递的其他参数
  • 返回由指定的 this 值和初始化参数改造的原函数拷贝(新函数)
  • 因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind(),比如改变定时器内部的this指向
1
2
3
4
5
6
7
8
function saythis() {
console.log(this)
}
let user = {
name: 'ikun'
}
let saythat = saythis.bind(user) // 不会调用原函数,而是返回原函数的拷贝
saythat()

image.png

三种方法的区别

相同点

  • 都可以改变函数内部的this指向

区别点

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

主要应用场景

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

性能优化

防抖

防抖(debounce),就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间

例如之前的商品放大镜案例,鼠标放入盒子就会在旁边出现放大效果,离开200ms后消失,但是如果在200ms之内鼠标再次返回盒子,则不会出现放大效果,所以需要设定重新计算执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//监听鼠标经过和离开
middle.addEventListener('mouseenter', show)
middle.addEventListener('mouseleave', hide)
large.addEventListener('mouseenter', show)
large.addEventListener('mouseleave', hide)
//timeId用于清除计时器
let timeId = null
//显示函数,在鼠标经过中盒子和大盒子都显示,通过修改display的值,显示时清除计时器
function show() {
clearTimeout(timeId)
large.style.display = 'block'
}
//隐藏函数,当鼠标离开中盒子和大盒子都隐藏,并设有计时器
function hide() {
timeId = setTimeout(function () {
large.style.display = 'none'
}, 200)
}

节流

节流(throttle),就是指连续触发事件但是在 n 秒中只执行一次函数

假如一张轮播图完成切换需要300ms, 不加节流效果,快速点击,则嗖嗖嗖的切换,加上节流效果,不管快速点击多少次,300ms时间内,只能切换一张图片。