JavaScript复习—闭包

·  阅读 147

什么是闭包

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

闭包是怎么形成的

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

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对象了。

分类:
前端
标签: