TypeScript Handbook 变量声明

本文档根据 TypeScript 1.8.10 版本翻译整理。

letconst 是 JavaScript 中新增的两种声明变量的关键词。let 相对于 var 可以让用户避免很多 var 的陷阱。而 const 可以防止声明的变量的值被修改。TypeScript 做为 JavaScript 的超集,自然也支持 letconst 。这里我们将阐述为何新的声明方式优于旧的方式。

如果你对JavaScript非常熟悉,那么你应该很清楚 var 中的一些陷阱。

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。在任何地方调用 ga 都能够从 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

这个原因是我们前面提到的变量捕获所导致的。

在任意位置调用 ga 的值都会从 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 即可。

let 声明

现在你已经发现了 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;
}

这里我们定义两个变脸 ab ,两个变量的作用域都在函数内,而 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 是另外一种声明变量的方式。

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 VS 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

这将创建名称为 firstsecond 的两个新变量。这相当于建立一个索引,使用起来更加方便。

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;

上面这个示例,创建变量 ab,对应的 o.ao.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 的参数,其需要两个属性 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