A闪的 BLOG 技术与人文
本文档根据 TypeScript 1.8.10 版本翻译整理。
let
和 const
是 JavaScript 中新增的两种声明变量的关键词。let
相对于 var
可以让用户避免很多 var
的陷阱。而 const
可以防止声明的变量的值被修改。TypeScript 做为 JavaScript 的超集,自然也支持 let
和 const
。这里我们将阐述为何新的声明方式优于旧的方式。
如果你对JavaScript非常熟悉,那么你应该很清楚 var
中的一些陷阱。
在 JavaScript 中声明一个变量使用 var
关键词。
var a = 10;
我们声明了一个值为10
的变量a
。
我们也可以在函数内部声明一个变量。
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
的值。即使 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
声明的作用域规则相对于其他语言有一些怪异。我们来举个例子:
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;
}
上面的规则也许看起来容易些,但当我们使用 for
循环时,因为作用域原因,变量 i
被错误的修改,有经验的开发人员知道如何避免类似的错误,以防止以外bug的出现。
快速判断下列代码所输出的结果是什么?
for (var i = 0; i < 10; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
setTimeout
方法指定若干毫秒后执行一段逻辑。
让我们来看一下执行结果:
10
10
10
10
10
10
10
10
10
10
如果你对JavaScript不太熟悉,那么你可能会以为输出的是如下结果:
0
1
2
3
4
5
6
7
8
9
这个原因是我们前面提到的变量捕获所导致的。
在任意位置调用
g
,a
的值都会从f
中获取。
简单思考一下上面的问题,当 setTimeout
指定第一次后,循环就已经停止,此时 i
的值是 10
,并始终停留在 10
。
想要解决这个问题,需要使用另外一种方法,避免变量 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
即可。
现在你已经发现了 var
的一些问题,这就是我们为什么使用 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;
}
这里我们定义两个变脸 a
和 b
,两个变量的作用域都在函数内,而 b
的作用域在 if
语句块内。
变量声明的作用域在 catch
语句块内同样生效。
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);
另一个作用域规则是,变量需要先定义后访问。如果调用了当前未定义的变量,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;
使用 var
声明变量,无论你声明多少次,你只会得到一个变量。
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
在上面这个示例中,x
的声明指的是同一个 x
变量。这往往会出现一些问题。
let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope
使用 let
关键词,在同一个作用域内声明相同变量名的变量,会提示错误。
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;
}
这个版本中的 for
循环就是正确的,我们使用 let
关键词屏蔽了内部和外部的变量。
屏蔽操作有利于我们编写清晰的代码,防止一些错误的发生。
我们将作用域变量捕获可以理解为,每一次一个范围被运行,它就创建了一个变量的“环境”,这种环境可以捕获内部的变量所在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
我们可以从当前的“环境”中捕获 city
,我们可以访问到它,尽管它在 if
语句块中。
回想一下我们之前的 setTimeout
的示例,我们需要捕获的是开始循环迭代中的一个变量状态,但事实上,我们每次都创造了一个新的变量环境。在TypeScript中,借助 let
我们可以做到我们想要的效果。
let
声明让我们的 for
循环行为有着极大的不同,我们依然使用旧的 setTimeout
示例,这次会得到我们想要的结果。
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 numLivesForCat = 9;
const
是声明一个常量,它和 let
有相同的作用域规则,但它的值会被锁定,你无法在程序运行时修改它的值。
下面的示例不要混淆,const
所指定的变量值是不可变的。
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
变量的内部状态仍然是可变的。
什么时候使用 let
,什么时候使用 const
,这是我们所面临的常见问题。
你应该遵循 最小特权原则 ,除非变量需要写权限,否则应该是 const
,使用 const
使得代码更可预测。
另外一方面,不要再使用 var
声明变量。
TypeScript中另外一个特点是 解构 。
数组解构
最简单形式的解构是解构分配数组。
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(input);
也可以使用 ...name
语法来定义可变长度的解构。
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
。同时省略掉了 o.c
。
你也可以像使用数组解构一样的语法,不需要声明。
({a, b} = {a: "baz", b: 101});
注意:必须要用
()
圆括号包围这个语句,因为 JavaScript 中会把{
符号作为块的开始。
属性重命名
你可以为属性提供不同的名称:
let {a: newName1, b: newName2} = o;
这里的语法稍微有点混乱,你可以将 a: newName1
理解为 a as 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