深拷贝与浅拷贝

关于JS的深拷贝和浅拷贝在开发中偶尔会用到,一不小心可能就导致数据渲染结果不尽如人意(没错,就是我总是踩坑……)。

要说到深拷贝和浅拷贝,我们就要先了解到在JavaScript中,基本数据类型(比如String,Number,Boolean,undefined,null)是通过值传递的,这意味着当你将一个基本类型的值赋给另一个变量时,系统会创建一个新的值拷贝给新的变量。新的变量会有自己的内存空间,并且与原始变量不共享内存。

但是如果在赋值操作中使用了对象类型(Object,Array,Function等)那么则会导致原数据和新数据共享一个内存空间,一旦我们更改其中一者数据另一个数据也会被更改,显然我们并不希望这样,因此就出现了浅拷贝和深拷贝的概念,可以说,深浅拷贝就是针对引用数据类型的。

浅拷贝

浅拷贝的概念:如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,那么拷贝的就是内存地址(新旧对象共享同一块内存),所以如果其中一个对象改变了这个地址,就会影响到另一个对象(只是拷贝了指针,使得两个指针指向同一个地址)。

关于浅拷贝的实现方式有以下几种,我们可以从下面的例子中更好地理解浅拷贝的机制。

数组的浅拷贝

  1. Array.prototype.concat

    1
    2
    3
    4
    5
    6
    7
    let arr = ['name', 'type', 'func', [0, 1]]
    let new_arr = arr.concat()
    new_arr[0] = 'na'
    new_arr[1] = 'ty'
    new_arr[3][0] = 1
    console.log(arr); // [ 'name', 'type', 'func', [ 1, 1 ] ]
    console.log(new_arr); // [ 'na', 'ty', 'func', [ 1, 1 ] ]

    可以看出通过Array.prototype.concat方法,如果元素是引用类型(比如上面的Array),我们修改该元素的某个索引值/属性值,拷贝的新数据和原数据都会改变,而基本类型的值则正常拷贝,这就是所谓的浅拷贝。

  2. ES6的展开运算符

    1
    2
    3
    4
    5
    6
    7
    let arr = ['name', 'type', 'func', [0, 1]]
    let new_arr = [...arr]
    new_arr[0] = 'na'
    new_arr[1] = 'ty'
    new_arr[3][0] = 1
    console.log(arr); // [ 'name', 'type', 'func', [ 1, 1 ] ]
    console.log(new_arr); // [ 'na', 'ty', 'func', [ 1, 1 ] ]

    ES6的展开运算符同理也是浅拷贝。

对象的浅拷贝

  1. Object.assign

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let obj = {
    name: 'obj',
    type: 'Object',
    arr: [1, 2, 3]
    }

    let new_obj = Object.assign({},obj)
    new_obj['name'] = 'name'
    new_obj['arr'][0] = 0
    console.log(obj); // { name: 'obj', type: 'Object', arr: [ 0, 2, 3 ] }
    console.log(new_obj); // { name: 'name', type: 'Object', arr: [ 0, 2, 3 ] }
  2. ES6的展开运算符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let obj = {
    name: 'obj',
    type: 'Object',
    arr: [1, 2, 3]
    }

    let new_obj = {...obj}
    new_obj['name'] = 'name'
    new_obj['arr'][0] = 0
    console.log(obj); // { name: 'obj', type: 'Object', arr: [ 0, 2, 3 ] }
    console.log(new_obj); // { name: 'name', type: 'Object', arr: [ 0, 2, 3 ] }

深拷贝

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象。

关于深拷贝的实现方式主要有以下几种。

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
function deepClone(obj) {
let newObj = Array.isArray(obj) ? [] : {}
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
if (obj[k] instanceof Object)
newObj[k] = deepClone(obj[k]);
else
newObj[k] = obj[k];
}
}
return newObj;
}

通过上述deepClone递归函数的实现,我们可以测试一下是否实现了深拷贝:

1
2
3
4
5
6
7
8
9
10
11
let obj = {
name: 'obj',
type: 'Object',
arr: [1, 2, 3],
}

let new_obj = deepClone(obj)
new_obj['name'] = 'name'
new_obj['arr'][0] = 0
console.log(obj); // { name: 'obj', type: 'Object', arr: [ 1, 2, 3 ] }
console.log(new_obj); // { name: 'name', type: 'Object', arr: [ 0, 2, 3 ] }

JSON.parse(JSON.stringify(obj))

1
2
3
4
5
6
7
8
9
10
11
let obj = {
name: 'obj',
type: 'Object',
arr: [1, 2, 3],
}

let new_obj = JSON.parse(JSON.stringify(obj))
new_obj['name'] = 'name'
new_obj['arr'][0] = 0
console.log(obj); // { name: 'obj', type: 'Object', arr: [ 1, 2, 3 ] }
console.log(new_obj); // { name: 'name', type: 'Object', arr: [ 0, 2, 3 ] }

相比而言这种方式稍微简单一点(,当然也有一些第三方库帮我们实现了深拷贝,比如lodash等等。

防抖和节流

防抖

概念

防抖是一种前端常用的优化技术,用于防止连续多次触发事件。从而减少不必要的计算、网络请求或者页面渲染,提高性能。当一个事件被触发时,防抖函数并不会立即执行,而是会等待一段指定的时间。如果在这段时间内该事件再次被触发,则会取消之前设定的执行计划,并重新开始计时。这样,只有当事件停止触发并经过了预设的时间后,才会真正执行。

应用场景

  1. 搜索框实时搜索:当用户在搜索框输入内容时,对于一些需求需要实时根据用户输入内容请求服务器的搜索结果,如果每次改变内容就请求,会导致请求过于频繁,用户体验不佳。通过防抖可能确保当用户停止输入一段时间后才发起搜索请求。
  2. 窗口大小调整:用户可能快速地调整浏览器窗口大小,导致 resize事件短时间内被大量触发。如果每次调整都重新计算布局或刷新数据,可能会引起性能问题。防抖能确保在窗口大小稳定后,才执行相关操作。
  3. 表单验证:对于一些输入手机号、邮箱号的表单验证,可以通过防抖实现在用户停止输入一段时间后再进行正确性验证,防止频繁触发验证函数。

代码实现

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn, delay) {
let timer = null;

return function (...args) {
const context = this;
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay)
}
}

这段代码定义了一个 debounce函数,它接受一个函数 fn和一个时间阈值 t。debounce函数返回一个新的函数,这个新函数会在每次被调用时取消之前设置的任何定时器,并设置一个新的定时器。只有当最后一次调用这个新函数后的 t毫秒内没有新的调用时,fn函数才会执行。

节流

概念

节流是指确保在指定的时间间隔内,无论触发了多少次事件,只有第一次事件会被执行,后续事件在这个间隔内都不会执行。(连续触发事件但是在 n 秒中只执行第一次触发函数)

应用场景

  1. 滚动事件:在处理滚动事件时,如无限滚动加载更多内容,节流可以限制触发事件处理程序的频率,避免过度触发导致性能问题。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, delay) {
let timer = null;
return function(...args){
const context = this;

if(!timer){
timer = setTimeout(function(){
fn.apply(context, args);
timer = null;
}, delay)
}
}
}

以上防抖和节流的基本实现方式,实际开发中可能还会遇到特殊的要求,比如防抖要求第一次要立即执行等等,就需要根据实际需求进行改写。