一道面试题引起的“血案”

es6以前,js的做用域只有两种,全局做用域和函数做用域,没有像C和java那样的块级做用域,因而对于学了C或者java这类语言的而后学习js的同窗来讲,会遇到不少坑。js的这个特性致使了代码的可阅读性、维护性和容错性都不太好。所以es6能够用let来申明变量,这种方式申明的变量是只能在块做用域里访问,不能跨块访问,也不能跨函数访问。那么咱们在使用let的时候,真的就彻底知道它怎么用了吗?java

引子

看到这样的一个面试题es6

for(let i = (setTimeout(()=>console.log(i), 2333) , 0); i < 2; i++) {
  
}
复制代码

你们猜猜2333毫秒后输出的结果是什么?这里就是“血案”现场了
A类同窗:2 ×
B类同窗: 0 √面试

我想A类同窗占了大多数,包括我在内bash

前期知识点

异步

js中的异步包含如下几种:
一、定时器
二、事件处理函数
三、Promise
四、回调函数
js异步的存在是由于,js是单线程的,若是一些任务须要处理时间比较耗时,那么下面的任务就会一直等这个任务执行完成才能继续,好比一些IO任务,这样就会致使执行效率低效,因此js的设计者意识到了这点,设计了异步执行任务,主线程没必要等待异步任务完成才执行下去,这样咱们就能够把一些耗时的任务设计成异步任务,将其挂起,让主线程处理完一些比较重要的任务(ui渲染等)后回头再来执行挂起的异步任务。闭包

做用域链

js存在两种类型的做用域,全局做用域和函数做用域。js执行的时候,会建立一个执行上下文(context),并将该执行上下文中的全部变量、函数和函数参数放入一个对象中AO/VO(变量对象|活动对象),而且会保存父级的AO/VO到[[scope]]属性当中。而后在查找变量的时候,会从当前的AO/VO中查找变量,若是没有,就往[[scope]]属性父级VO/AO查找变量,一直到全局的VO中,这样就造成了一个scope chain(做用域链)。通俗点来说,做用域链就是js在执行的时候用于搜索变量所在的一条链子,全部变量的获取变量会顺着这条链子往上查找,在本做用域内找不到变量的申明,就会往上一级的做用域中查找,直到在全局做用域中还找不到,就找不到该变量了。看下面的例子。异步

var outer = 1;
function func1() {
    var inner1 = 2;
    function func2() {
        var inner2 = 3
        console.log(inner2, inner1, outer); // 3 2 1
    }
    func2()
}

func1();

复制代码

一、首先获取inner2,在func2的做用域中(活动对象)找到了inner2的申明,找到了,而且是3;
二、接着获取inner1,发现func2的做用域中没有inner1的申明,那么往建立func2的做用域中查找,即func1中查找inner1的申明,而且为2;
三、接着获取outer,在func2中的做用域中找不到,往做用域链的上一级找,func1中也没有outer的申明,那么就继续往上一级找,在全局做用域中找到了outer,因此是1。

接着咱们讲下闭包,所谓闭包用一句话来讲就是,函数中的函数,而且里面的函数引用了外面的函数的变量。咱们了解了做用域链,那么咱们就知道,函数内部是能够访问函数外部的变量的,因此,若是咱们在函数中的函数中有访问函数外部变量,且该内部函数被返回的时候就造成了闭包。看下面例子:函数

function func() {
    var name = 'liming';
    var sayName = function() {
        console.log(name)
    }
    return sayName;
}

var sayName = func();
sayName(); // 输出liming
复制代码

如上面,就是闭包的一个例子,总结开来有两个特色:
一、外部函数包含内部函数,且内部函数访问的外部函数的变量
二、返回内部函数给外部调用
学习

闭包有个缺陷就是容易致使内存泄漏,普通函数调用完后,js引擎就会销毁函数里面的变量,可是闭包的话就不会释放了,因此须要注意点。ui

解析

选答案A的同窗

对于A类同窗,答案是错的,可是能够看出A类同窗对js的异步和闭包比较熟悉。咱们知道setTimeout里面的函数是异步执行的,属于js里面的宏任务(js的异步任务分宏任务和微任务),须要等待js的主线程执行完毕且等到设置的时间后才从宏队列里面取出来执行。因此,等到setTimeout的回调执行的时候,回调函数要获取i的值,这个时候回到函数里面没有i的定义,那么js引擎就会往上一级做用域链中找i,这个时候就找到上一级做用域中的i,A类同窗以为这个时候循环已经结束了(由于for是主线程),那么这个时候的i应该是2了,因此输出的应该++了两次的2。这也就是闭包的知识点,js的设计是,内部能够访问外部,而外部不能够访问内部,因此在setTimeout中的回调中,它能够访问获得外部的i,其实若是把let换成var的话,这个答案就是对的。spa

关于倒计时,这里有个东西多说一句,就是setInterval的倒计时不是在回调执行完毕后才开始的。这就会致使一种状况,就是若是回调函数里面执行的代码时间比倒计时时间长,那么下次插入队列中的回调就会被取消,也就是倒计时到了之后,此次回调不会执行了,因此建议统一使用setTimeout来代替setInterval。

选择B的同窗

选择B的同窗,要不就是刚学习js的(也多是蒙对☺),要不就是对let知识点很熟悉的。

let关键字

let关键字申明的变量具备块级做用域的做用,具备如下特色:
一、不可重复申明同个变量
二、不存在变量提高,因此必须先申明后使用
三、只有块内可见,不会影响块外的变量
其实let还有一个特色,就是在for循环当中,每轮循环都是一个新的值。看下面的的例子:

for(let i = 0; i < 2; i++) {
    setTimeout(() => {
        console.log(i); // 分别输出0和1
    }, 0)
}

复制代码

从这个例子能够看出,let变量在for循环中,都会被从新赋值一个新的值,所以上面代码中,for循环中获取的i值都是一个新的,而且这个新i的值是上一次循环的i的值。相似这样的伪代码:

for(var i = 0; i < 2; i++) {
    var new_i = i; // 新的i,且新的i应该是和真正的i关联的,好比是new_i_0、new_i_1之类的,这段是伪代码,用来讲明,评论的同窗说,let的i是被挟持了,这个解释很赞,因此for中的i其实都是被js引擎挟持了的i,不是咱们看到的i
    setTimeout(() => {
        console.log(new_i); // 分别输出0和1
    }, 0)
}
复制代码

我的以为,这个是let的块级做用域相关,每次循环的时候的i都是块级做用域,只对本次循环可见,下次循环不可见。

因此,咱们之后若是须要再for循环中获取循环项的时候,能够不用当即执行函数来实现了,能够改成let了。

回到正题。for循环的第一个语句是初始化,这个时候的i就是本来的i,初始化为0,后面的i都是每次循环新生成的i,与初始化的i无关,因此到2333毫秒之后,i的值任然为0,所以打印出来的i就是0了。

for(let i = (setTimeout(()=>console.log(i), 2333) , 0); i < 2; i++) {
  
}
复制代码

总结

本篇文章经过一个特殊的面试题,引出了js的异步、做用域链、闭包和let的知识点。

异步包含:
一、定时器
二、事件处理函数
三、Promise
四、部分回调函数的方式
异步函数的执行时须要主线程空闲的时候执行的,因此咱们会把耗时的任务处理为异步。

做用域链:
每一个函数执行的时候都会建立一个做用域链的对象,它包含了函数内的全部变量以及建立该函数的函数的全部变量,一直到全局变量,访问变量的时候就会沿着这条链子找。

闭包:
一、外部函数包含内部函数,且内部函数访问的外部函数的变量
二、返回内部函数给外部调用

let: 一、不可重复申明同个变量 二、不存在变量提高,因此必须先申明后使用 三、只有块内可见,不会影响块外的变量 还有在for循环中,每次循环获取let声明的变量都是一个新的变量,而不是初始化时候的那个变量。

相关文章
相关标签/搜索