TypeScript实战
上QQ阅读APP看书,第一时间看更新

2.5 变量

变量是一种占位符,用于引用计算机内存地址。变量是方便人来存取数据的,而内存地址是方便计算机来存取数据的。可以把变量看作存储数据的容器。类型分为值类型和引用类型,所以变量可分为值类型变量和引用类型变量。

JavaScript中的数据类型主要包括两种,一种是基本类型(值类型),另一种是引用类型(引用类型)。内存分为两个部分,栈内存和堆内存。基本类型值保存在栈内存中,引用类型值在堆内存中保存着对象、在栈内存中保存着指向堆内存的指针。

JavaScript中,基本类型值包括undefined、null、number、string和boolean,在内存中分别占有固定大小的空间。引用类型值只有object,这种值的大小不固定,可以动态添加属性和方法,而基本类型则不可以。

TypeScript和JavaScript类似。对基本类型值进行复制,复制的是值本身,相当于复制了一个副本,修改一个变量的值不会影响另外一个变量的值。而对引用类型值进行复制,复制的是对象所在的内存地址。所以两者指向的都是栈内存中的同一个数据,修改一个变量会导致另外一个变量的值也进行修改。

理解变量的值类型和引用类型是非常重要的。为了让读者更加直观地了解值类型和引用类型变量的核心区别,下面用一张C#语言的示例图来说明值类型和引用类型的差异,如图2.1所示。

图2.1 C#中的值类型和引用类型示意图

在图2.1中,由于int j=i中是int类型,为值类型变量,因此j变量的值是i变量值的副本,值都在Stack内存中,都是3,但是二者不是同一个对象。因此,修改i不会修改j,修改j也不会修改i。二者是独立的。

在Person p2=p语句中,Person为类,是引用类型,p2和p指向同一个Heap地址块,因此修改p2的值会影响p的值。

提示

在JavaScript中,函数的参数传递是按值传递,而且不能按引用传递。

在TypeScript中,字符串、布尔型和数值型都是值类型,而类、数组和元组等都是引用类型。从下面的代码2-50可以看出,数组是引用类型,变量a和b指向同一个内存地址,修改了b的值,同时也修改了a的值。

【代码2-50】引用类型示例:ref_var.ts

    01  let a = [1,2];
    02  let b = a;          //数组是引用类型
    03  b[2] = 3;
    04  console.log(a);     //[1,2,3]
    05  console.log(b);     //[1,2,3]

值类型就不是这样的,如字符串和数值类型。从下面的代码2-51可以看出,字符类型是值类型变量,变量a和b指向的是不同的内存地址,只是值一开始一致而已,修改了b的值,不会修改a的值。

【代码2-51】值类型示例:value_var.ts

    01  let a = "1,2";
    02  let b = a;
    03  b= b+",3";
    04  console.log(a); //1,2
    05  console.log(b); //1,2,3

2.5.1 声明变量

在ES5中声明变量的方法最常用的就是var。在ES6中,添加了let和const进行变量声明。在ES6环境下,一般的变量声明都采用let,而不建议使用var。

变量的命名一般都是有约定的。在TypeScript中,变量命名必须满足如下规则:

  •  变量名称可以包含数字和字母,如stuName01。
  •  除了下画线_和美元$符号外,不能包含其他特殊字符,包括空格,如_stuName和$tmp都是合法的变量名。
  •  变量名不能以数字开头,如9Num是错误的。

提示

TypeScript是区分大小写的,比如numA和NumA不同。

在TypeScript编码规范中,建议在使用前变量一定要先声明。变量声明可以使用以下4种方式:

(1)[var或let或const ] [变量名]:[类型]=值;

此范式进行变量声明,同时指定了声明变量的类型及初始值。代码2-52分别给出了用var、let和const声明变量的方式。

【代码2-52】变量声明并初始化的示例:declare_var1.ts

    01  var uname:string = "JackWang";
    02  let uname2:string = "JackWang";
    03  const version:string =  "1.0";

提示

const声明的常量变量一定要初始化,否则会报错。

(2)[var或let ] [变量名]:[类型] ;

此范式进行变量声明,只指定了声明变量的类型,初始值默认为undefined。代码2-53分别给出了用var和let声明变量的方式。

【代码2-53】变量声明不初始化的示例:declare_var2.ts

    01  var uname:string ;
    02  let uname2:string;

(3)[var或let ] [变量名] ;

此范式进行变量声明,只提供了变量名,声明变量的类型和初始值都未提供。变量类型默认为any,变量值默认为undefined,如代码2-54所示。

【代码2-54】变量声明未提供类型和初始值示例:declare_var3.ts

    01  var uname;
    02  let uname2;

(4)[var或let或const] [变量名] =值;

此范式进行变量声明,未指定声明变量的类型,但是给出了初始值,这时会用类型推断来确定变量的类型,这种写法更加简洁,如代码2-55所示。

【代码2-55】变量声明只给出初始值的示例:declare_var4.ts

    01  var uname = "JackWang";    //string
    02  let uname2 = 100 ;         //number
    03  const version =  "1.0";    //string

2.5.2 变量的作用域

变量的作用域就是定义的变量可以使用的代码范围。变量可以分为全局变量和局部变量。在日常的程序开发中,尽量少用全局变量,防止变量冲突。局部变量只在块作用域和函数体内有效,从而保证变量的安全访问。

程序中变量的可用性由变量作用域决定。TypeScript有以下几种作用域:

  •  全局作用域:全局变量定义在程序结构的外部,可以在代码的任何位置使用。
  •  类作用域:这个变量也可以称为字段。类变量声明在一个类里,但在类的方法外面无法访问。该变量可以通过类的实例对象来访问。类变量也可以是静态的,可以通过类名直接访问。
  •  局部作用域:局部变量,只能在声明它的一个代码块(如方法)中使用。

代码2-56说明了3种作用域的使用。此示例涉及类的相关知识点,但读者此时不要过多在意类的语法,这些会在后面的章节详细进行说明。此时只需理解变量作用域的相关知识点即可。

【代码2-56】变量作用域示例:scope_var.ts

    01  var global_var = 12                 // 全局变量
    02  class MyClazz {
    03      clazz_val = 13;                 // 类变量
    04      static sval = 10;               // 静态变量
    05      storeNum(): void {
    06          var local_var = 14;         // 局部变量
    07      }
    08  }
    09  console.log("全局变量为: " + global_var);
    10  console.log(MyClazz.sval);          // 静态变量
    11  var obj = new MyClazz();
    12  console.log("类变量: " + obj.clazz_val);

提示

var在函数或方法中声明变量,作用域限于此函数或方法中。

2.5.3 const声明变量

const和let在声明变量的用法上基本一致,只是const声明的变量被赋值后不能再改变,是只读的常量。因此,对于const声明的变量来说,只声明不赋值就会报错。

const声明的变量作用域和let一致,但是在某些情况下用const更加安全,可以防止在其他地方被修改从而影响程序的正常运行,如代码2-57所示。

【代码2-57】 const声明变量示例:const_var.ts

    01  const cVar = "hello";
    02  cVar = "change";       //错误,不能赋值
    03  console.log(cVar);

const实际上保证的并不是变量的值不能改动,而是变量指向的那个内存地址所保存的数据不能改动。对于简单类型的数据(如数值、字符串和布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。

对于复合类型的数据(主要是对象和数组),变量指向的是内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(总是指向一个固定的地址),至于它指向的数据结构是不是可变的,则完全不能控制。因此,将一个对象声明为常量类型,其对象的值也是可以修改的,如代码2-58所示。

【代码2-58】 const声明复合类型变量示例:const_var.ts

    01  interface Object{
    02      prop: string;
    03      func: () => string;
    04  }
    05  const foo: Object = {};
    06  // 为 foo 添加一个属性可以成功
    07  foo.prop = "123";          //说明值可以修改
    08  foo.func = function (): string {
    09      return "hello";
    10  }
    11  // 将 foo 指向另一个对象就会报错
    12  foo = {}; //报错
    13  const a = [];
    14  a.push('Hello');           // 可执行
    15  a.length = 0;              // 可执行
    16  a = ['Dave'];              // 报错

代码2-58涉及接口的相关知识点,此示例为了说明const声明的常量对象本身的值是可以修改的,必须借助接口来扩展Object的属性或方法。

在代码2-58中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

提示

在TypeScript中,Object类型的变量不能动态添加属性和方法,而只能通过接口扩展属性或者方法。

在代码2-58中,13行声明了一个数组常量a,这个数组中的相关属性是可以修改的,但是如果将另一个数组赋值给a就会报错。如果真的想将对象冻结,应该使用Object.freeze方法,如代码2-59所示。

【代码2-59】 const声明数值变量冻结示例:const_freeze.ts

    01  const foo = Object.freeze([]);
    02  foo.length = 1;       //报错