理解JavaScript之值类型与引用类型

先来看下面的例子输出什么?

1
2
3
4
5
6
7
8
9
10
function foo(args) {
args.push(4);
console.log(args); // 1、输出?
args = [4, 5, 6];
args.push(7);
console.log(args); // 2、输出?
}
var arr = [1, 2, 3];
foo(arr);
console.log(arr); // 3、输出?

1处输出[1, 2, 3, 4];2处输出[4, 5, 6, 7];3处输出[1, 2, 3, 4],为什么不是[4, 5, 6, 7]?

我们知道在最新的ECMAScript标准中定义了7种数据类型,其中,

6种原始类型:Boolean,Null,Undefined,Number,String,Symbol(ECMAScript 6新定义);

和Object。

而Javascript対值的引用和赋值或者传递完全由值的类型来决定。

对于简单值,总是通过值复制的方式来赋值或传递,包括null、undefined、字符串、数字、布尔以及ES6中的symbol;对于复合值总是通过引用复制的方式赋值或传递,包括对象(数组或封装的对象)、函数。

我们再来看上面的例子,变量arr是数组,属于复合值类型,它的赋值和传递都是通过引用的方式,来看看执行foo函数的时候发生了什么。

1
2
3
4
5
6
7
8
9
10
foo(arr);
// 对上面函数的执行做一个等价转换
function foo(args) {
var args = arr; // 创建变量args指向arr指向的值
args.push(4); // args和arr是分别指向同一个复合值[1, 2, 3]的两个不同引用。注意,args和arr仅仅是指向值[1, 2, 3],并非持有,所以调用.push(4)的时候,它们更改的是同一个值,然后它们都指向更改后的新值[1, 2, 3, 4]。
console.log(args); // [1, 2, 3, 4]
args = [4, 5, 6]; // 对args重新赋值,相当于重新改变了args的指向,当是并不影响arr原来的指向,也就是说arr还是指向[1, 2, 3, 4]
args.push(7); // args = [4, 5, 6, 7]
}
// 执行完foo函数后,arr的值为[1, 2, 3, 4]

从这个例子中我们可以得出:

  • 运算符=就是创建或修改变量在内存中的指向。
  • 初始化变量时为创建,重新赋值即为修改。
  • 我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。

下面我们再尝试从内存占用的角度来解释。

首先看一个简单的例子:

1
2
3
4
var o1 = { m: 1 }; // a1 = { m: 1 }
var o2 = o1; // o2 = { m: 1}
o1 = 2; // 对o1重新赋值
console.log(o2); // { m: 1 }

前两句代码的内存区情况如下:

o1, o2 [[Object]] (指向对象m,m为1)

接下来:

  1. 创建变量o1指向对象{m: 1};
  2. 创建变量o2指向对象{m: 1};
  3. 修改o1重新指向常量2;

现在再来看看内存区情况:

常量区
o1 2
o2 [[Object]] (指向对象m,m为1)

所以o2从头到尾都是指向对象{m: 1}

修改上面一个例子:

1
2
3
4
var o1 = { m: 1 }; // a1 = { m: 1 }
var o2 = o1; // o2 = { m: 1}
o1.m = 2; // 对o1重新赋值
console.log(o2); // { m: 2 }

前两句代码执行完o1, o2, m在内存中的分布:

常量区
o1, o2 [[Object]] (指向对象m,m为1)
m 1

执行完o1.m = 2后:

常量
o1, o2 [[Object]] (指向对象m,m为2)
m 2

执行o1.m = 2,相当于通过o1的间接引用改变了m的值,那么所有原来指向m的引用的值都改变了,在这里就是o2所引用的值也改变了。o1和o2从始至终都未改变过其引用,只是m改变了。

根据上面的理论知识,就会想到工作中为了分离变量间的引用所用的操作:

1
2
3
4
// 1、JSON.parse(JSON.stringfify({ a: 0 })); 
// 2、arr.slice()、arr.concat()
// 3、_.cloneDeep()
// 4、解构赋值

总结:

简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。

JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。




理解JavaScript之值类型与引用类型
https://keminu.github.io/2018/01/02/理解JavaScript之值类型与引用类型/
作者
xiaoka
发布于
2018年1月2日
许可协议