在线文档教程

Variable Declarations

Variable Declarations

变量声明

letconst是 JavaScript 中两种相对较新的变量声明类型。正如我们前面提到的,在某些方面let类似var,但允许用户避免用户在 JavaScript 中遇到的一些常见“陷阱”。const是一个增强let,它可以防止重新分配给一个变量。

使用 TypeScript 作为 JavaScript 的超集,该语言自然支持letconst。这里我们将详细阐述这些新的声明以及为什么他们更喜欢var

如果您已经使用 JavaScript,下一节可能是刷新记忆的好方法。如果你非常熟悉JavaScript中所有的var声明,可能会发现它更容易跳过。

var 声明

在 JavaScript 中声明一个变量一直以来都是用var关键字来完成的。

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捕获了a声明的变量f。在任何g被调用的地方,价值a都会与a 的值相关联f。即使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 declarations have some odd scoping rules for those used to other languages. Take the following example:

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会在一定的毫秒数后尝试执行一个函数(尽管等待其他任何东西停止运行)。

Ready? Take a look:

10 10 10 10 10 10 10 10 10 10

许多 JavaScript 开发人员都非常熟悉这种行为,但如果你感到惊讶,你当然不会孤单。大多数人期望输出是

0 1 2 3 4 5 6 7 8 9

还记得我们之前提到的变量捕获吗?我们传递的每个函数表达式setTimeout实际上都是i从相同的作用域引用的。

让我们花一分钟来考虑这意味着什么。setTimeout将在几毫秒后运行一个函数,但只有for循环停止执行后才会运行; 在for循环停止执行时,i的值10。所以每次给定的函数被调用时,它都会打印出来10

常见的解决方法是使用 IIFE(立即调用的函数表达式)i在每次迭代中捕获:

for (var i = 0; i < 10; i++) { // capture the current state of 'i' // by invoking a function with its current value (function(i) { setTimeout(function() { console.log(i }, 100 * i })(i }

这种奇怪的模式其实很常见。该i参数列表实际的阴影i中宣告for循环,但由于我们将它们命名为相同的,我们没有太多修改循环体。

let 声明

到目前为止,你已经发现var存在一些问题,这就是为什么要let引入语句。除了使用的关键字之外,let语句的写法与var语句相同。

let hello = "Hello!";

关键的区别不在于语法,而在于语义,我们现在将深入分析。

块作用域

当一个变量被声明使用时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; }

在这里,我们有两个局部变量aba的范围限定在所述主体f的同时b的范围仅限于含if语句的块。

catch子句中声明的变量也有类似的范围规则。

try { throw "oh no!"; } catch (e) { console.log("Oh well." } // Error: 'e' doesn't exist here console.log(e

块范围变量的另一个属性是它们在实际声明之前不能被读取或写入。虽然这些变量在整个范围内“存在”,但直到他们声明的所有点都是它们临时死区的一部分。这只是一种复杂的说法,你不能在let声明之前访问它们,幸好 TypeScript 会让你知道这一点。

a++; // illegal to use 'a' before it's declared; let a;

需要注意的是,在声明之前,您仍然可以捕获块范围的变量。唯一的问题是在声明之前调用该函数是非法的。如果瞄准 ES2015,现代运行时会抛出一个错误; 但是,现在 TypeScript 是宽容的,不会将此报告为错误。

function foo() { // okay to capture 'a' return a; } // illegal call 'foo' before 'a' is declared // runtimes should throw an error here foo( let a;

有关时间死区的更多信息,请参阅 Mozilla 开发者网络上的相关内容。

重新宣布和阴影

通过var声明,我们提到了你声明变量的次数并不重要; 你只有一个。

function f(x) { var x; var x; if (true) { var x; } }

在上面的例子中,所有的声明x实际上都是相同的 x,这是完全有效的。这通常最终成为错误的来源。值得庆幸的是,let宣言并非如此宽容。

let x = 10; let x = 20; // error: can't re-declare 'x' in the same scope

这些变量不一定都需要为 TypeScript 提供块范围,以告诉我们存在问题。

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' }

这并不是说块范围变量永远不能用函数范围变量来声明。块范围变量只需要在明显不同的块中声明。

function f(condition, x) { if (condition) { let x = 100; return x; } return x; } f(false, 0 // returns '0' f(true, 0 // returns '100'

在更多嵌套的作用域中引入新名称的操作称为阴影。这是一把双刃剑,它可以在意外隐藏的情况下自行引入某些错误,同时也可以防止某些错误。例如,假设我们已经sumMatrix使用let变量编写了我们早期的函数。

function sumMatrix(matrix: number[][]) { let sum = 0; for (let i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (let i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }

这个版本的循环实际上会执行正确的求和,因为内部循环的外部循环的i阴影i

为了编写更清晰的代码,通常应避免使用阴影。虽然在某些情况下可能适合利用它,但您应该使用最佳判断。

块范围变量捕获

当我们首先谈到使用var声明进行变量捕获的想法时,我们简要地介绍了变量如何捕获之后的行为。为了更好地理解这一点,每次运行一个范围时,它会创建一个变量的“环境”。即使在其范围内的所有内容完成执行后,该环境及其捕获的变量也可以存在。

function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity( }

因为我们已经city从其环境中捕获了数据,所以尽管if块已经执行完毕,我们仍然可以访问它。

回想一下,在我们之前的setTimeout例子中,我们最终需要使用 IIFE 捕获for循环中每次迭代的变量状态。实际上,我们正在为捕获的变量创建一个新的变量环境。这有点痛苦,但幸运的是,在 TypeScript 中你再也不需要这样做了。

let当声明为循环的一部分时,声明具有截然不同的行为。这些声明不是只为循环本身引入新的环境,而是每次迭代都会创建一个新的范围。既然这就是我们对IIFE所做的一切,我们可以将我们的旧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: "Aurora", numLives: numLivesForCat, } // Error kitty = { name: "Danielle", numLives: numLivesForCat }; // all "okay" kitty.name = "Rory"; kitty.name = "Kitty"; kitty.name = "Cat"; kitty.numLives--;

除非您采取特定措施来避免它,否则const变量的内部状态仍可修改。幸运的是,TypeScript 允许您指定对象的成员readonly。关于接口的章节有详细信息。

let vs. const

鉴于我们有两种类似的范围语义的声明,很自然地发现我们要求使用哪一种。像大多数广泛的问题一样,答案是:这取决于。

应用最小权限原则,除了您打算修改的所有声明都应该使用const。其基本原理是,如果一个变量不需要写入,其他人在相同的代码库上工作时不应该自动地写入该对象,并且需要考虑他们是否真的需要重新分配给变量。const在推理数据流时,使用代码也会使代码更具可预测性。

另一方面,let不会再写出来var,而且很多用户会更喜欢它的简洁。本手册的大部分使用let声明来表示这种兴趣。

使用你的最佳判断,如果适用,与你的团队的其他人讨论此事。

解构

TypeScript 的另一个 ECMAScript 2015 功能是解构。有关完整的参考资料,请参阅 Mozilla 开发人员网络上的文章。在本节中,我们将简要介绍一下。

数组解构

最简单的解构形式是数组解构赋值:

let input = [1, 2]; let [first, second] = input; console.log(first // outputs 1 console.log(second // outputs 2

这将创建两个名为first和second的新变量。这相当于使用索引,但更方便:

first = input[0]; second = input[1];

解构与已经声明的变量一起工作:

// swap variables [first, second] = [second, first];

并带参数到一个函数:

function f([first, second]: [number, number]) { console.log(first console.log(second } f([1, 2]

您可以使用以下语法为列表中的其余项创建变量...

let [first, ...rest] = [1, 2, 3, 4]; console.log(first // outputs 1 console.log(rest // outputs [ 2, 3, 4 ]

当然,因为这是 JavaScript,你可以忽略你不关心的尾随元素:

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;

这会创建新的变量,abo.ao.b。请注意,c如果你不需要它,你可以跳过。

像数组解构一样,你可以在没有声明的情况下赋值:

{ a, b } = { a: "baz", b: 101 }

请注意,我们必须用括号括住这条语句。JavaScript 通常将a分解{为块的开始。

您可以使用以下语法为对象中的其余项创建变量...

let { a, ...passthrough } = o; let total = passthrough.b + passthrough.c.length;

属性重命名

你也可以给属性赋予不同的名称:

let { a: newName1, b: newName2 } = o;

这里的语法开始变得混乱。你可以读a: newName1作“ aas newName1”。方向是从左到右,就像你写了:

let newName1 = o.a; let newName2 = o.b;

令人困惑的是,结肠这里并没有注明型号。如果您指定了该类型,则在整个解构之后仍然需要编写该类型:

let { a, b }: { a: string, b: number } = o;

默认值

在属性未定义的情况下,默认值可让您指定默认值:

function keepWholeObject(wholeObject: { a: string, b?: number }) { let { a, b = 1001 } = wholeObject; }

keepWholeObject现在有一种用于可变wholeObject以及属性ab,即使b是未定义的。

函数声明

解构也适用于函数声明。对于简单的情况,这很简单:

type C = { a: string, b?: number } function f{ a, b }: C): void { // ... }

但是指定默认值对于参数更为常见,并且通过解构来获取默认值是非常棘手的。首先,你需要记住把模式放在默认值之前。

function f{ a, b } = { a: "", b: 0 }): void { // ... } f( // ok, default to { a: "", b: 0 }

以上片段是类型推断的一个例子,稍后在手册中进行解释。

然后,您需要记住为解构结构属性(而不是主构造器)提供可选属性的默认值。请记住,它C是用b可选项定义的:

function f{ a, b = 0 } = { a: "" }): void { // ... } f{ a: "yes" } // ok, default b = 0 f( // ok, default to { a: "" }, which then defaults b = 0 f{} // error, 'a' is required if you supply an argument

谨慎使用解构。正如前面的例子所示,除了最简单的解构表达式之外,任何事情都会让人困惑。这是深层嵌套的解构,它得到更是如此真的很难理解,甚至没有重命名,默认值,然后键入注释。尽量保持解构表达式小而简单。你总是可以写出解构将自己产生的作业。

传播

传播运算符与解构相反。它允许您将数组分散到另一个数组中,或将一个对象分散到另一个对象中。例如:

let first = [1, 2]; let second = [3, 4]; let bothPlus = [0, ...first, ...second, 5];

这给了两个加值[0, 1, 2, 3, 4, 5]。传播创造了一个浅拷贝firstsecond。他们没有被传播改变。

You can also spread objects:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; let search = { ...defaults, food: "rich" };

现在search{ food: "rich", price: "$$", ambiance: "noisy" }。对象传播比阵列传播更复杂。像数组传播一样,它从左到右继续,但结果仍然是一个对象。这意味着稍后扩展对象中的属性将覆盖之前发布的属性。因此,如果我们修改前面的示例以在最后传播:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; let search = { food: "rich", ...defaults };

然后food属性在defaults重写food: "rich",这不是我们在这种情况下想要的。

对象传播还有其他一些令人惊讶的限制。首先,它只包含一个对象自己的可枚举属性。基本上,这意味着当你传播一个对象的实例时你会失去方法:

class C { p = 12; m() { } } let c = new C( let clone = { ...c }; clone.p; // ok clone.m( // error!

其次,Typescript 编译器不允许泛型函数的类型参数传播。该功能预计将在该语言的将来版本中使用。