关于javascript:对象深浅拷贝与WeakMap

一、浅拷贝

当咱们进行数据拷贝的时候,如果该数据是一个援用类型,并且拷贝的时候仅仅传递的是该对象的指针,那么就属于浅拷贝。因为拷贝过程中只传递了指针,并没有从新创立一个新的援用类型对象,所以二者共享同一片内存空间,即通过指针指向同一片内存空间。

常见的对象浅拷贝形式为:
① Object.assign()

const a = {msg: {name: "lihb"}};
const b = Object.assign({}, a);
a.msg.name = "lily";
console.log(b.msg.name); // lily

一旦批改对象a的msg的name属性值,克隆的b对象的msg的name属性也跟着变动了,所以属于浅拷贝。

② 扩大运算符(…)

const a = {msg: {name: "lihb"}};
const b = {...a};
a.msg.name = "lily";
console.log(b.msg.name); // lily

同样的,批改对象a中的name,克隆对象b中的name值也跟着变动了。

常见的数组浅拷贝形式为:
① slice()

const a = [{name: "lihb"}];
const b = a.slice();
a[0].name = "lily";
console.log(b[0].name); // lily

一旦批改对象a[0]的name属性值,克隆的对象b[0]的name属性值也跟着变动,所以属于浅拷贝。

② concat()

const a = [{name: "lihb"}];
const b = a.concat();
a[0].name = "lily";
console.log(b[0].name);// lily

同样的,批改对象a[0]的name属性值,克隆的对象b[0]的name属性值也跟着变动。

③ 扩大运算符(…)

const a = [{name: "lihb"}];
const b = [...a];
a[0].name = "lily";
console.log(b[0].name); // lily

同样的,批改对象a[0]的name属性值,克隆的对象b[0]的name属性值也跟着变动。

二、深拷贝

当咱们进行数据拷贝的时候,如果该数据是一个援用类型,并且拷贝的时候,传递的不是该对象的指针,而是创立一个新的与之雷同的援用类型数据,那么就属于深拷贝。因为拷贝过程中从新创立了一个新的援用类型数据,所以二者领有独立的内存空间,互相批改不会相互影响

常见的对象和数组深拷贝形式为:
① JSON.stringify()和JSON.parse()

const a = {msg: {name: "lihb"}, arr: [1, 2, 3]};
const b = JSON.parse(JSON.stringify(a));
a.msg.name = "lily";
console.log(b.msg.name); // lihb
a.arr.push(4);
console.log(b.arr[4]); // undefined

能够看到,对对象a进行批改后,拷贝的对象b中的数组和对象都没有受到影响,所以属于深拷贝。

尽管JSON.stringify()和JSON.parse()能实现深拷贝,然而其并不能解决所有数据类型,当数据为函数的时候,拷贝的后果为null;当数据为正则的时候,拷贝后果为一个空对象{},如:

const a = {
    fn: () => {},
    reg: new RegExp(/123/)
};
const b = JSON.parse(JSON.stringify(a));
console.log(b); // { reg: {} }

能够看到,JSON.stringify()和JSON.parse()对正则和函数深拷贝有效

三、实现深拷贝

进行深拷贝的时候,咱们次要关注的是对象类型,即在拷贝对象的时候,该对象必须创立的一个新的对象,如果对象的属性值依然为对象,则须要进行递归拷贝。对象类型次要为,DateRegExpArrayObject等。

function deepClone(source) {
    if (typeof source !== "object") { // 非对象类型(undefined、boolean、number、string、symbol),间接返回原值即可
        return source;
    }
    if (source === null) { // 为null类型的时候
        return source;
    }
    if (source instanceof Date) { // Date类型
        return new Date(source);
    }
    if (source instanceof RegExp) { // RegExp正则类型
        return new RegExp(source);
    }
    let result;
    if (Array.isArray(source)) { // 数组
        result = [];
        source.forEach((item) => {
            result.push(deepClone(item));
        });
        return result;
    } else { // 为对象的时候
        result = {};
        const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]; // 取出对象的key以及symbol类型的key
        keys.forEach(key => {
            let item = source[key];
            result[key] = deepClone(item);
        });
        return result;
    }
}
let a = {name: "a", msg: {name: "lihb"}, date: new Date("2020-09-17"), reg: new RegExp(/123/)};
let b = deepClone(a);
a.msg.name = "lily";
a.date = new Date("2020-08-08");
a.reg = new RegExp(/456/);
console.log(b);
// { name: 'a', msg: { name: 'lihb' }, date: 2020-09-17T00:00:00.000Z, reg: /123/ }

因为须要进行递归拷贝,所以对于非对象类型的数据间接返回原值即可。对于Date类型的值,则间接传入以后值new一个Date对象即可,对于RegExp对象的值,也是间接传入以后值new一个RegExp对象即可。对于数组类型,遍历数组的每一项并进行递归拷贝即可。对于对象,同样遍历对象的所有key值,同时对其值进行递归拷贝即可。对于对象还须要思考属性值为Symbol的类型,因为Symbol类型的key无奈间接通过Object.keys()枚举到

三、互相援用问题

下面的深拷贝实现看上去很欠缺,然而还有一种状况未思考到,那就是对象互相援用的状况,这种状况将会导致递归无奈完结

const a = {name: "a"};
const b = {name: "b"};
a.b = b;
b.a = a; // 互相援用
console.log(a); // { name: 'a', b: { name: 'b', a: [Circular] } }

对于下面这种状况,咱们须要怎么拷贝互相援用后的a对象呢?
咱们也是依照下面的形式进行递归拷贝:

// ① 创立一个空的对象,示意对a对象的拷贝后果
const aClone = {};
// ② 遍历a中的属性,name和b, 首先拷贝name属性和b属性
aClone.name = a.name;
// ③ 接着拷贝b属性,而b的属性值为b对象,须要进行递归拷贝,同时蕴含name和a属性,先拷贝name属性
const bClone = {};
bClone.name = b.name;
// ④ 接着拷贝a属性,而a的属性值为a对象,咱们须要将之前a的拷贝对象aClone赋值即可
bClone.a = aClone;
// ⑤ 此时bClone曾经拷贝实现,再将bClone赋值给aClone的b属性即可
aClone.b = bClone;
console.log(aClone); // { name: 'a', b: { name: 'b', a: [Circular] } }

其中最要害的就是第④步,这里就是完结递归的要害,咱们是拿到了a的拷贝后果进行了赋值,所以咱们须要记录下某个对象的拷贝后果,如果之前曾经拷贝过,那么咱们间接拿到拷贝后果赋值即可实现互相援用
而JS提供了一种WeakMap数据结构,其只能用对象作为key值进行存储,咱们能够用拷贝前的对象作为key拷贝后的后果对象作为value,当呈现互相援用关系的时候,咱们只须要从WeakMap对象中取出之前曾经拷贝的后果对象赋值即可造成互相援用关系。

function deepClone(source, map = new WeakMap()) { // 传入一个WeakMap对象用于记录拷贝前和拷贝后的映射关系
    if (typeof source !== "object") { // 非对象类型(undefined、boolean、number、string、symbol),间接返回原值即可
        return source;
    }
    if (source === null) { // 为null类型的时候
        return source;
    }
    if (source instanceof Date) { // Date类型
        return new Date(source);
    }
    if (source instanceof RegExp) { // RegExp正则类型
        return new RegExp(source);
    }
    if (map.get(source)) { // 如果存在互相援用,则从map中取出之前拷贝的后果对象并返回以便造成互相援用关系
        return map.get(source);
    }
    let result;
    if (Array.isArray(source)) { // 数组
        result = [];
        map.set(source, result); // 数组也会存在互相援用
        source.forEach((item) => {
            result.push(deepClone(item, map));
        });
        return result;
    } else { // 为对象的时候
        result = {};
        map.set(source, result); // 保留已拷贝的对象
        const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]; // 取出对象的key以及symbol类型的key
        keys.forEach(key => {
            let item = source[key];
            result[key] = deepClone(item, map);
        });
        return result;
    }
}

至此曾经实现了一个绝对比较完善的深拷贝。

四、WeakMap(补充)

WeakMap有一个特点就是属性值只能是对象,而Map的属性值则无限度,能够是任何类型。从其名字能够看出,WeakMap是一种弱援用,所以不会造成内存透露。接下来咱们就是要弄清楚为什么其是弱援用。

咱们首先看看WeakMap的polyfill实现,如下:

var WeakMap = function() {
    this.name = '__wm__' + uuid();
};
WeakMap.prototype = {
    set: function(key, value) { // 这里的key是一个对象,并且是局部变量
        Object.defineProperty(key, this.name, { // 给传入的对象上增加一个this.name属性,值为要保留的后果
            value: [key, value],
        });
        return this;
    },
    get: function(key) {
        var entry = key[this.name];
        return entry && (entry[0] === key ? entry[1] : undefined);
    }
};

从WeakMap的实现上咱们能够看到,WeakMap并没有间接援用传入的对象,当咱们调用WeakMap对象set()办法的时候,会传入一个对象,而后在传入的对象上增加一个this.name属性,值为一个数组,第一项为传入的对象,第二项为设置的值,当set办法调用完结后局部变量key被开释,所以WeakMap并没有间接援用传入的对象,即弱援用。

其执行过程等价于上面的办法调用:

var obj = {name: "lihb"};

function set(key, value) {
    var k = "this.name"; // 这里模仿this.name的值作为key
    key[k] = [key, value];
}
set(obj, "test"); // 这里模仿WeakMap的set()办法
obj = null; // obj将会被垃圾回收器回收

所以set的作用就是给传入的对象设置了一个属性而已,不存在被谁援用的关系

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理