《深拷贝与浅拷贝的实现》


2020-05-28 上次更新时间:4/29/2022, 9:34:08 AM 0 javascript

可先了解以下知识:

javascript 数据类型介绍

# 变量的存储

# 基本类型存储

  • 变量名和值都直接存储在栈内存中
  • 值与值之间独立存在,修改一个变量不会影响其他变量
var a = 20;
var b = a;
a++;
console.log(a, b) // 21 20
1
2
3
4

# 引用类型存储

  • 变量名保存在栈中,对应的值是对象的内存地址(指针)
  • 堆内存中才保存了真正的值

当改变一个变量的值时,另一个变量会跟着改变

var obj1 = new Object();
obj1.name = "mike";
var obj2 = obj1;
obj2.name = "jack";
console.log(obj1.name); //jack
1
2
3
4
5

清空一个变量的值时,只是断开该变量与对象的联系,另一个对象并不受影响

var obj1 = new Object();
obj1.name = "mike";
var obj2 = obj1;
obj2 = null;
console.log(obj1); // {name: "mike"}
console.log(obj2); // null
1
2
3
4
5
6

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

  • 堆比栈大,栈比堆速度快;
  • 基本数据类型比较稳定,而且相对来说占用的内存小;
  • 引用数据类型大小是动态的,而且是无限的,引用值的大小会改变,如果存放在栈中,会降低变量查找的速度,因此放在栈空间的值是该对象存储在堆中的地址,通过地址再去堆中查找真正的值;
  • 堆内存是无序存储,可以根据引用直接获取;

# 数据比较

  • 当比较两个基本数据类型的值时,就是比较值;
  • 当比较两个引用数据类型时,比较的是对象的内存地址;
var a = 10;
var b = 10;
console.log(a == b); // true => 值相等,为true
var obj1 = new Object();
var obj2 = new Object();
var obj3 = obj1;
console.log(obj1 == obj2);// false => 两个对象,引用地址不同
console.log(obj1 == obj3);// true => 引用地址相同
1
2
3
4
5
6
7
8

# 浅拷贝

从上面的介绍中可以知道,当改变一个引用类型变量的值时候,另一个变量值也会跟着改变。这是因为使用 = 赋值的时候,属于浅拷贝。

浅拷贝: 只拷贝对象的第一层属性,也就是只拷贝了其引用(指针),拷贝对象的改变会反应到原对象上。

# = 赋值

var obj1 = {
    a: 1,
    b: 2,
    c: {
        c1: 3,
        c2: 4
    }
};
var obj2 = obj1;
obj2.a = 100;
obj2.c.c1 = 300;
console.log(obj1.a, obj1.c.c1); // 100 300
console.log(obj2.a, obj2.c.c1);// 100 300
1
2
3
4
5
6
7
8
9
10
11
12
13

# Object.assign()

Object.assign() 可以实现第一层基本类型属性的深拷贝,其他更深层属于浅拷贝。

var obj1 = {
    a: 1,
    b: 2,
    c: {
        c1: 3,
        c2: 4
    }
};
var obj2 = Object.assign({},obj1); // 需传{},否则第一层也属于浅拷贝
obj2.a = 100;
obj2.c.c1 = 300;
console.log(obj1.a, obj1.c.c1); // 1 300
console.log(obj2.a, obj2.c.c1); // 100 300

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 手动实现浅拷贝

只循环第一层,实现第一层的深拷贝

function simpleCopy(obj1) {
   var obj2 = Array.isArray(obj1) ? [] : {};
   for (let i in obj1) {
   obj2[i] = obj1[i];
  }
   return obj2;
}
var obj1 = {
    a: 1,
    b: 2,
    c: {
        c1: 3,
        c2: 4
    }
};
var obj2 = simpleCopy(obj1);
obj2.a[0] = 100;
obj2.c.c1 = 300;
console.log(obj1.a, obj1.c.c1); // 1 300
console.log(obj2.a, obj2.c.c1); // 100 300
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 深拷贝

深拷贝:拷贝多层,每一级别的数据都会拷贝出来,拷贝对象的改变不会影响到原对象。

# 使用 JSON 对象方法

缺点:

  • 会抛弃对象的 constructor,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object;
  • 只有可以转成 JSON 格式的对象才可以这样用,像 function 就没办法转成 JSON;
var obj1 = {
    a: 1,
    b: ['a', 'b', 'c'],
    c: {
        c1: {
            x: 1
        }
    }
};
var obj2 = JSON.parse(JSON.stringify(obj1))
obj2.c.c1.x = 300;
console.log(obj1.c.c1.x); // 1
console.log(obj2.c.c1.x); // 300

// function 无法转换
var obj3 = {
    fun: function() {
        alert(333)
    }
}
var obj4 = JSON.parse(JSON.stringify(obj3))
console.log(typeof obj3.fun); // function
console.log(typeof obj4.fun); // undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# Object.assign()

  • 当基本数据类型时,比如String,Number,Boolean时,属于深拷贝
  • 当引用数据类型时,比如Object,Array时,属于浅拷贝
var obj1 = {
    a: 1,
    b: 2
};
var obj2 = Object.assign({},obj1); // 需要赋值给一个 {}
obj2.a = 100;
console.log(obj1.a) // 1
console.log(obj2.a) // 100
1
2
3
4
5
6
7
8

# 使用 Reflect

Reflect是ES6为操作对象而提供的新API。 Reflect介绍

function deepClone(obj) {
    if (!isObject(obj)) {
        throw new Error('obj 不是一个对象!')
    }

    let isArray = Array.isArray(obj)
    let cloneObj = isArray ? [...obj] : { ...obj }
    Reflect.ownKeys(cloneObj).forEach(key => {
        cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    })

    return cloneObj
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Object.create()

创建一个新对象

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

# 使用扩展运算符

  • 当基本数据类型时,比如String,Number,Boolean时,属于深拷贝
  • 当引用数据类型时,比如Object,Array时,属于浅拷贝

var car = {brand: "BMW", price: "380000", length: "5米"}
var car1 = { ...car, price: "500000" }
console.log(car1); // { brand: "BMW", price: "500000", length: "5米" }
console.log(car); // { brand: "BMW", price: "380000", length: "5米" }
1
2
3
4
5

# slice实现对数组的深拷贝

  • 当基本数据类型时,比如String,Number,Boolean时,属于深拷贝
  • 当引用数据类型时,比如Object,Array时,属于浅拷贝
var arr1 = ["1","2","3"]; 
var arr2 = arr1.slice(0);
arr2[1] = "9";
console.log("数组的原始值:" + arr1 );
console.log("数组的新值:" + arr2 );
1
2
3
4
5

# concat实现对数组的深拷贝

  • 当基本数据类型时,比如String,Number,Boolean时,属于深拷贝
  • 当引用数据类型时,比如Object,Array时,属于浅拷贝
// 当数组里面的值是基本数据类型,比如String,Number,Boolean时,属于深拷贝
var arr1 = ["1","2","3"];
var arr2 = arr1.concat();
arr2[1] = "9";
console.log("数组的原始值:" + arr1 );
console.log("数组的新值:" + arr2 );

// 当数组里面的值是引用数据类型,比如Object,Array时,属于浅拷贝
var arr1 = [{a:1},{b:2},{c:3}];
var arr2 = arr1.concat();
arr2[0].a = "9";
console.log("数组的原始值:" + arr1[0].a ); // 数组的原始值:9
console.log("数组的新值:" + arr2[0].a ); // 数组的新值:9
1
2
3
4
5
6
7
8
9
10
11
12
13

# lodash.js

使用工具库 lodash

var _ = require('lodash');

var obj1 = {
    a: 1,
    b: ['a', 'b', 'c'],
    c: {
        c1: {
            x: 1
        }
    }
};

var obj2 = _.cloneDeep(obj1);
console.log(obj1.c.c1.x === obj2.c.c1.x); // false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# $.extend

jquery 提供一个$.extend 方法实现深拷贝;

var $ = require('jquery');
var obj1 = {
    a: 1,
    b: ['a', 'b', 'c'],
    c: {
        c1: {
            x: 1
        }
    }
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.c.c1.x === obj2.c.c1.x);  // false
1
2
3
4
5
6
7
8
9
10
11
12

# 深度遍历优先拷贝


1

# 广度遍历优先拷贝

参考文章:

上次更新时间: 4/29/2022, 9:34:08 AM