JS的浅克隆和深克隆

一说到深层克隆,大家多会跃跃欲试的表明,大家都会,但是大家知道真 正的深层克隆是什么样子的么,本篇文章将会告诉你。

浅层克隆与深度克隆

浅层克隆也被称为浅克隆,浅克隆之所以被称为浅克隆,是因为对象只会被克隆最外部的一层,至于更深层的象,则依然是通过引用指向同一块堆内存.

浅层克隆代码实现:

function shallowClone(o) {
 const obj = {};
 for ( let i in o) {
 obj[i] = o[i];
 }
 return obj;
 }
 // 被克隆对象
 const oldObj = {
 name: 'pwd',
 list: [ 'e', 'f', 'g' ],
 obj: { h: { i: 2 } }
 };
 
 const newObj = shallowClone(oldObj);
 console.log(newObj.obj.h, oldObj.obj.h); // { i: 2 } 
{ i: 2 }
 console.log(oldObj.obj.h === newObj.obj.h); // true

我们可以看到,很明显,虽然 oldObj.obj.h 被克隆了,但是它还与 oldObj.obj.h相等,这表明他们依然指向同一段堆内存,这就造成了如果对 newObj.obj.h进行修改,也会影响 oldObj.obj.h,这就不是一版好的克隆.

newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' }

我们改变了newObj.c.h.i的值,oldObj.c.h.i也被改变了,这就是浅克隆的问题所在.
当然有一个新的 API :Object.assign()也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了。
在上面很明显我们想要的克隆,浅层克隆是远远不够的,我们的目标致力于:两个长的一模一样的对象,但是彼此之间没有关联。那么接下来开始我们的重头戏——深层克隆
废话不多说,先上代码(大家最最常见的深层克隆代码):

常见深层克隆

// 克隆函数
function deepClone(obj, newObj) {
    if (obj instanceof Array) {
        // 判断是否是数组
        newObj = [];
        return deepCloneArray(obj, newObj);
    } else if (obj instanceof Object) {
        // 判断是否是对象
        newObj = {};
        return deepCloneObject(obj, newObj);
    } else {
        return (newObj = obj);
    }
}

// 克隆对象
function deepCloneObject(obj, newObj) {
    for (var temp in obj) {
        if (obj.hasOwnProperty(temp)) { // 过滤原型属性
            if (obj[temp] instanceof Object || obj[temp] instanceof Array) { // 如果还是对象或者数组继续递归深层克隆
                var tempNewObj = {};
                newObj[temp] = deepClone(obj[temp],
                    tempNewObj);
            } else { // 不是直接赋值
                newObj[temp] = obj[temp];
            }
        }
    }
    return newObj;
}

// 克隆数组
function deepCloneArray(arr, newArr) {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] instanceof Object || arr[i] instanceof Array) { // 如果还是对象或者数组继续递归深层克隆
            var tempNewObj;
            newArr[i] = deepClone(arr[i], tempNewObj);
        } else { // 不是直接赋值
            newArr[i] = arr[i];
        }
    }
    return newArr;
}

这个代码是大家见到最常见的 “克隆代码”,对于一些简单的克隆是可以的,比如下面的:

var obj = {
    name: "panda",
    sex: 18,
    msg: {
        a: 1,
        b: 2
    },
    list: [1, 2, 3, 4]
}
var obj1 = deepClone(obj)
console.log(obj1.list, obj.list) // [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
console.log(obj1.list == obj.list) // false

以上的克隆对于一般数组和对象有效,但是我们的工作中对象不单纯只有普通对象把。当我们遇到函数,正则,日期对象会怎么样,看以下的问题:
1. 可以克隆函数么?
2. 可以克隆正则,Date 对象么?
3. 可以克隆原型么?
4. 可以解决循环引用么(环)
我们来测试一下
克隆函数:

var obj = {fn: function () {}}
var newObj = deepClone(obj)
console.log(newObj, obj)
//结果:{ fn: {} } { fn: [Function: fn] } 失败。

克隆正则

var obj = /abc/g
var newObj = deepClone(obj)
console.log(newObj, obj)
// 结果: {} /abc/g, 失败。

克隆 Date 对象

var obj = new Date()
var newObj = deepClone(obj)
console.log(newObj, obj)
// 结果: {} 2019-04-23T05:47:22.133Z 失败。

克隆对象原型

function Person() {}
Person.prototype.getMsg = function () {}
var p = new Person()
var obj = p
var newObj = deepClone(obj)
console.log(newObj.__proto__ == obj.__proto__)
// 结果: false 失败

在我们的对象中出现循环引用,又会怎么样?

var a={"name":"zzz"};
var b={"name":"vvv"};
a.child=b; b.parent=a;
var newObj = deepClone(b)
// 结果 :RangeError: Maximum call stack size exceeded (爆栈了)

这是我们的深层克隆函数,虽说有诸多的问题,我们在后面进行解决。接 下来我们在看一中简便的方法(江湖流传的妙招)。

JSON.parse 方法

前几年微博上流传着一个传说中最便捷实现深克隆的方法,JSON 对象parse 方法可以将 JSON 字符串反序列化成 JS 对象,stringify 方法可以将 JS对象序列化成 JSON 字符串,这两个方法结合起来就能产生一个便捷的深克隆.

const newObj = JSON.parse(JSON.stringify(oldObj));
const oldObj = {
    a: 1,
    b: ['e', 'f', 'g'],
    c: {
        h: {
            i: 2
        }
    }
};
const newObj = JSON.parse(JSON.stringify(oldObj));
console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 }
console.log(oldObj.c.h === newObj.c.h); // false
newObj.c.h.i = 'change';
console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 }

果然,这是一个实现深克隆的好方法,但是这个解决办法是不是太过简单了.确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑.
1.他无法实现对函数 、RegExp 等特殊对象的克隆。
2.会抛弃对象的 constructor,所有的构造函数会指向 Object。
3.对象有循环引用,会报错。
对于这里的问题和之前的问题差不多,我就不一一测试了,同学们可以回 去玩一玩,是具有这样的问题。
说了这么多,把几种常见的深层克隆的方法个大家讲解后,遗留了一堆问题。
接下来我们要写个能解决以上问题的深层克隆。
首先我们分析一下遗留问题,其实上面的问题我们可以分为三类:
1.对象类型问题,
2.原型问题,
3.循环引用问题
那我们就逐个击破就好了。

  1. 对象类型问题:
    由于要面对不同的对象(正则、数组、Date 等)要采用不同的处理方式,我们需要实现一个对象类型判断函数。
function isType(obj, type) {
    if (typeof obj !== 'object') return false;
    var typeString = Object.prototype.toString.call(obj);
    var flag;
    switch (type) {
        case 'Array':
            flag = typeString === '[object Array]';
            break;
        case 'Date':
            flag = typeString === '[object Date]';
            break;
        case 'RegExp':
            flag = typeString === '[object RegExp]';
            break;
        default:
            flag = false;
    }
    return flag;
};

通过这个函数 isType 我们可以根据 type 确定 obj 是不是对应的对象类型。这样我们就可以对特殊对象进行类型判断了,从而采用针对性的克隆策略。
对于正则对象,我们在处理之前要先补充一点新知识。
我们需要通过正则语法解到 flags 属性等等,因此我们需要实现一个提取
flags 的函数。

function getRegExp(re) {
    var flags = '';
    if (re.global) flags += 'g';
    if (re.ignoreCase) flags += 'i';
    if (re.multiline) flags += 'm';
    return flags;
};
  1. 原型问题:
    我们利用 Object.getPrototypeOf(); 获取对象原型,在利用 Object.create切断原型链即可。
  2. 循环引用问题:
    对于引用值,我们通过数组进行记录,每次碰到引用值后,遍历数组进行判断。然后处理。

做好了这些准备工作,我们就可以进行深克隆的实现了。
全部代码:

function getRegExp(re) {
    var flags = '';
    if (re.global) flags += 'g';
    if (re.ignoreCase) flags += 'i';
    if (re.multiline) flags += 'm';
    return flags;
};

function isType(obj, type) {
    if (typeof obj !== 'object') return false;
    var typeString = Object.prototype.toString.call(obj);
    var flag;
    switch (type) {
        case 'Array':
            flag = typeString === '[object Array]';
            break;
        case 'Date':
            flag = typeString === '[object Date]';
            break;
        case 'RegExp':
            flag = typeString === '[object RegExp]';
            break;
        default:
            flag = false;
    }
    return flag;
};

function clone(parent) {
    // 维护两个储存循环引用的数组
    var parents = [];
    var children = [];

    var _clone = parent => {
        if (parent === null) return null;
        if (typeof parent !== 'object') return parent;

        var child, proto;

        if (isType(parent, 'Array')) {
            // 对数组做特殊处理
            child = [];
        } else if (isType(parent, 'RegExp')) {
            // 对正则对象做特殊处理
            child = new RegExp(parent.source,
                getRegExp(parent));
            if (parent.lastIndex) child.lastIndex =
                parent.lastIndex;
        } else if (isType(parent, 'Date')) {
            // 对 Date 对象做特殊处理
            child = new Date(parent.getTime());
        } else {
            // 处理对象原型
            proto = Object.getPrototypeOf(parent);
            // 利用 Object.create 切断原型链
            child = Object.create(proto);
        }

        // 处理循环引用
        var index = parents.indexOf(parent);
        if (index != -1) {
            // 如果父数组存在本对象,说明之前已经被引用过,直接返回此
            对象
            return children[index];
        }
        parents.push(parent);
        children.push(child);
        for (var i in parent) {
            // 递归

            if (parent.hasOwnProperty(i)) { // 过滤原型属性
                child[i] = _clone(parent[i]);
            }
        }
        return child;
    };
    return _clone(parent);
};

当然,我们这个深克隆还不算完美,例如 Buffer 对象、Promise、Set、Map 可能都需要我们做特殊处理,另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间,不过一个基本的深克隆函数我们已经实现了。
实现一个深克隆是面试中常见的问题的,可是绝大多数面试者的答案都是不完整的,甚至是错误的,这个时候面试官会不断追问,看看你到底理解不理解深克隆的原理,很多情况下一些一知半解的面试者就原形毕漏了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350