【js进阶4】深浅拷贝

JavaScript | 2020-07-23 19:16:10 167次 2次

一、浅拷贝

浅拷贝,只拷贝一层,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。提示:以下的拷贝不涉及原型上方法。

第一种:采用 es6 中的扩展运算符:

let source = {
    d: 4,
    e: {
	e1: 'source e1'
    }
}

let s = {...source}
source.d = 5
source.e.e1 = 'source new value'
console.log(s)

//打印结果
d: 4
e: {e1: "source new value"}
__proto__: Object

第二种:可以通过 Object.assign 实现:

let source = {
    d: 4,
    e: {
	e1: 'source e1'
    }
}

let s = Object.assign({}, source)
source.d = 5
source.e.e1 = 'source new value'
console.log(s)

//结果如下
d: 4
e: {e1: "source new value"}

第三种:采取遍历的方式进行拷贝也是一样的效果:

function cloneShallow(source) {
    let target = {},
        hasProperty = Object.prototype.hasOwnProperty;
    for (let key in source) {
        if (hasProperty.call(source, key)) {
            target[key] = source[key];
        }
    }
    return target;
}

let s = cloneShallow(source)
source.d = 5
source.e.e1 = 'source new value'
console.log(s)

//打印结果
d: 4
e: {e1: "source new value"}
__proto__: Object

实现了将 source 拷贝到 target 上,可以看到对于 source 中的第二层(引用类型数据)修改后,目标对象中的 e1 同样被修改了,但是基本数据类型的值是不受影响的,验证了开头的那句话。

通过第三种这种方式,我们可以模拟实现下 Object.assign 方法:

Object.defineProperty(Object, "assignSelf", {
    value: function (arg) {
      //目标参数不能为 undefined或者null
      if (arg == null) {
        throw new TypeError('Cannot convert undefined or null to object');
      }

      // Object.assign(1, {a:1})  将target包装为对象
      let target = Object(arg),
          hasOwnProperty = Object.prototype.hasOwnProperty;

      for (let i = 1; i < arguments.length; i++) {
        let nextSource = arguments[i];
        // 过滤待拷贝的空项
        if (nextSource != null) {
          for (let nextKey in nextSource) {
            //只要自身上属性
            if (hasOwnProperty.call(nextSource, nextKey)) {
              target[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      //返回目标
      return target;
    },
    writable: true,
    configurable: true
});


二、深拷贝

深拷贝相当于不用拷贝了,而是我要抄袭你,完完全全的抄袭你,拷贝完成后两个对象相互不影响,因为连内存也拷贝过来了,而不是引用。比如我们最常用的深拷贝方法:

let source = {
    d: 4,
    e: {
	e1: 'source e1'
    }
}

let s = JSON.parse(JSON.stringify(source))
source.e.e1 = 'this is new value'
console.log(s, source)

//打印结果s
d: 4
e:
 e1: "source e1"
//打印结果 source
d: 4
e:
 e1: "this is new value"

可见此时的 source 中第二层引用类型的数值修改后,并不会影响已经拷贝出来的 s 数据。

采用浅拷贝遍历的方式再进行递归处理也可以深拷贝:

//是否对象
function isObject(val){
    return (typeof val === 'object' && val != null);
}

function deepClone(source){
    let target = {},
        hasOwnProperty = Object.prototype.hasOwnProperty;
		
    for(let k in source){
	if(hasOwnProperty.call(source, k)){
	    if(isObject(source[k])){
	        //递归调用
		target[k] = deepClone(source[k])
	    }else{
		target[k] = source[k]
	    }
	}
    }
    return target;
}
// 继续使用source进行拷贝
let b = deepClone(source);

sources.e.e1 = 111
console.log(b.e.e1) //source e1 并不会再被修改

看着是可以了,继续修改下待拷贝的数据源:

let source = {
    d: 4,
    e: {
	e1: 'source e1'
    },
    f: [1, 2, 3]
}

此时打印 f 会发现很奇怪,因为本身是一个数组,但是我们在定义 target 时是一个对象,所以需要考虑数组的情况:

function deepClone(source){
    // 判断一下就好了 或者 source instanceof array
    let target = Array.isArray(source) ? [] : {}
    ...
}

对象中也可以使用 Symbol 作为 key,所以再考虑一下 symbol 的情况

let sym = Symbol('1')
let source = {
    d: 4,
    e: {
        e1: 'source e1'
    },
    f: [1,2,3],
    [sym]: 1
}
console.log(Object.keys(source))  //["d", "e", "f"]
console.log(Object.getOwnPropertySymbols(source)) // [Symbol(1)]
console.log(Reflect.ownKeys(source))  // ["d", "e", "f", Symbol(1)]

可以看到通过 keys 这种方式拿不到 Symbol 类型,必须通过 getOwnPropertySymbols 特有的方法才可以取到,其中 Reflect 将来会取代 Object,通过 ownKeys 方式可以全部拿到,那么可以通过两种方式来实现拷贝,第一种就是在原来的基础上再增加一个 symbol key 遍历:

 function deepClone(source){
     ...
     let symKeys = Object.getOwnPropertySymbols(source);
     if (symKeys.length) { // 如果存在 symbol key
         symKeys.forEach(symKey => {
             if (isObject(source[symKey])) {
                 target[symKey] = deepClone(source[symKey], hash); 
              } else {
                  target[symKey] = source[symKey];
              }    
         });
     }
     ...
 }

第二种方式直接使用 Reflect 上的方法,全部遍历一次:

function deepClone(source){
    let target = Array.isArray(source) ? [] : {};
    Reflect.ownKeys(source).forEach(k => {
        if(isObject(source[k])){
            target[k] = deepClone(source[k])
        }else{
            target[k] = source[k]
        }
    })
    return target;
}

let s = deepClone(source)

source.e.e1 = 111
source.f.push(111)

console.log(s)
// 结果
d: 4
e: {e1: "source e1"}
f: (3) [1, 2, 3]
Symbol(1): 1

在使用 Reflect.ownKeys 时没有进行 hasOwnProperty 的判断,因为这个方法是不会枚举原型上的属性:

let s = Object.create({a: 1})
Reflect.ownKeys(s) // []

//使用 for in 则可以
for( let k in s){
    console.log(k) //a
}


三、深拷贝循环引用

再继续考虑一种情况,如果数据中出现了循环引用的情况,以上的方式就会无限递归爆栈:

let source = {
    d: 4,
    a: {}
}  
source.a.a = source.a
source.e = source

可以考虑使用 Map 来存储对象,但是这里更好的方式是使用 WeakMap,区别是后者只能将对象格式作为键名(null 除外):

...
function deepClone(source, hash = new WeakMap()){
    if(!isObject(source)){return source}
	
    if(hash.has(source)){
        return hash.get(source)
    }
	
    let target = Array.isArray(source) ? [] : {};
    //这里记录的是引用
    hash.set(source, target)

    Reflect.ownKeys(source).forEach(k => {
        if(isObject(source[k])){
            target[k] = deepClone(source[k], hash)
        }else{
            target[k] = source[k]
        }
    })
    return target;
}

let s = deepClone(source)

source.e.e.d = 10

console.log(s)
//结果
a: {a: {…}}
d: 4
e:
    a: {a: {…}}
    d: 4
    e: {d: 4, a: {…}, e: {…}}


四、深拷贝递归转遍历

采用递归的方式数据量大的时候可能会爆栈,深拷贝的方式貌似找不到尾递归优化的方式,所以可以改为队列遍历的方式实现,可以先参考下树和数组转换 分别采用了递归和遍历的方式实现。

function deepClone(x) {
    const root = {};
    // 队列
    const loopList = [{
        parent: root,
        key: undefined,
        data: x,
    }];
    let hash = new WeakMap();
    while(loopList.length) {
        // 广度优先
        const { parent, key, data } = loopList.shift();
        
        // res记录层
        let res = parent;
        if (key) {
            res = parent[key] = Array.isArray(data) ? [] : {};
        }
        hash.set(data, res)
        
        Reflect.ownKeys(data).forEach(k => {
            let item = data[k];
            if(isObject(item)){
                if(hash.has(item)){
                    res[k] = hash.get(item)
                }else{
                    // 追加队列数据
                    loopList.push({
                        parent: res,
                        key: k,
                        data: item,
                    })
                }
            }else{
                res[k] = item;
            }
        })
    }
    return root;
}

let s = {a: 1, b: { c: 1 }, arr:[1,2,3]}
s.d = s

let news = deepClone(s)

s.d.a = 2
s.b.c = 10
s.arr.push(4)
//打印 news 拷贝后数据,数据正常

这里面采取广度优先遍历的方式,不断的构造追加队列,直到为基本数据类型才赋值,队列中包含 parent, key, data 三个主要标记往哪里挂载和键值信息,同时也需要处理相互引用的数据。

2人赞

分享到: