JavaScript复习—闭包

  1. 什么是闭包
  2. 闭包是怎么形成的
  3. 作用域链
  4. 闭包的表现形式?(那些地方体现了闭包?)
  5. 如何销毁闭包

什么是闭包

闭包是可以访问另一个函数内部变量的函数,闭包的使用则是函数在它定义时的作用域以外被调用。

闭包是怎么形成的

闭包形成的本质其实是,存在指向父级作用域的引用

let value = 1; 
function f1() {
    let value = 2;
    function f2() {
       let value = 3;
       console.log(value);    //  这里引用了父级f1的作用域
    }
    return f2;
}
let x = f1();
x();    // 2

在ES5中存在两种作用域——全局作用域和函数作用域。

在上面这个栗子中,x是对f2函数的引用,而f2函数指向window、f1和f2本身的作用域,这就是一条作用域链。当访问value这个变量时,至底层往上寻找,先在f2本身的作用域中寻找,没找到就顺着作用域链到f1的作用域中寻找,找到了就返回该变量的值,如果找到window作用域了都没找到则报错。

作用域链

用下面的这个栗子来分析理解一下作用域链:

function foo() {
    let a = 1;
    function bar() {
        let a = 2;
        function baz() {
            let a = 3;
            console.log(a);
        }
        baz();
    }
    bar();
}
foo();
  • 在全局作用域中调用foo()时,创建了foo的临时OA对象:

    foo-OA {
        a: 1
        bar: function()
    }    
  • 在foo函数中调用了bar函数,同样生成了bar的临时OA对象:

    bar-OA {
        a: 2
        baz: function()
    }
  • 在bar函数中调用baz函数,生成baz的临时OA对象:

    baz-OA {
        a: 3
    }

这个时候就可以将作用域链连起来啦:window -> foo-OA -> bar-OA -> bar-OA;

于是我们访问变量a的时候,顺着底层开始寻找,在baz-OA对象中找到了a=3的结果;如果我们把baz-OA对象中的a:3去掉,那么JS引擎会找到bar-OA对象中的a=2,同理往下类推,如果最后在全局中也没有找到,就会报错。

再看看下面的这个栗子:

let a = 1;
function foo(){
  let a = 2;
  function baz(){            
    console.log(a);     
  }
  bar(baz);
}
function bar(fn){
  let a = 3;    
  fn();
}
foo();     

看看这个栗子,它输出的结果是2,不是3哦!

让我们看看作用域链:

作用域链:

window {
    a: 1
    foo: function()
}
-> foo-OA {
    a: 2
    baz: function()
    }
    -> bar-OA {
        a: 2
        baz: function()
      }
      -> baz-OA {
         a: -> foo-OA.a    // 访问了foo-OA的a,形成了闭包,把foo的a包裹到了它的作用域里。
        }

简单一句话就是:baz函数形成了闭包,把foo的a变量包裹到自己的作用域里,而baz作为参数传给了bar。

看看上面的作用域链,虽然baz没有自己作用域中的a变量,但是在定义的时候访问了foo-OA的a,形成了闭包,把foo的a包裹到了它的作用域里。所以在调用形成作用域链的时候,baz执行console.log的时候,要去找a变量,那么baz-OA中有a,所以输出2,并不用找到链的上层bar-OA中的a。

闭包的表现形式?(那些地方体现了闭包?)

  1. 返回一个函数

     let value = 1; 
        function f1() {
            let value = 2;
            function f2() {
               let value = 3;
               console.log(value);    
            }
            return f2;
        }
    let x = f1();
    x();
  2. 做为函数参数传递

    let a = 1;
    function foo(){
      let a = 2;
      function baz(){            
        console.log(a);     
      }
      bar(baz);
    }
    function bar(fn){
      fn();
    }
    foo();  
  3. 在定时器,事件监听,Ajax请求或者任何异步操作中,只要使用了回调函数,实际就是使用了闭包!

    // 定时器
    setTimeout(function timeHandler() {
        console.log('延时了100毫秒')
    },100)
    
    // 事件监听
    $('#app').click(function() {
        console.log('DOM Listener');
    })
  4. IIFE(立即执行函数)创建闭包。

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

如何销毁闭包

看看这个简单的栗子,这是最简单的返回函数的闭包:

function foo() {
    let a = 4;
    return function() {
        console.log(a);
    }
}
let bar = foo();

当执行let bar = foo()时,形成了foo-OA对象:

foo-OA {
    a: 4
}

一般的函数执行完之后,临时OA是会被消除的,但是这里不会

bar -> 引用foo返回函数 -> 返回函数闭包中包裹着foo-OA中的a变量

因此bar一直保持着对foo-OA的引用,所以当foo()执行结束后并不会销毁foo-OA对象。

那么我们怎么使用闭包呢?

bar();

这样就成功调用了,并且形成了bar的临时OA:

bar-OA {
    a: -> foo-OA.a
}

在bar调用结束后,bar-OA被销毁了,但是bar对foo-OA.a的引用还存在呀!

那么我们只需要bar = null 就可以销毁引用,进而销毁foo-OA对象了。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 610193653@qq.com,谢谢啦!

文章标题:JavaScript复习—闭包

本文作者:zzzwyyy

发布时间:2019-12-14, 18:50:05

最后更新:2019-12-16, 12:36:44

原始链接:http://yoursite.com/2019/12/14/JavaScript复习—闭包/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录