突变是你在 JavaScript 世界中经常听到的东西,但它们到底是什么,它们是否像人们所说的那样邪恶?
数据类型
JavaScript 中的每个值要么是原始值,要么是对象。有七种不同的原始数据类型:
- 数字,例如
3
,0
,-4
,0.625
- 字符串,例如
'Hello'
,"World"
,`Hi`
,''
- 布尔值
true
和false
null
undefined
- 符号 — 保证永远不会与其他符号冲突的唯一标记
BigInt
— 用于处理大整数值
任何不是原始值的都是对象,包括数组、日期、正则表达式,当然还有对象字面量。函数是一种特殊类型的对象。它们绝对是对象,因为它们具有属性和方法,但它们也可以被调用。
变量赋值
变量赋值是你在编码中学习的第一件事。例如,这就是我们将数字分配给3
变量的方式bears
:
const bears = 3;
变量的一个常见比喻是其中一个带有标签的盒子,里面有值。上面的示例将被描绘成一个包含标签“bears”的盒子,里面的值是 3。
另一种思考发生情况的方法是作为参考,将标签映射bears
到 的值3
:
如果我将数字分配3
给另一个变量,它引用与熊相同的值:
let musketeers = 3;
变量bears
和musketeers
都引用相同的原始值 3。我们可以使用严格相等运算符来验证这一点===
:
bears === musketeers
<< true
true
如果两个变量引用相同的值,则相等运算符返回。
处理对象时的一些问题
前面的示例显示了将原始值分配给变量。分配对象时使用相同的过程:
const ghostbusters = { number: 4 };
这个赋值意味着变量ghostbusters
引用了一个对象:
然而,将对象分配给变量时的一个很大区别是,如果您将另一个对象字面量分配给另一个变量,它将引用一个完全不同的对象——即使两个对象字面量看起来完全相同!例如,下面的赋值看起来像变量tmnt
(Teenage Mutant Ninja Turtles) 引用了与变量相同的对象ghostbusters
:
let tmnt = { number: 4 };
尽管变量ghostbusters
和tmnt
看起来它们引用了同一个对象,但它们实际上都引用了一个完全不同的对象,如果我们检查严格相等运算符,我们可以看到:
ghostbusters === tmnt
<< false
变量重新分配
在const
ES6 中引入关键字时,很多人误以为 JavaScript 中已经引入了常量,但事实并非如此。这个关键字的名称有点误导。
任何声明的变量const
都不能重新分配给另一个值。这适用于原始值和对象。例如,变量bears
是const
在上一节中声明的,所以它不能有另一个值分配给它。如果我们尝试将数字 2 分配给变量bears
,我们会得到一个错误:
bears = 2;
<< TypeError: Attempted to assign to readonly property.
对数字 3 的引用是固定的,bears
不能为变量重新分配另一个值。
这同样适用于对象。如果我们尝试为变量分配不同的对象ghostbusters
,我们会得到相同的错误:
ghostbusters = {number: 5};
TypeError: Attempted to assign to readonly property.
变量重新分配使用let
当关键字let
用于声明变量时,可以在以后的代码中重新分配它以引用不同的值。例如,我们musketeers
使用声明了变量let
,因此我们可以更改musketeers
引用的值。如果达达尼昂加入火枪手,他们的人数将增加到 4:
musketeers = 4;
可以这样做,因为let
用于声明变量。musketeers
我们可以随意更改引用的值。
该变量tmnt
也被声明为 using let
,因此它也可以被重新分配以引用另一个对象(或者如果我们愿意,可以完全使用不同的类型):
tmnt = {number: 5};
请注意,该变量tmnt
现在引用了一个完全不同的对象;我们不仅将number
属性更改为 5。
总之,如果你声明一个变量 using const
,它的值不能被重新赋值,并且总是引用它最初被赋值的相同的原始值或对象。如果使用 声明变量let
,则可以在程序稍后根据需要重新分配其值多次。
const
尽可能频繁地使用通常被认为是一种好的做法,因为这意味着变量的值保持不变,并且代码更加一致和可预测,从而更不容易出现错误和错误。
引用变量赋值
在原生 JavaScript 中,您只能为变量赋值。您不能分配变量来引用另一个变量,即使看起来可以。例如,Stooges 的数量与 Musketeers 的数量相同,因此我们可以使用以下命令分配变量stooges
以引用与变量相同的值musketeers
:
const stooges = musketeers;
这看起来变量stooges
引用了变量musketeers
,如下图所示:
然而,这在原生 JavaScript 中是不可能的:一个变量只能引用一个实际值;它不能引用另一个变量。当您进行这样的赋值时,实际发生的情况是,赋值左侧的变量将引用右侧变量所引用的值,因此该变量stooges
将引用与变量相同的值musketeers
,即数字 3。曾经这个赋值已经完成,stooges
变量根本没有连接到musketeers
变量。
这意味着如果达达尼昂加入火枪手,我们将 的值设置musketeers
为 4,则 的值stooges
将保持为 3。事实上,因为我们stooges
使用 声明了变量const
,所以我们不能将其设置为任何新值;它永远是 3。
总而言之:如果您声明一个变量 usingconst
并将其设置为原始值,即使是通过对另一个变量的引用,那么它的值也不会改变。这对您的代码有好处,因为这意味着它将更加一致和可预测。
突变
如果值可以更改,则称该值是可变的。这就是它的全部内容:突变是改变值属性的行为。
JavaScript 中的所有原始值都是不可变的:您永远无法更改它们的属性。例如,如果我们将字符串分配"cake"
给 variable food
,我们可以看到我们无法更改它的任何属性:
const food = "cake";
如果我们尝试将第一个字母更改为“f”,看起来它已经改变了:
food[0] = "f";
<< "f"
但是如果我们看一下变量的值,我们会发现实际上并没有改变:
food
<< "cake"
如果我们尝试更改长度属性,也会发生同样的事情:
food.length = 10;
<< 10
尽管返回值暗示长度属性已更改,但快速检查表明它没有:
food.length
<< 4
const
请注意,这与使用而不是声明变量无关let
。如果我们使用了let
,我们可以设置food
为引用另一个字符串,但我们不能更改它的任何属性。不可能更改原始数据类型的任何属性,因为它们是不可变的。
JavaScript 中的可变性和对象
相反,JavaScript 中的所有对象都是可变的,这意味着它们的属性可以更改,即使它们是使用声明的const
(记住let
并且const
只控制变量是否可以重新分配,与可变性无关)。例如,我们可以使用以下代码更改数组的第一项:
const food = ['🍏','🍌','🥕','🍩'];
food[0] = '🍎';
food
<< ['🍎','🍌','🥕','🍩']
请注意,尽管我们food
使用const
. 这表明 usingconst
并不能阻止对象发生变异。
我们还可以更改数组的长度属性,即使它已使用 声明const
:
food.length = 2;
<< 2
food
<< ['🍎','🍌']
通过引用复制
请记住,当我们将变量分配给对象字面量时,变量将引用完全不同的对象,即使它们看起来相同:
const ghostbusters = {number: 4};
const tmnt = {number: 4};
但是如果我们将一个变量分配fantastic4
给另一个变量,它们都将引用同一个对象:
const fantastic4 = tmnt;
这将分配变量fantastic4
以引用该变量引用的同一对象tmnt
,而不是完全不同的对象。
这通常被称为通过引用复制,因为这两个变量都被分配以引用同一个对象。
这很重要,因为对这个对象所做的任何突变都会在两个变量中看到。
因此,如果蜘蛛侠加入神奇四侠,我们可能会更新number
对象中的值:
fantastic4.number = 5;
这是一个突变,因为我们更改了number
属性而不是设置fantastic4
为引用一个新对象。
这给我们带来了一个问题,因为 will 的number
属性tmnt
也发生了变化,可能我们甚至没有意识到:
tmnt.number
<< 5
这是因为两者tmnt
和都引用同一个对象,因此对其中任何一个或将影响它们两者的fantastic4
任何突变。tmnt
fantastic4
这突出了 JavaScript 中的一个重要概念:当对象通过引用复制并随后发生变异时,变异将影响引用该对象的任何其他变量。这可能会导致难以追踪的意外副作用和错误。
传播运营商的救援!
那么如何在不创建对原始对象的引用的情况下复制对象呢?答案是使用扩展运算符!
ES2015 中的数组和字符串以及 ES2018 中的对象引入了扩展运算符。它允许您轻松地制作对象的浅表副本,而无需创建对原始对象的引用。
下面的示例显示了我们如何设置变量fantastic4
以引用对象的副本tmnt
。此副本将与对象完全相同tmnt
,但fantastic4
将引用一个全新的对象。这是通过将要复制的变量的名称放在一个对象字面量中来完成的,并在其前面使用扩展运算符:
const tmnt = {number: 4};
const fantastic4 = {...tmnt};
我们在这里实际所做的是将变量分配fantastic4
给一个新的对象字面量,然后使用扩展运算符复制tmnt
变量引用的对象的所有可枚举属性。因为这些属性是值,所以它们fantastic4
按值而不是按引用复制到对象中。
现在,对任一对象所做的任何更改都不会影响另一个对象。例如,如果我们number
将变量的属性更新fantastic4
为 5,它不会影响tmnt
变量:
fantastic4.number = 5;
fantastic4.number
<< 5
tmnt.number
<< 4
扩展运算符还有一个有用的快捷表示法,可用于制作对象的副本,然后在一行代码中对新对象进行一些更改。
例如,假设我们想要创建一个对象来为忍者神龟建模。我们可以创建第一个海龟对象,并将变量分配leonardo
给它:
const leonardo = {
animal: 'turtle',
color: 'blue',
shell: true,
ninja: true,
weapon: 'katana'
}
其他海龟都具有相同的属性,除了weapon
和color
属性,每个海龟都不同。使用扩展运算符复制引用的对象是有意义的leonardo
,然后更改weapon
andcolor
属性,如下所示:
const michaelangelo = {...leonardo};
michaelangelo.weapon = 'nunchuks';
michaelangelo.color = 'orange';
我们可以通过在对扩展对象的引用之后添加我们想要更改的属性来在一行中完成此操作。donatello
这是为变量和创建新对象的代码raphael
:
const donatello = {...leonardo, weapon: 'bo staff', color: 'purpple'}
const raphael = {...leonardo, weapon: 'sai', color: 'purple'}
请注意,以这种方式使用扩展运算符只会生成对象的浅表副本。要进行深层复制,您必须递归地执行此操作,或者使用库。就个人而言,我建议你尽量让你的对象尽可能浅