添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

对于堆资源(也就是对象数组这样的引用类型)来说,就是在开辟一片堆内存,把原来的内容拷贝。 注意:常规来说需要深拷贝的数据只有数组和对象类型,其他类型拷贝没有意义,比如:函数和正则等等

  • 为了不影响原始数据
  • 二、浅拷贝和深拷贝的区别

    1. 浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

    2. 深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组/d对象的所有元素)拷贝过来,是“值”而不是“引用”

    3. 深拷贝和浅拷贝都是针对的引用类型, 而对引用类型赋值,则会进行地址的拷贝

    其实深拷贝和浅拷贝都是针对的引用类型,JS中的变量类型分为值类型(基本类型)和引用类型;对值类型进行复制操作会对值进行一份拷贝,而对引用类型赋值,则会进行地址的拷贝,最终两个变量指向同一份数据

    // 基本类型
    var a = 1;
    var b = a;
    a = 2;
    console.log(a, b); // 2, 1 ,a b指向不同的数据
    // 引用类型指向同一份数据
    var a = {c: 1};
    var b = a;
    a.c = 2;
    console.log(a.c, b.c); // 2, 2 全是2,a b指向同一份数据
    

    对于引用类型,会导致a b指向同一份数据,此时如果对其中一个进行修改,就会影响到另外一个,有时候这可能不是我们想要的结果,如果对这种现象不清楚的话,还可能造成不必要的bug

    那么如何切断a和b之间的关系呢,可以拷贝一份a的数据,根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝

    var a1 = {b: {c: {}};
    var a2 = shallowClone(a1); // 浅拷贝
    a2.b.c === a1.b.c // true
    var a3 = clone(a1); // 深拷贝
    a3.b.c === a1.b.c // false
    

    浅拷贝的实现非常简单,而且还有多种方法,其实就是遍历对象属性的问题,这里只给出一种,如果看不懂下面的方法,或对其他方法感兴趣,可以看我的这篇文章

    function shallowClone(source) {
        var target = {};
        for(var i in source) {
            if (source.hasOwnProperty(i)) {
                target[i] = source[i];
        return target;
    

    三、最简单的深拷贝

    深拷贝的问题其实可以分解成两个问题,浅拷贝+递归,什么意思呢?假设我们有如下数据

    var a1 = {b: {c: {d: 1}};
    

    只需稍加改动上面浅拷贝的代码即可,注意区别

    function clone(source) {
        var target = {};
        for(var i in source) {
            if (source.hasOwnProperty(i)) {
                if (typeof source[i] === 'object') {
                    target[i] = clone(source[i]); // 注意这里
                } else {
                    target[i] = source[i];
        return target;
    

    大部分人都能写出上面的代码,但当我问上面的代码有什么问题吗?就很少有人答得上来了,聪明的你能找到问题吗?

    其实上面的代码问题太多了,先来举几个例子吧

  • 没有对参数做检验
  • 判断是否对象的逻辑不够严谨
  • 没有考虑数组的兼容
  • 就不考虑数组的情况了,其实ES6之后还要考虑set, map, weakset, weakmap
  • 其实吧这三个都是小问题,其实递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出

    下面的代码可以生成指定深度和每层广度的代码,这段代码我们后面还会再次用到

    1) 数据的深度与广度

    function createData(deep, breadth) {
        var data = {};
        var temp = data;
        for (var i = 0; i < deep; i++) {
            temp = temp['data'] = {};
            for (var j = 0; j < breadth; j++) {
                temp[j] = j;
        return data;
    createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
    createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}
    

    当clone层级很深的话就会栈溢出,但数据的广度不会造成溢出

    clone(createData(1000)); // ok
    clone(createData(10000)); // Maximum call stack size exceeded
    clone(createData(10, 100000)); // ok 广度不会溢出
    

    其实大部分情况下不会出现这么深层级的数据,但这种方式还有一个致命的问题,就是循环引用,举个例子

    var a = {};
    a.a = a;
    clone(a) // Maximum call stack size exceeded 直接死循环了有没有,/(ㄒoㄒ)/~~
    

    关于循环引用的问题解决思路有两种,一是循环检测,一种是暴力破解,关于循环检测大家可以自己思考下;关于暴力破解我们会在下面的内容中详细讲解

    四、一行代码的深拷贝

    function cloneJSON(source) {
        return JSON.parse(JSON.stringify(source));
    

    下面来测试下cloneJSON有没有溢出的问题,看起来cloneJSON内部也是使用递归的方式

    cloneJSON(createData(10000)); // Maximum call stack size exceeded
    

    既然是用了递归,那循环引用呢?并没有因为死循环而导致栈溢出啊,原来是JSON.stringify内部做了循环引用的检测,正是我们上面提到破解循环引用的第一种方法:循环检测

    var a = {};
    a.a = a;
    cloneJSON(a) // Uncaught TypeError: Converting circular structure to JSON
    

    五、 递归爆栈

    其实破解递归爆栈的方法有两条路,第一种是消除尾递归,但在这个例子中貌似行不通,第二种方法就是干脆不用递归,改用循环,当我提出用循环来实现时,基本上90%的前端都是写不出来的代码的,这其实让我很震惊

    举个例子,假设有如下的数据结构

    var a = {
        a1: 1,
        a2: {
            b1: 1,
            b2: {
                c1: 1
    

    这不就是一个树吗,其实只要把数据横过来看就非常明显了

    / \ a1 a2 | / \ 1 b1 b2 | |

    用循环遍历一棵树,需要借助一个栈,当栈为空时就遍历完了,栈里面存储下一个需要拷贝的节点

    首先我们往栈里放入种子数据,key用来存储放哪一个父元素的那一个子元素拷贝对象

    然后遍历当前节点下的子元素,如果是对象就放到栈里,否则直接拷贝

    function cloneLoop(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;
    

    改用循环后,再也不会出现爆栈的问题了,但是对于循环引用依然无力应对

    六、 循环引用

    有没有一种办法可以破解循环应用呢?别着急,我们先来看另一个问题,上面的三种方法都存在的一个问题就是引用丢失,这在某些情况下也许是不能接受的

    举个例子,假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象,o(╯□╰)o

    var b = {};
    var a = {a1: b, a2: b};
    a.a1 === a.a2 // true
    var c = clone(a);
    c.a1 === c.a2 // false
    

    如果我们发现个新对象就把这个对象和他的拷贝存下来,每次拷贝对象前,都先看一下这个对象是不是已经拷贝过了,如果拷贝过了,就不需要拷贝了,直接用原来的,这样我们就能够保留引用关系了,✧(≖ ◡ ≖✿)嘿嘿

    但是代码怎么写呢,o(╯□╰)o,别急往下看,其实和循环的代码大体一样,不一样的地方我用// ==========标注出来了

    引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了

    find是抽象的一个函数,其实就是遍历uniqueList

    // 保持引用关系
    function cloneForce(x) {
        // =============
        const uniqueList = []; // 用来去重
        // =============
        let 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] = {};
            // =============
            // 数据已经存在
            let uniqueData = find(uniqueList, data);
            if (uniqueData) {
                parent[key] = uniqueData.target;
                continue; // 中断本次循环
            // 数据不存在
            // 保存源数据,在拷贝数据中对应的引用
            uniqueList.push({
                source: data,
                target: res,
            // =============
            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;
    function find(arr, item) {
        for(let i = 0; i < arr.length; i++) {
            if (arr[i].source === item) {
                return arr[i];
        return null;
    

    下面来验证一下效果,amazing

    var b = {};
    var a = {a1: b, a2: b};
    a.a1 === a.a2 // true
    var c = cloneForce(a);
    c.a1 === c.a2 // true
    

    接下来再说一下如何破解循环引用,等一下,上面的代码好像可以破解循环引用啊,赶紧验证一下

    惊不惊喜,(^__^) 嘻嘻……

    var a = {};
    a.a = a;
    cloneForce(a)
    

    看起来完美的cloneForce是不是就没问题呢?cloneForce有两个问题

    第一个问题,所谓成也萧何,败也萧何,如果保持引用不是你想要的,那就不能用cloneForce了;

    第二个问题,cloneForce在对象数量很多时会出现很大的问题,如果数据量很大不适合使用cloneForce

    七、 性能对比

  • juejin.cn/post/684490…
  • 尺有所短寸有所长,无关乎好坏优劣,其实每种方法都有自己的优缺点,和适用场景,人尽其才,物尽其用,方是真理

    下面对各种方法进行对比,希望给大家提供一些帮助

    clonecloneJSONcloneLoopcloneForce
    难度☆☆☆☆☆☆☆☆☆
    兼容性ie6ie8ie6ie6
    循环引用一层不支持一层支持
    栈溢出不会不会
    保持引用
    适合场景一般数据拷贝一般数据拷贝层级很多保持引用关系
  • yanhaijing.com/javascript/…
  • github.com/jsmini/clon…
  • - 思考: 深拷贝就是无限层级拷贝,那么JS没有能力得知目前电脑的内存大小,以及分配给浏览器的内存大小,所以,没有办法知道达到多少层级会爆栈

    - 其实递归方法最大的问题在于爆栈,当数据的层次很深是就会栈溢出

    - 数据的深度与广度

    - 当clone层级很深的话就会栈溢出,但数据的广度不会造成溢出

    - 关于循环引用的问题解决思路有两种,一是循环检测,一种是暴力破解

    - 破解递归爆栈的方法有2: 第一种是消除尾递归,第二种 改用循环

    - 引用丢失的问题处理:看此属性是否拷贝过了,拷贝过的话就用原来的。

    - 感谢原作者,爆赞~