• 深入js——闭包


    前言

    闭包一直是很多前端开发人员跨不过去的一个坎,我也是一样。每次看各种文章好像弄懂了,但隔段时间好像又模模糊糊了,再看好像又有了新的理解,但感觉总是不能完全理解透彻。
    在理解了变量环境、词法环境和作用域链等概念后,我发现理解起来容易多了,这次感觉是真的理解了。
    同样,本文的内容是基于对作用域链和变量对象的理解,因此在阅读本文之前,最好先看下深入js——变量对象深入js——作用域链

    如何定义闭包

    不同平台和书籍对闭包的定义都不一样:

    • MDN:函数与对其状态即词法环境的引用共同构成闭包。
    • 《高级程序设计》:闭包是指有权访问另一个函数作用域中的变量的函数。
    • 维基百科:是引用了自由变量的函数。

    这些定义给我一个感受:里面的每个字我都认识,咋凑在一起就不懂了呢?
    我的看法是,不要纠结于一个标准的定义,真正理解闭包如何产生、如何使用才是关键。真正理解了之后每个人都可以有自己对闭包的定义,不必拘泥于具体的文字定义。

    闭包

    这里我主要通过之前提到的浏览器控制台的scope和[[scope]]来理解闭包。
    首先来看一个简单的例子

    function bar () {
        var a = 1;
        function foo () {
            console.log(a)
        }
        console.dir(foo)
        return foo
    }
    bar()()
    

    这是闭包最典型的形式,在foo函数里打断点

    右侧Scope非常清楚地告诉我们,这儿有个闭包Closure。
    这个Closure表示,当bar的执行上下文被销毁后(此时bar已经执行完毕),foo的作用域链为[Local, Closure(bar), Global],也就是说此时foo仍然能访问到bar函数内的变量a。
    执行完函数,查看控制台

    可以看到,foo.[[scope]](foo的父级作用域链)里有刚刚提到的Closure(bar)。
    根据之前文章讲到的[[scope]]和作用域链,并结合以上分析,我们可以这样理解:
    在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,即内部函数的作用域链上会存在这些变量的结合,即使外部函数执行结束也不会改变内部函数的作用域链,也就是这些变量依然会保存在内存中,我们把这个变量的集合就是闭包。
    现在在回头去看上面的各种定义,虽然各不相同,但其实都是表达同一个意思。

    Q:很多人理解闭包一定是外部函数中返回的内部函数
    从我们上面提到的例子好像也是这样,但其实完全不一定要返回

    function bar () {
    	var a = 1;
    	function foo () {
    		console.log(a)
    	}
    	foo()
    }
    bar()
    

    直接在bar内执行foo,打开控制台可以看到Scope的状态与返回foo时是一样的,都会有Closure。
    这也很好理解,闭包其实就是与作用域链有关的一个东西,而作用域链只是与函数定义的位置有关,至于函数最终赋值给了谁,在哪里调用,怎么调用都不会影响作用域链,当然也就不会影响是否存在闭包。

    Q:注释掉foo中的console.log(a),还会有Closure么?

    实践下会发现,执行到在foo函数时,Closure不存在了,foo.[[scope]]中也没有了。
    这一点其实能帮我们更好的理解闭包的是什么,闭包就是内部函数对外部函数变量的一个引用集合,所以当内部函数没有用到外部函数的变量,那内部函数的作用域链上也就不需要有外部函数了。
    所以,有些文章简单概括闭包是函数内部的函数,是不太准确的。严格来说,必须要引用了外部函数的变量,才算形成了闭包。

    所有的函数都是闭包

    上面都是在从函数内部的函数角度在讨论闭包,主要是大部分情况下,大家默认的也都只是把上述讨论的场景认为是闭包;其实,所有函数都可以成为闭包。
    因为,闭包本质上就是有权访问上层作用域的函数,而所有函数其实都能访问全局上下文Global,也就是所有函数的[[scope]]都有Global,从这个意义上来说,所有函数都是闭包

    典型题分析

    讲了这么多,最后以《高级程序设计》中的例子来结束本文,因为我一次接触闭包就是看的它,当时基本是蒙的。

    function createFunctions() {
    	var result = new Array();
    	for (var i = 0; i < 3; i++) {
    		result[i] = function() {
    			return i;
    		}
    	}
    	return result;
    }
    var funcList = createFunctions()
    console.log(funcList[0]())
    console.log(funcList[1]())
    console.log(funcList[2]())
    

    结果不会打印出0,1,2,而是打印出3个3,这个大家基本都知道。
    但是如何能输出0,1,2呢?方案大家也基本都知道。通过创建一个匿名函数的方式加一个闭包:

    function createFunctions() {
    	var result = new Array();
    	for (var i = 0; i < 3; i++) {
    		result[i] = function(num) {
    			return function() {
    				return num;
    			}
    		}(i)
    	}
    	return result;
    }
    var funcList = createFunctions()
    console.log(funcList[0]())
    console.log(funcList[1]())
    console.log(funcList[2]())
    

    两个例子中,都存在闭包,但为什么上面的例子没有得到0,1,2,而下面的方案就可以,我们具体来看下。
    打开控制台结合Scope分析会更清楚,但为了保持逻辑清晰,以下就不放图了,大家可以自行调试分析。

    错误例子

    上面的例子中,result数组的每一项的作用域链都是

    [Local,Closure(createFunctions),Global]
    

    而每一项的Closure(createFunctions)是共享的,也就是当createFunctions函数执行完,此时createFunctions中的i的值为3了,所以当执行result数组的任一项时,根据作用域链查找就会查到Closure(createFunctions)里的i,此时i为3,所以就都打印出了3。

    正确例子

    加了一层闭包后,result数组里各项函数的作用域链就变为了

    // result[0]
    [Local,Closure({num:0}),Global]
    // result[1]
    [Local,Closure({num:1}),Global]
    // result[2]
    [Local,Closure({num:2}),Global]
    

    也就是此时各项函数作用域链里的闭包均是单独创建的,是相互独立的三个不同闭包,并且也独立与createFunctions的作用域。所以当createFunctions执行完后,createFunctions里的i同样是3,但已经不会影响到result数组里的各项函数了。这些函数在执行时,都会顺着自己的作用域链找到相应的Closure,并返回其中的变量num的值。

    小结

    写完有种感觉,闭包真是一个即使理解了也不太容易用文字讲述清楚的东西,有点只可意会不可言传的意思。
    另外,在文中,大家会发现我一会说闭包是一个函数,一会说是一个变量集合。其实,这就和文章开头所说的一样,不用刻意定义闭包是什么,准确来说它是一种使用场景,但至于这个场景里哪部分是闭包,没有深究的必要。

  • 相关阅读:
    iOS开发之--隐藏状态栏
    iOS开发之--iPhone X 适配:MJRefresh上拉加载适配
    iOS开发之--为UITextField监听数值变化的三种方法
    ios开发之--为父view上的子view添加阴影
    iOS开发之--在UIWindow上展示/移除一个View
    iOS开发之--Masonry多个平均布局
    CocoaPods更新过程中出现的坑及解决方法
    那些已成定局的人和事
    两个陌生人的对话
    好好写代码吧,没事别瞎B去创业!
  • 原文地址:https://www.cnblogs.com/youhong/p/12218661.html
Copyright © 2020-2023  润新知