Variable Declarations
Variable Declarations
变量声明
let
和const
是 JavaScript 中两种相对较新的变量声明类型。正如我们前面提到的,在某些方面let
类似var
,但允许用户避免用户在 JavaScript 中遇到的一些常见“陷阱”。const
是一个增强let
,它可以防止重新分配给一个变量。
使用 TypeScript 作为 JavaScript 的超集,该语言自然支持let
和const
。这里我们将详细阐述这些新的声明以及为什么他们更喜欢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;
}
在这里,我们有两个局部变量a
和b
。a
的范围限定在所述主体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;
这会创建新的变量,a
和b
从o.a
和o.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
作“ a
a
s 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
以及属性a
和b
,即使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]
。传播创造了一个浅拷贝first
和second
。他们没有被传播改变。
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 编译器不允许泛型函数的类型参数传播。该功能预计将在该语言的将来版本中使用。