TypeScript 变量声明
-
定义
let 和 const 是 JavaScript 里相对较新的变量声明方式。 像我们之前提到过的,let 在很多方面与 var 是相似的,但是可以帮助大家避免在 JavaScript 里常见一些问题。 const 是对 let 的一个增强,它能阻止对一个变量再次赋值。因为 TypeScript 是 JavaScript 的超集,所以它本身就支持 let 和 const。下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var。如果你之前使用 JavaScript 时没有特别在意,那么这节内容会唤起你的回忆。 如果你已经对 var 声明的怪异之处了如指掌,那么你可以轻松地略过这节。
-
var 声明
一直以来我们都是通过 var 关键字定义 JavaScript 变量。var a = 10;
大家都能理解,这里定义了一个名为 a 值为 10 的变量。我们也可以在函数内部定义变量:function f() { var message = "Hello, world!"; return message; }
并且我们也可以在其它函数内部访问相同的变量。function f() { var a = 10; return function g() { var b = a + 1; return b; } } var g = f(); g(); // returns 11;
上面的例子里,g 可以获取到f函数里定义的 a 变量。 每当g被调用时,它都可以访问到 f 里的 a 变量。 即使当 g 在 f 已经执行完后才被调用,它仍然可以访问及修改 a。function f() { var a = 1; a = 2; var b = g(); a = 3; return b; function g() { return a; } } f(); // returns 2
作用域规则
对于熟悉其它语言的人来说,var 声明有些奇怪的作用域规则。 看下面的例子:function f(shouldInitialize: boolean) { if (shouldInitialize) { var x = 10; } return x; } f(true); // returns '10' f(false); // returns 'undefined'
有些读者可能要多看几遍这个例子。 变量 x 是定义在 if 语句里面,但是我们却可以在语句的外面访问它。 这是因为 var 声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为 var 作用域或函数作用域。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:function sumMatrix(matrix: number[][]) { var sum = 0; for (var i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (var i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }
这里很容易看出一些问题,里层的 for 循环会覆盖变量 i,因为所有i都引用相同的函数作用域内的变量;有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。快速的猜一下下面的代码会返回什么:for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); }
介绍一下,setTimeout 会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。好吧,看一下结果:10 10 10 10 10 10 10 10 10 10
很多 JavaScript 程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:0 1 2 3 4 5 6 7 8 9
让我们花点时间考虑在这个上下文里的情况;setTimeout 在若干毫秒后执行一个函数,并且是在for循环结束后;for 循环结束后,i的值为 10;所以当函数被调用的时候,它会打印出 10!一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:for (var i = 0; i < 10; i++) { // 捕获“ i”的当前状态 // 通过调用具有当前值的函数 (function(i) { setTimeout(function() { console.log(i); }, 100 * i); })(i); }
这种奇怪的形式我们已经司空见惯了。 参数 i 会覆盖 for 循环里的 i,但是因为我们起了同样的名字,所以我们不用怎么改 for 循环体里的代码。 -
let 声明
当用 let 声明一个变量,它使用的是词法作用域或块作用域;不同于使用 var 声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或 for 循环之外是不能访问的。function f(input: boolean) { let a = 100; if (input) { // Still okay to reference 'a' let b = a + 1; return b; } // Error: 'b' doesn't exist here return b; }
这里我们定义了2个变量a和b。 a的作用域是f函数体内,而b的作用域是if语句块里。拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区。 它只是用来说明我们不能在 let 语句之前访问它们,幸运的是 TypeScript 可以告诉我们这些信息。a++; // illegal to use 'a' before it's declared; let a; let x = 10; let x = 20; // 错误,不能在1个作用域里多次声明`x` function f(x) { let x = 100; // error: interferes with parameter declaration } function g() { let x = 100; var x = 100; // error: can't have both declarations of 'x' }
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。function foo() { // okay to capture 'a' return a; } // 不能在'a'被声明前调用'foo' // 运行时应该抛出错误 foo(); let a;
当 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 与 let 声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变;换句话说,它们拥有与let相同的作用域规则,但是不能对它们重新赋值。这很好理解,它们引用的值是不可变的。const numLivesForCat = 9; const kitty = { name: "Aurora", numLives: numLivesForCat, } // Error kitty = { name: "Danielle", numLives: numLivesForCat }; // all "okay" kitty.name = "Rory"; kitty.name = "Kitty"; kitty.name = "Cat"; kitty.numLives--;
除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的;幸运的是,TypeScript允许你将对象的成员设置成只读的;接口一章有详细说明。