返回

数据的世界:揭秘手写深拷贝的艺术

前端

手写深拷贝的艺术

在着手写代码之前我们须先知道什么是深拷贝与前拷贝以前二者的区别是什么。 在JS中,数据类型分为基本数据类型和引用数据类型两种,对于基本数据类型来说,它的值直接存储在栈内存中,而对于引用类型来说,它的值存储在堆内存中,它本身在栈内存中存储的是这个值的地址。基本数据类型包括:字符串,数字,布尔值,Undefined,Null;引用数据类型包括:对象,数组,函数。当我们对基本数据类型赋值时,它是直接对这个值进行赋值。当我们对引用数据类型赋值时,是将这个引用数据类型的地址进行赋值。

浅拷贝,是按引用传递的方式,将原对象的地址传给新对象,新旧两个对象共用一个地址空间,修改新对象等价于修改源对象。深拷贝,是按值传递的方式,将原对象的值重新拷贝一份传给新对象,新旧对象互不影响。

了解完二者的区别,我们接下来要实现的就是如何通过纯JavaScript代码实现深拷贝功能。我们先从简单的数据类型开始,再到复杂的数据类型。

基本数据类型

对于基本数据类型,我们只需简单地将其值赋给新变量即可。例如:

let x = 10;
let y = x;

x++;

console.log(x); // 11
console.log(y); // 10

在这个示例中,我们创建了一个变量x并将其值设置为10。然后,我们创建了另一个变量y并将其值设置为x。当我们对x进行自增操作时,它的值变为11。但是,y的值保持不变,因为它是x的副本,而不是x的引用。

引用数据类型

对于引用数据类型,我们需要使用不同的方法来创建副本。一种方法是使用Object.assign()方法。该方法接受两个参数:要复制的对象和目标对象。它将源对象的所有属性复制到目标对象。例如:

let person = {
  name: 'John Doe',
  age: 30
};

let person2 = Object.assign({}, person);

person2.name = 'Jane Doe';

console.log(person.name); // John Doe
console.log(person2.name); // Jane Doe

在这个示例中,我们创建了一个对象person并设置了一些属性。然后,我们使用Object.assign()方法将person的所有属性复制到一个新的对象person2中。当我们修改person2的name属性时,它的值变为Jane Doe。但是,person的name属性保持不变,因为person2是person的副本,而不是person的引用。

另一种创建引用数据类型副本的方法是使用JSON.parse()和JSON.stringify()方法。 这两种方法可以将对象转换为JSON字符串,然后将其解析回对象。例如:

let person = {
  name: 'John Doe',
  age: 30
};

let person2 = JSON.parse(JSON.stringify(person));

person2.name = 'Jane Doe';

console.log(person.name); // John Doe
console.log(person2.name); // Jane Doe

在这个示例中,我们创建了一个对象person并设置了一些属性。然后,我们使用JSON.stringify()方法将person转换为JSON字符串。接下来,我们使用JSON.parse()方法将JSON字符串解析回对象person2。当我们修改person2的name属性时,它的值变为Jane Doe。但是,person的name属性保持不变,因为person2是person的副本,而不是person的引用。

数组

数组也是一种引用数据类型。我们可以使用多种方法来创建数组的副本。一种方法是使用Array.slice()方法。该方法接受一个参数:要复制的数组。它将返回一个新数组,该数组包含源数组的所有元素。例如:

let arr = [1, 2, 3];

let arr2 = arr.slice();

arr2.push(4);

console.log(arr); // [1, 2, 3]
console.log(arr2); // [1, 2, 3, 4]

在这个示例中,我们创建了一个数组arr并设置了一些元素。然后,我们使用Array.slice()方法将arr的所有元素复制到一个新数组arr2中。当我们向arr2中添加一个新元素时,它的长度变为4。但是,arr的长度保持不变,因为arr2是arr的副本,而不是arr的引用。

另一种创建数组副本的方法是使用扩展运算符(...)。 扩展运算符可以将数组展开成单个元素。例如:

let arr = [1, 2, 3];

let arr2 = [...arr];

arr2.push(4);

console.log(arr); // [1, 2, 3]
console.log(arr2); // [1, 2, 3, 4]

在这个示例中,我们创建了一个数组arr并设置了一些元素。然后,我们使用扩展运算符将arr的所有元素展开成单个元素,并将其赋给一个新数组arr2。当我们向arr2中添加一个新元素时,它的长度变为4。但是,arr的长度保持不变,因为arr2是arr的副本,而不是arr的引用。

对象

对象也是一种引用数据类型。我们可以使用多种方法来创建对象的副本。一种方法是使用Object.assign()方法。该方法接受两个参数:要复制的对象和目标对象。它将源对象的所有属性复制到目标对象。例如:

let person = {
  name: 'John Doe',
  age: 30
};

let person2 = Object.assign({}, person);

person2.name = 'Jane Doe';

console.log(person.name); // John Doe
console.log(person2.name); // Jane Doe

在这个示例中,我们创建了一个对象person并设置了一些属性。然后,我们使用Object.assign()方法将person的所有属性复制到一个新对象person2中。当我们修改person2的name属性时,它的值变为Jane Doe。但是,person的name属性保持不变,因为person2是person的副本,而不是person的引用。

另一种创建对象副本的方法是使用JSON.parse()和JSON.stringify()方法。 这两种方法可以将对象转换为JSON字符串,然后将其解析回对象。例如:

let person = {
  name: 'John Doe',
  age: 30
};

let person2 = JSON.parse(JSON.stringify(person));

person2.name = 'Jane Doe';

console.log(person.name); // John Doe
console.log(person2.name); // Jane Doe

在这个示例中,我们创建了一个对象person并设置了一些属性。然后,我们使用JSON.stringify()方法将person转换为JSON字符串。接下来,我们使用JSON.parse()方法将JSON字符串解析回对象person2。当我们修改person2的name属性时,它的值变为Jane Doe。但是,person的name属性保持不变,因为person2是person的副本,而不是person的引用。

函数

函数也是一种引用数据类型。我们可以使用多种方法来创建函数的副本。一种方法是使用Function.prototype.bind()方法。该方法接受一个参数:要绑定的函数。它返回一个新函数,该函数具有与源函数相同的代码,但其this值被绑定到指定的this值。例如:

function greet() {
  console.log(this.name);
}

let person = {
  name: 'John Doe'
};

let greetJohn = greet.bind(person);

greetJohn(); // John Doe

在这个示例中,我们创建了一个函数greet并设置了一些代码。然后,我们创建了一个对象person并设置了一些属性。接下来,我们使用Function.prototype.bind()方法将greet绑定到person对象。当我们调用greetJohn时,它将输出John Doe,因为this值被绑定到person对象。

另一种创建函数副本的方法是使用箭头函数。 箭头函数是匿名函数,它没有自己的this值。因此,它总是使用其父函数的this值。例如:

function greet() {
  console.log(this.name);
}

let person = {
  name: 'John Doe'
};

let greetJohn = () => {
  console.log(this.name);
};

greetJohn(); // John Doe