原始值与引用值

TypeScript 中包含以下两种类型的值。

  • 原始值:存储在栈(stack)中的数据,它们的值直接存储在变量的存储空间中。

  • 引用值:存储在堆(heap)中的对象,存储在变量中的值是一个指针,它指向实际存储对象的内存地址

前面介绍了布尔类型、数值类型、长整型和字符串类型等原始类型,它们的值即原始值。这些原始类型占据的空间通常是固定的,所以可将它们存储在较小的内存区域——栈中,便于迅速查询变量的值。

引用类型通常是由多个原始值组成的复合对象类型,这些类型(数组、函数、对象与类等)将在后面一一介绍。对于引用类型的值,由于它们的大小并不固定,且通常较大,因此不能把它们放在栈中,否则会降低变量查询的速度。栈中只存放了对象在堆中的地址,而对象实际存储在堆中。

原始值与引用值在堆和栈中的存储方式如图6-1所示。

image 2024 02 18 08 36 23 363
Figure 1. 图6-1 原始值与引用值在堆和栈中的存储方式

接下来,将分别从值的复制、传递和比较这3个层面说明原始值和引用值之间的区别。

值的复制

对于原始值,赋值时会在栈中产生一个新的副本,因此复制的值和原来的值之间没有任何联系,它们各自位于不同的栈区。示例代码如下。

let number1 = 7;
let number2 = number1; //将number1的值复制到number2
let bool1 = true;
let bool2 = bool1;     //将bool1的值复制到bool2

这些原始值在栈中的存储方式如图6-2所示。

image 2024 02 18 08 41 32 101
Figure 2. 图6-2 原始值在栈中的存储方式

当发生修改时,各变量的栈区独立变化,互不干扰。例如,在以下代码中,对变量 number1bool1 的操作不会影响 number2bool2 的值。

number1 = 8;
console.log(number2); // 输出7
bool1 = false;
console.log(bool2);   //输出true

这些原始值修改后在栈中的存储方式如图6-3所示。

image 2024 02 18 08 47 42 219
Figure 3. 图6-3 原始值修改后在栈中的存储方式

对于引用值,在赋值时会赋予变量对象的引用(即对象的存储地址),而并非对象本身,因此复制时变量复制了相同的引用地址。例如,以下代码分别创建了名为 object1object2 的两个字面量对象和名为 array1array2 的两个数组(关于字面量对象和数组,会在后面详细介绍)。

let object1 = { property1: 1 };
let object2 = object1; //将object1的引用地址复制到object2
let array1 = ["a", "b", "c"]
let array2 = array1; //将array1的引用地址复制到array2

这些引用值在堆和栈中的存储方式如图6-4所示。

image 2024 02 18 08 50 10 634
Figure 4. 图6-4 引用值在堆和栈中的存储方式

由于多个变量实际上引用了同一个对象,因此对该对象的修改会在其他相关引用中体现出来,示例代码如下。

object1.property1 = 2;
console.log(object2);  //输出{ property1: 2 }
array1[1] = "x";
console.log(array2); //输出["a", "x", "c"]

引用值的对象修改后在堆和栈中的存储方式如图6-5所示。

image 2024 02 18 08 51 22 512
Figure 5. 图6-5 引用值的对象修改后在堆和栈中的存储方式

但如果重新给引用变量赋新值,引用发生改变,指向另外的堆地址,变量和原有对象不再有任何关系,两者之间互不影响。示例代码如下。

object2 = { property1: 3 };
array2 = ["x", "y", "z"]
console.log(object1); //输出{ property1: 2 }
console.log(array1);  //输出["a", "x", "c"]

引用值重新赋值后在堆和栈中的存储方式如图6-6所示。

image 2024 02 18 08 52 52 071
Figure 6. 图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所示。

image 2024 02 18 09 08 03 089
Figure 7. 图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所示。

image 2024 02 18 09 14 33 303
Figure 8. 图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 关键字,后面将会详细介绍。