深入理解JavaScript引用类型的深拷贝、浅拷贝和按值传参

对C++熟悉的同学肯定很熟悉:值和引用。比如按值传参和按引用传参,按值返回和按引用返回。那在javascript里边,变量复制以及传参时,又会是神马情况呢?不同语言之间,有些基本概念的区别还是需要细细品味的。

首先明确JavaScript(ECMAScript)中的基本概念:
变量包括两种:基本类型和引用类型。
基本类型:Undefined,Null,Boolean,Number,String
引用类型:Object,Array,Data,RegExp,Function

在 JavaScript 中, 引用类型是一种数据结构,用于将数据和功能组织在一起。它也常被称为类。(实际上和C++的中的类不同,叫做类并不妥当,只是说和类相似)。引用类型的值(即对象)是引用类型的一个实例。

注意:
1、引用类型的值是保存在内存中的对象。与C++不同, JavaScript 没有指针,不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。
2、在很多语言中,字符串以对象的形式来表示,因此被认为是引用类型的。JavaScript 放弃了这一传统。String是基本类型。

1、基本类型的变量复制/访问

如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上,新值是和原值是完全独立的一个副本,两者无任何关系

var num1 = 5;
var num2 = num1;

在此, num1 中保存的值是 5。当使用 num1 的值来初始化 num2 时, num2 中也保存了值 5。但 num2中的 5 与 num1 中的 5 是完全独立的,该值只是 num1 中 5 的一个副本。此后,这两个变量可以参与任
何操作而不会相互影响。

2、引用类型的变量复制/访问

当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,改变其中一个变量,就会影响另一个变量。也就是我们说的浅拷贝。

var obj1 = new Object();
var obj2 = obj1;
obj1.name = "Nicholas";
alert(obj2.name);     //结果为"Nicholas"

首先,变量 obj1 保存了一个对象的新实例。然后,这个值被复制到了 obj2 中;换句话说, obj1
和 obj2 ,相当于指针,都指向内存中同一个对象。这样,当为 obj1 添加 name 属性后,可以通过 obj2 来访问这个属性,因为这两个变量引用的都是同一个对象。
在这里插入图片描述

3、基本类型和引用类型传参:均为按值传参

ECMAScript 中所有函数的参数都是按值传递的。

也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。有不少开发人员在这一点上可能会感到困惑,因为访问变量有按值和按引用两种方式,而参数只能按值传递。
而且熟悉C++的同学会糊涂,为什么引用类型在传参时是按值传参呢?
我的理解,这里的按值传参的含义是,在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量。这个复制的过程就像上边所讲过的一样,产生的局部变量实际上是相当于指向原对象内存的指针。所以即使引用类型是按值传递,但是实际上这个局部变量的变化会反映在函数的外部。

function setName(obj) {
    obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name);     //函数外部的实参person属性也变为了"Nicholas"

以上代码中创建一个对象,并将其保存在了变量 person 中。然后,这个变量被传递到 setName()
函数中之后就被复制给了 obj。在这个函数内部, obj 和 person 引用的是同一个对象。换句话说,即
使这个变量是按值传递的, obj 也会按引用来访问同一个对象。于是,当在函数内部为 obj 添加 name
属性后,函数外部的 person 也将有所反映;因为 person 指向的对象在堆内存中只有一个,而且是全
局对象。

扫描二维码关注公众号,回复: 10558256 查看本文章

有很多开发人员错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,这不就说明
参数是按引用传递的嘛,不是跟C++的按引用传参一样吗?为了证明引用对象是按值传递的,我们再看一看下面这个经过修改的例子:

function setName(obj) {
    obj.name = "Nicholas";    //见3
    obj = new Object();       //见4
    obj.name = "Greg";        //见5
}                             //见6

var person = new Object();    //见1
setName(person);              //见2
alert(person.name);     //并没有成为"Greg",而是仍旧为"Nicholas"

如果 person 是按引用传递的,那么obj的name变为"Greg"时,person应该也会变成新new出来的对象并且name为"Greg"。但是 person.name 时实际上仍然是"Nicholas"。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这个变量引用的就是新new出来的局部对象。而这个局部对象会在函数执行完毕后立即被销毁。

详细过程分析如下:
1、person引用到内存中new出来的一个对象。
2、经过引用类型的按值传参之后,生成了副本obj(实际上是指针),可以想象它是函数中的一个局部变量,跟person一样引用到了内存中的同一个对象。
3、修改obj对象属性之后,内存中这个对象属性变了,person和obj引用的都是这个对象,属性当然也会跟着变。
4、新new出来一个对象之后赋给obj,导致obj从此以后不引用原来内存的对象,而是引用到了新的对象。
5、obj与person分别指向了不同的两个对象,那么修改obj当然并不会影响person了。
6、函数执行完毕后,局部变量obj自动销毁。

讲到这里,应该能明白这里说的引用类型的按值传递是怎么回事了吧。回想一下C++的按值传递和按引用传递,(按值传参是产生了一个完全独立的副本。按引用传参实际上相当于实参的一个别名。)应该能明白区别了吧。

4、如何实现深拷贝?

深拷贝在于引用类型的时候,浅拷贝只复制地址值,实际上还是指向同一堆内存中的数据,深拷贝则是重新创建了一个相同的数据,二者指向的堆内存的地址是不同的。这个时候修改赋值前的变量数据不会影响赋值后的变量。

js深拷贝是一件看起来很简单的事情,但其实一点儿也不简单。对于循环引用的问题还有一些数据类型的拷贝,如Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型。处理起来并非像想象的那么简单。
为了节省时间,这里仅仅给出一种最简单的实现方法,可以用于通常情况下使用。

使用如下两个方法:
JSON对象parse方法可以将JSON字符串反序列化成JS对象。
JSON对象stringify方法可以将JS对象序列化成JSON字符串。

var a = {age:18, name: "Tom", info: {address: "wuhan", interest: "playCards"}};
var b = JSON.parse(JSON.stringify(a));
a.info.address = "shenzhen";
发布了14 篇原创文章 · 获赞 0 · 访问量 342

猜你喜欢

转载自blog.csdn.net/sksukai/article/details/105212249