前言:今天看到一道结合了JS执行机制+作用域的题目,关于JS执行机制之前已经写过一篇文章:https://www.jianshu.com/p/871665d10db7,下面开始关于作用域的学习!
首先上代码
预期的结果可能是顺序打印12345,但实际上输出结果为55555
首先来分析整个程序运行的顺序,setTimeout()为异步任务,所以每执行一次for循环,都将setTimout里面的函数放入event table(事件表)中,并在i*1000秒后推入event queue(事件队列),因此当for循环全部执行完后,event queue里有5个console.log(i)等待执行。
接着,由于在setTimeout函数内寻找不到关于i的定义,于是向上去查找i,由于i是用var定义的,具有函数级的作用域,因此可以被访问到,此时for循环已全部执行完,i=5,所以最后的打印结果都为5。
如果希望代码与预期一样打印12345,可以简单的将var 改成 let,因为let的作用域是块级的,当for循环执行结束后,作用域在setTimeout()未执行前都不会被释放,每一次console.log(i)都会引用for循环代码块作用域下的i。
定义变量看起来虽然只是一行代码的事情,但真要研究起来,却会发现里面门道有不少,在ES6之前,定义变量都是使用var,ES6以后增加了let和const,那么为什么要增加这2中定义变量的方法呢?他们之间又有哪些区别呢?下面一一来做探究。
一、var
1、var具有函数级作用域
首先来了解一下JS中的作用域,有以下3种
(1)全局作用域--不使用关键字声明,直接赋值
(2)函数作用域--var
(3)块级作用域--let,ES6新增,块作用域由{}包括,if语句和for循环语句种的{}也属于块作用域
接下来看一段代码以更好的进行理解
function test(){
for ( var i = 0; i < 3; i++) {
console.log(i) //依次打印 0,1,2
}
console.log(i); // 打印 2,因为是函数级作用域,因此在整个函数体内都能访问到
}
console.log(i); //报错,i 未定义,无法跨函数作用域访问 i
i定义在test函数 for 循环的代码块中,但它在整个函数体内都能访问到,因为var定义的变量具有函数级的作用域,可以实现跨块级作用域的访问,但注意,通过var定义的变量是不能够跨函数作用域访问的,在函数外部直接访问会报错 undefined。
2、var可以在相同作用域反复声明相同标识符的变量
var i = 5;
i // 输出5
var i = 6;
i // 输出6
3、var支持变量提升
首先看一个例子
console.log(a); //undefined
var a = '123';
虽然这里在a声明前调用了a,但是执行代码并未报错,而是返回了一个undefined的值,实际上是因为var支持变量提升(即var在当前作用域下声明的所有变量和函数都会提到函数的顶部声明,并且会使用undefined作为缺省值),a在调用前就已经被声明了,只是没有被初始化,上面的代码实际上可以理解成
var a;
console.log(a); //undefined
a = '123';
二、let
let是ES6中用来替代var的设计,它与var主要有以下不同
1、let使用块级作用域
function test() {
for (let i = 0; i < 3; i++){
console.log(i) // 依次打印0,1,2
}
console.log(i) // Uncaught ReferenceError: i is not defined,块作用域变量在花括号 {} 外无法访问到
}
console.log(i) //Uncaught ReferenceError: i is not defined
由于let定义的函数作用域是块级的,即其创建的变量只存活于当前变量所在的花括号{}内部,花括号外部是无法访问到的(但注意,let是支持向上查找的),实现原理是let使用了匿名函数自调,let的出现让js变得更加安全和规范,有效的解决了内存泄漏的问题,如开篇讲的关于for循环的问题通过let可以很方便的实现想要的效果。
2、let不支持在同作用域中声明重复声明相同的变量
let i = 5;
i // 输出5
let i = 6; //Uncaught SyntaxError: Identifier 'a' has already been declared
之前在var的介绍中可知,var是可以重复定义相同的变量的,但let不支持,重复定义会抛出变量已经被定义的错误
3、let支持变量提升,但它使用了TDZ(暂时死区)禁止了声明前访问
同样先看一个例子
console.log(a); //Uncaught ReferenceError: a is not defined
let a = '123';
我们通过前面的例子可以知道,var因为支持变量提升的关系,上述相同的代码在用var定义的情况下会返回一个undefined,既然let也支持变量提升,为什么会报错呢?
这是因为let的死区设计,let与const在ES6标准中有如下的文字说明
The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated.
即是说在程序的控制流程序在作用域进行实例化时,由let和const声明的变量会在作用域中先被创建出来,但此时创建的变量还未进行语法绑定(即还没有被赋值),所以是不能访问的,访问就会报错。因此在控制流进入作用域创建变量,到变量可以开始访问之间的这一段时间,就被称之为暂时死区(TDZ)。
三、const
const也是在ES6后才出现的,主要用于常量的定义,它与let在许多方面性能都类似,主要区别在于:const定义的变量不可再修改,而且必须初始化
最后再倒回开头给出的程序,要想让打印结果为12345,除了用let来替代var的方法外,还有以下2种解决方案:
1、通过立即执行函数创造作用域,保存i的值
2、另外还可以通过创建一个一个新的函数来捕获每一次for循环的i值
网友评论