当用 let
声明一个变量,它使用的是块作用域。 不同于使用 var
声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或 for
循环之外是不能访问的。
function f(input: boolean) {
let a = 100
if (input) {
// OK: 仍然能访问到 a
let b = a + 1
return b
}
// Error: 'b' 在这里不存在
return b
}
这里我们定义了 2 个变量 a
和 b
。 a
的作用域是 f
函数体内,而 b
的作用域是 if
语句块里。
在 catch
语句里声明的变量也具有同样的作用域规则。
try {
throw 'Oh no!';
}
catch (e) {
console.log('Catch it.')
}
// Error: 'e' 在这里不存在
console.log(e)
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区。 它只是用来说明我们不能在 let
语句之前访问它们,幸运的是 TypeScript
可以告诉我们这些信息。
a++ // TS2448: Block-scoped variable 'a' used before its declaration.
let a
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为 ES2015,现代的运行时会抛出一个错误;然而,现今 TypeScript 是不会报错的。
function foo() {
// okay to capture 'a'
return a
}
// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo()
let a
关于暂时性死区的更多信息,查看这里 Mozilla Developer Network。
重定义及屏蔽
我们提过使用 var
声明时,它不在乎你声明多少次;你只会得到 1 个。
function f(x) {
var x
var x
if (true) {
var x
}
}
在上面的例子里,所有 x
的声明实际上都引用一个相同的x
,并且这是完全有效的代码,但这经常会成为 bug
的来源。幸运的是 let
的声明就不会这么宽松了。
let x = 10
let x = 20 // 错误,不能在 1 个作用域里多次声明 x
并不是要求两个均是块级作用域的声明 TypeScript 才会给出一个错误的警告。
function f(x) {
let x = 100 // Error: 干扰参数声明
}
function g() {
let x = 100
var x = 100 // Error: 不能同时具有 x 的两个声明
}
并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。
function f(condition, x) {
if (condition) {
let x = 100
return x
}
return x
}
f(false, 0) // returns 0
f(true, 0) // returns 100
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let
重写之前的 sumMatrix
函数。
function sumMatrix(matrix: number[][]) {
let sum = 0
for (let i = 0; i < matrix.length; i++) {
let currentRow = matrix[i]
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i]
}
}
return sum
}
这个版本的循环能得到正确的结果,因为内层循环的 i
可以屏蔽掉外层循环的 i
。
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好权衡一下。
块级作用域变量的获取
每次进入一个作用域时,let
会创建一个变量的环境。就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
回想一下前面 setTimeout
的例子,我们最后需要使用立即执行的函数表达式来获取每次 for
循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在 TypeScript
里这样做了。
当 let
声明出现在循环体里时拥有完全不同的行为。不仅是在循环里引入了一个新的变量环境,而且针对每次迭代都会创建这样一个新作用域,这就相当于我们在使用立即执行的函数表达式时做的事。所以在 setTimeout
例子里我们仅使用 let
声明就可以了。
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {
console.log(i)
}, 100 \* i)
}
会输出与预料一致的结果:
0
1
2
3
4
5
6
7
8
9
const 声明
const
声明是声明变量的另一种方式。
const numLivesForCat = 9
它们与 let
声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let
相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
const numLivesForCat = 9
const kitty = {
name: 'Kitty',
numLives: numLivesForCat
}
// Error
kitty = {
name: 'Tommy',
numLives: numLivesForCat
};
// OK
kitty.name = 'Jerry'
kitty.numLives--
除非你使用特殊的方法去避免,实际上 const
变量的内部状态是可修改的。 幸运的是,TypeScript
允许你将对象的成员设置成只读的。接口一章有详细说明。
let vs. const
现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。与大多数泛泛的问题一样,答案是:依情况而定。
使用最小特权原则,所有变量除了你计划去修改的都应该使用 const
。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。使用 const
也可以让我们更容易的推测数据的流动。
解构
解构数组
最简单的解构莫过于数组的解构赋值了:
let input = [1, 2]
let [first, second] = input
console.log(first) // outputs 1
console.log(second) // outputs 2
这创建了 2 个命名变量 first
和 second
。 相当于使用了索引,但更为方便:
let first = input[0]
let second = input[1]
作用于函数参数:
let input: [number, number] = [1, 2]
function f([first, second]: [number, number]) {
console.log(first)
console.log(second)
}
f(input)
你可以在数组里使用 ...
语法创建剩余变量:
let [first, ...rest] = [1, 2, 3, 4]
console.log(first) // outputs 1
console.log(rest) // outputs [ 2, 3, 4 ]
你也可以忽略你不关心的尾随元素:
let [first] = [1, 2, 3, 4]
console.log(first) // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4]
对象解构
你也可以解构对象:
let o = {
a: 'foo',
b: 12,
c: 'bar'
}
let { a, b } = o
这通过 o.a
和 o.b
创建了 a
和 b
。 注意,如果你不需要 c
你可以忽略它。
你可以在对象里使用 ...
语法创建剩余变量:
let { a, ...passthrough } = o
let total = passthrough.b + passthrough.c.length
属性重命名
你也可以给属性以不同的名字:
let { a: newName1, b: newName2 } = o
这里的语法开始变得混乱。 你可以将 a: newName1
读做 "a 作为 newName1"
。 方向是从左到右,好像你写成了以下样子:
let newName1 = o.a
let newName2 = o.b
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型,仍然需要在其后写上完整的模式。
let {a, b}: {a: string, b: number} = o
默认值
默认值可以让你在属性为 undefined
时使用缺省值:
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject
}
现在,即使 b
为 undefined
, keepWholeObject
函数的变量 wholeObject
的属性 a
和 b
都会有值。
函数声明
解构也能用于函数声明。 看以下简单的情况:
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。
function f({ a = '', b = 0 } = {}): void {
// ...
}
f()
上面的代码是一个类型推断的例子,将在后续章节介绍。
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
ction f({ a, b }: C): void {
// …
}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。
function f({ a = ‘’, b = 0 } = {}): void {
// …
}
f()
>
> 上面的代码是一个类型推断的例子,将在后续章节介绍。
>
>
>
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:
[外链图片转存中...(img-4zbjWL53-1726105681688)]
[外链图片转存中...(img-Vw0pRiUY-1726105681689)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**