关于javascript闭包我想说几句

2,870 阅读7分钟

声明:部分内容参考文章 Ice-shou:闭包详解一

一.前言

在我们的开发当中会经常用到闭包,因为他确确实实解决了一些问题,在面试的时候,也会经常被问到对闭包的理解,然而实际上闭包的概念并没有统一的说法,但不管是怎么描述的,它的核心都是那样,下面就来探究一下闭包到底是怎么理解的

二.基础知识:

1.变量的作用链

  • JavaScript变量有两种:全局变量,局部变量

  • 局部变量作用域一般在函数里面,在函数之外的视为全局变量

  • 一般来说,在函数里面可以访问全局的变量,在函数外面不可以访问函数里面的变量

  • Javascript存在“链式作用域”结构(chain scope),这里的链式作用域可以理解为函数嵌套,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立.

    var str1="hello";//全局变量
    function fun(){
    	var res="i am coming";//fun()内的局部变量
    }
    function funa(){
    	var str2=" world";//funa()内的局部变量
    	function funb(){
            function func(){
                console.log(str1);//hello
                console.log(str2);//world
    			console.log(res);//报错,undefined
            }
    		func();
    	} 
    	funb();
    }
    funa();
    console.log(str2);//报错
    

2.匿名函数的调用

  • 函数表达式调用法

    var exc=function(){
        console.log("hello world");
    }
    exc();
    
  • 自调用

    (function(){
          console.log("hello world");
    })();
    
  • 逐层调用

    function fun1(){
        var a=0;
        console.log(a);
        return function(){
            a++;
            console.log(a);
        }
    }
    //注意这种调用的结果:
    fun1();
    fun1()();
    fun1()();
    输出
    0
    //
    0
    1
    //
    0
    1
    //解释:每一次先执行fun1(),a都会初始化为0,再执行匿名函数,a++得到1
    
  • 先赋值给一个变量再由变量调用

    function fun1(){
        var a=0;
        console.log(a);
        return function(){
            a++;
            console.log(a);
        }
    }
    //注意这种调用的结果:
    var res=fun1();
    res();
    res();
    res();
    输出
    0
    //
    1
    //
    2
    //
    3
    //解释:fun1()只执行一次,所以a=0只执行一次,以后每次执行res()是在执行匿名函数,每执行一次,a自增一次
    

三.分析

下面我们就分别根据几本书的描述来写几个模型来看看他们所描述的闭包是怎么样子的,对我们实际的应用有没有帮助或者说我们为什么需要写闭包

首先来看看《JavaScript高级程序设计》《JavaScript权威指南》这两本书的描述

--《JavaScript高级程序设计》

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

--《JavaScript权威指南》

从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链。

function fn1() {
	var str = 'hello,world';
	function fn2() {
		console.log(str);//可以访问fn1()函数
	}
	fn2();
}
fn1();

说明

对于上面两本书对闭包的定义都比较迷,按照定义,fn2()函数就是一个闭包,如果这就是闭包了的话?对我们实际应用的帮助并不是特别明显,这不就是一个遵循了变量作用域访问规则的例子而已吗?下面我们看一个更加明显的例子...

function fn1() {
	var str = 'hello world';
	function fn2() {
		console.log(str);
	}
	return fn2;
}
var fn3 = fn1();
fn3();

说明

  • fn2的词法作用域能访问fn1的作用域
  • 将fn2当做一个值返回
  • fn1执行后,将fn2的引用赋值给fn3
  • 执行fn3,输出了变量str

好了,这个例子,我们看到了闭包的对我们实际应用的帮助了,我们认为在函数里面可以访问函数外面的变量,在函数外面不能访问函数里面的变量,那么如果我有这样的需求在函数外面访问函数里面的变量怎么办?上面的例子就做到了

下面看看《你不知道的JavaScript》是怎么描述的

--《你不知道的JavaScript》

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function fun1(){
    var a=0;
    console.log(a);
    return function(){
        a++;
        console.log(a);
    }
}
//注意这种调用的结果:
var res=fun1();
res();
res();
res();
输出
0
1
2
3

说明

  • 第一次执行var res=fun1(),把匿名函数的引用传给了变量res
  • 每次执行res()即在调用匿名函数,注意调用的位置是在匿名函数作用域之外
  • 一般来说局部变量在函数执行之后就会别垃圾回收机制回收,但是调用res()之后变量a并没有被回收,每执行一次res(),a的值自增一次
  • res()函数可以记住并访问原来所在的词法作用域

好了,上面这个例子很明显我们看到了闭包的另一个作用,可以访问并记住原来的词法作用域,那么我们会在什么情况下需要访问并记住原来的词法作用域呢?这个后面我会讲

四.总结

1.什么是闭包

一个可以访问其他作用域的变量,并且能够记住所在的词法作用域的函数

2.怎么判断是一个闭包

当一个函数的返回值是另一个函数,这个返回的函数在原来所在的函数外部被执行了,并且访问了原来函数的变量,那这个上下文便产生了闭包环境

3.闭包的优点和缺点

  • 优点

    • 实现了可以访问其他作用域变量,并且避免了全局变量对自身词法作用域变量的污染

    • 可以把局部变量(自身作用域的变量)驻留在内存中一直保存着上一次执行的值,不会被垃圾回收机制回收,从而避免使用全局变量

  • 缺点

    • 局部变量一直驻留在内存中不会被回收,导致内存被爆满,影响程序性能

4.闭包缺点的解决方案

  • 建议在非常有必要的时候才使用闭包
  • 使用完变量确定不再使用, 将null赋值给变量,var a = null;

5.闭包的常见形式

上面我用了不少的例子来解释什么是闭包,不难发现闭包存在的形式,就是一个满足闭包定义的各种条件的函数,而且常以匿名函数的形式出现(注意:并不是匿名函数都是闭包,二者不能等同)

形式一:

function fun1(){
    return function(){
        //闭包主体
    }
}
var res=fun1();
res();//闭包函数调用

形式二:

(function(i){   
	//闭包主体
})(i);//闭包函数自调用

五.闭包的应用的典型案例

异步程序中避免因执行时间不一致导致变量丢失

//打印1-10
for (var i = 1; i <= 10; i++) {
	setTimeout(function () {
		console.log(i);
	}, 1000);
}
//结果打印了10个11

原因分析:

  • i是声明在全局作用中的,定时器中的匿名函数也是执行在全局作用域中,打印i值得时候向上逐层寻找变量
  • for循环执行的速度要远比定时器执行的速度快,所以,定时器还未来得及打印i,for循环已经循环完毕,此时i的值是11,所以定时器打印出来的就是11

解决方案:

上例很明显就是在匿名函数里面访问全局变量,由于异步原因导致并未能准确打印出全局变量的值,所以解决方案就是循环i的时候,把i保存在私有作用域中并且一直保存,使用闭包来实现

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