V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
Mark24
V2EX  ›  问与答

关于闭包一大堆问题

  •  
  •   Mark24 · 2015-12-21 15:37:10 +08:00 · 1995 次点击
    这是一个创建于 3259 天前的主题,其中的信息可能已经有所发展或是发生改变。

    老生常谈
    1.闭包是什么?
    2.闭包有什么用?

    3.附加题 JS 的闭包和 Python3 的闭包各有什么特点?

    想吐槽

    4.大多对于闭包的解释,都是:
    a.从结果出发(黑盒)
    b.从底层原理出发(实现)
    实际上,我从未看过这样子解释一个东西,这样解释,缺乏对闭包的深刻了解。我相信,即使你懂了,你也不知道如何去创造性的使用闭包,仅仅是复制前人的例子而已。
    不清不楚的东西,要不就是遗忘,要不就是从来不用。

    看了这么多 blog 和资料,没有一个能够解答心中的疑惑的,郁闷呢……
    

    5.闭包真的是一个好的设计么?
    很多资料都是表示,自由变量,可以常驻内存,过量使用,会导致内存泄漏,所以谨慎使用
    额……为毛追捧一个,会故意导致内存泄漏的特性
    还夸上天……我怎么觉得呵呵
    这是明显的设计漏洞啊

    对 JS 各种黑科技深深的不服,设计的漏洞百出。
    我倾向于,这是一个漏洞,历史,或者被误用而夸大的特性
    而不是经过深深的思考,释放出来的特性
    
    11 条回复    2020-06-19 16:52:05 +08:00
    SpicyCat
        1
    SpicyCat  
       2015-12-21 16:15:56 +08:00
    建议看看这个了解下 closure 。
    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

    如果没有 closure, JS 将一文不值。所谓有失必有得,灵活性的代价就是稳定性。
    banricho
        2
    banricho  
       2015-12-21 16:51:33 +08:00   ❤️ 1
    JS 的闭包涉及到作用域的特性,这两天也整理了一下,算是对 MDN 中例子的补充。
    欢迎讨论,欢迎指正,相互学习~

    https://github.com/banricho/webLog/issues/4#issuecomment-165320894
    YuJianrong
        3
    YuJianrong  
       2015-12-21 16:58:10 +08:00   ❤️ 1
    不懂就不懂唧唧歪歪半天,还内存泄露……
    你造一个 array 只添不删也是内存泄露知道吗!(而且所谓闭包的内存泄露就和这个差不多)

    闭包是个 60 年代就创造 70 年代就实现的语言概念,现在 C++ 11 , ObjectiveC , Java8 都支持或者开始支持的概念你说是明显的设计漏洞?

    呵呵。
    yangtze
        4
    yangtze  
       2015-12-21 16:59:37 +08:00 via iPhone
    闭包还是很好用的,产生原因是 ES5 之前 javascript 仅支持全局作用域和函数作用域,而没有块级作用域

    不过 ES6 中对全局作用域这块添加了另一种解决方案,把 var 改成 let , function 改成 class ,就有块级作用域了,基本就不要专门搞闭包了
    ChefIsAwesome
        5
    ChefIsAwesome  
       2015-12-21 17:02:44 +08:00 via Android
    知道多少语言有闭包这个概念吗
    banricho
        6
    banricho  
       2015-12-21 17:11:08 +08:00
    @yangtze

    我认为 let 只是方便了开发,避免了部分情况下手动创建闭包。从下面的例子来看,即使用了 let 还是创建了很多个闭包。

    function demo() {
    'use strict';

    var links = document.getElementsByTagName('a'),
    len = links.length;

    for (let i = 0; i < len; i++) {
    links[i].onclick = function() {
    console.log(i);
    return false;
    }
    }
    }

    闭包本身不是罪,内存就是拿来用的,合理使用就行…
    Mark24
        7
    Mark24  
    OP
       2015-12-21 23:45:22 +08:00
    @banricho 感谢整理,感谢回答。给了我很大灵感

    我还是自问自答吧
    刚才又看了一会资料。个人的粗糙理解:

    以 Python3 的闭包为例:
    在我理解,闭包的最大意义就在于
    1.保存环境:实际上创造了第三种作用域,内部的变量可以不被回收
    2.函数的面向对象:将函数和数据捆绑了起来,第三种作用域内部的函数可以看出私有变量(因为别人访问不到)
    a.包装了函数 b.捆绑了变量数据

    顺便区分一下,装饰器,偏函数,闭包
    函数式编程,这三者真的是纠缠在一起,没搞清楚就会混淆

    装饰器,偏函数其实并不是闭包
    他们也基本上没有什么关系
    他们只是形式上有些相似,就是函数嵌套函数,并且返回函数

    把函数当做参数,和当成返回值,这就叫函数式编程,因为可以实现数学上的 f(f(f(x)))这种感觉

    装饰器,就是嵌套函数,传入函数 f ,对 f 进行加工,添加一些功能,再返回函数,依然给 f
    这个变量就切换指向了
    实际上是丢弃了原来的函数,变成了增强函数,这就是装饰器

    偏函数,函数的某些变量被固定了
    这样子返回的是一个函数,可以在适合的时候, f( ) 起作用
    经常用在 GUI ,比如一个按钮,先固定尺寸,颜色主题,等固定的变量,空出一个标题和信号绑定
    然后后面合适的时候,不断地绑定

    ======
    下面来讲闭包:

    闭包,形式上和上面两个有点像,很接近,其实很不一样
    首先,闭包的语法形式上要素是:
    1.嵌套函数
    2.返回的是函数
    3.内部函数,引用外部函数的变量,包括嵌套函数的 f(x)的直接参数 x

    如:
    def f(x):
    ....a=1
    ....def g( ):
    ........nonlocal a,x
    ........a += 1
    ........x += 1
    ........print("a:{},x:{}".format(a,x))
    ....return g

    s = f(5)
    s()
    s()
    s()
    ====
    输出:
    a:2,x:6
    a:3,x:7
    a:4,x:8
    [Finished in 0.1s]


    就比如这个例子,内部嵌套函数, g 引用了 x , a
    这就算闭包了。
    要想解释清楚这个名字,得从调用说起。

    s = f(5) #这步调用,其实是生成了一个函数 g( ) ,变量名 g 付给了 s
    只有当 s( ) 的时候,才启用

    退一步,我们都知道,函数会产生自己的局部作用域,对 s 来讲,就是 g 函数
    g 函数产生了自己的作用域,但是 g 函数调用了 a , x 两个变量, a,x 又在 f 中,可是 f 又调用结束了,该回收了

    解释器陷入两难,于是,算是特殊情况
    于是解释器,生成了一个特殊作用域

    介于 全局作用域和 f 的局部作用域之间的,作用域

    当你反复调用,生成多个函数,该作用域中 a , x 是彼此独立的,这个比较特别,要记住
    想一个封闭的包裹,夹在 f 的局部作用域和 全局作用域之间
    这个从属于外部函数 f 局部作用域的变量 a,x ,被函数“封闭”起来了。
    被封闭起来的变量的寿命,与封闭它的函数寿命相等
    名字起得很形象,夹在中间,作用域封闭起来,称为闭包( Closure )。

    封闭的作用域,外界访问不到
    还有就是,他是不会被回收的,据说,寿命和函数一样长。
    如果你设置变量为累加
    反复调用,就会积累数值

    这个作用域和不会被回收的变量,就叫做环境
    所谓的记住环境,就是这些变量的数值,可以保持

    变量被称作自由变量
    我没看出哪里自由,高等数学里自由变量的意思是可以是任意值,表示为 C
    这里大概的意思是,解释器管不着,不被回收

    总结一下闭包,之后的特点:
    作用域内的变量
    1.不会被回收
    2.相对封闭,不会被访问到,想一个封闭的包内的变量
    3.反复调用,可以积累
    4.for 循环使用的时候,总是以最后一个为准,因为是临解释的时候,才使用

    换个角度看闭包:

    def f(x):
    ....a=1
    ....def g( ):
    ........nonlocal a,x
    ........a += 1
    ........x += 1
    ........print("a:{},x:{}".format(a,x))
    ....return g

    闭包实际上就是记录了外层嵌套函数的作用域中的变量
    通过这个 f , g 例子,可以建立多个自定义函数

    这很容易让人联想到面向对象编程
    f 更像是 g 的构造器,
    a,x 是一个私有变量
    数据和变量捆绑,函数版本的面向对象
    闭包意味着数据与函数结合起来了,这和面向对象思想中的“对象”的概念很接近。

    于是闭包有了各种用途

    1.可以返回加强的函数,作为函数工厂函数,比如时间戳装饰器
    2.可以贮藏闭包作用域中的变量,可以做一个独立环境的计数器
    3.可以构造一个私有变量( JS )
    4.记住环境, JS 中,可以带着 DOM 的 div 信息,跟到最新点击信息

    5.既然都是面向对象,闭包可以实现的, OOP 都可以实现,反过来
    OOP 可以实现的功能,闭包有时候可以化繁为简。
    banricho
        8
    banricho  
       2015-12-22 00:52:24 +08:00
    @Mark24 感谢整理,用的语言比我精炼多了,也对我启发很大~
    banricho
        9
    banricho  
       2015-12-22 21:28:34 +08:00
    今天重新整理了一下,之前我的表述和逻辑都存在一定错误…
    Mark24
        10
    Mark24  
    OP
       2020-06-19 16:48:54 +08:00
    突然看到有人收藏。

    研究过《 Ruby 原理剖析》,至于 Python,Ruby,JavaScript 都差不多。至少模型是通用的

    我简单再自问自答一下:


    闭包产生的形式代码 大概是—— 函数 A 内部定义了函数 B,内部函数 B 引用了外部函数 A 的变量,A 函数返回了函数 B 。
    一般来说,一个函数执行完毕,就会被回收掉。这里有点区别,函数 A 执行完毕,返回的是 B 。B 还引用者 A 的变量。

    这就是闭包了。被 B 引用的变量,由于存在引用关系无法被切断。就像一个小背包一样,永远携带着。永远可以访问。
    反过来,也就无法被回收内存。

    本质上底层,是函数 B 保存了对外部环境也就是 A 的作用域链的引用,其实是一个指针。Python,Ruby 都是这样实现的。JS 没看过 V8 源码,应该也是一样的。


    闭包有什么好处呢?
    其实是有好处的 —— 可以极大地简化代码。
    如果没有闭包,会如何? 访问变量,必须靠传参。闭包可以自动向外顺着保存的作用域链的指针,向外自动查找变量。无形中大量简化了代码。

    JS,Ruby 中大量的使用闭包,可以让代码非常简洁清晰。
    这就是闭包,一个聪明的设计,把一个频繁使用的行为,固化到解释器内部,帮你做。
    Mark24
        11
    Mark24  
    OP
       2020-06-19 16:52:05 +08:00
    5 年前刚入行。哈哈,5 年后。时间过得真快。

    当时的理解趋于表象。也没错,但是没有触及本质。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1240 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 18:01 · PVG 02:01 · LAX 10:01 · JFK 13:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.