一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧17: 使用 readonly 避免变量修改产生的错误
这有一段代码打印三角数:
function printTriangles(n: number) {
const nums = [];
for (let i = 0; i < n; i++) {
nums.push(i);
console.log(arraySum(nums));
}
}
这段代码很直观, 但是当你运行后:
> printTriangles(5)
0
1
2
3
4
那是因为你错误的认为: arraySum 不会修改 nums. 但是这是我的实现:
function arraySum(arr: number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) {
sum += num;
}
return sum;
}
这个函数能够计算数组的和, 但是它会去清空数组.如果能够保证 arraySum不会去修改 nums,这个问题就解决了. 这就是 readonly 的作用:
function arraySum(arr: readonly number[]) {
let sum = 0, num;
while ((num = arr.pop()) !== undefined) {
// ~~~ 'pop' does not exist on type 'readonly number[]'
sum += num;
}
return sum;
}
readonly number[] 和 number[]区别在于如下几点:
- 你可以读取数组中的元素但是不能修改
- 你可以读取数组的length, 但是不能修改length
- 你不能使用 pop,或者其他会修改这个数组的函数
由于 number[]比 readonly number[]更为严格, 可以说 number[]是readonly number[]的子集(如果不理解这一点, 见 技巧7). 所以你可以指定 可变array 给 readonly array, 反过来就不行:
const a: number[] = [1, 2, 3];
const b: readonly number[] = a;
const c: number[] = b;
// ~ Type 'readonly number[]' is 'readonly' and cannot be
// assigned to the mutable type 'number[]'
如果你在函数f中定义了参数p为 readonly , 有几件事发生:
- ts 检查 该函数f中不会改变参数p
- 调用函数f的人, 能够相信参数p不会被改变
- 调用函数f的人可以传递一个 readonly 数组
js 和 ts 通常有个约定, 除非明确指出, 函数不会改变其参数. 但是这个没有约束力的约定, 往往导致很多错误. 最好让这个约定有强制约束力, 同时有利于 人们和tsc理解.
ok, 我们来修复arraySum 函数:
function arraySum(arr: readonly number[]) {
let sum = 0;
for (const num of arr) {
sum += num;
}
return sum;
}
printTriangles 也能正确执行:
> printTriangles(5)
0
1
3
6
10
所以尽可能的将只读参数标记为: readonly , 这么做几乎没有什么缺点. 唯一的缺点可能就是: 你可能调用一个函数, 这个函数还没有来得及标记成 readonly.
readonly 可能会引起连锁反应.一但你用readonly标记了一个函数 ,其他人想要调用这个函数后, 返回值也必须标记为 readonly. 这种连锁反应非常好. 但是如果你调用了其他库的函数, 你无法修改他的 类型申明. 那么你只好求助于类型断言.
readonly 也可以捕捉由局部变量引起的整个class的计算异常.比如你正在写一个工具处理一篇小说, 你得到lines组成的数组, 你想将他们按照段落进行分组:
Frankenstein; or, The Modern Prometheus
by Mary Shelley
You will rejoice to hear that no disaster has accompanied the commencement
of an enterprise which you have regarded with such evil forebodings. I arrived
here yesterday, and my first task is to assure my dear sister of my welfare and
increasing confidence in the success of my undertaking.
I am already far north of London, and as I walk in the streets of Petersburgh,
I feel a cold northern breeze play upon my cheeks, which braces my nerves and
fills me with delight.
这是第一次尝试:
function parseTaggedText(lines: string[]): string[][] {
const paragraphs: string[][] = [];
const currPara: string[] = [];
const addParagraph = () => {
if (currPara.length) {
paragraphs.push(currPara);
currPara.length = 0; // Clear the lines
}
};
for (const line of lines) {
if (!line) {
addParagraph();
} else {
currPara.push(line);
}
}
addParagraph();
return paragraphs;
}
但是当你运行这个例子后得到:
[ [], [], [] ]
这段代码问题在于 aliasing(技巧24) 和 mutation 的同时组合. aliasing发生在:
paragraphs.push(currPara);
你将 currPara 引用push到了paragraphs中, 然后将currPara修改为空[], 那么paragraphs中也将收到空[]. 总之下列代码就是错误的根源:
paragraphs.push(currPara);
currPara.length = 0; // Clear lines
你可以通过声明readonly 来让 currPara不可变:
function parseTaggedText(lines: string[]): string[][] {
const currPara: readonly string[] = [];
const paragraphs: string[][] = [];
const addParagraph = () => {
if (currPara.length) {
paragraphs.push(
currPara
// ~~~~~~~~ Type 'readonly string[]' is 'readonly' and
// cannot be assigned to the mutable type 'string[]'
);
currPara.length = 0; // Clear lines
// ~~~~~~ Cannot assign to 'length' because it is a read-only
// property
}
};
for (const line of lines) {
if (!line) {
addParagraph();
} else {
currPara.push(line);
// ~~~~ Property 'push' does not exist on type 'readonly string[]'
}
}
addParagraph();
return paragraphs;
}
你可以通过申明用let 来申明 currPara配合是有非变动的 方法:
let currPara: readonly string[] = [];
// ...
currPara = []; // Clear lines
// ...
currPara = currPara.concat([line]);
不同与push, concat返回一个新 array, 让原来的currPara没有改变. 你现在可以自由改变currPara的指向array, 但是不改变这些array本身.
产生的错误有三种办法解决:
- 你可以复制一份currPara:
paragraphs.push([...currPara]); - 改变paragraphs的声明, 变成 readonly
注意: 这里的paragraphs 是一个可变array, 只是paragraphs每个元素是 readonly array.const paragraphs: (readonly string[])[] = []; - 可以使用断言消除错误:
paragraphs.push(currPara as string[]);
有一个很重要的警告: readonly 是浅层的.比如你有一个 readonly array , array的元素是object, 那么object是可变的.
const dates: readonly Date[] = [new Date()];
dates.push(new Date());
// ~~~~ Property 'push' does not exist on type 'readonly Date[]'
dates[0].setFullYear(2037); // OK
类似还有 readonly的object, 那么object的 属性的属性是可变的:
interface Outer {
inner: {
x: number;
}
}
const o: Readonly<Outer> = { inner: { x: 0 }};
o.inner = { x: 1 };
// ~~~~ Cannot assign to 'inner' because it is a read-only property
o.inner.x = 1; // OK
用ts-essentials里面的泛型: DeepReadonly可以实现 深度readonly,
你也可以将 readonly用于索引签名:
let obj: {readonly [k: string]: number} = {};
// Or Readonly<{[k: string]: number}
obj.hi = 45;
// ~~ Index signature in type ... only permits reading
obj = {...obj, hi: 12}; // OK
obj = {...obj, bye: 34}; // OK
这也能解决aliasing(技巧24) 和 mutation 的组合产生的错误.