加载中... JS中的深浅拷贝 - semyin's blog
67°

JS中的深浅拷贝

在说深浅拷贝之前,我们先来了解下面的基础概念

JS中的数据类型

众所周知~,JS中的数据类型有这几种数据类型 number, string, boolean, null, undefined, object, symbol
但是,在这片文中我们不讲这几种类型,我们把上面7中类型分为两类:

1 基本数据类型

包括 number, string, boolean, null, undefined, symbol 平常我们在编码过程中基本类型数据从一个变量复制到另一个变量是不会相互影响

var a = 'semyin'
var b = a
b = 'tom'
console.log(b) // tom
console.log(a) // semyin

结论:对基本类型的数据赋值,不会相互影响

原因:他们的值在内存中占据着固定大小的空间,并被保存在栈内存中。当一个变量向另一个变量复制基本类型的值,会创建这个值的副本

2 复杂数据类型(以下称引用类型)object

来看下引用类型的数据,从一个变量复制到两一个变量会不会改变

const obj = {
  name: 'semyin',
  age: 26,
  sex: 'male'
}
const obj2 = obj
obj2.name = 'tom'
console.log(obj2) // { name: 'tom', age: 26, sex: 'male' }
console.log(obj) // { name: 'semyin', age: 26, sex: 'male' }
obj.name = 'jason'
console.log(obj2) // { name: 'jason', age: 26, sex: 'male' }
console.log(obj) // { name: 'jason', age: 26, sex: 'male' }

结论:我们可以清楚的看到,对引用类型的数据从一个变量复制到另一个变量,改变其中一个变量,另外一个也会跟着改变

原因:引用类型的数据是保存在堆内存中的,包含引用类型值的变量实际上包含的不是对象本身,而是一个指向该对象的指针。从一个变量向另一个变量复制引用类型的值,复制的其实是指针地址而已,因此两个变量最终都指向同一个地址

通常在开发中并不希望改变变量 a 之后会影响到变量 b,这时就需要用到浅拷贝和深拷贝

浅拷贝

什么是浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

打个通俗易懂的比方,现在有一个对象

const obj = {
  name: 'semyin',
  age: 26,
  sex: 'male',
  info: {
    address: '深圳',
    tel: '123'
  }
}

假如我们有一个方法shallowClone对这个进行了浅拷贝处理的到 obj2 ,那么改变 obj2name, age, sex 的值 obj 不会随之改变,但是改变 obj.info 里面的数据 obj 也会跟着改变,那么我们的 shallowClone 方法就称之为浅拷贝。

因为我们前面讲过:引用类型的数据是保存在堆内存中的,包含引用类型值的变量实际上包含的不是对象本身,而是一个指向该对象的指针 要想实现拷贝,我们需要创建一个全新的 Object,并且对原始数据进行遍历,最后添加进去,

// 简易版
function shallowClone(obj) {
  const clone = {}
  for (let i in obj) {
    clone[i] = obj[i]
  }
  return clone
}

// 测试一下!
const obj = {
  name: 'semyin',
  age: 26,
  sex: 'male'
}
const obj2 = shallowClone(obj)
obj2.name = 'jason'
console.log(obj) // { name: 'semyin', age: 26, sex: 'male' }
console.log(obj2) // { name: 'jason', age: 26, sex: 'male' }

上面的 shallowClone 是我们实现的浅拷贝方法,可以发现它不能 Obj 里面的 info 进行拷贝值,而是直接拷贝 info 的地址。

除了我们自己实现拷贝方法,常用JS拷贝方法有

// 对于对象
// ES6 展开运算符(spreed)
const obj2 = {...obj}

// Object.assign
const obj2 = Object.assign({}, obj)



// 对于数组
// ES 展开运算符(spreed)
const arr2 = [...arr]

// Array.prototype.slice
const arr2 = arr.slice()

它们都不能对象里面的引用类型数据进行值的拷贝,而是进行地址拷贝

深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

常用的拷贝方法

// JSON.parse(JSON.stringify())
const obj = {
  name: 'semyin',
  age: 26,
  sex: 'male',
  info: {
    address: '深圳',
    tel: '123'
  }
}

const obj2 = JSON.parse(JSON.stringify(obj))
obj2.info.address = '上海'
console.log(obj2.info.address) // 上海
console.log(obj.info.address) // 深圳

但是会有下面几个问题

1 不能序列化函数
2 不能解决循环引用
3 不能处理正则, new Date(), symbol, undefined

当一个对象里面出现上面3种类型,不能正确的拷贝

1 undefined, symbol, 函数 会直接忽略掉

// 摘自 木易杨 https://muyiy.cn/blog/4/4.1.html#%E4%B8%89%E3%80%81%E6%B7%B1%E6%8B%B7%E8%B4%9D%EF%BC%88deep-copy%EF%BC%89
let obj = {
    name: 'muyiy',
    a: undefined,
    b: Symbol('muyiy'),
    c: function() {}
}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy"}

2 循环引用情况下,会报错。

// 摘自 木易杨 https://muyiy.cn/blog/4/4.1.html#%E4%B8%89%E3%80%81%E6%B7%B1%E6%8B%B7%E8%B4%9D%EF%BC%88deep-copy%EF%BC%89
let obj = {
    a: 1,
    b: {
        c: 2,
   		d: 3
    }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));
// Uncaught TypeError: Converting circular structure to JSON

3 new Date 情况下,转换结果不正确

// 摘自 木易杨 https://muyiy.cn/blog/4/4.1.html#%E4%B8%89%E3%80%81%E6%B7%B1%E6%8B%B7%E8%B4%9D%EF%BC%88deep-copy%EF%BC%89
new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

4 正则情况下

// 摘自 木易杨 https://muyiy.cn/blog/4/4.1.html#%E4%B8%89%E3%80%81%E6%B7%B1%E6%8B%B7%E8%B4%9D%EF%BC%88deep-copy%EF%BC%89
let obj = {
    name: "muyiy",
    a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}

面试题之如何实现一个深拷贝

/**
 *  deepClone
 * @param source
 * @returns {Array|*}
 */
function deepClone(source) {
  /**
   *  判断是否是对象
   * @param obj
   * @returns {boolean}
   */
  var isObject = function (obj) {
    return typeof obj === 'object' && obj !== null
  }

  if(!isObject(obj)) return source // 非对象返回自身

  var target = Array.isArray(source) ? [] : {}

  for (var key in source) {
    if(Object.prototype.hasOwnProperty.call(source, key)) {
      if(isObject(source[key])) {
        target[key] = deepClone(source[key]) // 递归
      } else {
        target[key] = source[key]
      }
    }
  }

  return target
}

问题:上面的方法没有解决 symbol 和 递归爆栈,循环引用

我们先解决 symbol 和 循环引用

function deepClone(source, hash = new WeakMap()) {

    if (!isObject(source)) return source; 
    if (hash.has(source)) return hash.get(source); 
      
    let target = Array.isArray(source) ? [...source] : { ...source }; // 改动 1
    hash.set(source, target);
    
  	Reflect.ownKeys(target).forEach(key => { // 改动 2
        if (isObject(source[key])) {
            target[key] = deepClone(source[key], hash); 
        } else {
            target[key] = source[key];
        }  
  	});
    return target;
}

破解递归爆栈摘自木易杨

function cloneDeep5(x) {
    const root = {};

    // 栈
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // 广度优先
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // 下一次循环
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

参考: 木易杨 掘金


已有 0 条评论

    我有话说: