1. 浅拷贝与深拷贝_js版

基础储备

1. 数据类型与数据结构

JavaScript数据类型与数据结构

关于null和undefined:

有一个小故事:

1995年JavaScript诞生时,最初像Java一样,只设置了null作为表示”无”的值。根据C语言的传统,null被设计成可以自动转为0。但是,JavaScript的设计者,觉得这样做还不够,主要有以下两个原因:

  1. null像在Java里一样,被当成一个对象。但是,JavaScript的数据类型分成原始类型(primitive)和合成类型(complex)两大类,作者觉得表示”无”的值最好不是对象。

  2. JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果null自动转为0,很不容易发现错误。

因此,作者又设计了一个undefined。
先有null后有undefined,出来undefined是为了填补之前的坑。

  • null是一个表示”无”的对象(空对象指针),转为数值时为0;

  • undefined是一个表示”无”的原始值,转为数值时为NaN;

1
2
3
//null的类型是对象,undefined的类型是undefined
null === undefined // false
typeof null == typeof undefined // false

2. 关于修改栈与堆

栈

堆

图中,obj1和obj2都指向同一个object,修改obj2==修改了引用地址==修改了那个对象

3. 堆与栈比较

1
2
3
4
5
6
7
8
9
10
var a = [1,2,3,4,5]; //a是array,属于引用类型
var b = a; //传址
var c = a[0];//传值
console.log(b);//1,2,3,4,5
alert(c);//1
//改变数值
b[4] = 6;
c = 7;
console.log(a[4]);//6,改变b把a也改变了
console.log(a[0]);//1,改变c不会把a改变

由上可知:

  • 对象中传给变量的数据是引用类型的,会存储在堆中;

  • 把对象中的属性/数组中的数组项赋值给变量,这时变量c是基本数据类型,存储在栈内存中;

  • 改变栈中的数据不会影响堆中的数据;

3. 区别

1. 声明时内存分配不同

  • 基本数据类型: 栈,便于迅速查询变量的值

  • 引用类型: 堆,栈中存储的变量只是引用地址(指针)

    引用类型的大小会改变,如果放在栈中,会降低其变量查询的速度。放在堆中,地址的大小是固定的,将地址存储在栈中对变量的性能没有任何负面影响

2. 不同的内存分配带来的不同访问机制

  • 基本数据类型:直接访问
  • 引用类型:通过地址去获得object的值

3. 复制变量时不同

  • 基本数据类型: 复制之后完全独立,各自修改互不影响

  • 引用类型: 复制的是引用地址

    此时两者都指向同一个对象,修改会一起修改(复制并不会在堆内存中新生成一个一模一样的对象,只是复制多一个保存指向这个对象指针的变量)

    复制的是引用地址

4. 参数传递不同

即把实参复制给形参的过程

正文

1. 介绍

浅拷贝

复制的标识堆内存中的数据,而是指向堆中的栈内存的指针,即复制的是引用地址

只复制一层对象的属性,不包括对象里面的引用类型的数据

只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存

深拷贝

递归复制了所有层级,程度深

复制并创建一个一摸一样的对象,不共享内存,修改新对象,旧对象保持不变

2. 浅拷贝的实现

  1. 直接赋值

  2. 使用object.assign()

    1
    Object.assign(target, ...sources)
    1
    2
    3
    4
    5
    var obj = { a: {a: "hello", b: 21} };
    var newObj = Object.assign({}, obj);

    newObj.a.a = "changed";
    console.log(obj.a.a); // "changed"

    但可以处理一层深度拷贝

    1
    2
    3
    4
    5
    var obj1 = { a: 10, b: 20, c: 30 };
    var obj2 = Object.assign({}, obj1);
    obj2.b = 100;
    console.log(obj1); // { a: 10, b: 20, c: 30 }
    console.log(obj2); // { a: 10, b: 100, c: 30 }

3. 深拷贝的实现

  1. 手动复制

    1
    2
    3
    4
    5
    var obj1 = { a: 10, b: 20, c: 30 };
    var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
    obj2.b = 100;
    console.log(obj1); // { a: 10, b: 20, c: 30 }
    console.log(obj2); // { a: 10, b: 100, c: 30 }
  2. JSON做字符转换

    使用JSON.stringify()把对象转成字符串,再用JSON.parse()转换为新的对象

    1
    2
    3
    4
    5
    var obj1 = { body: { a: 10 } };
    var obj2 = JSON.parse(JSON.stringify(obj1));
    obj2.body.a = 20;
    console.log(obj1); // { body: { a: 10 } }
    console.log(obj2); // { body: { a: 20 } }

    缺点

    抛弃对象的constructor => 深拷贝之后,会变成Object

    故一般是Number,String,BOOlean,Array这些扁平对象去使用(可以被json直接表示的数据结构)

    而RegExp、Function、Symbol等等不能转换成json的是无法通过这种方式进行深拷贝

  3. 递归拷贝

    实现原理: 定义一个新的对象,遍历源对象的属性 并 赋给新对象的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function deepClone(Obj, newObj) {    
    var obj = newObj || {};
    for (var i in Obj) {
    var prop = Obj[i]; // 避免相互引用对象导致死循环,如Obj.a = Obj的情况
    if(prop === obj) {
    continue;
    }
    if (typeof prop === 'object') {
    obj[i] = (prop.constructor === Array) ? [] : {}; // array || string
    arguments.callee(prop, obj[i]);
    } else {
    obj[i] = prop;
    }
    }
    return obj;
    }

    var str = {};
    var obj = { a: {a: "hello", b: 21} };
    deepClone(obj, str);
    console.log(str.a); // {a: "hello", b: 21}
  4. 使用Object.create()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function deepClone(Obj, newObj) {    // 大致实现和递归差不多
    var obj = newObj || {};
    for (var i in Obj) {
    var prop = Obj[i];
    if(prop === obj) {
    continue;
    }
    if (typeof prop === 'object') {
    obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
    } else {
    obj[i] = prop;
    }
    }
    return obj;
    }
  5. 第三方函数

    直接使用一些函数库来做到深拷贝

4. 特殊

对一维数组,可以实现深拷贝,例如:

  • 扩展运算符

  • concat()slice()

    1
    2
    3
    4
    5
    var arr = [1,2,3,4,5]
    var [ ...arr2 ] = arr
    arr[2] = 5
    console.log(arr) //[1,2,5,4,5]
    console.log(arr2) //[1,2,3,4,5]
    1
    2
    3
    4
    5
    var arr = ['a', 'b', 'c'];
    var arrCopy = arr.concat();
    arrCopy[0] = 'test'
    console.log(arr); // ["a", "b", "c"]
    console.log(arrCopy); // ["test", "b", "c"]
    1
    2
    3
    4
    5
    var arr = ['a', 'b', 'c'];
    var arrCopy = arr.slice(0);
    arrCopy[0] = 'test'
    console.log(arr); // ["a", "b", "c"]
    console.log(arrCopy); // ["test", "b", "c"]

    如果是多层的话,除了第一层其余的都只是进行浅拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const time = {
    year: 2021,
    month: 7,
    day: { value: 1 },
    };
    const copyTime = { ...time };
    copyTime.day.value = 2;
    copyTime.month = 6;
    console.log(copyTime); // { year: 2021, month: 6, day: { value: 2 } }
    console.log(time); // { year: 2021, month: 7, day: { value: 2 } }
    //第一层的没被改变,而第二层的就被改变了

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!