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

2.4 let与var

在JavaScript中定义变量一般都是用var关键词来声明,在ES6中引入let也可以声明变量。在TypeScript语言中,支持用var和let进行变量声明,但二者在变量声明上有着明显的区别。

通过var定义的变量,作用域是整个封闭函数,是全域的。通过let定义的变量,作用域是在块级或者子块中。因此,采用let声明变量更加安全,也更容易规避一些不易发现的错误。

在JavaScript中有一个变量提升机制,浏览器中JavaScript引擎在运行代码之前会进行预解析,首先将函数声明和变量声明进行解析,然后对函数和变量进行调用和赋值等,这种机制就是变量提升。代码2-41中包含3段代码,注意这3段代码应该分别运行,不要一起运行。

【代码2-41】变量提升示例:var_hosit.ts

    01  //代码段1--------------------------
    02  var myvar :string = '变量值';
    03  console.log(myvar);                // 变量值
    04  //代码段2--------------------------
    05  var myvar :string = '变量值';
    06  (function() {
    07    console.log(myvar);              //变量值
    08  })();
    09  //代码段3----------------------------
    10  var myvar :string = '变量值';
    11  (function() {
    12    console.log(myvar);              // undefined
    13    var myvar :string = '内部变量值';
    14  })();

在代码2-41中,代码段1会在控制台打印出“变量值”,这很容易理解;代码段2也会在控制台打印出“变量值”,Javascript编译器首先在匿名函数内部作用域(Scope)查看变量myvar是否声明,发现没有就继续向上一级的作用域(Scope)查看是否声明myvar,发现存在就打印出该作用域的myvar值。代码段3只是对代码段2做一个微调,结果却输出了undefined。

可理解为内部变量myvar在匿名函数内最后一行进行变量声明并赋值,但是JavaScript解释器会将变量声明(不包含赋值)提升(Hositing)到匿名函数的第一行(顶部),由于只是声明myvar变量,在执行console.log(myvar)语句时并未对myvar进行赋值,因此最终在控制台输出undefined。

在Javascript语言中,变量的声明(注意不包含变量初始化)会被提升(置顶)到声明所在的上下文,也就是说,在变量的作用域内,不管变量在何处声明,都会被提升到作用域的顶部,但是变量初始化的顺序不变。

即使var声明的变量处于当前作用域的末尾,也会提升到作用域的头部并初始化为undefined,在此之前都可以进行调用,并不会出现变量未定义的错误。

提示

let声明的变量会进行变量提升,但是在作用域所在的顶部和let声明变量之前let声明的变量都无法访问,从而保证安全。

let是ES6新增的变量声明方式,是用来替代var的设计,本节要介绍的就是它与var的不同。

2.4.1 let声明的变量是块级作用域

let声明的变量是块级作用域,大括号{}包围的区域是一个独立的作用域,如下面的代码2-42所示。

【代码2-42】变量let声明块级作用域示例:let1.ts

    01  if (true) {
    02      let msg = "hello";
    03  }
    04  console.log(msg);       //错误

在if块级作用域中用let声明一个msg变量,但是在块级作用域外不能访问此msg变量,如果将let换成var则可以在if块级作用域外进行访问。因此建议用let替代var进行变量声明,以提升代码的可读性和防止变量冲突。

2.4.2 let不允许在同域中声明同名变量

let声明的变量是块级作用域。在同一个作用域中,一旦let声明完一个变量后,就不允许再次声明一个同名的变量,即使用var进行声明也不可以,如代码2-43所示。

【代码2-43】同域中声明同名变量示例:let2.ts

    01  //块变量不允许重名
    02  let myvar: string = '变量值';
    03  var myvar: string = "var值";
    04  console.log(myvar);        // 变量值

提示

函数的参数和函数体属于同一个作用域,因此let命名的函数也不允许和参数名同名。

下面的代码2-44中的函数func中有一个参数arg,和02行let声明的arg在同一个作用域,由于二者变量名相同,因此会报错。另外,在TypeScript中,函数名也不允许重复。

【代码2-44】 let函数中声明同名变量示例:let3.ts

    01  function func(arg) {
    02    let arg = 2;     //和参数arg重名
    03  }
    04  func("2");
    05  //函数不能重名
    06  function func(arg) {
    07    {
    08      let arg2 = arg + "2";
    09    }
    10  }
    11  func("3") ;

2.4.3 let禁止声明前访问

let用死区(temporal dead zone)规避了变量提升带来的问题,因此也就无法在声明前对变量进行调用。在下面的代码2-45中,04行用let声明了一个变量tmp,那么在if块作用域中,03行访问变量tmp会报错。并且,let声明的变量生命周期仅在块作用域中,不会污染外部的变量tmp,因此06行打印的仍然是123。

【代码2-45】 let声明禁止声明前访问示例:let4.ts

    01  var tmp = 123;
    02  if (true) {
    03    tmp = 'abc';    //块作用域变量tmp在声明之前无法调用
    04    let tmp;
    05  }
    06  alert(tmp);       //输出值为123,全局tmp与局部tmp不影响

为什么需要块级作用域?var创建的变量只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景,这种方式往往让代码难于让人理解其意图。概括起来,var声明的变量往往有如下问题:

(1)内层变量可能会覆盖外层变量

下面的代码2-46按照一般理解会打印出"外部变量",但实际情况是打印出了undefined。由于var不是块级作用域,再加上变量提升机制(var msg = undefined提升到02行和03行之间),在调用func时,首先将msg赋值为undefined,然后打印出值,导致内层的msg变量覆盖了外层的msg变量。

【代码2-46】 var内层变量覆盖外层变量示例:var1.ts

    01  var msg : string = "外部变量";
    02  function func() {
    03    console.log(msg);
    04    if (false) {
    05      var msg : string = '内部变量';
    06    }
    07  }
    08  func(); // undefined

(2)用来计数的循环变量泄露为全局变量

for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域,如代码2-47所示。

【代码2-47】 var for循环变量泄露为全局变量示例:var2.ts

    01  var msg:string = 'hello wolrd';
    02  for (var i = 0; i < msg.length; i++) {
    03    console.log(msg[i]);
    04  }
    05  console.log(i);        // 11

上面的代码2-47中,变量i只用来控制循环,但是循环结束后它并没有消失,泄露成了全局变量。let声明的变量就不会出现这种问题。for循环中用let可以避免循环变量泄露为全局变量的问题,如代码2-48所示。

【代码2-48】 let for循环变量不会泄露为全局变量示例:var3.ts

    01  var msg:string = 'hello wolrd';
    02  for (let i = 0; i < msg.length; i++) {
    03    console.log(msg[i]);
    04  }
    05  console.log(i);        //报错

let允许块级作用域的任意嵌套,外层作用域无法读取内层作用域的变量,且内层作用域可以定义外层作用域的同名变量,如代码2-49所示。

【代码2-49】变量var声明示例:var4.ts

    01  {
    02      let n: number = 9;
    03      {
    04          let msg:string = 'Hello World';
    05          let n: number = 10;         //不同块级作用域可以同名
    06      }
    07      console.log(msg);           //报错,无法找到msg
    08  };

提示

let块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)显得没有那么必要了。