js闭包如何从朦胧到理解!!!

304 阅读9分钟

前言

JavaScript 中的闭包是相当重要的概念,并且与作用域相关知识的指向密切相关。

闭包前的准备:

作用域基本介绍:

JavaScript 的作用域通俗来讲,就是指变量能够被访问到的范围,在 JavaScript 中作用域也分为好几种,ES5 之前只有全局作用域和函数作用域两种。

ES6 出现之后,又新增了块级作用域。

全局作用域:

在编程语言中,不论 Java 也好,JavaScript 也罢,变量一般都会分为全局变量局部变量两种。那么变量定义在函数外部,代码最前面的一般情况下都是全局变量。 在 JavaScript 中,全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量。

代码如下:

var a = '全局变量'        
function getName(){            
    console.log(a);            
    var b = 'in getName';            
    console.log(b);            
    globalName = '自动变全局变量';        
}        
getName();        
console.log(a,'a');        
console.log(globalName,'globalName');        
console.log(window.globalName,'window globalName');        
console.log(b,'b');

output:

             

从这段代码中我们可以看到:

  1. a 这个变量无论在什么地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 b 变量是不具备这种能力的。
  2. 如果在 JavaScript 中所有没有经过定义,而直接被赋值的变量默认就是一个全局变量,比如上面代码中 getName 函数里面的globalName 变量一样。 我们可以发现全局变量也是拥有全局的作用域,无论你在何处都可以使用。在浏览器控制台输入 window.globalName 的时候,就可以访问到 window 上的globalName全局变量。
  3. 当然全局作用域有相应的缺点,我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题。
  4. _函数作用域:_b这个变量是在 getName 函数中进行定义的,所以 b 是一个局部的变量,它的作用域就是在 getName 这个函数里边,也称作函数作用域。 除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的b 是访问不到的。

块级作用域:

ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。块级作用域由最近的一对花括号{  }界定的。换句话说,if块、while块、function块、甚至连单独的块也是let声明变量的作用域。

示例:

{           
    let b = 'oppo';       
}       
console.log(b,'let b')

执行上下文和作用域链:

  • 执行上下文中的代码,在执行的时候会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。其中全局执行上下文是最外层的执行上下文。
  • 当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

代码示例:

var Color = 'blue';      
function changeColor() {          
    let anotherColor = 'green';          
    function swapColor(){             
        let tempColor = anotherColor;              
        anotherColor = Color;              
        Color = tempColor;              
        // 这里可以访问到 Color tempColor anotherColor         
     }          
    //  这里可以访问到 Color anotherColor          
    swapColor();      
}      
// 这里只可以访问到 Color      
changeColor();

上面示例的作用域链:

这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。 由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用(《java核心原理精讲》 - 若离)

闭包的出现:

什么是闭包?

红宝书闭包的定义:

     闭包是指有权访问另外一个函数作用域中的变量的函数。

MDN:

     一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

你不知道的javaScript:

    函数是在当前词法作用域之外执行的

闭包的基本概念:

闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。

因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。

代码如下

function foo() {         
    var a = 2;         
    function bar() {             
        console.log(a);         
    }         
    return bar;     
}     
var baz = foo();     
baz();  // 2  这就是闭包的效果。

从上面这段代码可以看出:

  1. 这里 bar 会拿到父级作用域中的变量,输出 2。因为在当前环境中,含有对bar函数的引用,bar 函数恰恰引用了 window、fun1 和 bar 的作用域。因此 bar 函数是可以访问到foo函数的作用域的变量。
  2. 拜bar()所声明的位置所致,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用,bar()依然持有对该作用域的引用,而这个引用就叫做闭包!
  3. 由闭包产生的本质就是:当前环境中存在指向父级作用域的引用

上诉代码可变为:

var baz;     
function foo() {         
    var a = 2;         
    baz = function () {             
        console.log(a);         
    }             
}    
foo();    
baz();

就是在给baz 函数赋值后,baz 函数就拥有了 window、foo 和 baz本身这几个作用域的访问权限;然后还是从下往上查找,直到找到foo 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。这个baz()依然存在指向foo()作用域的引用

闭包的表现形式 "

1. 返回一个函数。

2. 在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

请看下面这段代码:

 // 定时器
    setTimeout(function handler(){
      console.log('1');
    },1000);
    // 事件监听
    $('#app').click(function(){
      console.log('Event Listener');
    });

3. 作为函数参数传递的形式,比如下面的例子。

    var a = 1;
    function foo(){
      var a = 2;
      function baz(){
        console.log(a);
      }
      bar(baz);
    }
    function bar(fn){
      // 这就是闭包
      fn();
    }
    foo();  // 输出2,而不是1

4. IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量,如下所示。

    var a = 2;
    (function IIFE(){
      console.log(a);  // 输出2
    })();

IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

闭包的循环输出问题:

for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}

从控制台执行的结果可以看出来,结果输出的是 5 个 6,

  1. 为什么都是 6?
  2. 实现输出 1、2、3、4、5 的话怎么办呢?

第一个问题:为什么都是 6?

  1. setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行。

  2. 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。

第二个问题:实现输出 1、2、3、4、5 的话怎么办呢?

1.     可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。

复制代码

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}

可以看到,通过这样改造使用 IIFE(立即执行函数),可以实现序号的依次输出。

2.     ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过改造后的代码,可以实现上面想要的结果。

复制代码

for(let i = 1; i <= 5; i++){
  setTimeout(function() {
    console.log(i);
  },0)
}

从上面的代码可以看出,通过 let 定义变量的方式,重新定义 i 变量,则可以用最少的改动成本,解决该问题。

3.     setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下。

for(var i=1;i<=5;i++){
  setTimeout(function(j) {
    console.log(j)
  }, 0, i)
}

从中可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现我们想要的结果,这也是一种解决循环输出问题的途径。

闭包的思考问题:

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);   //undefined, ? , ? , ?
var b = fun(0).fun(1).fun(2).fun(3);               //undefined, ? , ? , ?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);       //undefined, ? , ? , ?

题目摘自        www.cnblogs.com/xxcanghai/p…

问:    三行 a, b , c的输出分别是什么?

总结:

  闭包产生的本质就是:当前环境中存在指向父级作用域的引用