0%

变量声明

letconst是JavaScript中的两种声明方式。就像前文说过的那样,在某些方面letvar是相似的,但是使用let可以避免许多JavaScript程序员常常遇见的陷阱。const 是对let的增强,它可以防止对变量的从新赋值。

TypeScript是JavaScript的超集,所以letconst在TypeScript中自然可用。本文将会仔细介绍这两种声明方式,。。。

var 声明方式

在JavaScript中一直(注:ES6之前)使用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,当g被调用的时候,尽管这时候f的调用已经完成了,a都可以被访问,就像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块中声明,但我们却能在if块的外面访问它(注:在常见的语言中会有一个编译时错误:x 未声明)!导致这一现象的原因是,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的值,因为i的作用域在这整个函数!类似的bug不容易在code review时(注:代码审查,开发环节)被发现,而成为问题之源。

变量捕获的怪异行为

快速指出下列代码的输出:

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

(致不熟悉setTimeout的人:setTimeout会在指定的多少毫秒后执行指定的函数)

答案是什么呢?

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 会在若干毫秒后执行传给它的函数(注:哪怕是setTime(fn,0),请参考《异步JavaScript》),这时候for循环已经完成,i 是 10。所以,之后运行的函数(setTimeout的回调)输出都是10。

一个常用的解决方案是在每次循环中用立即执行函数表达式(IIFE,Immediately Invoked Function Expression)来捕获i

for (var i = 0; i < 10; i++) {
    // capture the current state of 'i'
    // by invoking a function with its current value
    (function(i) {//在了的i覆盖了上层作用域的i
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}

这种看起来奇奇怪怪的模式实际上是非常通用的。参数i实际上覆盖了for循环中的i,只是名字相同而矣(注:若理解起来别扭,把IIFE内部的i改成j)。

如下

for (var i = 0; i < 10; i++) {
    // capture the current state of 'i'
    // by invoking a function with its current value
    (function(j) {
        setTimeout(function() { console.log(j); }, 100 * j);
    })(i);
}

let 方式声明

现在,你已经知道var的这些问题了,这也正是let被引入的原因,除了关键字不同,两种声明的写法是一样的:

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

块作用域变量的另一个性质是在其声明前是不可以被读写的。(注:有句废话没翻译)

a++; // illegal to use 'a' before it's declared;
let a;

值得注意的是,对于块级变量,你仍可以在声明前捕获它。

function aFun(){
    function foo(){
        console.log(a);
    }
    // illegal call 'foo' before 'a' is declared
    // runtimes should throw an error here
    /*
     * 注:是否有runtime错误要看tsc编译参数,如果编译成ES5及更早版本则没有runtime错误
     * 请对比(徦设文件名为test.ts):
     * tsc -t ES5 test.ts 
     * tsc -t ES2016 test.ts
     */
    foo();
    let a=1;
    return foo;
}

重声明以及覆盖

上文提及的var方式声明的变量可以在同一作用域被声明多次,而不会报错:

function f(x) {
    var x;
    var x;

    if (true) {
        var x;
    }
}

在这个例子中,所有对x的声明都指向了同一个x,这样做的后果常常是成为了bug之源。因此,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'

这种在子作用域声明一个和父作用域同名的变量的形为就是——变量覆盖(shadowing)。这是一把双刃剑,在“不小心”覆盖了另一个变量的时候就可能会引入一些bug,同时变量覆盖也能访止一些bug,如前面的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;
}

这段代码是能够完成矩阵求和的,因为内层for循环的i覆盖了外层循环的i,那么外层的i就不会被意外的修改了。

在意于写清晰明了的代码的时候应该必免使用变量覆盖这一特性。尽管在有些场合,这一特性会带来一些优势,你最好仔细考量。

块作用域变量捕获

当我们首次了解var变量的捕获的情况的时候,我们粗略的调查了变量
被捕获后的形为。详细说来,每当一个作用域(中的代码)在执行的时候,有一个包含变量的“环境”被创建出来。这个环境和其捕获的变量在其所在的作用域执行完成后依然可以存在!

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}

因为我们捕获了city变量,所以在if块执行之后我们仍可以访问它。
回想前面setTimeout那个例子,我们需要使用IIFE来捕获变量。
实际上,我们创建了一个新的变量环境来捕获变绿。IIFE的方式写起来有些老火,幸运的是在TypeScript中不必那样。
let方式在循环语句中声明的变量很是不同,除了创建一个新的环境,这类声明(指let和const)也会在每次循环的时候创建一个新的作用域,这正是我们使用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很像,但是,就像其名字所暗示的那样,其值是不能够被修改的。换句话说,constlet具有一样的作用域规则,但是你不能为之重赋值。

可别和不可变(immutable)相混淆:

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变量的内部状态(注:对像的属性,数组的元素等)是可变的(注:const 变量指的是引用不变)。所幸,TypeScript可指定属性为readonly来避免属性被修改。接口章会详述这个问题。

let vs. const

设计两种具有相似作用域语义的变量声明方式,这使我们自然会问“使用哪个?”。答案和很多问题一样:看情况。

根据最少特权原则,所有无计划修改的变量应声明成const。原因是,如果一个变量不需要被修改,那么其头代码也不应有能修改这些变量的能力,也需要思考它们是否真得需要重赋值这些变量。使用const也使的在推算数据流的时候(注:即在脑中执行代码)代码的行为更可被预测。

[placeholder]

解构

TypeScript拥有的es2015的另一个特性是解构。[这里]是关于解构的完整说明,本节将大略的描述解构。

数组解构

最简单的数组解构是数组的解构赋值。

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

你可以使用…语法来创建一个list变量来保存剩余元素:

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

对象解构

你也可以解构对象:

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

这里从a对象创建了a、b变量,其值分别是o.a,o.b并忽略了a.c

和数组解构一样你也可以不要声明而赋值:

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

注意,我们必须用园括号把代码包起来,js会把{作为块来解析
你也可以使用...语法来创建一个包含其它未被解构属性的变量

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

属性重命名

你也可以给属性一个不同的名字

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

这种语法开始让人有点迷糊,你可以把a:newName1解释为newName1是a,这和下面的代码是等价的:

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

这里的不是表示类型,如果你想指定变量的类型,那么需要在整个解构后。

默认值

默认值可以在某个属性是undefined的时候指定一个默认的值。

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

这里,在函数keepWholeObject中有变量:a,b,以及wholeObject

函数声明

解构也可用在函数的声明中,如下例:

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

[placeholder]

延展

不知道spread对应的术语,乱译为延展

spread运算和解构运算作用是相对的,它可以把一个数组中的元素放到其他数组中。或者把一个对象的属性放到其它对象中。
比如:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

bothPlus数组为[0,1,2,3,4,5],Spread创建了firstsecond浅拷贝
你也可以spread一个对象

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

那么得到的search的food属性的值将会是spicy

对象延展还有一些限制,结果只会包含对象自身的、可枚举的属性(注:自身的表示非原型链上的属性)
所以,当你延展一个对象时,将丢失其上的方法。

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

此外,Typescript编译器不允许泛型函数的类型参数。这一特型在将来的版本中可能会受支持。

Boolean

最基本的数据类型为简单的true/false值,在TypeScript中被称为boolean 值。

let isDone:boolean = false;

Number

和JavaScript 一样,所有数字都是浮点数,即number类型
除了十六进制和十进制字面量,TypeScript也支持ECMAScript 2015引入的二进制和八进制字面量

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

String

在JavaScript中,编写程序的一项基本工作是对文本数据的处理。和其它的语言一样,我们使用string来表示这些文本数据类型。和JavaScript一样,TypeScript也使用双引号和单引号来表示字符串数据。

let color: string = "blue";
color = 'red';

你也可以使用模版字符串,在模版字符串中可以嵌入表达式,同时,模版串内可以换行。模版串用反引号来表示,其内嵌的表达式形如${ expression }

let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.

I'll be ${ age + 1 } years old next month.`;

这和按如下方式定义sentence 等价:

let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

数组

和JavaScript一样,TypeScript允许你使用数组。数组的类型可有两种写法。
第一种是在元素类型的后面跟上一对方括号:

let list:number[] = [1,2,3]

第二种是使用泛型数组参数:

let list:Array<number> = [1,2,3]

元组(Tuple)

一个数组元素个数固定且类型已知,用元组来表示。例如,你可能需要表示一个string和一个number构成的二元组

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error

当我们访问一个索引已知的元组元素,那么这个元素的类型也就是已知的了。

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr' typescript 知道x[1]是一个number类型的东西

当访问元组索引范围外的元素的时候,TypeScript会将其视为该元组已知类型的并类型(Union types).

x[3] = "world"; // OK, 'string' can be assigned to 'string | number'

console.log(x[5].toString()); // OK, 'string' and 'number' both have 'toString'

x[6] = true; // Error, 'boolean' isn't 'string | number'

并类型是一个高级话题,将在后文讨论。

枚举

除了JavaScript的数据类型,TypeScript还增加了一种有用的类型——枚举(enum). 和C#一样,枚举是给一组数字取一个友好的名字的方式。(注: typescript 2.4 中枚举也可以是字符串)

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,枚举值从0开始(后续加1)。你可以手动为枚举的某个成员设置一个值来改变其默认的值。

enum Color {Red = 1, Green, Blue} 
let c: Color = Color.Green; // 2

或者,可以为枚举的每个成员设置值。

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

一个方便的特性是你根据枚举值得到其对应的名字.

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName); // Green

Any 类型

我们也许需要描述这样一种变量——我们在写程序的时候并不知道其类型,这些变量值可能来自一些动态的内容,例如——第三方库。这种情况,我们想对这些变量停用类型检察以便通过编译。为了做到这点,我们使用any 类型

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

any 类型是和JavaScript库交互的有效方式,这可让你逐渐的启用或停用类型检察。你也许希望Object类现会扮演同样的角色,在其它某些语言中的却如此。但在typescript中,Object类型的变量只允许你将any类型的变量赋值给它,而不能调用上面的任意方法,那怕是的确存在的方法。

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

Void 类型

void有些像是和any相反,它表示——没有类型。你可能常在函数的返回值类型声明的地方见到该类型。

function warnUser(): void {
    alert("This is my warning message");
}

用void来声明变量就没什么意义了,因为你只能给它赋undefinednull给它。

Null 和 Undefined

默认情况下,null和undefined 是所有其它类型的子类型,所以null和undefined可以赋值给任何变量。
但当--strictNullChecks选项启用后,null和undefined就只能够赋值给void或其对应的类型(注:null,undefined 即是类型,也是值)。这有助于避免多数常见错误。
当你希望传递string或null或undefined的类型的值的时候则需要使用并类型 string | null | undefined

推荐使用--strictNullChecks,但该教程徦设该选项是关闭了的。

Never 类型

never 类型代表了“永远不会出现”的类型,never 是那些总是抛出错误或永不返回的函数的“返回类型”()。
(Variables also acquire the type never when narrowed by any type guards that can never be true. ???)
never 类型也是所有类型的子类型,同时,没有类型是never类型的子类型,所以除了never自己,没有类型可以赋值给never。 any也不可以赋值给never。
函数返回类型为never的例子:

// Function returning never must have unreachable end point
function error(message: string): never {
    throw new Error(message);
}

// Inferred return type is never
function fail() {
    return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
    while (true) {
    }
}

类型断言

有时候你会遇到这样的情况:你比TypeScript更清楚一个值是什么类型的。
类型断言是一种指定类型的方法,相当于告诉编译器:“相信我,我知道为在做什么”。类型断言和其它语言中的类型转换(type cast)是类似的,但不会执行特别的检察或数据重构操作。也没有运行时的开销,只是纯粹的为编译器使用而矣。TypeScript徦设你已经做过必要的检察了。
类型断言有两种形式,一种是尖括号语法:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

另一种是as语法:

    let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

这两种方式是等价的,使用哪种取决于你的偏好。但是,如果在TypeScript中使用JSX, 则只有as方式是可用的(注:JSX 用尖括号了表示组件,这会引入二意性)。

关于 let

你可能注意到了,我们使用let关键字代替了JavaScript中的var关键字。let 实际上是JavaScript 新标准的东西,TypeScript使其提前可用了。后面我们会进一步讨论let,let可以缓解JavaScript中的很多问题,所以,尽可能使用let来代替var吧!

进一步阅读

never 和 void 的区别