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

赋值、浅拷贝、深拷贝区别 #119

Open
funnycoderstar opened this issue Mar 22, 2020 · 0 comments
Open

赋值、浅拷贝、深拷贝区别 #119

funnycoderstar opened this issue Mar 22, 2020 · 0 comments

Comments

@funnycoderstar
Copy link
Owner

funnycoderstar commented Mar 22, 2020

数据类型存储

基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。

为什么基本数据类型存在栈内存,引用数据类型存在堆内存?

  • 基本数据类型比较稳当,相对来说占用的内存较小
  • 引用数据类型是动态的,大小不固定,占用的内存较大,但内存地址大小是固定的,因此可以将内存地址保存在栈中

浅拷贝和赋值(=)的区别

基本类型赋值,系统会为新的变量在栈内存中分配一个新值,这个很好理解。
引用类型赋值,系统会为新的变量在栈内存中分配一个值,这个值仅仅是指向同一个对象的引用,和原对象指向的都是堆内存中的同一个对象。

var obj1 = new Object();
var obj2 = obj1;
obj1.name = 'lucyStar';
console.log(obj2.name);
// lucyStar

我们可以看到,obj1保存了一个对象的实例,这个值被赋值到 Obj2中。赋值操作完成后,两个变量实际引用的是同一个对象,改变了其中一个,会影响另外一个值。

什么是浅拷贝?
如果是对象类型,则只拷贝一层,如果对象的属性又是一个对象,那么此时拷贝的就是此属性的引用。

简单实现一个浅拷贝

function shadowCopy(obj) {
    const newObj = {};
    for(let prop in obj) {
        if(obj.hasOwnProperty(prop)){
            newObj[prop] = obj[prop];
        }
    }
    return newObj;
}
const obj1 = {
    name: 'litterStar',
    a: {
        b: '1'
    }
};
const obj2 = shadowCopy(obj1);
obj2.name = 'lucyStar';
obj2.a.b = '2';
console.log(obj1);
// { name: 'litterStar', a: { b: '2' } }
console.log(obj2);
// { name: 'lucyStar', a: { b: '2' } }

可以看到修改obj2name 属性不会影响 obj1,但是修改 obj2的 a属性(是个对象)的 b,就会影响 obj1.a.b

使用下面这些函数得到的都是浅拷贝:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

深拷贝

什么是深拷贝?
浅拷贝是只拷贝一层,深拷贝会拷贝所有的属性。深拷贝前后两个对象互不影响。

深拷贝的实现

  • JSON.parse(JSON.stringify())
  • 手写递归函数
  • 函数库lodash

JSON.parse(JSON.stringify())有存在以下问题:

  • 无法解决循环引用问题
  • 无法拷贝特殊的对象,比如:RegExp, Date, Set, Map等在序列化的时候会丢失。
  • 无法拷贝函数
let obj = {
    fun: function(){},
    syb: Symbol('foo'),
    a: undefined,
    b: NaN,
    c: Infinity,
    reg : /^abc$/,
    date: new Date(),
    set: new Set([1, 2, 3, 4, 4]),
    map: new Map([
        ['name', '张三'],
        ['title', 'Author']
      ]),
    text:'aaa',
}
let cloneObj = JSON.parse(JSON.stringify(obj));
console.log(cloneObj);
// 结果如下
{ reg: {},
  b: null,
  c: null,
  date: '2020-05-05T09:32:52.533Z',
  set: {},
  map: {},
  text: 'aaa' 
}
console.log(typeof obj.date); // object
console.log(typeof cloneObj.date); // string

可以看到

  1. 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
  2. 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
  3. 如果obj里有RegExp、 Set, Map等,则序列化的结果将只得到空对象;
  4. 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
  5. 循环引用,则直接报错 TypeError: Converting circular structure to JSON
const obj = {
    a: '111',
}
obj.b = obj;

let cloneObj = JSON.parse(JSON.stringify(obj));
console.log(cloneObj); // TypeError: Converting circular structure to JSON

尝试自己写一个深拷贝,需要考虑下面这几种情况

  1. 属性是基本类型
  2. 属性是对象
  3. 属性是数组
  4. 循环引用的情况,比如 obj.prop1 = obj
function deepCopy(originObj, map = new WeakMap()) {
    // 判断是否为基本数据类型
    if(typeof originObj === 'object') {
        // 判断是都否为数组
        const cloneObj = Array.isArray(originObj) ? [] : {};
        // 判断是否为循环引用
        if(map.get(originObj)) {
            return map.get(originObj);
        }
        map.set(originObj, cloneObj);
        for(const prop in originObj) {
            cloneObj[prop] = deepCopy(originObj[prop], map);
        }
        return cloneObj;
    } else {
        return originObj;
    }
}

const obj1 = {
    a: '111',
}
obj1.obj2 = obj1;

const aa = deepCopy(obj1);
console.log(aa);
// { a: '111', obj2: [Circular] }

上面的实现存在一些问题

  1. 一些特殊类型的对象,比如 Date, 正则,Set,Map等没有处理
  2. 使用typeof 来判断是否是对象是有问题的,typeof null 的结果也是 'object'

下面这一版本

function deepCopy(originObj, map = new WeakMap()) {
    // 判断是否为基本数据类型
    if(isObject(originObj)) {
        // 判断是否为循环引用
        if(map.get(originObj)) {
            return map.get(originObj);
        }
       
        // 判断是否为几种特殊需要处理的类型
        let type = [Date, RegExp, Set, Map, WeakMap, WeakSet];
        if(type.includes(originObj.constructor)) {
            return new originObj.constructor(originObj);
        }
        // 其他类型
        let allDesc = Object.getOwnPropertyDescriptors(originObj);
        let cloneObj = Object.create(Object.getPrototypeOf(originObj), allDesc);

        // Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
        for(const prop of Reflect.ownKeys(originObj)) {
            cloneObj[prop] = isObject(originObj[prop]) && typeof originObj[prop] !== 'function' ? deepCopy(originObj[prop], map) : originObj[prop];
        }
        return cloneObj;
    } else {
        return originObj;
    }
}

// 是否为引用类型
function isObject(obj) {
    return typeof obj === 'object' || typeof obj === 'function' && obj !== null;
}

let obj = {
    fun: function(){},
    syb: Symbol('foo'),
    a: undefined,
    b: NaN,
    c: Infinity,
    reg : /^abc$/,
    date: new Date(),
    set: new Set([1, 2, 3, 4, 4]),
    map: new Map([
        ['name', '张三'],
        ['title', 'Author']
      ]),
    text:'aaa',
}
let cloneObj = deepCopy(obj);
console.log(cloneObj);

打印的结果如下

{ fun: [Function: fun],
  syb: Symbol(foo),
  a: undefined,
  b: NaN,
  c: Infinity,
  reg: /^abc$/,
  date: 2020-05-05T13:57:14.904Z,
  set: Set { 1, 2, 3, 4 },
  map: Map { 'name' => '张三', 'title' => 'Author' },
  text: 'aaa' 
}

更多细节的实现可以参考 lodash 中的 cloneDeep方法。

总结

和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含对象
赋值 改变会使原数据一同改变 改变会使原数据一同改变
浅拷贝 改变不会使原数据一同改变 改变会使原数据一同改变
深拷贝 改变不会使原数据一同改变 改变不会使原数据一同改变

备注:markdown 表格在 https://tableconvert.com/ 该网站生成,很方便。

参考

@funnycoderstar funnycoderstar changed the title 赋值、深拷贝、浅拷贝 赋值、浅拷贝、深拷贝区别 Mar 23, 2020
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