变量

TypeScript 是 JavaScript 的超集,同 JavaScript 一样,声明变量可以采用下面三个关键字:var let const

变量是一种使用方便的占位符,用于引用计算机内存地址。我们可以把变量看做存储数据的容器。TypeScript 变量的命名规则:

  • 变量名称可以包含数字和字母。
  • 除了下划线 _ 和美元 $ 符号外,不能包含其他特殊字符,包括空格。
  • 变量名不能以数字开头。

变量使用前必须先声明,语法如下:

// 语法
var [变量名] : [类型] = 值;
// 实例
var name:string = "HelloWorld";

声明变量时没有定义初始值,该变量值默认为 undefined。

// 语法
var [变量名] : [类型];
// 实例
var name:string;

如果声明变量时没有定义类型,该变量可以为任意类型。

// 语法
var [变量名] = 值;
// 实例
var name = "HelloWorld";

声明变量时没有定义类型和初始值,则类型可以是任意类型,默认初始值为 undefined:

// 语法
var [变量名];
// 实例
var name;

类型断言(Type Assertion)

类型断言可以用来手动指定一个值的类型,即允许变量从一种类型更改为另一种类型。

// 语法
<类型>值
// 或这样
值 as 类型
// 实例
var str1 = '1';
var str2:number = <number> <any> str1;   //str1、str2 是 string 类型

TypeScript 如何确定单个断言是否足够

当 S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T。这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 any。

它之所以不被称为类型转换 ,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也是一种为编译器提供关于如何分析代码的方法。

编译后,以上代码会生成如下 JavaScript 代码:

var str1 = '1';
var str2 = str1;  //str1、str2 是 string 类型

类型推断

声明变量时没有定义类型,TypeScript 编译器利用类型推断来推断类型。如果因为缺乏声明而不能推断出类型,那么它的类型被视作默认的动态 any 类型。如:

var count = 1; // 类型推断为 number
var name = "HelloWorld"; // 类型推断为 string

为声明了类型的变量赋予类型不匹配的值时,会抛出异常。如:

var count = 1; // 类型推断为 number
count = "HelloWorld"; // 错误
  • 第一行代码声明了变量 count 并设置初始值为 1(注意变量声明没有指定类型)。因此,程序使用类型推断来确定变量的数据类型为 number。
  • 第二行代码再次为变量设置字符串类型的值时,会抛出编译错误。error TS2322: Type '"HelloWorld"' is not assignable to type 'number'.

变量作用域

TypeScript 有以下几种作用域:

  • 全局作用域:全局变量定义在程序结构的外部,它可以在你代码的任何位置使用。
  • 类作用域:这个变量也可以称为 属性 。类变量可以通过类的对象来访问。类变量也可以是静态的,静态的变量可以通过类名直接访问。
  • 局部作用域:局部变量,局部变量只能在声明它的一个代码块(如:方法)中使用。
var global_num = 12          // 全局变量
class Numbers { 
   num_val = 13;             // 实例变量
   static sval = 10;         // 静态变量
   
   storeNum():void { 
      var local_num = 14;    // 局部变量
   } 
} 

避免使用 var

问题1

快速的猜一下下面的代码会打印出什么?

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

根据代码,我们期望输出的是:

0
1
2
3
4
5
6
7
8
9

然而,实际结果却是这样的:

10
10
10
10
10
10
10
10
10
10

因为 i 是在全局作用域中的,只存在一个值。 由于 setTimeout 是异步函数,在若干毫秒后执行,因此在 for 循环执行过程中并不会同步执行 setTimeout 中的程序,当 for 循环结束后,i的值为10,若干毫秒后 setTimeout 中的程序才被执行,因此打印出 10

那么如何打印出我们想要的结果呢?有以下几种方法:

a. 通过调用函数,创建函数作用域

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

b. 立即执行函数,创建函数作用域

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

c. 通过 let 关键字创建块级作用域

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

问题2

以下代码是否会编译错误?

console.log(a) // undefined
var a = 1 

并不会,它等价于:

var a
console.log(a) // undefined
a = 1

从上面的例子中可以看出,关键词 var 会进行变量提升。很容易造成代码和执行的混乱。如果把上面的 var 换成 let 呢?

console.log(a) // ReferenceError: a is not defined
let a = 1 

会报错 a is not defined 未定义,这里也可以看出 varlet 在变量提升的不同:

  • var 会将变量的 创建初始化 都进行提升
  • let 只会将 创建 提升,而 初始化 未被提升,称之为 暂时性死区

let 声明变量

上例中我们知道了 var 存在全局作用域和变量提升的问题,这恰好说明了为什么用 let 语句来声明变量。下面介绍 let 的一些特点:

块作用域

在 ES6 之前,ECMAScript 的作用域只有两种:

  • 全局作用域
  • 函数作用域

现在通过关键字 letconst 新增了“块级作用域”,定义在代码块中的变量在代码块被执行结束后会被释放掉:

function block() {
  if (true) {
    let a = 10
    console.log(a) // 10
  }
  console.log(a) // Cannot find name 'a'.ts(2304)
}

第 3 行,在 if{} 代码块中用 let 关键字声明了变量 a,同样在 if{} 代码块中的第 4 行则正常输出了 10。而第7行在if{} 代码块之外,因此变量 a 已经被释放掉了,所以会报错误。

重定义

在使用 var 关键字声明变量时,不论声明几次,内存中都只存在一份:

var a
var a
var a

这是一个完全有效的代码,所有 a 的声明其实都指向了同一个引用,但这也是很多 bug 产生的原因。 let 的声明就严格了许多:

let a
let a // Cannot redeclare block-scoped variable 'a'

第 2 行,重新声明了块作用域的变量 a 。

const 声明常量

关键字 const 声明变量,它被赋值后不能再改变(常量)。 换句话说,它拥有与 let 相同的作用域规则,但是不能重新赋值。

const 声明变量,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。用 const 声明初始数据类型如布尔值、数字、字符串,可以理解为声明常量,因为这些初始类型的值就保存在变量所指向的那个内存地址。

const num = 10
const brand = 'imooc'
const registered = true
num = 20 // Cannot assign to 'num' because it is a constant.ts(2588)

第 5 行,在给 num 第二次赋值时会报错。

对于复合类型的数据来说,变量所指向的内存地址保存的只是一个指针,const 能够保证其指针不变,但属性值是可变的:

const person = {
  name: 'Tom',
  address: 'Baker Street 221b'
}
// error
person = {
  name: 'Sherlock',
  address: 'Baker Street 221b'
}
// ok
person.name = 'Sherlock'

第 7 行,对已经使用 const 声明的变量重新赋值报错,但是在第 13 行,只是对这个对象的属性赋值是可以的。

小结

阅读本小节中三种不同的变量声明,我们知道了:

  • letcount 实现了块级作用域。
  • 所有变量除了计划要修改的都应该使用 const
  • 尽量使用 letconst 来声明变量,减少 var 的使用。
下一节:为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。