回想一下Basic TypeScript,当我们讨论快照图时,有些对象是不可变的:一旦创建,它们总是表示相同的值。其他对象是可变的:它们具有更改对象值的方法。
字符串是不可变类型的一个示例。对象始终表示相同的字符串。数组是可变类型的一个示例。string
由于是不可变的,因此一旦创建,对象始终具有相同的值。要在 的末尾添加一些内容,您必须创建一个新对象:string
string
string
string
let s = "a";
s = s.concat("b"); // s+="b" and s=s+"b" also mean the same thing
相比之下,对象是可变的。此类具有更改对象值的方法,而不仅仅是返回新值:Array
let sarr:Array<string> = [ "a" ];
sarr.push("b");
那又怎样?在这两种情况下,您最终都会得到并基本上引用 字符序列 。当只有一个对对象的引用时,可变性和不可变性之间的区别并不重要。但是,当存在对对象的其他引用时,它们的行为方式存在很大差异。例如,当另一个变量指向与 相同的对象,而另一个变量指向相同的对象时,则不可变对象和可变对象之间的差异变得更加明显:s
sarr
ab
t
string
s
tarr
Array
sarr
let t = s;
t = t + "c";
let tarr = sarr;
tarr.push("c");
快照图显示,更改对 没有影响,但更改也会受到影响 - 可能让程序员感到惊讶。这就是我们在阅读中将要讨论的问题的本质。
可变类型似乎比不可变类型强大得多。如果你在Datatype超市购物,你必须在不可变的ReadonlyArray和一个超级强大的可变数组之间做出选择,那么你为什么要选择不可变的数组呢? 应该能够做所有能做的事,加上和其他一切。Array
ReadonlyArray
push()
pop()
答案是,不可变类型更安全,更容易被错误影响,更容易理解,并且更容易进行更改。可变性使得理解程序正在做什么变得更加困难,并且更难执行合同。这里有两个例子说明了原因。
风险示例 #1:传递可变值
让我们从一个简单的函数开始,该函数对数组中的数字求和:
/** * @returns the sum of the numbers in the array */
function sum(list:Array<number>):number {
let sum = 0;
for (const x of list) {
sum += x;
}
return sum;
}
假设我们还需要一个对绝对值求和的函数。遵循良好的 DRY 实践(不要重复自己),实现者编写一个使用的方法:sum()
/** * @returns the sum of the absolute values of the numbers in the array */
function sumAbsolute(list:Array<number>):number {
// let's reuse sum(), because DRY, so first we take absolute values
for (let i = 0; i < list.length; ++i) {
list[i] = Math.abs(list[i]);
}
return sum(list);
}
请注意,此方法通过直接更改数组来完成其工作。对于实现者来说,这似乎是明智的,因为重用现有数组会更有效率。如果数组的长度为数百万个项目,则可以节省生成新的百万个项目绝对值数组的时间和内存。因此,实现者有两个很好的设计理由:DRY和性能。
但是由此产生的行为对任何使用它的人来说都会非常令人惊讶!例如:
// meanwhile, somewhere else in the code...
let myData:Array<number> = [-5, -3, -2];
console.log(sumAbsolute(myData));
console.log(sum(myData));
此代码将打印什么?会是 10 后跟 -10 吗?还是别的什么?
风险示例 #2:返回可变值
我们刚刚看到一个示例,其中将可变对象传递给方法会导致问题。如何返回可变对象?
让我们考虑一下 Date,它是内置的 JavaScript 类之一。 碰巧是可变类型。假设我们写一个确定春天第一天的方法:Date
/** * @returns the first day of spring this year */
public static startOfSpring():Date {
return this.askGroundhog();
}
在这里,我们使用众所周知的土拨鼠算法来计算春天何时开始(Harold Ramis,Bill Murray等人土拨鼠日,1993)。
客户开始使用这种方法,例如计划他们的大型派对:
// somewhere else in the code...
public static partyPlanning():void {
let partyDate:Date = this.startOfSpring();
// ...
}
所有的代码都有效,人们很高兴。现在,独立地,发生了两件事。首先,的实施者意识到土拨鼠开始因为不断被问及春天何时开始而感到恼火。因此,代码被重写为最多询问土拨鼠一次,然后缓存土拨鼠的答案以备将来调用:startOfSpring()
/** * @returns the first day of spring this year */
public static startOfSpring():Date {
if (this.groundhogAnswer === null) {
this.groundhogAnswer = this.askGroundhog();
}
return this.groundhogAnswer;
}
private static groundhogAnswer:Date = null;
(题外话:请注意对缓存的答案使用私有静态变量。你会认为这是一个全局变量,还是不考虑?
其次,其中一位客户认为春天的实际第一天对派对来说太冷了,所以派对将在一个月后进行:startOfSpring()
// somewhere else in the code...
public static partyPlanning():void {
// let's have a party one month after spring starts!
let partyDate:Date = this.startOfSpring();
partyDate.setMonth(partyDate.getMonth() + 1);
// ...
}
当这两个决策相互作用时会发生什么?更糟糕的是,想想谁会首先发现这个错误 - 它会吗?会吗?还是会是一些完全无辜的第三段代码也调用?startOfSpring()
partyPlanning()
startOfSpring()