饥人谷技术博客

JS 深拷贝

2020-01-13  本文已影响0人  202b37dbe4bd

什么是深拷贝?

关于深拷贝其实并没有什么统一的定义,每个人都可以有自己的理解。
我的理解:<b>将源数据复制一份结构和数据一模一样的拷贝,并且修改这份拷贝中的任何一个地方源数据也不会发生改变。</b>

如何实现深拷贝?

了解了深拷贝的定义后,我们还得考虑选择什么方式来实现这个功能。

1. 使用 JSON.stringify() 和 JSON.parse()

const obj1 = {
    a: 1,
    b: 2,
    c: 3
}
const obj2 = JSON.parse(JSON.stringify(obj1));

这个方法虽然很简单,而且不需要使用其他的库,但是它也有很多局限。

  1. JSON 支持的数据类型只有:字符串(string)、数值(number)、true、false、 null、对象(object)或者数组(array)。如果你的数据中包含此外的其他类型(函数、正则、Date 等等),JSON会自动忽略或错误地拷贝这些数据。
  2. 这种方法不支持环状的数据结构:
const obj = {
    name: 'obj'
}
obj.next = obj;
JSON.parse(JSON.stringify(obj)); //  Uncaught TypeError: Converting circular structure to JSON

2. 使用 Object.assign 方法

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

const obj1 = {
    a: 1,
    b: 2,
    c: 3
};
const obj2 = {};
Object.assign(obj2, obj1);
console.log(obj2); // {a: 1, b: 2, c: 3}

但是这种方法的缺点也很明显:
当属性的值为引用类型时,会把引用类型的地址复制到目标对象,这样无论在源对象还是在目标对象中修改了这个引用类型的值,另一个对象都会发生改变。

const obj1 = {
    a: 1
};
const obj2 = {
    b: 2,
    c: obj1
};
const obj3 = {};
Object.assign(obj3, obj2);
console.log(obj3); // {b: 2, c: {a: 1}}
obj2.c.a = 3;
console.log(obj3); // {b: 2, c: {a: 3}}

3. 使用 Lodash.cloneDeep()

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。
这个库里就提供了深拷贝的方法:

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

但是这个这个方法不支持对函数的拷贝:

const _ = require("lodash");
const fn1 = function(){};
const fn2 = _.cloneDeep(fn1);
console.log(fn1); // [Function: fn1]
console.log(fn2); // {}

此外,还有一个缺点,就是要下载这个库才能使用。

4. 使用扩展操作符

const obj1 = {
    a: 1,
    b: 2,
    c: 3
};
const obj2 = {...obj1};
obj1.a = 111111111111;
console.log(obj1); // { a: 111111111111, b: 2, c: 3 }
console.log(obj2); // { a: 1, b: 2, c: 3 }

这个方法的问题很明显,与 Object.assign() 一样,当属性为引用类型时,拷贝后数据中的引用和原始数据中的引用是同一个引用。

5. 造轮子

上面的方法都多少有一些局限,在实际使用时可能无法满足需求,这个时候就可以开 jia 心 ban 造轮子了。(花费🐒的时间是造轮子的主要缺点之一,还有可靠性,兼容性等其他隐患)
在造轮子之前还是要先明确需求,确定接下来要写的这个轮子要实现哪些功能,不用实现哪些功能。
这样既可以保证作品满足要求,又可以防止写代码的过程中陷入头脑风暴,把坑越挖越深。。。
那么,我先确定我这个深拷贝功能要满足以下功能:

  1. 它可以拷贝基本数据类型。
  2. 它可以拷贝对象。
  3. 它可以拷贝函数。
  4. 它可以拷贝数组。
  5. 它可以拷贝环形数据结构。
  6. 它可以拷贝正则。
  7. 它可以拷贝 Date 类型。

此外还需要明确它的边界:

  1. 不需要拷贝原型上的属性。
  2. 不需要考虑爆栈。
  3. 不需要拷贝属性的 set、get 等方法。
    拿到需求以后,把功能一条条实现,最终就得到了我们一开始想要的深拷贝函数了:
let cache = [];

function deepClone(source, isRecursionCall) {
    if (!isRecursionCall) {
        cache = [];
    }
    if (source instanceof Object) {
        let result;
        if (source instanceof RegExp) {
            result = new RegExp(source.source, source.flags);
        } else if (source instanceof Date) {
            result = new Date(source);
        } else if (source instanceof Function) {
            result = function () {
                return source.apply(this, arguments);
            };
        } else if (source instanceof Array) {
            result = [];
        } else {
            result = {};
        }
        for (let key in source) {
            if (source[key] instanceof Object) {
                let i;
                for (i = 0; i < cache.length; i++) {
                    if (cache[i][0] === source[key]) {
                        break;
                    }
                }
                if (i < cache.length) {
                    result[key] = cache[i][1];
                } else {
                    cache.push([source[key], result[key]]);
                    result[key] = deepClone(source[key], true);
                }
            } else {
                result[key] = source[key];
            }
        }
        return result;
    } else {
        return source;
    }
}

module.exports = deepClone;

github 地址

总结

在遇到问题时,应该优先看既有的一些方法是否可以满足需求。如果无法满足,在明确需求和边界后,在时间和能力都够用的情况下就可以自己造轮子了。

PS:本文正在参加“写编程博客瓜分千元现金”活动,关注公众号“饥人谷”回复“编程博客”参与活动。

上一篇 下一篇

猜你喜欢

热点阅读