手撕值传递&对象深浅拷贝

| 字数 1491
  1. 1. 值传递
    1. 1.1. JS 中的基本类型
    2. 1.2. 基本类型&引用类型存储
    3. 1.3. 值传递的过程
  2. 2. 引用类型的深浅拷贝
  3. 3. 补充
  1. 1. 值传递
    1. 1.1. JS 中的基本类型
    2. 1.2. 基本类型&引用类型存储
    3. 1.3. 值传递的过程
  2. 2. 引用类型的深浅拷贝
  3. 3. 补充

值传递

  1. JS 中的基本类型&引用类型分别是什么?
  2. 基本类型&引用类型如何存储?
  3. 值传递的由来&举例说明值传递的过程
  4. 扩展阅读(call-by-sharing)

JS 中的基本类型

JS 中的基本类型分别是:Number、String、Boolean、undefined、null;引用类型分别是:Function、Object、Array 等。

基本类型&引用类型存储

基本类型均存储在栈中而且

在栈中的大小是在引擎中固定的,所以基本类型的包装类型(Number&String&Boolean)的生命周期很短,因为一旦包装类型的生命周期变长,对应的栈中内存会发生变化,导致内存出现问题。而引用类型的存储分为俩部分,在栈中存储对应变量的引用(理解为地址好一点),堆中存放真正的数据。每次都是根据栈中的地址而找到对应的堆中存放的位置,进行读写。代码和图示如下:

1
2
3
var pen = 'hero';
var pencil = 'zhonghua';
var pencil_box = { eraser: 'xiaoxiao' };

这里补充一个关于包装对象的问题。代码如下:

1
2
3
var test = 'test';
test.color = 'red';
console.log(test.color); // test.color 输出什么?

输出 undefined,引用类型与基本包装类型的主要区别就是对象的生命周期(在上述已经说明生命周期过长会发生什么)。自动创建的基本包装类型的对象只存在于执行代码后的瞬间,然后被销毁。

值传递的过程

ECMAScript 中所有函数的参数都是按值传递。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。访问变量有按值和按引用俩种方式,而参数只能按值传递。值传递代码和图示如下:

1
2
3
4
5
6
7
8
function addTen (num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
console.log(count); // 20
console.log(result); // 30

改为对象再试试看:

1
2
3
4
5
6
function setName (obj) {
obj.name = 'Nicholas';
}
var person = {};
setName(person);
console.log(person.name); // "Nicholas"

因为 person 指向的对象在堆内存中只有一个,而且是全局的。有很多人错误地认为:在局部作用域中(setName 中)修改的对象会在全局作用域中(console.log( person.name ))反映出来就说明参数按引用传递。为了证明对象是按值传递,再来看下面的例子:

1
2
3
4
5
6
7
8
function setName (obj) {
obj.name = 'Nicholas';
obj = new Object();
obj.name = 'Greg';
}
var person = new Object();
setName(person);
console.log(person.name);

这个时候输出的会是什么?是 “Nicholas”?还是 “Greg”?好吧,先来一个错误答案

“Greg”(即引用传递) 的图示:

调用 setName 执行到 obj = new Object(); 时会断开 person 原先指向堆中的连接(图中红叉处)从而指向新开辟的内存空间(new Object()),然后设置 obj 的 name 属性。正确的结果应该是 “Nicholas” 图示如下:

传入的 obj 只是对 person 对象的引用进行了复制,执行 obj = new Object(); 时只是让复制后的 person 对象的引用指向了 新开辟的空间(new Object())。

引用类型的深浅拷贝

浅拷贝最简单的方法就是直接利用 = 赋值,这样的话改变一个值,另一个也跟着变化。原理就是因为这样简单地赋值结果共用的是同一块内存。这个不管是 Array 还是 Object都很简单就不举例了。在这里想着重说的一个知识点是关于 Array 的 slice&concat 方法,很多很多网文都把这俩个方法说成了 Array 的深拷贝方法,其实这是错的。为什么?请看下例:

1
2
3
4
5
var arr1 = [1, 2, [3, 4] ];
var arr2 = arr1.slice(); // 换成 var arr2 = arr1.concat(); 结果不变
console.log(arr1,arr2);
arr1[2][0] = 'ss';
console.log(arr1,arr2); // arr1[2][0] = 'ss' , arr2[2][0] = 'ss'

如果是二维数组,如上代码。通过查看 arr1[2][0]&arr2[2][0] 的结果我们可以发现这并不是深拷贝,因为改变了 arr1[2][0]arr2[2][0] 也顺势改变。但如果是一维数组,则不会有任何问题。如下代码:

1
2
3
4
5
var arr1 = [1, 2, 3];
var arr2 = arr1.slice();
console.log(arr1,arr2);
arr1[0] = 'ss';
console.log(arr1,arr2); // arr1 = ['ss', 2, 3] , arr2 = [1, 2, 3]

难道只是因为数组的维数不同吗?不是的,应该说是我们并没有深层次地明白什么叫浅拷贝?什么叫深拷贝?

浅拷贝只复制一层引用类型对象的属性。深拷贝不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。代码和图示,请查看知乎上关于这个问题的回答 javascript中的深拷贝和浅拷贝?
通吃数组与对象的深拷贝代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var cloneObj = function(obj){
var str, newobj = obj.constructor === Array ? [] : {};
if(typeof obj !== 'object'){
return;
} else if(window.JSON){
str = JSON.stringify(obj), //系列化对象
newobj = JSON.parse(str); //还原
} else {
for(var i in obj){
newobj[i] = typeof obj[i] === 'object' ?
cloneObj(obj[i]) : obj[i];
}
}
return newobj;
};

JSON.stringify()&JSON.parse()可以进行引用对象的拷贝,但是对古老浏览器(IE6—IE8)的兼容性问题即你得先查看(window.JSON)浏览器是否有 JSON 对象。如果没有可以引用 json2.js 文件。如果数组值为函数,该方法也是不行的。

补充

  
  尽管 pass-by-reference(引用传递)与 pass-by-value (值传递)存在了很长一段时间,但是外国小哥提出了一种 pass-by-sharing 的说法,感兴趣的同学可以看看。Is JavaScript a pass-by-reference or pass-by-value language?