js中变量所持有的值可分为两种:值类型和引用类型。
- 值类型:主要是指基本类型,即
number
,string
,boolean
,undefined
,null
,symbol
。它们总是通过值复制的方式赋值和传递值。 - 引用类型:除上述值类型外的对象类型。它们总是通过引用复制的方式赋值和传递值。
值类型
let a = 2;
let b = a;
b++;
console.log(a, b); // 2, 3
值类型的数据是不可变的,在内存中占有固定大小的空间,它们都会被存储在栈(stack)中。
上述代码在内存中的存储过程如下:
a
所持有的值是值类型,所以当执行b = a
时,b
会持有2
的一个副本。所以b
改变时,a
并未受到影响。a
和b
所持有的值相互独立。
在调用函数时,传递给函数参数如果是值类型,也是通过值复制的方式传递:
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
时,b
和a
指向同一个引用。
但是第三步中b++
(等价于b = b + 1
),在执行b + 1
时,进行了隐式拆箱,将b
从Number
对象提取为基本类型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)中,但还是会把它们在堆中的内存地址存在栈中。在查询引用类型数据时,先从栈中读取所持有的数据在堆中的内存地址,然后根据地址找到实际的数据。
上述代码的内存中的存储过程如下:
a
所持有的值是引用类型,所以当执行b = a
时,b
会复制a
的引用(堆内存地址),即a
和b
指向对一个对象。所以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
,所以a
和b
指向对一个对象,结果就是改变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}
上述代码的内存中的存储过程如下:
关键是在b = { value: 123 }
这一步,实际上是更改b
的指向,使得a
和b
指向不同的对象。但是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.