事情发生在我看到Vue3的开源项目中,const声明的ref变量可以被修改。
当时我大受震撼,以为我看错了,当时忙着赶项目,没在意这个,也就跟着一起敲了,现在终于有时间了,索性梳理一下。
正文
const
关键字用于声明一个变量,该变量的值在其生命周期中不会被重新赋值。
现象
我们都知道const声明的常量不可改变,但是为什么Vue3中的ref和reactive声明的变量就可以修改?
这里我们先看几个例子,我们会发现,不止是ref和reactive,const声明的引用类数据,都是会被改变的。
基本数据类型
对于基本数据类型(如数字、字符串、布尔值),const
确保变量的值不会改变。
1 | const num = 42; |
对象
对于对象,仍然可以修改对象的属性,但不能重新赋值整个对象。
1 | const girlfriend = { |
数组
对于数组,你可以修改、添加或删除元素,但不能重新赋值整个数组。
1 | const arr = [1, 2, 3]; |
原理
在JavaScript中,const
并不是让变量的值变得不可变,而是让变量指向的内存地址不可变。
换句话说,使用const
声明的变量不能被重新赋值,但是其所指向的内存中的数据是可以被修改的。
使用const
后,实际上是确保该变量的引用地址不变,而不是其内容。
其实,这就是一次浅拷贝,只要地址指向的位置不发生改变,你做什么操作都可以,关于深浅拷贝不清楚的,可以参考一下我的:10分钟了解深浅拷贝。
基础数据类型定死了内存地址,所以当你重新赋值的时候,自然就会报错。
而引用数据类型虽然也定死了内存地址,但是其中内容未完全定死,所以可以操作和修改,这就是为什么引用类的数据类型使用const修改不会报错,但是重新赋值就会报错的原因了。
当const声明一个变量并赋值为一个对象或数组,这个变量实际上存储的是这个对象或数组在内存中的地址,形如
0x00ABCDEF`(这只是一个示例地址,实际地址会有所不同),而不是它的内容。这就是为什么我们说变量“引用”了这个对象或数组。
实际应用
这种看似矛盾的特性实际上在开发中经常用到。
例如,在开发过程中,可能希望保持一个对象的引用不变,同时允许修改对象的属性。这可以通过使用const
来实现。
考虑以下示例:
假设你正在开发一个应用,该应用允许用户自定义一些配置设置。当用户首次登录时,你可能会为他们提供一组默认的配置。但随着时间的推移,用户可能会更改某些配置。
1 | // 默认配置 |
在这个例子中,我们首先定义了一个userSettings
对象,它包含了用户的默认配置。
尽管我们使用const
来声明这个对象,但我们仍然可以随后更改其属性来反映用户的新配置。
这种模式在实际开发中很有用,因为它允许我们确保userSettings
始终指向同一个对象(即我们不会意外地将其指向另一个对象),同时还能够灵活地更新该对象的内容以反映用户的选择。
为什么不用let
以上所以案例中,使用let都是可行,但它的语义和用途相对不同,主要从这几个方面进行考虑:
- 不变性:使用
const
声明的变量意味着你不打算重新为该变量赋值。这为其他开发人员提供了一个明确的信号,即该变量的引用不会改变。在上述例子中,我们不打算将userSettings
重新赋值为另一个对象,我们只是修改其属性。因此,使用const
可以更好地传达这一意图。 - 错误预防:使用
const
可以防止意外地重新赋值给变量。如果你试图为const
变量重新赋值,JavaScript会抛出错误。这可以帮助捕获潜在的错误,特别是在大型项目或团队合作中。 - 代码清晰度:对于那些只读取和修改对象属性而不重新赋值的场景,使用
const
可以提高代码的清晰度,可以提醒看到这段代码的人:“这个变量的引用是不变的,但其内容可能会变。”
一般我们默认使用const
,除非确定需要重新赋值,这时再考虑使用let
。这种方法旨在鼓励不变性,并使代码更加可预测和易于维护。
由此,我们应该也明白,ref和reactive使用const声明,而非使用let了。
避免修改
如果我们想要避免修改const
声明的变量,当然也是可以的。
例如,我们可以使用浅拷贝来创建一个具有相同内容的新对象或数组,从而避免直接修改原始对象或数组。这可以通过以下方式实现:
1 | const originalArray = [1, 2, 3]; |
结语
const
声明的变量之所以看似可以被修改,是因为const
限制的是变量指向的内存地址的改变,而不是内存中数据的改变。
这种特性在实际开发中有其应用场景,允许我们保持引用不变,同时修改数据内容。
然而,如果我们确实需要避免修改数据内容,可以采取适当的措施,如浅拷贝。