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

javaScript-深浅拷贝 #8

Open
dark9wesley opened this issue Mar 17, 2021 · 0 comments
Open

javaScript-深浅拷贝 #8

dark9wesley opened this issue Mar 17, 2021 · 0 comments

Comments

@dark9wesley
Copy link
Owner

在实际的开发工作中,有些场景往往需要我们对已有对象进行再加工。

但在原对象上直接修改是不可取的,这会造成不可逆的数据篡改。

这时候就需要有合适的方法,对原对象进行拷贝(复制),然后再修改拷贝对象上的值。

浅拷贝

浅拷贝复制一份原对象上的数据到新对象。
如果原对象上的是基本类型的数据,则复制值本身到新对象。
如果原对象上的是引用类型的数据,则复制引用地址到新对象。

简单来说,浅拷贝只拷贝一层,也就是原对象与新对象本身并不相等,修改第一层的数据并不会影响对方。

但如果原对象是多层级的,那修改新对象中的子对象,原对象中的子对象,也会发生改变。

JS中的浅拷贝方法

1.Array.slice

slice: 从原数组切分出新数组;

slice方法接收两个可选的参数(begin, end),并根据这两个参数返回一个新的数组。

let a = [1,2,3,{a: 4, b: 5}];

let b = a.slice();

b[0] = 100;

b[3].c = 6;

console.log(a,b)
//a: [1, 2, 3, {a: 4, b: 5, c: 6}];
//b: [100, 2, 3, {a: 4, b: 5, c: 6}];

可以看到,修改b[0] = 100时,a对象并不会产生变化,但当修改b[3].c = 6时,a对象也发生了改变。这就是浅拷贝,只拷贝一层。

2.Array.concat

concat: 合并数组

concat方法接受任意多个值,这些值可以是具体的值,也可以是数组对象。并返回一个合并后的新数组

concat方法的代码示例和slice方法差不多,这里就不重复举例了。

3.Object.assign

assign: 将所有源对象的可枚举属性值分配到目标对象。

assign方法接收一个目标对象以及任意多个源对象,并返回修改后的目标对象。如果键名重复,则会进行覆盖。

let a = [1,2,3,{a: 4, b: 5}];

let b = Object.assign({}, a);

b[0] = 100;

b[3].c = 6;

console.log(a,b)
//a: [1, 2, 3, {a: 4, b: 5, c: 6}];
//b: [100, 2, 3, {a: 4, b: 5, c: 6}];

只拷贝了一层,浅拷贝无疑。

除了这三种,还有使用展开运算符也是浅拷贝,这里就不赘述了。

下面来手动实现一下浅拷贝。

手动实现浅拷贝

浅拷贝的实现相对来说比较简单,就是遍历原对象上的属性,并复制一份到新对象上。

但要注意几点:

  1. 对参数进行校验
  2. 兼容对象和数组两种情况
function isObject(source){
    return typeof source === 'object' && source !== null;
}

function shallowClone(source){
    //检查参数是否是一个对象,如果不是,直接返回;
    if(!isObject(source)) return source;

    //检查原对象是数组还是对象,根据原对象的类型创建新对象;
    let target = Array.isArray(source) ? [] : {};

    //遍历原对象上的属性
    for(let i in source){

        //由于for in会遍历出原型上的属性
        //所以这里要用hasOwnProperty检查属性是否在原对象本身上
        if(source.hasOwnProperty(i)){
            //拷贝给新对象
            target[i] = source[i]
        }
    }

    //返回新对象
    return target;
}

深拷贝

深拷贝精确复制原对象上的数据到新对象
无论是基础数据类型还是引用数据类型。都是复制值本身。

深拷贝相对于浅拷贝来说,拷贝的层级是无限深的。

无论怎么修改深拷贝后的对象,都不会影响到原对象。

现有的深拷贝方法

JSON.parse(JSON.stringify(source))

let a = ['hello world', {a: 1, b: ['p', 'y']}, Symbol('a'), [100, 200], function test(){}, undefined];

let b = JSON.parse(JSON.stringify(a));

b[0] = 'hi new world';

b[1].b = 'new';

console.log(a,b)

//a: ['hello world', {a: 1, b: ['p', 'y']}, Symbol('a'), [100, 200], function test(){}, undefined];
//b: ['hi new world', {a: 1, b: 'new'}, null, [100, 200], null, null];

可以发现,修改新对象的任何层级的值,都不会影响到原对象,这就实现了深拷贝。

但是,这个方法有几个问题:

  1. undefined、symbol、函数会被忽略,或者被替换为null(数组对象中)
  2. 对于循环引用的对象,会抛出错误。
  3. 无法处理正则对象以及Date对象

如果开发中不需要考虑以上的情况,直接用这个方法是极好的。

此外,jQuery.extend() 和 lodash.cloneDeep()也可以实现深拷贝。

下面,让我们自己实现一个深拷贝。

手动实现深拷贝

在实现深拷贝之前,再来思考下深拷贝比浅拷贝多了什么特点?

没错,就是无限层级的拷贝。

那么在浅拷贝的基础上,加上递归,是不是就能实现深拷贝了?

动手试试。

function isObject(source){
    return typeof source === 'object' && source !== null;
}

function deepClone(source){

    if(!isObject(source)) return source;

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

    for(let i in source){

        if(source.hasOwnProperty(i)){

            // ============= 改动代码
            // 检测要赋值给新对象的是否是一个对象,如果是,进行递归处理。
            target[i] = isObject(source[i]) ? deepClone(source[i]) : source[i];
            // ============= 
        }
    }

    return target;
}

测试一下:

let a = {
    a: 1, 
    b: {ary: [1,2,3], str: 'hi'},
    fn: () => {}, 
    date: new Date(), 
    reg: new RegExp(), 
    n: null, 
    un: undefined
}

let s = Symbol('a');
a[s] = 'local symbol';

let b = deepClone(a);

console.log(a.b === b.b) //false;
console.log(a.b.ary === b.b.ary) //false;
console.log(a, b)

可以看到,深层次的对象已经不再相等,证明已经完成了深层次的拷贝。

但这个深拷贝的实现还有问题,将上面的测试代码复制到浏览器的控制台查看运行结果。

可以发现:

  1. symbol丢失。

  2. Date对象和正则对象都变成了空的对象。

这个结果显然不是我们想要的。接下来一一解决。

symbol丢失

首先要思考的是,为什么symbol会丢失?是哪一步引起了symbol的丢失?

这里直接给出答案,原因是我们使用了for...in。

MDN上对for...in的定义
for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性。包括原型上的属性。

那么为了能够拷贝symbol,我们就不能使用for...in语句了,我们需要能遍历出symbol属性的方法。

方法一:Object.getOwnPropertySymbols(...)

Object.getOwnPropertySymbols方法返回一个由给定对象自身的symbol属性组成的数组,如果没有,则返回一个空数组。

方法二:Reflect.ownKeys(...)

Reflect.ownKeys()返回一个由目标对象自身的属性键组成的数组。无论该属性是否可被枚举。

这里用方法一来作示例,感兴趣的同学可以自行思考一下方法二如何实现。

方法一的思路就是先获取原对象中的所有symbol,先处理symbol,处理完再处理其他的属性。

代码如下:

function isObject(source){
    return typeof source === 'object' && source !== null;
}

function deepClone(source){
    if(!isObject(source)) return source;

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

    // ============= 新增代码
    let symbolList = Object.getOwnPropertySymbols(source);
    if(symbolList.length){
        symbolList.forEach(i => {
            target[i] = isObject(source[i]) ? deepClone(source[i]) : source[i];
        })
    }
    // ============= 

    for(let i in source){
        if(source.hasOwnProperty(i)){
            target[i] = isObject(source[i]) ? deepClone(source[i]) : source[i];
        }
    }

    return target;
}

再用刚才的测试代码测试一遍,完美解决Symbol丢失的问题。

处理Date对象以及正则对象

其实这个问题解决起来也很简单,就是我们的代码中没有对Date对象和正则对象进行判断和处理。

首先封装一个判断具体类型的方法

function typeName(source){
    return Object.prototype.toString.call(source).slice(8, -1);
}

然后对正则和Date对象进行相应的处理,代码改动如下:

function isObject(source){
    return typeof source === 'object' && source !== null;
}

function deepClone(source){
    if(!isObject(source)) return source;
    
    // ============= 新增代码
    if(typeName(source) === 'Date') return new Date(source);
    if(typeName(source) === 'RegExp') return new RegExp(source);
    // ============= 

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

    let symbolList = Object.getOwnPropertySymbols(source);
    if(symbolList.length){
        symbolList.forEach(i => {
            target[i] = isObject(source[i]) ? deepClone(source[i]) : source[i];
        })
    }

    for(let i in source){
        if(source.hasOwnProperty(i)){
            target[i] = isObject(source[i]) ? deepClone(source[i]) : source[i];
        }
    }

    return target;
}

再用刚才的测试代码测试一遍,完美解决Date对象以及正则对象为空的问题。

对于日常的开发来说,这里的深拷贝代码已经完全够用了。

但这个实现还有待改进的问题,感兴趣的同学可以继续阅读。

循环引用与相同引用

思考一下,有一种情况,一个对象,它的子对象引用了它本身。

let a = {b: 'Hello world'};
a.c = a;

在控制台中打印这个对象,会发现它是一个无限嵌套的对象。

这时候用我们手写的深拷贝来处理这个对象,会发生什么呢?

果然,栈爆了。

另一种情况,一个对象中,它的两个属性指向同一个对象。

但用我们手写的深拷贝来处理后发现两个属性不再指向同一个对象。

那么结果显然也是不对的。

那我们该如何处理这种循环引用以及相同引用的对象呢?

想一想,对于已经处理过的对象,将它存起来,再遇到时,直接返回已经处理过的对象,是不是就可以解决问题了?

动手试试:

function isObject(source){
    return typeof source === 'object' && source !== null;
}

function typeName(source){
    return Object.prototype.toString.call(source).slice(8, -1);
}

//这里用WeakMap来存取已处理过的对象,当然用数组也可以;
function deepClone(source, hash = new WeakMap()){
    if(!isObject(source)) return source;
    
    //检查是否已经处理过该属性,如果有,直接返回
    if(hash.has(source)) return hash.get(source);

    if(typeName(source) === 'Date') return new Date(source);
    if(typeName(source) === 'RegExp') return new RegExp(source);

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

    //没有处理过该属性,将处理后的对象存储起来
    hash.set(source, target);

    let symbolList = Object.getOwnPropertySymbols(source);
    if(symbolList.length){
        symbolList.forEach(i => {
            //递归时要注意将表进行传递,否则会创建新的表
            target[i] = isObject(source[i]) ? deepClone(source[i], hash) : source[i];
        })
    }

    for(let i in source){
        if(source.hasOwnProperty(i)){
             //递归时要注意将表进行传递,否则会创建新的表
            target[i] = isObject(source[i]) ? deepClone(source[i], hash) : source[i];
        }
    }

    return target;
}

测试一下:

let a = {
    a: 1, 
    b: {ary: [1,2,3], str: 'hi'},
    fn: () => {}, 
    date: new Date(), 
    reg: new RegExp(), 
    n: null, 
    un: undefined
}

let s = Symbol('a');
a[s] = 'local symbol';
a.self = a;
a.copySelf = a;

let b = deepClone(a);

console.log(a.b === b.b) //false;
console.log(a.b.ary === b.b.ary) //false;
console.log(a, b)

完美解决循环引用以及相同引用的问题。

还存在的问题

目前手写的深拷贝方法还有问题。

由于使用的是递归,如果对象的嵌套层级过深,那么就会引起爆栈。

关于爆栈的解决方法可以参考:

深拷贝的终极探索(99%的人都不知道)

这里便不再赘述了,主要是我也还没弄懂呐。

参考文章

【进阶4-1期】详细解析赋值、浅拷贝和深拷贝的区别
【进阶4-3期】面试题之如何实现一个深拷贝
JavaScript专题之深浅拷贝

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

No branches or pull requests

1 participant