声明变量
本课程介绍了 TypeScript 中仍支持的过时关键字 var,并说明了使用这种传统方式声明变量所带来的代价。我们将学习如何通过 let 和 const 来优化变量声明。
使用 var 声明变量
我们从 var 开始。自 JavaScript 诞生以来,var 就是定义变量的传统方式。然而,ECMAScript2015 的发布引入了 let 和 const 这两种新的声明方式,它们解决了早期版本中存在的一些问题。
在许多现代编程语言中,声明一个变量的规则非常简单:变量的作用域仅限于其被定义的位置。
例如,在条件语句中声明的变量,只能在该条件语句内部访问,而不能在它之前或之后使用。
var 的一个问题在于变量的作用域位置难以预测。当 var 在函数内部声明时,它是函数作用域的;而当它在函数外部声明时,则是全局作用域的。此外,var 不会阻止你重复定义同一个变量,这会导致初始声明或初始化被覆盖。
1function varFunction(){ 2 var x = "111"; 3 if(true){ 4 var x = "999"; // 变量 x 被重新定义 5 } 6 console.log(x); 7} 8varFunction()
如上例所示,由于变量 x 位于 if 语句的闭包之外,新声明(第4行)会重新定义 x,并覆盖其之前的值(第2行)。
但新的定义仍然局限于函数的作用域内。例如,尝试在函数外部输出 x 时,结果将是 undefined。
1var x; 2function varFunction(){ 3 var x = "111"; 4 if(true){ 5 var x = "999"; 6 } 7 console.log(x); 8} 9varFunction() 10console.log(x)
需要注意的是,如果不声明变量 x,TypeScript 会提示“Cannot find name x”。但如果 TypeScript 没有启用 strict: true,代码虽然会报错,但仍能生成 JavaScript 文件,并运行后显示 undefined。
1var x; 2function varFunction(){ 3 var x = "111"; 4} 5console.log(x)
TypeScript 允许在发现错误时停止生成 JavaScript 文件。通常建议在 TypeScript 发现问题时不要生成 JavaScript 文件。为了启用此功能,请打开 TypeScript 配置文件 tsconfig.json,并将 "noEmitOnError": true 设置为 true。在上述最后一个例子中,代码将不会生成任何 JavaScript 文件。
使用 let 声明变量
let 关键字通过将变量的生命期限定在其声明的块内来解决问题。这种按块作用域定义变量的行为,正是许多现代语言中的标准做法。花括号 {} 定义了一个块。
例如,如果你在 if 语句中用 let 声明一个变量,一旦执行离开该 if 语句,这个变量就不可再访问。这一规则适用于函数、循环和类。在循环中,如果你定义一个 for 循环,并且使用 let 声明循环变量 i,那么它只在循环内部有效,同时允许修改其值。
1function letFunction() { 2 let x = "111"; 3 if (true) { 4 let x = 999; 5 } 6 console.log(x); 7} 8letFunction();
在这个例子中,对比 let 和 var 有两个要点:
-
尽管输出结果符合预期,意味着值
999仅存在于条件语句的作用域中,而值"111"仅存在于函数作用域中。 -
由于这两个
x是两个独立的变量,它们可以有不同的类型。目前我们没有显式指定类型,但 TypeScript 会推断出前者是string类型,后者是number类型。
📜注意:以下代码应引发错误,如解释所示 ❌
1function varLetFunction(){ 2 var myStringValue = "varStringValue"; 3 let myStringValueLet = "letStringValue"; 4 if(true){ 5 var myStringValue = 123; 6 let myStringValueLet = 12345; 7 } 8}
上述代码无法编译,原因是 var 变量的问题。你会看到如下错误:
1error TS2403: 后续变量声明必须具有相同类型。变量 'myStringValue' 必须是 'string' 类型,但此处为 'number' 类型。
正如所见,var 会造成混淆,而 TypeScript 通过提供提示帮助解决这个问题。作为一条经验法则,自从 let 和 const 被广泛采用后,var 已不再使用,因为它们提供了更清晰的作用域定义。
关于作用域,let、var 或即将出现的 const 都允许内部作用域能够访问外部变量。例如,如果你在一个模块、命名空间或全局范围内定义了一个变量,就可以在函数内部访问它。
1var myVarVariableOutSide = "I am outside"; 2let myLetVariableOutSide = "I am outside too"; 3function letOutsideFunction() { 4 console.log(myVarVariableOutSide); 5 console.log(myLetVariableOutSide); 6} 7letOutsideFunction();
条件语句和循环语句也是如此。
1var myVarVariableOutSide = "I am outside"; 2let myLetVariableOutSide = "I am outside too"; 3function letOutsideFunction2() { 4 myVarVariableOutSide += " and inside"; 5 myLetVariableOutSide += " and inside as well"; 6 console.log(myVarVariableOutSide); 7 console.log(myLetVariableOutSide); 8 if(true){ 9 console.log(myVarVariableOutSide); 10 console.log(myLetVariableOutSide); 11 } 12} 13letOutsideFunction2();
使用 const 声明变量
const(常量)关键字在作用域生命周期方面与 let 相似。但它只能在声明时初始化一次。这一限制非常强大,不仅从语法上表明该变量不应被更改,TypeScript 也会确保无法再次赋值。需要理解的是,如果一个变量是一个常量对象,那么该对象的引用不能改变,但其内部属性仍可修改。
📜注意:以下代码应引发错误,如解释所示 ❌
1const x = "111"; 2x = "this won't compile";
对常量变量成员的赋值是允许的。例如,假设你声明并初始化一个变量来保存当前应用的用户信息为常量,你就不能再将当前用户赋值为其他用户。下面的代码展示了 user2 无法被赋给 user1。
📜注意:以下代码应引发错误,如解释所示 ❌
1const user1 = { id: 1, name: "MyName1" }; 2const user2 = { id: 2, name: "MyName2" }; 3 4user1 = user2; // 不能给常量赋值!
但是,如果对象有公开成员,你可以修改其属性值。
1const user1 = { id: 1, name: "MyName1" }; 2const user2 = { id: 2, name: "MyName2" }; 3 4user1.name = user2.name; // 合法! 5user1.id = 1000; // 合法!
在开发过程中,你会发现大多数变量都可以声明为 const。这为直接赋值提供了更强的保护。
接下来讨论数组与常量。
1function constFunction() { 2 const myList = [1, 2, 3]; 3 myList.push(4); 4 console.log(myList); 5} 6constFunction();
上述代码可以正常运行,因为尽管列表内容发生了变化,但指向该列表的引用并未改变。对于对象也是一样:你可以修改对象内部的值,但不能将一个新的对象赋给该变量。否则,TypeScript 会报错,如下所示:
1error TS2588: 无法分配给 'myList',因为它是一个常量。
📜注意:以下代码应引发错误,如解释所示 ❌
1function constBlockedFunction() { 2 const myList = [1, 2, 3]; 3 myList = [1, 2, 3, 4] 4 console.log(myList); 5} 6constBlockedFunction();
下面是一个类似对象的例子。obj1 使用 let 声明,可以通过 = 重新赋值。但 obj2 不能被重新赋值。尽管 obj2 是 const 声明,其引用不能改变,但其内部的值是可以修改的。在下面的代码片段中,如果删除第6行,代码将可以编译。
📜注意:以下代码应引发错误,如解释所示 ❌
1function constChangeObject() { 2 let obj1 = { p1: "p1value" }; 3 obj1 = { p1: "p1value changed" }; 4 5 const obj2 = { p2: "p2value" }; 6 obj2 = { p2: "Does not compile" }; 7 obj2.p2 = "Work!"; 8}
关于 TypeScript 和变量的一点小提示:TypeScript 对拼写错误非常敏感。例如,如果你不小心写了 mylist 而不是 myList,代码将无法编译。这对所有声明方式(var、let、const)都适用。
错误信息还会提供修正建议。例如,以下代码会给出:
1error TS2552: 无法找到名称 'mylist'。您是否想写 'myList'?
📜注意:以下代码应引发错误,如解释所示 ❌
1function constBlockedFunction() { 2 const myList = [1, 2, 3]; 3 mylist = [1, 2, 3, 4] 4 console.log(myList); 5} 6constBlockedFunction();
const 是声明变量的首选方式。它是最严格的,能确保变量在未来不会被更改。
关于类型
到目前为止,你已经声明了变量但未指定其类型。然而,TypeScript 的一个特性就是能够自动为所有变量推断类型。
在接下来的课程中,我们将学习各种基本类型和自定义类型,以及 TypeScript 如何自动使用这些类型,还有如何显式地为变量指定类型。
简而言之,指定类型是在变量名后使用冒号完成的。
1var varName1: number; 2let varName2: string; 3const varName3: boolean;
无论采用隐式还是显式方式,一旦变量类型被确定,TypeScript 都会尊重并强制执行该类型。
避免类型变更有助于保持代码的一致性。