深浅拷贝详解

一 前置知识-数据类型 && 赋值传参

Q1 JS中的数据类型有哪些

A:

S1 基础类型: Boolean/ Number/ String/ Null/ Undefined/ Symbol

S2 引用类型: Object

  • Object
  • Function
  • Array
  • Date
  • Regexp

Q2 基础类型(原始类型) 和 引用类型的区别

A:

S1 存储方式: 基础类型的值存储在内存栈中; 引用类型的实际值存储在内存堆中,栈只是存储 堆地址

S2 值是否可变: 基础类型的值是不可变的; 引用类型的值是可变的

  • String类型的值 有些方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值
js 复制代码
let str = 'ygm';
str.slice(1);
str.toUpperCase(0);
str[0] = 'A';
// 基础类型的值是不可变的
console.log(str);  // ygm

// 看起来值是变化了,其实是在内存中开辟了一个新的栈空间,然后让变量指向了这个新值
str += '666'
console.log(str);  // ygm666

S3 变量赋值: 基础类型直接复制变量值,变量间互不影响; 引用类型复制的是堆地址,变量间互相影响

js 复制代码
// 基础类型: 直接复制变量值; 互不影响
let name = 'ygm';
let name2 = name;
name2 = '新name2';
console.log(name); // ygm;
console.log(name2); // 新name2;

// 引用类型: 复制的是堆地址; 会互相影响
let obj = {name:'ygm'};
let obj2 = obj;
obj2.name = '新name2';
console.log(obj.name);  // 新name2
console.log(obj2.name); // 新name2

S4 变量相等比较: 基础类型 比较变量值是否相同; 引用类型 比较堆内存地址是否相同

js 复制代码
const name = 'ygm';
const name2 = 'ygm';
const obj = {name:'ygm'};
const obj2 = {name:'ygm'};
// 基础类型 直接比较变量值
console.log(name === name2); // true
// 引用类型 比较堆地址
console.log(obj === obj2);   // false

Q3 变量赋值、函数传参、浅拷贝、深拷贝的区别

A:

S1 变量赋值: 基础类型是传参,引用类型是传址

S2 函数传参: 在JS中都是按值传递: 传递的都是变量在内存栈中的值 (即 变量值/堆内存地址)

js 复制代码
let name = 'ygm';
function changeValue(name2){
  name2 = '函数name';
}
// 基础类型: 按值穿递- name和name2互不影响
changeValue(name);
console.log(name); // ygm

// 引用类型: 同样按值传递- 只不过传递的值是堆内存地址
let obj = {};
function changeValue(obj2) {
  obj2.name = 'ygm';
  // 这里obj2指向了新的堆地址,所以不会再影响到obj指向的值
  obj2 = { name:'指向新name' };
  return obj2
}
const obj2 = changeValue(obj);
console.log(obj.name); // ygm
console.log(obj2.name); // 指向新name

S3 赋值 VS 浅拷贝 VS 深拷贝

下列表格中,× 表示互不影响; √ 表示互相影响

操作类型 基础数据类型a 第一层数据对象b b中包含子对象c
赋值 ×
浅拷贝 × ×
深拷贝 × × x

具体代码示例为:

js 复制代码
const obj1 = { name: 'ygm', age: 10, friends: ['a', 'b'] }
const fObj = obj1
const shallowObj = {...obj1}
const deepObj = JSON.parse(JSON.stringify(obj1))
// 赋值: fObj对象内的 基础类型和引用类型都会影响 原对象obj1
fObj.name='新name'
fObj.friends.push('c')
// 浅拷贝: shallowObj对象内的 基础类型不影响原对象obj1; 引用类型会影响obj1
shallowObj.age = 20
shallowObj.friends.push('d')
// 深拷贝: deepObj对象内的 基础类型和引用类型 都不影响原对象obj1
deepObj.friends.push('e')

console.log(obj1)  // { name: '新name', age: 10, friends: [ 'a', 'b', 'c', 'd' ] }

二 实现 浅拷贝

Q1 如何实现对象类型 的浅拷贝

A:

  1. 方法1: Object.create(obj)
js 复制代码
// 新对象的原型链指向obj + 只能克隆原始对象的属性,不能克隆原始对象的方法
let obj = {a: 1, b: {c: 2}};
let newObj = Object.create(obj);
console.log(newObj); // {}
console.log(newObj.a); // 1
console.log(newObj.b); // {c: 2}
  1. 方法2: Object.assign({}, obj)
js 复制代码
// 只能克隆原始对象的自身属性,不能克隆原始对象的 继承属性和 方法
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = Object.assign({}, a, b)  // { 1: "cc", 2: "bb", 4: "dd"}
  1. 方法3: 扩展运算符...
js 复制代码
let obj = { a: 1, b: {c: 2} };
let newObj = {...obj}
console.log(newObj);              // { a:1, b: {c:2} }
console.log(newObj.b === obj.b);  // true

Q2 如何实现数组类型 的浅拷贝

A:

  1. 方法1: [].concat(arr)
js 复制代码
// 只能克隆一维数组 + 只会复制源对象的自身属性,不会复制继承属性
let arr = [1, 2, {a: 3}];
let newArr = [].concat(arr);
console.log(newArr);               // [ 1, 2, { a: 3 } ]
console.log(newArr[2] === arr[2]); // true
  1. 方法2: Object.assign([], arr1, arr2)
js 复制代码
// a和c指向同一个内存地址,会互相影响
let a = [1,2]
let b = [3,4]
let c = Object.assign(a, b) // [3, 4]
  1. 方法3: 扩展运算符...
js 复制代码
let a = [1,2]
let b = [3,4]
let c = [...a, ...b]  // 	[1, 2, 3, 4]

Q3 Object.assign和扩展运算法 两者区别是什么

A:

  1. 返回值不同:
  • 扩展运算符 总是返回一个 拷贝后的新对象
  • Object.assign()函数是 直接修改第一个传入对象obj 并返回
  • 即 Object.assign()可能会影响 原对象的基础类型值,而 扩展运算符必然不会影响拷贝的 基础类型值
js 复制代码
// 例1.1 此时,key相同的属性 后面元素值覆盖前面 + a/b/c指向的是不同 内存地址,互不影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = Object.assign({}, a, b) // { 1: "cc", 2: "bb",  4: "dd" }

// 例1.2 此时,key相同的属性 后面元素值覆盖前面 + a和c指向的是相同 内存地址,互相影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = Object.assign(a, b)  // { 1: "cc", 2: "bb", 4: "dd" }

// 例1.3 此时,key相同的属性 后面元素值覆盖前面 + a/b/c指向的是不同 内存地址,互不影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = {...a, ...b}  // 	{ 1: "cc", 2: "bb", 4: "dd" }

// 例1.4 此时,key相同的属性 后面元素值覆盖前面 + a/b/c指向的是不同 内存地址,互不影响
let a = { 1: 'aa', 2: 'bb' }
let b = { 1: 'cc', 4: 'dd' }
let c = {...{}, ...a, ...b}  // 	{ 1: "cc", 2: "bb", 4: "dd" }
  1. 实现本质上不同:
  • Object.assign是使用 for..in循环遍历出 所有可枚举的+自有属性

    • 所以 Object.assign会触发ES6的setter拦截,扩展运算符不会
  • 扩展运算符是调用 该数据结构的 Interator遍历器接口(对象没有Interator,所以是特殊处理的)

    • 所以 扩展操作符可以复制ES6的symbols属性,Object.assign()不能

三 实现 深拷贝

Q1 如何实现 深拷贝

A:

  1. 方法1: JSON.parse(JSON.stringify)
js 复制代码
// 问题: 由于是依赖于JSON,所以无法支持 JSON不支持的类型/特性,具体表现为:

// 缺点1: 当对象内有 Fucntion类型/Undefined类型值: undefined/function值 会直接丢失
const obj = {
  undef: undefined,
  fun: () => { console.log("我是函数") },
};
const objCopy = JSON.parse(JSON.stringify(obj));
console.log("obj", obj);  // obj   { undef: undefined, fun: [Function: fun] }
console.log("objCopy", objCopy); // objCopy  {}


// 缺点2: 当对象中有 RegExp正则类型值: 它的值会变成 {}空对象
const obj = {
  re: /hello/ig
}
const objCopy = JSON.parse(JSON.stringify(obj));
console.log("obj", obj);              // obj { re: /hello/gi }
console.log("objCopy", objCopy);  // objCopy { re: {} }
console.log("objCopy-typeof", typeof objCopy.re); // 'object'


// 缺点3: 当对象中有NaN/ Infinity/ -Infinity这3种值: 它的值会变成 null
const obj = {
  nan: NaN,
  infinityMax: Number.POSITIVE_INFINITY,
  infinityMin: Number.NEGATIVE_INFINITY,
}
const objCopy = JSON.parse(JSON.stringify(obj));
// obj { nan: NaN, infinityMax: Infinity, infinityMin: -Infinity }
console.log("obj", obj);  
// objCopy { nan: null, infinityMax: null, infinityMin: null }
console.log("objCopy", objCopy); 


// 缺点4: 当对象内的 Date类型值: Date类型会被变成 字符串类型
const obj = {
  date: new Date()
}
const objCopy = JSON.parse(JSON.stringify(obj));
console.log('obj', typeof obj.date === 'object')          // true
console.log('objCopy', typeof objCopy.date === 'string')  // true


// 缺点5: 当对象 存在循环引用时: 会报错
const obj = {
  self: null
}
obj.self = obj;
const objCopy = JSON.parse(JSON.stringify(obj));
// TypeError: Converting circular structure to JSON 
console.log("objCopy", objCopy); 

  1. 方法2: 递归实现

2.1 版本1: 递归支持 基本数据类型+ 简单对象类型(不支持 Array/Func/Date/RegExp等)

js 复制代码
function isObject(target) {
  const type = typeof target;
  return target && (type === 'object' || type === 'function');
}

function deepClone(target) {
  // 递归中止条件: 类型值为基本数据类型
  if (!isObject(target)) {
    return target;
  }
  // 对于引用类型,递归进行深拷贝即可
  const cloned = {};
  for (let key in target) {
    cloned[key] = deepClone(target[key]);
  }
  return cloned;
}

const obj1 = {
  name: "hello",
  child: {
    name: "我是child",
  },
};
const obj2 = deepClone(obj1);
console.log('obj2', obj2.child !== obj1.child) // true

2.2 版本2: 支持基本数据类型+ 简单引用类型+ Array/RegExp/Date/Func

js 复制代码
function isObject(target) {
  const type = typeof target;
  return target && (type === 'object' || type === 'function');
}

function deepClone(target) {
  // 递归中止条件: 类型值为基本数据类型
  if (!isObject(target)) {
    return target;
  }
  // 对于引用类型,递归进行深拷贝
  // const cloned = {};
  // 根据不同引用类型,初始化对应类型值
  let cloned;
  if (target instanceof Array) {
    cloned = [];
  } else if (target instanceof RegExp) {
    // source属性能 获取到正则的模式,flags属性 能获取到 正则的参数
    cloned = new RegExp(target.source, target.flags);
  } else if (target instanceof Date) {
    cloned = new Date(target);
  } else if (target instanceof Function) {
    cloned = function() {
      // 在函数中去执行原来的函数,确保 返回值相同
      return target.call(this, ...arguments)
    }
  } else {
    cloned = {};
  }
  // 拷贝属性值
  for (let key in target) {
    cloned[key] = deepClone(target[key]);
  }
  return cloned;
}

const obj1 = {
  name: "hello",
  time: new Date()
};
const obj2 = deepClone(obj1);
console.log('obj2', obj2.time.getTime) // obj2 [Function: getTime]

2.3 版本3: 优化实现

  • 优化点1: 不克隆target原型上的 属性
  • 优化点2: 解决 循环对象导致的 递归爆栈问题==> weakMap(src, clone)
  • 注意,该版本在对象嵌套层级过深时,还是会出现递归爆栈的情况
js 复制代码
function isObject(target) {
  const type = typeof target;
  return target && (type === 'object' || type === 'function');
}

// weakMap不会对target进行强引用,从而避免无法释放内存
function deepClone(target, cache = new weakMap()) {
  // 递归中止条件1: 类型值为基本数据类型
  if (!isObject(target)) {
    return target;
  }
  // 递归中止条件2: 循环遇到 已克隆的缓存对象,直接返回缓存的克隆值即可
  if (cache.get(target)) {
    return cache.get(target);
  }

  // 对于引用类型,递归进行深拷贝
  // 根据不同引用类型,初始化对应类型值
  let cloned;
  if (target instanceof Array) {
    cloned = [];
  } else if (target instanceof RegExp) {
    // source属性能 获取到正则的模式,flags属性 能获取到 正则的参数
    cloned = new RegExp(target.source, target.flags);
  } else if (target instanceof Date) {
    cloned = new Date(target);
  } else if (target instanceof Function) {
    cloned = function () {
      // 在函数中去执行原来的函数,确保 返回值相同
      return target.call(this, ...arguments);
    };
  } else {
    cloned = {};
  }
  // 把 属性值-拷贝后的属性值 存入缓存map里
  cache.set(target, cloned);

  // 拷贝属性值
  // for...in 会遍历包括原型上的 所有可迭代属性
  for (let key in target) {
    // 优化点1: 过滤掉原型身上的属性, 即只遍历本身的属性
    if (target.hasOwnProperty(key)) {
      cloned[key] = deepClone(target[key], cache);
    }
  }
  return cloned;
}

const obj1 = {
  name: "hello",
  friends: ["a", "b"],
  self: null,
};
obj1.self = obj1;
const obj2 = deepClone(obj1);
// { name: 'hello', friends: [ 'a', 'b' ], self: [Circular *1] }
console.log("obj2", obj2); 

  1. 方法3: 递归实现2- 代码优化版

这版本的实现思路是和上文的 方法2是一致的,只是优化了具体实现的代码语法, 所以不再分步骤赘述 每一步的实现思路,直接给出完整代码。

js 复制代码
function isObject(obj) {
  return obj !== null && (typeof obj === "object" || typeof obj === "function");
}

function getType(target) {
  const typeStr = Object.prototype.toString.call(target);
  return typeStr.match(/\[object (.*)\]/)[1];
}

function deepClone(target, cache = new WeakMap()) {
  // S1.1 递归中止条件1: 类型值为基本数据类型
  if (!isObject(target)) {
    return target;
  }
  // S1.2 递归中止条件2: 特殊处理无法继续进行递归知道类型,如Date等
  const targetType = getType(target);
  const deepTypes = ["Object", "Array", "Map", "Set", "Arguments"];
  if (!deepTypes.includes(targetType)) {
    // 此处处理的是 基本类型的包装对象、日期、正则等特殊对象类型
    if (["Date", "Error", "Number", "Boolean", "String"].includes(targetType)) {
      return new target.constructor(target);
    } else if (targetType === "RegExp") {
      return new target.constructor(target.source, target.flags);
    } else if (targetType === "Symbol") {
      return Object(Symbol.prototype.valueOf.call(target));
    } else if (targetType === "Function") {
      return target;
    } else {
      return null;
    }
  }
  // S1.3 递归中止条件3: 循环遇到 已克隆的缓存对象,直接返回缓存的克隆值即可
  const cacheVal = cache.get(target);
  if (cacheVal) {
    return cacheVal;
  }

  // S2 支持各种数据类型
  let cloned = new target.constructor();
  // S2.2 特殊处理Set和Map类型
  if (targetType === "Set") {
    target.forEach((v) => {
      cloned.add(deepClone(v));
    });
    return cloned;
  }
  if (targetType === "Map") {
    target.forEach((v, k) => {
      cloned.set(deepClone(k), deepClone(v));
    });
    return cloned;
  }

  // S3 把 属性值-拷贝后的属性值 存入缓存map里
  cache.set(target, cloned);

  // S4 递归实现深拷贝
  for (const key in target) {
    // S4.2 性能优化:for...in 会遍历包括原型上的 所有可迭代属性
    // 这里可以 过滤掉对象原型身上的属性, 即只遍历对象本身的 属性
    if (target.hasOwnProperty(key)) {
      cloned[key] = deepClone(target[key], cache);
    }
  }
  // S5 返回结果
  return cloned;
}

四 参考文档

01【JS 进阶】你真的掌握变量和类型了吗

02 Object.assign()和展开运算符...的区别

03 这一次彻底掌握深拷贝

04 如何写出一个惊艳面试官的深拷贝

相关推荐
一斤代码4 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子5 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年5 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子5 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina5 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路6 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_6 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说6 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409196 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding6 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js