首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Variable Declarations

变量声明

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

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

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

var 声明

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

代码语言:javascript
复制
var a = 10;

正如你可能已经想出的那样,我们只是声明了一个a用该值命名的变量10

我们也可以在函数内声明一个变量:

代码语言:javascript
复制
function f() {
  var message = "Hello, world!";

  return message;
}

我们也可以在其他函数中访问这些相同的变量:

代码语言:javascript
复制
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

代码语言:javascript
复制
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:

代码语言:javascript
复制
function f(shouldInitialize: boolean) {
  if (shouldInitialize) {
    var x = 10;
  }

  return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'

在这个例子中,一些读者可能会采取双重做法。该变量在块x中声明,但我们能够从该if块外部访问它。这是因为var声明可以在其包含的函数,模块,名称空间或全局范围内的任何地方访问 - 我们稍后将继续讨论 - 无论包含块如何。有些人称之var__-范围界定功能范围界定。参数也是函数作用域。

这些范围规则可能导致几种类型的错误。他们加重的一个问题是,多次声明相同变量不是错误的:

代码语言:javascript
复制
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引用了相同的函数范围变量。正如有经验的开发人员现在知道的那样,类似的错误会通过代码审查,并且可能是无尽的挫折源泉。

变量捕捉怪癖

快速猜测下面代码片段的输出是什么:

代码语言:javascript
复制
for (var i = 0; i < 10; i++) {
  setTimeout(function() { console.log(i); }, 100 * i);
}

对于那些不熟悉的人,setTimeout会在一定的毫秒数后尝试执行一个函数(尽管等待其他任何东西停止运行)。

Ready? Take a look:

代码语言:javascript
复制
10
10
10
10
10
10
10
10
10
10

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

代码语言:javascript
复制
0
1
2
3
4
5
6
7
8
9

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

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

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

代码语言:javascript
复制
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语句相同。

代码语言:javascript
复制
let hello = "Hello!";

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

块作用域

当一个变量被声明使用时let,它使用了一些被称为词法范围块范围的内容。与其声明var范围泄漏到其包含函数的变量不同,块范围变量在最近的包含块或for循环之外是不可见的。

代码语言:javascript
复制
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子句中声明的变量也有类似的范围规则。

代码语言:javascript
复制
try {
  throw "oh no!";
}
catch (e) {
  console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);

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

代码语言:javascript
复制
a++; // illegal to use 'a' before it's declared;
let a;

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

代码语言:javascript
复制
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声明,我们提到了你声明变量的次数并不重要; 你只有一个。

代码语言:javascript
复制
function f(x) {
  var x;
  var x;

  if (true) {
    var x;
  }
}

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

代码语言:javascript
复制
let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope

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

代码语言:javascript
复制
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'
}

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

代码语言:javascript
复制
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变量编写了我们早期的函数。

代码语言:javascript
复制
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声明进行变量捕获的想法时,我们简要地介绍了变量如何捕获之后的行为。为了更好地理解这一点,每次运行一个范围时,它会创建一个变量的“环境”。即使在其范围内的所有内容完成执行后,该环境及其捕获的变量也可以存在。

代码语言:javascript
复制
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声明。

代码语言:javascript
复制
for (let i = 0; i < 10 ; i++) {
  setTimeout(function() { console.log(i); }, 100 * i);
}

如预期的那样,这将打印出来

代码语言:javascript
复制
0
1
2
3
4
5
6
7
8
9

const 声明

const 声明是另一种声明变量的方法。

代码语言:javascript
复制
const numLivesForCat = 9;

他们就像let声明一样,但正如他们的名字所暗示的那样,他们的价值一旦被约束就无法改变。换句话说,他们有相同的范围规则let,但不能重新分配给他们。

这不应该与它们所指的值是不可改变的想法混淆。

代码语言:javascript
复制
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 开发人员网络上的文章。在本节中,我们将简要介绍一下。

数组解构

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

代码语言:javascript
复制
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

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

代码语言:javascript
复制
first = input[0];
second = input[1];

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

代码语言:javascript
复制
// swap variables
[first, second] = [second, first];

并带参数到一个函数:

代码语言:javascript
复制
function f([first, second]: [number, number]) {
  console.log(first);
  console.log(second);
}
f([1, 2]);

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

代码语言:javascript
复制
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

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

代码语言:javascript
复制
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

或其他元素:

代码语言:javascript
复制
let [, second, , fourth] = [1, 2, 3, 4];

对象解构

你也可以解构对象:

代码语言:javascript
复制
let o = {
  a: "foo",
  b: 12,
  c: "bar"
};
let { a, b } = o;

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

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

代码语言:javascript
复制
({ a, b } = { a: "baz", b: 101 });

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

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

代码语言:javascript
复制
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

属性重命名

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

代码语言:javascript
复制
let { a: newName1, b: newName2 } = o;

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

代码语言:javascript
复制
let newName1 = o.a;
let newName2 = o.b;

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

代码语言:javascript
复制
let { a, b }: { a: string, b: number } = o;

默认值

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

代码语言:javascript
复制
function keepWholeObject(wholeObject: { a: string, b?: number }) {
  let { a, b = 1001 } = wholeObject;
}

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

函数声明

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

代码语言:javascript
复制
type C = { a: string, b?: number }
function f({ a, b }: C): void {
  // ...
}

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

代码语言:javascript
复制
function f({ a, b } = { a: "", b: 0 }): void {
  // ...
}
f(); // ok, default to { a: "", b: 0 }

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

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

代码语言:javascript
复制
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

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

传播

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

代码语言:javascript
复制
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:

代码语言:javascript
复制
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

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

代码语言:javascript
复制
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

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

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

代码语言:javascript
复制
class C {
  p = 12;
  m() {
  }
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

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

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com