Skip to content
索引

赋值与深浅拷贝

JavaScript中存在两大数据类型:

  • 基本类型,数据保存在在栈内存
  • 引用类型,数据保存在堆内存中,引用类型的变量是一个指向堆内存中实际对象的引用(指针),存在

赋值

赋值是将一个变量赋给另一个变量的操作

因为 JavaScript中的两大数据类型,所以赋值也分为两类:

1、基本类型:赋值,赋值之后两个变量互不影响

2、引用类型:赋址,实际上是把一个变量的地址赋给另一个变量,两个变量具有相同的引用,指向堆内存中同一个对象,修改其中一个变量的值,相互之间有影响

js
// 基本类型两个值不会互相影响
let a = "test"
let b = a
b = "change"
console.log(a, b)  // test change

// 引用类型的两个值会互相影响
let c = { name: "test" }
let d = c
d.name = "change"
console.log(c, d) // { name: "change" } { name: "change" }

let e = [0, 1, 2]
let f = e
f[0] = 4
console.log(e, f) // [4,1,2]
// 基本类型两个值不会互相影响
let a = "test"
let b = a
b = "change"
console.log(a, b)  // test change

// 引用类型的两个值会互相影响
let c = { name: "test" }
let d = c
d.name = "change"
console.log(c, d) // { name: "change" } { name: "change" }

let e = [0, 1, 2]
let f = e
f[0] = 4
console.log(e, f) // [4,1,2]

很多时候,我们想要的结果是两个初始值相同的变量互不影响,由上可知我们其实只需要处理引用类型的变量

所以就要使用到拷贝(分为深浅两种)来处理引用类型

深浅拷贝定义

综上,让我们整理一下深浅拷贝的定义:

基本类型没有深浅拷贝的概念,从内存堆栈的角度来说,深浅拷贝只针对引用类型

浅拷贝

浅拷贝,拷贝一个引用类型基本类型, 该引用类型中的引用类型则仍是相同引用 修改浅拷贝后的引用类型中的基本类型,不会影响原引用类型,修改浅拷贝后的引用类型中的引用类型,则会影响原引用类型 什么意思?举个例子,下方obj对象是一个引用类型,其基本类型name,num,其引用类型fisrtObj,testArr 浅拷贝后修改namenum基本类型不会影响原变量,而修改fisrtObjtestArr引用类型就会影响obj对象

bash
const obj = {
  name: "name",
  num:0,
  fisrtObj: {
    name: "name"
  },
  testArr:[1,2,3]
}
const obj = {
  name: "name",
  num:0,
  fisrtObj: {
    name: "name"
  },
  testArr:[1,2,3]
}

对象浅拷贝

分别使用

  • 遍历对象属性与值来新建对象
  • Object.assign
  • 拓展运算符

遍历对象属性与值来新建对象

实现一个方法

js
function shallowClone(obj) {
    const newObj = {}
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop]
        }
    }
    return newObj
}
function shallowClone(obj) {
    const newObj = {}
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop]
        }
    }
    return newObj
}

由输出结果,可知只是拷贝基本类型,引用类型则仍是相同引用,属于浅拷贝

js
const obj = {
  name: "name",
  testObj: {
    name: "name"
  }
}
const cloneObj = shallowClone(obj)
cloneObj.name = "test"
cloneObj.testObj.name = "test"
console.log(obj, cloneObj) 
// 打印结果:
//  {                  {
//  name: "name",       name: "test", 
//  testObj: {          testObj: {
//    name: "test"      name: "test"
//  }                     }
// }                    }
const obj = {
  name: "name",
  testObj: {
    name: "name"
  }
}
const cloneObj = shallowClone(obj)
cloneObj.name = "test"
cloneObj.testObj.name = "test"
console.log(obj, cloneObj) 
// 打印结果:
//  {                  {
//  name: "name",       name: "test", 
//  testObj: {          testObj: {
//    name: "test"      name: "test"
//  }                     }
// }                    }

Object.assign

由输出结果,可知只是拷贝基本类型,引用类型则仍是相同引用,属于浅拷贝

js
const obj = {
  num: 0,
  arr: ["one", "two"],
}
const cloneObj = Object.assign({}, obj)
cloneObj.num = 10
cloneObj.arr[0] = "test"
console.log(obj, cloneObj)
// 打印结果:
//  {                       {
//    num: 0,                 num: 10, 
//    arr: ["test", "two"]    arr: ["test", "two"]
// }                         } 
const obj = {
  num: 0,
  arr: ["one", "two"],
}
const cloneObj = Object.assign({}, obj)
cloneObj.num = 10
cloneObj.arr[0] = "test"
console.log(obj, cloneObj)
// 打印结果:
//  {                       {
//    num: 0,                 num: 10, 
//    arr: ["test", "two"]    arr: ["test", "two"]
// }                         } 

解构赋值

解构赋值属于赋值的概念,不属于深浅拷贝的概念

由输出结果,可知即使是基本类型的修改,也会影响原变量,因为没有拷贝原引用类型,所以不属于深浅拷贝

js
const obj = {
   testObj: {
     name: "name"
  }
}
const { testObj } = obj
testObj.name = "test"
console.log(obj, testObj)
// 打印结果:
//  {                        
//    testObj: {             {
//      name: "test"            name: "test"
//    }                      }
// }   
const obj = {
   testObj: {
     name: "name"
  }
}
const { testObj } = obj
testObj.name = "test"
console.log(obj, testObj)
// 打印结果:
//  {                        
//    testObj: {             {
//      name: "test"            name: "test"
//    }                      }
// }   

拓展运算符

由输出结果,可知只是拷贝基本类型,引用类型则仍是相同引用,属于浅拷贝

js
const obj = {
   testObj: {
     name: "name",
     arr: [1, 2, 3]
  }
}
// 属于赋值,不属于深浅拷贝
const { testObj } = obj
testObj.name = "test"
testObj.arr[0] = [4]
// 拓展运算符,属于浅拷贝
const cloneObj = { ...testObj }
cloneObj.name = "last" // 修改基本类型不会影响原引用类型
cloneObj.arr[1] = [5]  // 浅拷贝修改引用类型仍是相同引用
console.log(obj, testObj, cloneObj)
// 打印结果:
//  {                        
//    testObj: {        {                   {
//      name: "test",     name: "test",       name: "last",
//      arr: [4, 5, 3]    arr: [4, 5, 3]      arr: [4, 5, 3]
//    }                 }                   }
// }
const obj = {
   testObj: {
     name: "name",
     arr: [1, 2, 3]
  }
}
// 属于赋值,不属于深浅拷贝
const { testObj } = obj
testObj.name = "test"
testObj.arr[0] = [4]
// 拓展运算符,属于浅拷贝
const cloneObj = { ...testObj }
cloneObj.name = "last" // 修改基本类型不会影响原引用类型
cloneObj.arr[1] = [5]  // 浅拷贝修改引用类型仍是相同引用
console.log(obj, testObj, cloneObj)
// 打印结果:
//  {                        
//    testObj: {        {                   {
//      name: "test",     name: "test",       name: "last",
//      arr: [4, 5, 3]    arr: [4, 5, 3]      arr: [4, 5, 3]
//    }                 }                   }
// }

数组浅拷贝

分别使用slice, concat,拓展运算符

slice

由输出结果,可知只是拷贝基本类型,引用类型则仍是相同引用,属于浅拷贝

js
const arr = [{ name: "name" }, "one", "two"]
const cloneArr = arr.slice(0)
cloneArr[0].name = "test"
cloneArr[1] = "three"
console.log(arr, cloneArr) // [{ name: "test" }, "one", "two"]  [{ name: "test" }, "three", "two"]
const arr = [{ name: "name" }, "one", "two"]
const cloneArr = arr.slice(0)
cloneArr[0].name = "test"
cloneArr[1] = "three"
console.log(arr, cloneArr) // [{ name: "test" }, "one", "two"]  [{ name: "test" }, "three", "two"]

concat

由输出结果,可知只是拷贝基本类型,引用类型则仍是相同引用,属于浅拷贝

js
const arr = [{ name: "name" }, "one", "two"]
const cloneArr = arr.concat([])
cloneArr[0].name = "test"
cloneArr[1] = "three"
console.log(arr, cloneArr) // [{ name: "test" }, "one", "two"]  [{ name: "test" }, "three", "two"]
const arr = [{ name: "name" }, "one", "two"]
const cloneArr = arr.concat([])
cloneArr[0].name = "test"
cloneArr[1] = "three"
console.log(arr, cloneArr) // [{ name: "test" }, "one", "two"]  [{ name: "test" }, "three", "two"]

拓展运算符

由输出结果,可知只是拷贝基本类型,引用类型则仍是相同引用,属于浅拷贝

js
const arr = [{ name: "name" }, "one", "two"]
const cloneArr = [...arr]
cloneArr[0].name = "test"
cloneArr[1] = "three"
console.log(arr, cloneArr) // [{ name: "test" }, "one", "two"]  [{ name: "test" }, "three", "two"]
const arr = [{ name: "name" }, "one", "two"]
const cloneArr = [...arr]
cloneArr[0].name = "test"
cloneArr[1] = "three"
console.log(arr, cloneArr) // [{ name: "test" }, "one", "two"]  [{ name: "test" }, "three", "two"]

深拷贝

深拷贝完全拷贝一个引用类型,不但拷贝了这个引用类型中的所有基本类型,还为这个引用类型中的所有的引用类型开辟新栈新堆新指针

所以修改一个深拷贝后的引用类型的属性或元素,不会影响原引用类型的属性或元素,无论是修改其属性或元素中的基本类型还是引用类型

常用的深拷贝方法

  • JSON.parse(JSON.stringify())
  • 循环递归

JSON.parse(JSON.stringify())

js
const obj2= JSON.parse(JSON.stringify(obj1))
const obj2= JSON.parse(JSON.stringify(obj1))

JSON.stringify():用于将JavaScript值转换为JSON字符串 JSON.parse():用于将一个JSON字符串转换为JavaScript对象

但是这种方式存在局限性,会忽略undefinedsymbol函数

js
const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2) // {name: "A"}
const obj = {
    name: 'A',
    name1: undefined,
    name3: function() {},
    name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2) // {name: "A"}

循环递归

写法不是固定的,自行写得出一种即可

js
function deepClone(source) {
  const targetObj = source.constructor === Array ? [] : {} // 判断复制的目标是数组还是对象
  for (let keys in source) {
    // 遍历目标
    if (source.hasOwnProperty(keys)) {
      if (source[keys] && typeof source[keys] === 'object') {
        // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {}
        targetObj[keys] = deepClone(source[keys])
      } else {
        // 如果不是,就直接赋值
        targetObj[keys] = source[keys]
      }
    }
  }
  return targetObj
}
const obj = {
  num: 0,
  testObj: {
    name: "name"
  }
}
const shallowobj = obj
const deepobj = deepClone(obj)
// 浅拷贝
shallowobj.num = 1
shallowobj.testObj.name = "test"
// 深拷贝
deepobj.num = 2
deepobj.testObj.name = "last"
console.log(obj) 
console.log(shallowobj) 
console.log(deepobj) 
// 打印结果:
//  {   
//    num:1,            num:1,              num:2
//    testObj: {        {                   {
//      name: "test",     name: "test",       name: "last",
//    }                 }                   }
// }
function deepClone(source) {
  const targetObj = source.constructor === Array ? [] : {} // 判断复制的目标是数组还是对象
  for (let keys in source) {
    // 遍历目标
    if (source.hasOwnProperty(keys)) {
      if (source[keys] && typeof source[keys] === 'object') {
        // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {}
        targetObj[keys] = deepClone(source[keys])
      } else {
        // 如果不是,就直接赋值
        targetObj[keys] = source[keys]
      }
    }
  }
  return targetObj
}
const obj = {
  num: 0,
  testObj: {
    name: "name"
  }
}
const shallowobj = obj
const deepobj = deepClone(obj)
// 浅拷贝
shallowobj.num = 1
shallowobj.testObj.name = "test"
// 深拷贝
deepobj.num = 2
deepobj.testObj.name = "last"
console.log(obj) 
console.log(shallowobj) 
console.log(deepobj) 
// 打印结果:
//  {   
//    num:1,            num:1,              num:2
//    testObj: {        {                   {
//      name: "test",     name: "test",       name: "last",
//    }                 }                   }
// }
js
// 深度克隆
export function deepClone(obj) {
	// 对常见的“非”值,直接返回原来值
	if ([null, undefined, NaN, false].includes(obj)) return obj;
	if (typeof obj !== "object" && typeof obj !== 'function') {
		//原始类型直接返回
		return obj;
	}
	var o = isArray(obj) ? [] : {};
	for (let i in obj) {
		if (obj.hasOwnProperty(i)) {
			o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
		}
	}
	return o;
}
// 深度克隆
export function deepClone(obj) {
	// 对常见的“非”值,直接返回原来值
	if ([null, undefined, NaN, false].includes(obj)) return obj;
	if (typeof obj !== "object" && typeof obj !== 'function') {
		//原始类型直接返回
		return obj;
	}
	var o = isArray(obj) ? [] : {};
	for (let i in obj) {
		if (obj.hasOwnProperty(i)) {
			o[i] = typeof obj[i] === "object" ? deepClone(obj[i]) : obj[i];
		}
	}
	return o;
}

深浅拷贝区别

深浅拷贝都拷贝了引用类型基本类型,深浅拷贝后,修改引用类型中的基本类型,都不影响原引用类型中的基本类型

区别在于:

是否拷贝了引用类型中的引用类型

也就是修改拷贝后的引用类型中的引用类型的值,是否会影响原引用类型中的引用类型的值

问题

加深印象,试着回答下问题吧

提问1,什么是深浅拷贝?

都用于拷贝引用类型

浅拷贝是拷贝了一层, 拷贝了引用类型基本类型,而引用类型中的引用类型仍是相同引用

深拷贝则是完全拷贝,即拷贝了引用类型基本类型,也拷贝引用类型中的引用类型(即开辟新堆新栈新指针

提问2,为什么需要深浅拷贝?

因为JavaScript中引用类型的数据类型,数据保存在堆内存中,其变量是一个指向堆内存中实际对象的引用(指针),存在

所以引用类型的赋值操作,实际上是赋址,是把一个变量的地址赋给另一个变量,两个变量具有相同的引用,指向堆内存中同一个对象,修改其中一个变量的值,相互之间有影响

而很多时候,我们想要的结果是两个初始值相同的变量互不影响,需要处理引用类型的变量

所以就要使用到拷贝(分为深浅两种)来处理引用类型

提问3.解构赋值属于深浅拷贝嘛?

不属于,属于赋值概念,既不属于浅拷贝,也不属于深拷贝

提问4.常见的浅拷贝对象有哪些方法?

  • 遍历对象属性与值来新建对象
  • Object.assign
  • 拓展运算符

提问4.常见的浅拷贝数组有哪些方法?

  • slice
  • concat
  • 拓展运算符

提问5.手写一个循环递归深拷贝

是否能手写出来,不行的话再去上方背一背吧

Object.assign是浅拷贝

Released under the MIT License.