【JS基础】搞不懂变量中的深浅拷贝?来!看完工作中的类似问题都能解决了

前言

首先要知道什么是值类型和引用类型,这个基础的知识点如果还不知道就自己先去查下。

例如:

  • 值类型:数字、字符、undefined、symbol等。
  • 引用类型:对象、函数(也可以单独看作是函数类型)、数组、null(指向空地址)等。

因为我们的深浅拷贝就基于那两大类做的,太基础这里咱就不补充了。


存储方式

值类型

学习时,我们可以把抽象的东西具象化,先了解下值类型在栈内存中的存储方式是怎样的,例如一个字符变量和数字类型变量:

let name = '小马'
let age = 12
keyvalue
name小马
age12

就像表格一样,井然有序。

引用类型

那么引用类型的堆存储咱们也可以来具象化一下(地址写法我随便表示下),例如下面两个对象:

let a = {name: '小马'}
let b = {a: 1}
地址value
00xx xxxx xxxx xxxx{name: '小马'}
001x xxxx xxxx xxxx{a: 1}

然后在栈中变量的对应关系是地址而不是值:

keyvalue
a00xx xxxx xxxx xxxx
b001x xxxx xxxx xxxx

背后原因

这种存储机制就是为了优化空间和性能

  • 空间:例如,栈的空间可以做到很小,因为引用类型放在栈中只是一个引用地址而已。

  • 性能:一般来说值类型的数据量不会很大,但是引用类型可以做到很大,例如十几mb的树结构数据,十几mb的图表数组数据等等,引用类型做拷贝时只是复制地址,所以性能较好。


细说浅拷贝

只要对引用类型进行直接赋值拷贝,都会发生浅拷贝现象。因为引用类型存的其实是堆内存中引用特定某个存储空间的地址,而真正内容在这个地址对应的存储空间上。也就是多个引用类型可能共用一个存储空间。

举例

比如,数组

var arr = [1, 2, 3];
var brr = arr; // 将arr拷贝给brr,也就是指针的拷贝
arr[0] = 'zhangsan';  // 改变arr某个值
console.log(arr, brr); // 都发生了改变

再比如对象

var obj1 = {
	a: "hello" 
}
var obj2 = obj1; // 引用类型拷贝,都指向同一个原生对象
obj2.a = "world";
console.log(obj1.a, obj2.a) // a都变成"world"

属性赋值呢

当然,这样子把对象的中为基本类型的属性,直接赋值给其他变量者不会发生浅拷贝现象:

let obj = {
	a: '1'
}
let a = obj.a
a = '2'
consolo.log(obj.a) // 还是1

但如果是值为引用类型的属性直接赋值给其他变量,就会发生浅拷贝现象了。

来个简单的题开个胃:

var obj1 = {
	age:18,
	arr:[1,2,3] //object类型 引用类型
}
function copy(obj1){ // 函数:把一个对象里的属性遍历赋值给一个新的对象并返出
	var obj2 = {}
	for(var k in obj1){
		obj2[k]=obj1[k]  //万变不离其宗,当把属性值为数组的东西赋值给另一个对象的属性,也是引用
	}
	return obj2;
}
var obj2 = copy(obj1);
obj1.arr[0]='ooo';
console.log(obj1.arr,obj2.arr) // 请自己思考一下再去打印

注意

obj1={a:1}; obj2={a:1}这两者虽然值是一样的,但是引用地址是不一样的,所以二者不相等。


细说深拷贝

举例

与浅拷贝相对应的就是深拷贝,是单独开一个堆内存空间存放变量的内容。也就是一个变量对应一个单独的内存空间。

所以引用类型想进行深拷贝,就要对其进行新的内存空间开辟。

举个实现深拷贝函数的例子:

function deepClone(value = {}) {
    // 递归中,如果是值类型和函数类型直接返回
    if (typeof value !== 'object' || value === null) return value
    // 开辟新空间
    let result = value instanceof Array ? [] : {}
    Object.setPrototypeOf(result, Object.getPrototypeOf(value)) // 原型也拷贝
    // 循环进行递归
    for (let i in value) {
        if (value.hasOwnProperty(i)) result[i] = deepClone(value[i]) // 先判断不是原型上的属性才进行递归操作
    }
    // 递归返回
    return result
}

使用:

let arr = [1, 2, 3, [4, 5]]
let brr = deepClone(arr)
arr[3][0] = 6
console.log(arr[3], brr[3]) // [6, 5] [4, 5]

引用类型作为函数入参问题

问:如果把一个引用类型变量作为实参传入一个函数中,函数内部再对其重新赋值,那么函数外的这个变量的引用地址会被改变吗?

var type = "image";
var size = { width: 800, height: 600 };
var format = ['jpg', 'png'];

function change(type, size, format) {
    type = 'video';
    size = { width: 1024, height: 768 };
    format.push('mp4');
}
change(type, size, format); // 考点:函数的传参也会分析参数的引用类型
console.log(type, size, format);  // 思考一下再去运行啊

答:把引用类型当做参数去使用时,如果是重新赋值,那么会为它单独创建一个新的堆内存空间。但如果是修改就还是原来那个变量。

不过,在前端规范中,是不建议重新赋值输入参数的,咱们还是需要重新声明一个变量

重点:如果遇到符合这个情况的函数就不要多写一步深拷贝了,但!如果本身就是个递归函数,涉及到了把赋得的值传入递归函数中,就会形成闭包,需要手动在赋值的哪一步进行深拷贝操作!!!!

例如,一个变量需要不断的重复作为入参调用某个函数,就需要进行深拷贝。

连续赋值能把你弄晕

来道连续赋值的题(以下简称连等)

var a = {n:1}
var b = a
a.x = a = {n:2}

// 请问一下打印什么
console.log(a.x)
console.log(b.x)

是不是打印出来有点蒙?哈哈。

先记住规则:

  • 连等是从右向左分析
  • 连等赋值之前,程序会把变量的引用都保存起来,连等过程都是假设等于,等到全部假设完了再一起赋值

解释:

容我用ppt画几个图。

首先a变量引用堆内存中的{n:1},b变量也引用它。

然后执行到连等 a.x = a = {n:2}
先分析右边第一个等,a = {n:2},a假设引用{n:2}(但是还没真正引用,所以a还是在引用{n:1})。
然后再分析a.x = a,a在引用{n:1}的内存空间内又假设x:{n:2}

最后,两个假设全部成立,此时a引用对象成为{n:2},b因为自始至终都引用{n:1}的那个内存空间,所以b为{n:1,x:{n:2}}

够清楚了吧,哈哈。


大家常用的深拷贝方法

有时候,浅拷贝使用得当能很灵活的处理一些问题,但如果所有情况都不加以控制,工程量一大,容易导致数据流出问题,下面举例个人在工作中喜欢使用的深拷贝方法。

对象和数组都可以使用

json转换(不推荐)

let obj = JSON.parse(JSON.stringify(obj)) // json转换,但这种方式如果数据量很大的话,会非常影响性能

这个方式的好处是,无论一维还是多维的对象和数组都是可以深拷贝的。

但也有缺点:例如对象中存在函数属性转换后会直接消失,存在Date类型的属性转换后会变成字符串格式,超长的数字字符串转换完后会有精度损失等。所以使用的时候还是要谨慎。

注意,JSON对象上的这些方法,本身就不是专门用来做拷贝的,平时我们开发时不应该使用

es6的…(一维可用)

let arr = [1, 2, 3, ...brr] // es6语法,只能拷贝一维数组
let obj = {...obj1, ...obj2} // 只能拷贝一维对象

这种拷贝方式个人感觉比Object.assign() 要灵活。

Object.assign(一维可用)

let obj = Object.assign({},obj) // 只能拷贝一维对象
let obj = Object.assign([],arr) // 只能拷贝一维数组

这个方式有副作用,如果把原值放在第一个参数,那么原值就会被改变:

let obj = { a: 1 }
Object.assign(obj, { b: 1 })
console.log(obj) // { a: 1, b: 1 }

拆分拷贝deepClone(必装)

具体看上面深拷贝函数实现的例子,但是我们一般不会就这么简单的用在项目上,会对其内部进行扩展改写,例如支持moment对象。

推荐这样子,一般情况下直接使用lodash里的深拷贝方法:

npm i --save lodash.clonedeep

然后直接使用:

import cloneDeep from 'lodash.clonedeep'

let newObj = cloneDeep(Obj) // 深拷贝 支持组件拷贝

然后对于有特殊对象的例如moment对象,就可以用我们自己封装的深拷贝方法(当然也可以先试试lodash是否可行)

structuredClone(了解)

这是个新原生api(也不新了其实),只是我们开发者跟不上前端的更新,多以大多数人都不清楚哈哈。这个可以提前了解下,使用的时候考虑下兼容性。

数组专用

主要还是数组的api

concat与slice(一维可用)

arr1 = [].concat(arr2) // 只能拷贝一维数组
brr = arr.slice() // 只能拷贝一维数组

注意:通过以上方式进行的深拷贝都不会== 或者 === 于原来的变量。


深拷贝方法的拓展

咱们对上面手写的那个deepClone函数进行一个知识拓展。假设我有个变量,是个对象,这个对象中的其中一个属性赋值为自己。

let obj = {
    a: '1'
}

obj.b = obj
console.log('obj', obj);

你会看到对象属性无限嵌套,这时候你再去用deepClone肯定是不行的,无限递归,控制台给你说栈溢出了。

在这里插入图片描述

所以需要weakMap这个对象来帮你

weakMap具体可看

但首先我们要了解两个概念:弱引用和强引用

强引用

强引用是最常见的引用,它是指一个对象被一个变量或函数所引用,该对象就具有了强引用。只要有至少一个强引用存在,对象真正的内存占用就不会被垃圾回收回收。如下面的例子:

let obj = {a: 1}
let obj1 = {
    a: obj
}

obj = null
console.log(obj1);

在这里插入图片描述

是不是黑人问号???在这个例子中,由于obj1持有了obj对象的引用,obj就具有了强引用,obj的值在内存中一直存在。只有当obj1被设置为null后,该值才会被垃圾回收。

冷静下。其实你仔细想想,这样的设计才是符合直觉的,我们的JS默认的就是强引用。

弱引用

弱引用是指一个对象并不因为被引用而被保留在内存中,如果一个对象只被弱引用所引用,那么它就可以被垃圾回收掉。在JavaScript中,WeakMap是一种基于弱引用的映射类型,它的键是弱引用的,键所引用的对象没有强引用时,就会被垃圾回收掉。

例如:

let obj = {a: 1}
let map = new WeakMap()
map.set(obj, 1)
obj = null
console.log(map.get(obj));

在这里插入图片描述
可以看到当obj置为null时,内存中的值确实是被销毁了,所以map里的值也没了。

综上:

WeakMap的一个用法是可以用来避免内存泄漏和循环引用问题。在处理需要被回收机制管理的大量数据时,使用WeakMap可以更好地管理程序的性能和内存。它解决了一些应用场景下无法使用普通JavaScript对象的弊端,同时兼顾了性能和开发效率。

把深拷贝方法完善一下

function deepClone(value, map = new WeakMap()){
    if (typeof value !== 'object' || value === null) return value
    // 如果已经有过引用了,直接返回弱引用的值
    if (map.get(value)) {
        return map.get(value)
    }
    let obj = Array.isArray(value) ? [] : {}
    Object.setPrototypeOf(obj, Object.getPrototypeOf(value)) // 原型也拷贝
    map.set(value, obj) // 每个对象类型的值都存入WeakMap中
    for(let key in value) {
        // 可以多个hasOwnProperty的判断
        obj[key] = deepClone(value[key], map) // 记得把WeakMap也递归
    }
    return obj
}

这样深拷贝出来的虽然也是无限嵌套对象,但起码不会内存溢出了。


拷贝不是引用传递

什么叫引用传递,例如:

var arr = [1, 2, 3];
var brr = arr;

arr和brr共用一个内存空间才叫引用传递,而我们的JS是给两个变量都创建了独立的栈内存空间,然后引用了相同的堆内存地址而已!

JS唯一有引用传递的是模块化的导入导出,导入后的东西和js源文件里的东西是同一个内存里的东西,所以你会发现你一旦修改了导入的东西,js源文件里的对应地方就会被改变。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值