Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

面试官:深拷贝浅拷贝的区别?如何实现一个深拷贝? #56

Open
huihuiha opened this issue Feb 21, 2021 · 15 comments
Open

Comments

@huihuiha
Copy link
Contributor

一、数据类型存储

前面文章我们讲到,JavaScript中存在两大数据类型:

  • 基本类型
  • 引用类型

基本类型数据保存在在栈内存中

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

二、浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

下面简单实现一个浅拷贝

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

JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

Object.assign

var obj = {
    age: 18,
    nature: ['smart', 'good'],
    names: {
        name1: 'fx',
        name2: 'xka'
    },
    love: function () {
        console.log('fx is a great girl')
    }
}
var newObj = Object.assign({}, fxObj);

slice()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

concat()

const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

拓展运算符

const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

三、深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()

  • jQuery.extend()

  • JSON.stringify()

  • 手写循环递归

_.cloneDeep()

const _ = require('lodash');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

jQuery.extend()

const $ = require('jquery');
const obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

JSON.stringify()

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

但是这种方式存在弊端,会忽略undefinedsymbol函数

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

循环递归

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

四、区别

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别

从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象

// 浅拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存

console.log('obj1',obj1) // obj1 { name: 'init',  arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

// 深拷贝
const obj1 = {
    name : 'init',
    arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存

console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }

小结

前提为拷贝类型为引用类型的情况下:

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址

  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

@zenghp2015
Copy link

作者你好,请问一下
在浅拷贝 shallowClone 中,在什么情况下会出现 obj.hasOwnProperty(prop) === false 的情况?

@cquptgh
Copy link

cquptgh commented Aug 11, 2021

@zero2015 当prop属性存在于原型链上时,并不是obj自身属性的时候

@bemzhao
Copy link

bemzhao commented Sep 29, 2021

@zero2015 当prop属性存在于原型链上时,并不是obj自身属性的时候

因为 hasOwnProperty 并不是保留的关键字,所以如果对象上有 hasOwnProperty 作为属性名的时候,obj.hasOwnProperty(prop) 还有可能会报错

obj.hasOwnProperty is not a function

所以应该用 Object 上真正的 hasOwnProperty 方法
if (obj.hasOwnProperty(prop)) {
if (Object.hasOwnProperty.call(obj, prop)) {

@1455667325
Copy link

1455667325 commented Dec 22, 2021

既然slice 和concat 实现的是浅拷贝 那为什么改变其中一个另外一个不变呢? concat 方法返回一个新的数组,不会影响到原数组 这不是深拷贝的意思吗

@liboliang01
Copy link

既然slice 和concat 实现的是浅拷贝 那为什么改变其中一个另外一个不变呢? concat 方法返回一个新的数组,不会影响到原数组 这不是深拷贝的意思吗

不知道你说的是不是对于数字或者字符串的情况。对于原始值(数字、字符串)来说,浅拷贝和深拷贝是一样的,都是对值的拷贝,拷贝出来是独立的两个值;而对于引用类型来说(对象),浅拷贝拷贝的是值的引用,这个时候才会导致改了一个另外一个也变化。

@Cxq0
Copy link

Cxq0 commented Feb 26, 2022

扩展运算符是深拷贝吧,只能拷贝一层

@bemzhao
Copy link

bemzhao commented Feb 27, 2022

扩展运算符是深拷贝吧,只能拷贝一层

首先,拷贝的意思就是大家都能把对象中的指针也拷贝一份,但如果对象中还有对象呢?
深拷贝是深层次的拷贝,浅拷贝是浅层次的拷贝;
如果对象中又是一个对象(引用类型);
浅拷贝出来的对象中的对象被修改时,原对象也会被修改;

image

而深拷贝出来的对象中的对象被修改时,原对象不会受到影响;

image

@chenchunyuan
Copy link

浅拷贝只是复制了内存指引,最终指向的内存地址还是同一个,所以当发生变化的时候,所有的相关引用都会变,因为指向的是同一个对象,

@printjs
Copy link

printjs commented May 31, 2022

难道深拷贝的时候不考虑循环引用的问题吗?

@Gluttonys
Copy link

要不要试试这个方法 window.structuredClone 🚀🚀🚀

@Numsina
Copy link

Numsina commented Jun 22, 2022

手写拷贝, obj == null 不是 obj === null

@XiangBo-EvanZuo
Copy link

你没清楚for in

@scottMan1001
Copy link

Object.assign 生成的新对象,使用严格比较。结果为FALSE, 它不是浅拷贝吗

@liujiayi0531
Copy link

slice concat和...不都是深拷贝吗?

@clarencesea
Copy link

既然slice 和concat 实现的是浅拷贝 那为什么改变其中一个另外一个不变呢? concat 方法返回一个新的数组,不会影响到原数组 这不是深拷贝的意思吗

如果数组中元素是对象,你试下

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests