原始值与引用值
TypeScript 中包含以下两种类型的值。
-
原始值:存储在栈(
stack
)中的数据,它们的值直接存储在变量的存储空间中。 -
引用值:存储在堆(
heap
)中的对象,存储在变量中的值是一个指针,它指向实际存储对象的内存地址
前面介绍了布尔类型、数值类型、长整型和字符串类型等原始类型,它们的值即原始值。这些原始类型占据的空间通常是固定的,所以可将它们存储在较小的内存区域——栈中,便于迅速查询变量的值。
引用类型通常是由多个原始值组成的复合对象类型,这些类型(数组、函数、对象与类等)将在后面一一介绍。对于引用类型的值,由于它们的大小并不固定,且通常较大,因此不能把它们放在栈中,否则会降低变量查询的速度。栈中只存放了对象在堆中的地址,而对象实际存储在堆中。
原始值与引用值在堆和栈中的存储方式如图6-1所示。

接下来,将分别从值的复制、传递和比较这3个层面说明原始值和引用值之间的区别。
值的复制
对于原始值,赋值时会在栈中产生一个新的副本,因此复制的值和原来的值之间没有任何联系,它们各自位于不同的栈区。示例代码如下。
let number1 = 7;
let number2 = number1; //将number1的值复制到number2
let bool1 = true;
let bool2 = bool1; //将bool1的值复制到bool2
这些原始值在栈中的存储方式如图6-2所示。

当发生修改时,各变量的栈区独立变化,互不干扰。例如,在以下代码中,对变量 number1
和 bool1
的操作不会影响 number2
和 bool2
的值。
number1 = 8;
console.log(number2); // 输出7
bool1 = false;
console.log(bool2); //输出true
这些原始值修改后在栈中的存储方式如图6-3所示。

对于引用值,在赋值时会赋予变量对象的引用(即对象的存储地址),而并非对象本身,因此复制时变量复制了相同的引用地址。例如,以下代码分别创建了名为 object1
、object2
的两个字面量对象和名为 array1
、array2
的两个数组(关于字面量对象和数组,会在后面详细介绍)。
let object1 = { property1: 1 };
let object2 = object1; //将object1的引用地址复制到object2
let array1 = ["a", "b", "c"]
let array2 = array1; //将array1的引用地址复制到array2
这些引用值在堆和栈中的存储方式如图6-4所示。

由于多个变量实际上引用了同一个对象,因此对该对象的修改会在其他相关引用中体现出来,示例代码如下。
object1.property1 = 2;
console.log(object2); //输出{ property1: 2 }
array1[1] = "x";
console.log(array2); //输出["a", "x", "c"]
引用值的对象修改后在堆和栈中的存储方式如图6-5所示。

但如果重新给引用变量赋新值,引用发生改变,指向另外的堆地址,变量和原有对象不再有任何关系,两者之间互不影响。示例代码如下。
object2 = { property1: 3 };
array2 = ["x", "y", "z"]
console.log(object1); //输出{ property1: 2 }
console.log(array1); //输出["a", "x", "c"]
引用值重新赋值后在堆和栈中的存储方式如图6-6所示。

值的传递
值的传递和值的复制具有相似的规则。对于原始值,复制各自独立的副本;而对于引用值,复制相同的引用地址。
当把原始值传递给函数的参数时(函数及其参数会在后面详细介绍),参数是全新的副本。在函数中修改参数值,并不会影响原来的值。示例代码如下。
let number1 = 7;
function testNumber(para: number) {
para = 8;
}
testNumber(number1);
console.log(number1);//输出7
let bool1 = true;
function testBool(para: boolean) {
bool1 = false;
}
testBool(bool1);
console.log(bool1); //输出true
当把引用值传递给函数时,传递给函数的是对原值的引用,在函数内部可以使用此引用来修改对象本身的值。示例代码如下。
let object1 = { property1: 1 };
function testObject(para: any) {
para.property1 = 2;
}
testObject(object1);
console.log(object1); //输出{ property1: 2 }
let array1 = ["a", "b", "c"]
function testArray(para: string[]) {
para[1] = "x";
}
testArray(array1);
console.log(array1); //输出["a", "x", "c"]
如果给函数参数赋予新值,引用就会发生改变,指向另外的堆地址,参数和原有对象不再有任何关系,两者之间互不影响。示例代码如下。
function testObject2(para: any) {
para = { property1: 3 }; // 对象整体赋值
}
testObject2(object1);
console.log(object1); //输出{ property1: 2 }
function testArray2(para: string[]) {
para = ["x", "y", "z"]; // 数组整体赋值
}
testArray(array1);
console.log(array1); //输出["a", "x", "c"]
值的比较
当对原始值进行比较时,会逐字节地比较,以判断它们是否相等。注意,比较的是值本身,而不是值所处的栈的位置。当比较结果为相等时,表示它们在栈中所包含的字节信息是相同的。示例代码如下。
let number1 = 7;
let number2 = 7;
//number1 和number2的值具有相同的字节信息,比较结果为相等,输出true
console.log(number1 == number2);
let bool1 = true;
let bool2 = true;
//bool1和bool2的值具有相同的字节信息,比较结果为相等,输出true
console.log(number1 == number2);
原始值的比较方式如图6-7所示。

当对引用值进行比较时,比较的是两个引用地址,看它们引用的是否是同一个对象,而不是比较它们的字节信息是否相同。即使两个引用值引用的对象具有相同的字节信息,如果引用的堆地址不同,它们也不是相等的。示例代码如下。
let object1 = { property1: 1 };
let object2 = { property1: 1 };
//object1和object2指向不同的对象地址,因此不相等,以下语句输出false
console.log(object1 == object2);
let object3 = object1;
//object1和object3均指向同一个对象地址,因此相等,以下语句输出true
console.log(object1 == object3);
object1.property1 = 5;
console.log(object1 == object3); //输出true
let array1 = ["a", "b", "c"];
let array2 = ["a", "b", "c"];
//array1 和array2 指向不同的对象地址,因此不相等,
//以下语句输出false
console.log(array1 == array2);
let array3 = array1;
//array1 和array3均指向同一个对象地址,因此相等,
//以下语句输出true
console.log(array1 == array3);
array1[1] = "x";
console.log(array1 == array3); //输出true
引用值的比较方式如图6-8所示。

常量的使用
前面提到,使用 const
关键字声明常量,而常量的值是不可改变的。例如,在以下代码中,修改常量的值会引起编译错误。
const number1 = 7;
const bool1 = true;
//编译错误:无法分配到 "number1",因为它是常数。ts(2588)
number1 = 8;
//编译错误:无法分配到 "bool1",因为它是常数。ts(2588)
bool1 = false;
然而,严格来说,常量仅能限定栈上的内容不可编辑,但堆上的内容可以编辑。例如,以下代码不会引起编译错误。
const object1 = { property1: 1 };
const array1 = ["a", "b", "c"]
object1.property1 = 2;
array1[1] = "x";
但如果更改栈上的引用地址,就会引起编译错误,示例代码如下。
const object1 = { property1: 1 };
const array1 = ["a", "b", "c"];
// 编译错误:无法分配到 "object1",因为它是常数。ts(2588)
object1 = { property1: 1 };
// 编译错误:无法分配到 "array1",因为它是常数。ts(2588)
array1 = ["a", "b", "c"];
虽然 const
关键字限定了栈上的内容不可编辑,但堆上的内容可以编辑,因此对于引用类型来说,要使堆上的内容不可编辑,需要额外使用 readonly
关键字,后面将会详细介绍。