值类型和引用类型 -- Javascript基础探究篇(4)

2,372 阅读5分钟

js中变量所持有的值可分为两种:值类型和引用类型。

  • 值类型:主要是指基本类型,即numberstringbooleanundefinednullsymbol。它们总是通过值复制的方式赋值和传递值。
  • 引用类型:除上述值类型外的对象类型。它们总是通过引用复制的方式赋值和传递值。

值类型

let a = 2;
let b = a;
b++;
console.log(a, b);  // 2, 3

值类型的数据是不可变的,在内存中占有固定大小的空间,它们都会被存储在栈(stack)中。

上述代码在内存中的存储过程如下:

value-type

a所持有的值是值类型,所以当执行b = a时,b会持有2的一个副本。所以b改变时,a并未受到影响。ab所持有的值相互独立。

在调用函数时,传递给函数参数如果是值类型,也是通过值复制的方式传递:

let a = 2;

function foo(b) {
  b++;
  console.log(b); // 3
}

foo(a);
console.log(a); // 2

实际上在调用foo函数时,会把实参a的值复制给b,所以b所持有的值也是2,但是和a相互独立,所以更改b的值同样不影响a

如果我们使用构造函数声明一个基本类型,并改变它会怎么样?

let a = new Number(10);
let b = a;
b++;
console.log(a); // Number {[[PrimitiveValue]]: 10}
console.log(b); // 11

实际上,因为a是通过构造函数声明的,所以它所持有的值是引用类型,所以在第二步let b = a时,ba指向同一个引用。

但是第三步中b++(等价于b = b + 1),在执行b + 1时,进行了隐式拆箱,将bNumber对象提取为基本类型10,所以最后b的值变成了11,而a的值并未受到影响。

引用类型

const a = { name: "zhangsan", age: 20 };
const b = a;

b.name = "lisi";
console.log(a); // {name:"lisi", age:20}
console.log(b); // {name:"lisi", age:20}

引用类型的数据大小不固定,所以把它们的值存在堆(Heap)中,但还是会把它们在堆中的内存地址存在栈中。在查询引用类型数据时,先从栈中读取所持有的数据在堆中的内存地址,然后根据地址找到实际的数据。

上述代码的内存中的存储过程如下:

reference-type

a所持有的值是引用类型,所以当执行b = a时,b会复制a的引用(堆内存地址),即ab指向对一个对象。所以b改变name属性时,a会受到影响。

在调用函数时,传递给函数参数如果是引用类型,也是通过引用复制的方式传递:

const a = { name: "zhangsan", age: 20 };

function foo(b) {
  b.name = "lisi";
  console.log(b);// {name:"lisi", age:20}
}

foo(a);
console.log(a); // {name:"lisi", age:20}

实际上在调用foo函数时,会把实参a的引用复制给b,所以ab指向对一个对象,结果就是改变b的属性,a同样会受到影响。

由于引用指向的是值本身,而不是变量,所以一个引用无法更改另一引用的指向。

let a = { name: "zhangsan", age: 20 };
let b = a;
b = { value: 123 };

console.log(a); // {name:"zhangsan", age:20}
console.log(b); // {value:123}

上述代码的内存中的存储过程如下:

reference-type-change

关键是在b = { value: 123 }这一步,实际上是更改b的指向,使得ab指向不同的对象。但是b指向的改变并不会影响a的值。

为什么会有栈内存和堆内存?

通常与gc(垃圾回收机制)有关。为了使程序运行时占用的内存最小。

当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会被逐个放入这块栈内存里,当方法执行结束,这个方法的内存栈也会被销毁。因此,所有在方法中定义的变量都存放在栈内存中。

但当在程序创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法调用结束后,只要这个对象还可能被另一个变量所引用,则这个对象就不会被销毁;只有当一个对象没有被任何变量引用它时,系统的垃圾回收机制才会回收它。

在es6中我们使用const关键字表示常量。即变量所持有的值不可变。

如果变量所持有的值是值类型,那么这个值确定不可变。如果持有的值是引用类型,我们依旧可以更改该值的内容。这是因为const保证的是栈内存中数据不变性:

  • 对于值类型来说,栈内存中的数据就是所持有的值
  • 而对于引用类型来说,栈内存中的数据只是所持有的值在堆内存中的内存地址,我们改变该值的内部属性并不会影响它在堆内存中的内存地址。但如果重新赋值一个新的引用类型的值是不合法的,因为这会修改变量所绑定内存地址
const a = 1;
a = 2; // TypeError: Assignment to constant variable.

const obj = {};
obj.name = "xxx"; // 合法
obj = { a: 1 }; // TypeError: Assignment to constant variable.