# 作用域和闭包

# setTimeout

//  请你预测一下代码会输出什么?
for(var i = 0; i <= 5; i++) {
    setTimeout(function() {
        console.log(i);
    },1000)
}
  • 如果你没有理解透js作用域和闭包的知识点的话,你可能会认为这道题的输出顺序是: for循环,输出1,2,3,4,5. 或者循环输出1~5

  • 但是实际的答案在log之后循环输出了五个数字6!!

  • 接着可能面试官会让你改下代码,期望结果是每间隔一秒输出一个数字,即等待1秒 输出1,等待2秒 输出2,等到3秒 输出3....

  • 回过来看看这段代码的执行顺序,首先for循环执行,在js引擎读到setTimeout时,因为setTimeout不是立即执行的,他们的回调会被push到宏任务队列中,再回头执行任务队列中的回调函数时,变量i早就变成了6。知道了原因,我们着手解决问题。这里我们需要给setTimeout创建一个闭包的环境,让它的回调函数顺利取到循环中的变量i就解决问题了。

  1. 使用IIFE(立即执行的匿名函数)
//间隔1秒依次输出1,2,3,4, 5
for(var i = 1; i <= 5; i++) {
    (function(i){
        setTimeout(function() {
            console.log(i);
        }, i*1000)
    })(i);
}
  1. 使用ES6语法中的let来声明变量i ==es6中的let声明的变量是具有块级作用域的,所以我们可以大胆的使用==
for(let i = 1;i <=5; i++) {
    setTimeout(function() {
        console.log(i);
    },i*1000)
}
  1. 使用bind方法
for(var i = 1; i <= 5; i++) {
     setTimeout(function(i) {
        console.log(i);
    }.bind(null, i),i*1000)
}
  1. 利用setTimeout的第三个参数!!注意:setTineout的第三个参数及以后的参数都可以作为回调函数的参数哦
for(var i = 1; i<= 5;i++) {
    setTimeout(function time(i) {
        console.log(i);
    },i*1000,i)
 }
  • 关于setTimeout的延时参数
setTimeout(function() {
  console.log('代码执行了');
},3000)
  • 我们一般说代码在3秒之后执行,这样的说法是不严谨的。
  • 准确的解释是:3秒后,setTimeout里的函数被推入event queue,而event queue里的任务,只有在主线程空闲下来之后才会去执行。 如果主线程上有很多任务执行,超过3秒,比如执行了10秒,那么这个函数只能在10秒之后才能执行
  • 另外:为了确保浏览器的执行一致,HTML5规范规定设置的最小延迟是4ms

# 作用域

  • 作用域是根据名称查找变量的一套规则
  • 作用域查找:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止
  • JS中有全局作用域,函数作用域,块级作用域

# 词法作用域

  • 词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域意味着作用域是由书写代码时函数声明的位置来决定的
  • JavaScript所采用的作用域模型,就是词法作用域模型
  • 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
  • (function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域

# 声明提升

  • 变量声明和函数声明会被提升到所在作用域的顶部,这个过程就叫作提升
  • 块作用域中没有声明提升,声明提升只出现在全局作用域和函数作用域中
  • 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地
  • 函数声明会被提升,但是函数表达式却不会被提升
  • 顺序问题:函数声明会首先被提升,然后才是变量声明
  • 覆盖问题,同名的声明,后面的会覆盖前面的
  • 一个普通块内部的函数声明通常会被提升到所在作用域的顶部,通常会提升出该块
  • 下面代码中foo()放在不同的位置会有不同的结果
//foo()  2
var foo = '4'
//foo()  会报错
function foo(){
  console.log('1')
}
//foo() 会报错
var foo = function(){
  console.log('3')
}
//foo() 3
function foo(){
  console.log('2')
}

# 闭包

  • 定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行, 闭包就是该函数对原始定义作用域的引用

# 闭包的使用

  • 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,这个引用就是闭包
  • 由于内部函数一直持有对原始定义作用域的引用,因此该作用域不会被垃圾回收机制回收,很容易造成内存泄漏,所以要慎用闭包
  • 本质上无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
function foo(){
  var a =2;
  function bar(){
    console.log(a)
  }
  bar()
}
foo() //2

var a = 3;
(function IIFE(){
  console.log(a) //3
})()
  • 第一段代码最准确地用来解释bar()对a的引用的方法是词法作用域的查找规则,而这些规则只是闭包的一部分。(但却是非常重要的一部分!)
  • 第二段代码可以正常工作,但严格来讲它并不是闭包。为什么?因为函数(示例代码中的IIFE)并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的