在说深拷贝、浅拷贝之前,首先先说一下js里存在的两种数据类型:基本数据类型和引用数据类型。
- 基本数据类型有:boolean Null Undefined Number String Symbol
- 引用类型有:Object(包含 Array、Function、RegExp、Date等等)
区别:基本数据类型名值都在栈内存中,而引用数据类型的名存在栈内存中,值存在堆内存中,但是栈内存会提供一个引用地址指向堆内存中的值。
因为JS不允许直接访问堆内存,也不能直接对堆内存进行操作,所以,引用类型的值是按引用访问的。
- 深拷贝 & 浅拷贝
深拷贝、浅拷贝是专门针对于引用数据类型的一种概念,因为对于引用数据类型的名、值存在不同的内存空间当中,所以当发生拷贝行为的时候,系统会开辟一块新的栈内存空间,存放的是被复制的变量在栈内存中的值,即堆内存中的值的引用地址。
即新的变量复制得到的是被复制对象的引用地址,当变量发生改变值的操作时,指向该对象的地址的其他变量的值也会随之变化,此为浅拷贝。
当然,我们有时候并不希望这样的场景发生,所以这时候就需要用到深拷贝的方法去进行拷贝。
- 深拷贝 & 浅拷贝常用方法
- 浅拷贝方法:
( = 号只是引用,并没有发生拷贝行为,在内存中并没有开辟新的空间!!!)
1. slice() // 操作对象:数组 let a = [1,2,3,"4",{name:"lme"}]; let b = a.slice(0); console.log("a: ",a); // [1,2,3,"4",{name:"lme"}] console.log("b: ",b); // [1,2,3,"4",{name:"lme"}] b[4].name = "dsg"; console.log("修改b,打印a: ",a); // [1,2,3,"4",{name:"dsg"}] 2. concat() // 操作对象:数组 let a = [1,2,3,"4",{name:"lme"}]; let b = a.concat(); console.log("a: ",a); // [1,2,3,"4",{name:"lme"}] console.log("b: ",b); // [1,2,3,"4",{name:"lme"}] b[4].name = "cool"; console.log("修改b,打印a: ",a); // [1,2,3,"4",{name:"cool"}] 3. Array.from() // 操作对象:数组 let a = [1,2,3,"4",{name:"lme"}]; let b = Array.from(a); console.log("a: ",a); // [1,2,3,"4",{name:"lme"}] console.log("b: ",b); // [1,2,3,"4",{name:"lme"}] b[4].name = "cool"; console.log("修改b,打印a: ",a); // [1,2,3,"4",{name:"cool"}] 4. ...操作符 // 操作对象:数组 && 对象 let a = [1,2,3,"4",{name:"lme"}]; let b = [...a]; console.log("a: ",a); // [1,2,3,"4",{name:"lme"}] console.log("b: ",b); // [1,2,3,"4",{name:"lme"}] b[4].name = "cool"; console.log("修改b,打印a: ",a); // [1,2,3,"4",{name:"cool"}] let c = {age:18,sex:1,child:{name:"小明",age:2,sex:1}}; let d = {...c}; console.log("c: ", c); // {age:18,sex:1,child:{name: "小明",age: 2,sex: 1}} console.log("d: ", d); // {age:18,sex:1,child:{name: "小明",age: 2,sex: 1}} d.child.name = "小红"; console.log("修改d,打印c: ", c); // {age:18,sex:1,child:{name: "小红",age: 2,sex: 1}} 5. Object.assign() // 操作对象:对象 let c = {age:18,sex:1,child:{name:"小明",age:2,sex:1}}; let d = Object.assign({},c); console.log("c: ", c); // {age:18,sex:1,child:{name: "小明",age: 2,sex: 1}} console.log("d: ", d); // {age:18,sex:1,child:{name: "小明",age: 2,sex: 1}} d.child.name = "小红"; console.log("修改d,打印c: ", c); // {age:18,sex:1,child:{name: "小红",age: 2,sex: 1}}
- 深拷贝方法:
1. // 当所拷贝的数组或对象的一维元素或属性都是 基本数据类型 的时候 // 可以使用以上浅拷贝的方法去达成一次深拷贝的结果。 slice()、concat、Array.from()、...操作符、Object.assign() // 注意:当且仅当 数组或对象的一维元素或者属性全部都是 基本数据类型 的时候 // 拷贝结果才是深拷贝,否则为浅拷贝!!! 2. JSON.parse(JSON.stringify(obj)) // 序列化 -> 反序列化 // 将数组或对象序列化转成字符串,然后再反序列化转成对象,由于字符串是String类型, // 属于基本类型,所以会切断与源对象引用地址的联系而形成一个新的对象。 // 该方法可以对多维对象进行深拷贝,但是需要注意的是,在序列化时,某些特定的值或者类型将会丢失 如下: 1. Date时间对象 // 如果obj里的某一条属性的值为时间对象,则序列化之后再反序列化得到的结果, // 时间将只是字符串的形式存在,而不是时间对象。 2. RegExp、Error对象 // 如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象。 3. 函数、undefined // 如果obj里有函数、undefined,则序列化的结果会把函数或undefine丢失。 4. NaN // 如果obj里有NaN,则序列化的结果会变成null 5. constructor // 如果obj中的某一条属性的值是由构造函数生成的对象,则使用 // JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor构造函数。 // 比如: function Person(name) { this.name = name; } let lme = new Person('lme'); let a = { name: 'a', data: lme, }; let b = JSON.parse(JSON.stringify(a)); console.log("a",a); // 构造函数:Person类型 console.log("b",b); // 构造函数:无 b.name = "gyj"; b.data.name = "gyj is lme's wife" console.log("a1: ",a) // 原始a对象没有被修改 console.log("b1: ",b) // b对象被修改 // 感兴趣的同学可以打印一下看看呦~ PS: 该方法简单粗暴,适用于简单的引用类型数据,使用前请三思~ 3. 自行编写函数进行递归遍历复制,实现深拷贝。 // 先根据要拷贝的变量的类型声明一个新的变量,用for in遍历要拷贝的变量,然后判断如果当前 // 循环中的key(属性)的值是一个对象,那么便递归,对该属性继续进行遍历拷贝,否则直接将 // 该属性的值赋值给新的对象的属性。 // 如果需要将原型链上继承过来的属性过滤掉的话,可使用hasOwnProperty(),该方法会判断传 // 入的参数是否在对象上,如果是原型链上的属性或方法,则会返回false。 例: function deepCopy(obj) { let newObj = obj.constructor === Array ? [] : {} for (let key in obj) { // Array实际上也是一个对象,即也是引用数据类型 typeof obj[key] === 'object' ? newObj[key] = deepCopy(obj[key]) : newObj[key] = obj[key] } return newObj }
- 浅拷贝方法:
(限于本人技术有限,本文如有表述不当的地方,欢迎赐教~)
(转载到其他平台请包含本文的链接或说明出处~)