likes
comments
collection
share

变量声明 Variable Declaration-官网Reference

作者站长头像
站长
· 阅读数 16

Variable Declaration

letconst 是 JavaScript 中变量声明的两个相对较新的概念。let 在某些方面类似于 var,但增加了块级作用域和暂时性死区等特性。

constlet 的扩充,它可以防止给变量重新赋值。

TypeScript 是 JavaScript 的扩展,该语言自然支持 letconst。在这里,我们将详细说明这些新声明以及它们为什么比 var 更好。

如果你非常熟悉 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 捕获了在 f 中声明的变量 a。在 g 被调用的时候,a 的值将与 f 中的 a 的值绑定。即使在 f 完成运行后调用 g,它也能够访问和修改 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 声明没有块级作用域,只有函数作用域。参数也是函数作用域的。

这些作用域规则可能导致几种类型的错误。更恶劣的问题是,多次声明同一个变量并不是错误:

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

对于一些经验丰富的 JavaScript 开发人员来说,这可能很容易发现,内部 for 循环会意外地覆盖变量 i,因为 i 引用了相同的函数作用域变量。

变量捕获怪癖

猜一下以下代码片段会输出什么:

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100 * i);
}

对于不熟悉 JavaScript 的人来说,会认为 setTimeout 将在一定毫秒数后执行回调,打印 0-9。

可事实是:

10 10 10 10 10 10 10 10 10 10

确实是一定毫秒后执行了,可是打印的却都是 10setTimeout 的每个函数表达式实际上都引用了来自同一作用域的同一个 i

让我们花点时间考虑一下这是什么意思。setTimeout 将在一定毫秒后运行一个回调,但是只会在 for 循环执行完毕之后执行;也就是说,for 循环执行还未完毕,就算 seTimeout 毫秒数时间到了,也是不会执行的。(可查阅 JS 微任务,宏任务相关资料)

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 实际上储存了 for 循环中声明的 i,只是由于我们将它们命名为相同的名称。(可查阅 JS 闭包相关资料)

let 声明

到目前为止,你已经发现 var 存在的一些问题,这正是引入 let 语句的原因。除了关键字不同之外,let 语句的编写方式与 var 语句相同。

let hello = "Hello!";

关键的区别不在于语法,而在于语义,现在让我们深入研究这一点。

块级作用域

当使用 let 声明变量时,它使用了所谓的 词法作用域 或 块作用域。块作用域的变量在 最近的包含块 或 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++; // 在声明之前非法使用 'a'
let a;

下面例子中,需要注意的是,你仍然可以在声明块级作用域变量之前捕获它。唯一的问题是,在声明之前调用该函数是非法的。如果目标是 ES2015,运行时将抛出错误;然而,在 TypeScript 是允许的,不会将此报告为错误,但是编译为 JS 运行时还是会抛出错误:

function foo() {
  // 可以捕获 'a'
  return a;
}
// 在声明 'a' 之前非法调用 'foo'
// 运行时应该在这里抛出一个错误
foo();
let a;

有关暂时性死区的更多信息,请参阅 Mozilla Developer Network

重新声明和遮蔽

var 声明中,我们提到过你在相同作用域声明变量多少次并不重要;你只会得到同一个:

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

在上面的例子中,x 的所有声明实际上都指向同一个 x,这是完全有效的。但也往往最终是成为 bug 的来源。值得庆幸的是,let 声明没有那么宽松:

let x = 10;
let x = 20; // error: 不能在相同作用域重复声明 'x'

重复声明 TypeScript 就会告诉我们有问题:

function f(x:number) {
  let x = 100; // error: 与参数冲突
}
function g() {
  let x = 100;
  var x = 100; // error: 不能声明两个 'x'
}

这并不是说块级作用域的变量永远不能用函数作用域的变量声明。而是,块级作用域的变量需要在一个明显不同的块中声明:

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

在多层嵌套的作用域中,使用上层作用域变量名称的行为称为遮蔽。例如,假设我们使用 let 变量编写之前的 sumMatrix 函数:

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 循环的每次迭代的变量状态。实际上,我们所做的是为我们捕获的变量创建一个新的变量环境。这有点痛苦,但幸运的是,你再也不用这么做了。

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 允许你指定对象的成员是只读的。详情可参考 接口章节

let vs. const

letconst声明类型具有相同的作用域语义,我们很自然地会问应该使用哪一种。答案是:视情况而定。

应用 最小权限原则,除了计划修改的声明外,所有声明都应该使用 const。其基本原理是,如果一个变量不需要写入,那么在同一代码库上工作的其他人也不应该能够写入对象,不需要考虑他们是否需要重新赋值给该变量。在对数据流进行推理时,使用 const 还使代码更加可预测。

如果觉得适用的话,可以和你团队的其他成员商量一下。

本手册的大部分使用 let 声明。

解构

TypeScript 的另一个 ECMAScript 2015 特性是解构。要获得完整的参考资料,请参阅 Mozilla Developer Network。在本节中,我们将简要概述。

数组解构

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

let input = [1, 2];

let [first, second] = input;

console.log(first); // outputs 1

console.log(second); // outputs 2

这将创建两个新变量,名为 firstsecond。这等同于使用索引,但更方便:

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

解构也适用于已经声明的变量:

// 变量互换
[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 ]

可以忽略你不关心的尾随元素:

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

或不关心某些元素:

let [, second, , fourth] = [1, 2, 3, 4];
console.log(second); // outputs 2
console.log(fourth); // outputs 4

元组解构

元组可以像数组一样被解构;解构变量获得相应元组元素的类型:

let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c] = tuple; // a: number, b: string, c: boolean

在元组的元素范围之外解构元组是错误的:

let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c, d] = tuple; // Error, 索引 3 没有元素

与数组一样,你可以使用 ... 来解构元组的其余部分,获得另一个更短的元组:

let tuple: [number, string, boolean] = [7, "hello", true];
let [a, ...bc] = tuple; // bc: [string, boolean]
let [a, b, c, ...d] = tuple; // d: [], 空元组

或者忽略尾随元素或其他元素:

let [a] = tuple; // a: number
let [, b] = tuple; // b: string

对象解构

你也可以解构对象:

let o = {
  a: "foo",
  b: 12,
  c: "bar",
};
let { a, b } = o;

这将从 o.ao.b 中创建新的变量 ab。注意,如果不需要 c,可以跳过它。

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

({ a, b } = { a: "baz", b: 101 });

注意,我们必须用圆括号把这个语句括起来。JavaScript 通常解析 { 为"块"的开头。

你可以使用语法 ... 为对象中的剩余项创建一个变量:

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

属性重命名

你也可以给属性起不同的别名:

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

这里的语法开始变得奇怪。实际上你可以把 a: newName1 读成 "a as newName1"。方向是从左到右:

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

令人困惑的是,这里的冒号并不表示类型。如果要指定类型,需要在整个解构之后写入:

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

默认值

默认值,允许你在属性未定义的情况下指定默认值:

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

在这个例子中,b? 表示 b 是可选的,因此可能为 undefined,所以在解构赋值的时候,设置 b 默认值为 1001

函数声明

解构也适用于函数声明:

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

但是对于参数来说,指定默认值更为常见,而通过解构获得正确的默认值可能看起来有点混乱。

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];

bothPlus 值为 [0,1,2,3,4,5]。扩展创建了 firstsecond 的浅拷贝。它们本身不会因扩展而改变。

对象也可以扩展:

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

defaults 中的 food: "spicy" 属性会覆盖 food: "rich"

对象扩展还有其他一些令人惊讶的限制。首先,它只包含对象 自身的可枚举属性。这意味着当你扩展一个对象的实例时,你会丢失原型上的方法:

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

感谢观看,如有错误,望指正

官网文档地址: www.typescriptlang.org/docs/handbo…

本章已上传 github: github.com/Mario-Mario…

转载自:https://juejin.cn/post/7244818841502679097
评论
请登录