浅析const声明的ref,reactive变量为什么可以修改
2024-12-09 11:13:16

事情发生在我看到Vue3的开源项目中,const声明的ref变量可以被修改。

当时我大受震撼,以为我看错了,当时忙着赶项目,没在意这个,也就跟着一起敲了,现在终于有时间了,索性梳理一下。

正文

const关键字用于声明一个变量,该变量的值在其生命周期中不会被重新赋值。

现象

我们都知道const声明的常量不可改变,但是为什么Vue3中的ref和reactive声明的变量就可以修改?

这里我们先看几个例子,我们会发现,不止是ref和reactive,const声明的引用类数据,都是会被改变的。

基本数据类型

对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。

1
2
const num = 42;
// num = 43; // 这会抛出错误

对象

对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。

1
2
3
4
5
6
7
const girlfriend = {
name: "小宝贝"
};

girlfriend.name = "亲爱的"; // 这是允许的,因为你只是修改了对象的一个属性

// girlfriend = { name: "亲爱的" }; // 这会抛出错误,因为你试图改变obj的引用

数组

对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。

1
2
3
4
5
6
const arr = [1, 2, 3];

arr[0] = 4; // 这是允许的,因为你只是修改了数组的一个元素
arr.push(5); // 这也是允许的,因为你只是向数组添加了一个元素

// arr = [6, 7, 8]; // 这会抛出一个错误,因为你试图改变arr的引用

原理

在JavaScript中,const并不是让变量的值变得不可变,而是让变量指向的内存地址不可变。

换句话说,使用const声明的变量不能被重新赋值,但是其所指向的内存中的数据是可以被修改的。

使用const后,实际上是确保该变量的引用地址不变,而不是其内容。

其实,这就是一次浅拷贝,只要地址指向的位置不发生改变,你做什么操作都可以,关于深浅拷贝不清楚的,可以参考一下我的:10分钟了解深浅拷贝

基础数据类型定死了内存地址,所以当你重新赋值的时候,自然就会报错。

而引用数据类型虽然也定死了内存地址,但是其中内容未完全定死,所以可以操作和修改,这就是为什么引用类的数据类型使用const修改不会报错,但是重新赋值就会报错的原因了。

当const声明一个变量并赋值为一个对象或数组,这个变量实际上存储的是这个对象或数组在内存中的地址,形如0x00ABCDEF`(这只是一个示例地址,实际地址会有所不同),而不是它的内容。这就是为什么我们说变量“引用”了这个对象或数组。

实际应用

这种看似矛盾的特性实际上在开发中经常用到。

例如,在开发过程中,可能希望保持一个对象的引用不变,同时允许修改对象的属性。这可以通过使用const来实现。

考虑以下示例:

假设你正在开发一个应用,该应用允许用户自定义一些配置设置。当用户首次登录时,你可能会为他们提供一组默认的配置。但随着时间的推移,用户可能会更改某些配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 默认配置
const userSettings = {
theme: "light", // 主题颜色
notifications: true, // 是否开启通知
language: "en" // 默认语言
};

// 在某个时间点,用户决定更改主题颜色和语言
function updateUserSettings(newTheme, newLanguage) {
userSettings.theme = newTheme;
userSettings.language = newLanguage;
}

// 用户调用函数,将主题更改为"dark",语言更改为"zh"
updateUserSettings("dark", "zh");

console.log(userSettings); // 输出:{ theme: "dark", notifications: true, language: "zh" }

在这个例子中,我们首先定义了一个userSettings对象,它包含了用户的默认配置。

尽管我们使用const来声明这个对象,但我们仍然可以随后更改其属性来反映用户的新配置。

这种模式在实际开发中很有用,因为它允许我们确保userSettings始终指向同一个对象(即我们不会意外地将其指向另一个对象),同时还能够灵活地更新该对象的内容以反映用户的选择。

为什么不用let

以上所以案例中,使用let都是可行,但它的语义和用途相对不同,主要从这几个方面进行考虑:

  1. 不变性:使用const声明的变量意味着你不打算重新为该变量赋值。这为其他开发人员提供了一个明确的信号,即该变量的引用不会改变。在上述例子中,我们不打算将userSettings重新赋值为另一个对象,我们只是修改其属性。因此,使用const可以更好地传达这一意图。
  2. 错误预防:使用const可以防止意外地重新赋值给变量。如果你试图为const变量重新赋值,JavaScript会抛出错误。这可以帮助捕获潜在的错误,特别是在大型项目或团队合作中。
  3. 代码清晰度:对于那些只读取和修改对象属性而不重新赋值的场景,使用const可以提高代码的清晰度,可以提醒看到这段代码的人:“这个变量的引用是不变的,但其内容可能会变。”

一般我们默认使用const,除非确定需要重新赋值,这时再考虑使用let。这种方法旨在鼓励不变性,并使代码更加可预测和易于维护。

由此,我们应该也明白,ref和reactive使用const声明,而非使用let了。

避免修改

如果我们想要避免修改const声明的变量,当然也是可以的。

例如,我们可以使用浅拷贝来创建一个具有相同内容的新对象或数组,从而避免直接修改原始对象或数组。这可以通过以下方式实现:

1
2
3
4
5
const originalArray = [1, 2, 3];
const newArray = [...originalArray]; // 创建一个原始数组的浅拷贝
newArray.push(4); // 不会影响原始数组
console.log(originalArray); // 输出: [1, 2, 3]
console.log(newArray); // 输出: [1, 2, 3, 4]

结语

const声明的变量之所以看似可以被修改,是因为const限制的是变量指向的内存地址的改变,而不是内存中数据的改变。

这种特性在实际开发中有其应用场景,允许我们保持引用不变,同时修改数据内容。

然而,如果我们确实需要避免修改数据内容,可以采取适当的措施,如浅拷贝。

参考

const声明的变量还能修改?原理都在这了