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)显得没有那么必要了。