从 λ 演算看 JS 与 JAVA8 闭包

  关于 λ 演算在这篇博客 λ表达式与邱奇数,JAVA lamda表达式实现 中做了一个初步的介绍,这次我们来看一些实际应用中的例子:闭包。闭包的知识点有很多,但核心概念就一个,从 λ 演算的角度看便是:自由变量的替换依赖于定义函数的上下文环境。也就是说上下文环境的改变会通过影响函数中的自由变量而直接影响函数的定义。

  在 js 中闭包的使用非常多,js 的闭包基于函数作用域链,可被用来定义命名空间及进行变量逃逸,是 js 模块化的基础。但在 java 中用的相对较少,因为 java 的语法限制,从 λ 演算的角度看,java为了语言的简洁和正确性,禁止了我们对函数中自由变量的修改。这便是 java 与 js 闭包不同的核心。在 java 看来,因为本身已经有非常完备的类型支持,不需要借助闭包来定义命名空间。修改自由变量(即在lambda函数之外定义的任何对象)的Lambda函数可能会产生混淆。其他功能的副作用可能会导致不必要的错误。
  我们首先来看一下自由变量,对于一个 λ 表达式:

   λx.x+1

  等价于:

  f(x)=x+1

  在上述函数中,x为入参,被 λ 绑定,因此 x 是一个绑定变量,这是相对于自由变量的一个概念。而对于如下表达式:

  λx.x+y

  等价于:

  f(x)=x+y

  其中 x 为入参,被 λ 绑定。而 y 并没有被绑定,y 便是该表达式中的自由变量。

  在实际的生产中,纯粹的自由变量是不存在的,因为如果一个变量始终都不会被赋值,那么该变量对于函数的运行将毫无意义。通常一个 λ 表达式中的自由变量会被更外层的 λ 表达式进行 λ 绑定。也就是说 λx.x+y 的外层通常会有一个 λy 对该表达式进行了绑定(不是两个入参的函数的柯里化表示,而是函数嵌套调用), 或者是在函数声明时,y 便被替换为了实参。

  对于第一种情况,外层再次绑定的结果便是函数的嵌套调用,即 λy.λx.x+y ,等价于:

  f(x)=x;

  f(y)=f(x)+y;

  (柯里化的情况等价于 f(x,y)=x+y)

  而对于第二种情况,便是我们所说的闭包。λx.x+y 中的自由变量 y 始终未被绑定(没有被任何一层函数调用作为入参),而是在函数声明时,被替换成了某个具体的值。下面我们看 java 中具体的例子:

    public Consumer<Integer> getCosumer(){
        Integer i0=1;
        Integer i1=2;
        Consumer<Integer> f=(inConsumer)->{
            System.out.println(i0+inConsumer);
        };
        f.accept(i1);
        return f;
    }

  我们在函数  getCosumer()  中又定义了另一个函数 f.accept() 。

   f.accept() 的 λ 表示为 λ inConsumer.i0+inConsumer 。其中 i0 是一个自由变量,依赖外层函数 getCosumer() 中的局部变量 i0 。也就是说  f.accept()  的定义是依赖于 getCosumer()  的执行的。如果 getCosumer() 的上下文不存在,则  f.accept() 是不完整的,因为 i0 始终是一个变量无法替换为有意义的实际值。

  对于 java 来说,i0 的传递依赖的是匿名内部类的传参,也就是说 i0 必须是值不可变的 final 类型(代码中没有用 final 修饰 i0 是因为 java8 及之后的版本,编译器会为我们自动将向匿名内部类传递的参数声明为 final )。我们尝试改变 i0 的值,在编译期会直接报错:

  而对 i1 的改变则没有限制。 i1 是入参,供函数执行时使用,但对函数的定义没有影响。而 i0 是函数中的自由变量,依赖其所处的运行环境,是函数的定义的一部分。

  如果还觉着抽象我们再看一个例子,改一下上面的 getCosumer() 方法:

    public static  void main(String[] args){
        ClosureTest c=new ClosureTest();
        c.getCosumer(1);
        c.getCosumer(2);
    }

    public Consumer<Integer> getCosumer(Integer para){
        Integer i0=para;
        Integer i1=2;
        Consumer<Integer> f=(inConsumer)->{
            System.out.println(in+inConsumer);
        };
        i0=5;
        f.accept(i1);
        return f;
    }

  我们执行两次外层方法 getCosumer() ,获得两个上下文中的  f.accept(),对于两个  f.accept() 来说,虽然它们的入参 i1 都是2,但因为上下文不同导致了 i0 的不同。

  可以这么说, 两个上下文中的  f.accept() 函数不是同一个函数,分别是 λ.inConsumer 1 + inConsumer 与 λ.inConsumer 2 + inConsumer 。自由变量的替换将直接影响函数的定义。

  在这种情况下,如果修改 getCosumer() 上下文中的 i0 的值,其内部函数 f.accept() 的定义也会随着改变,所以  java  禁止我们对 i0 的值进行改变,必须对其用 final 修饰。而在向 f.accept() 方法传递 i0 的值时则是传递了一份变量的副本,而不是直接传递 i0 的引用。在 getCosumer() 执行完后, 栈中的 i0 随着栈帧被释放掉,返回的 f.accept() 中保存了一份 i0 的副本(值)。

  js 的闭包的本质与上述 java 代码相同,但 js 允许对自由变量进行修改。我们来看一段代码:

var fun=function(){
    var a=‘我是外部函数的变量a‘;
    return function(){console.log(a);}
}

var result=fun();
result();

  内部匿名方法做为返回值,其定义依赖外部函数 fun() 中的局部变量 a 。与 java 不同的是,在 js 中函数也是对象(而 java 中依赖实现了函数接口的匿名内部类对象来定义函数对象),内部匿名方法声明后,外部方法 fun() 的执行上下文并没有随着 fun() 的执行结束而被销毁。因为 fun() 将本次调用上下文中变量 a 的引用直接传递给了内部匿名函数(而 java 中如果传递给内部类的是方法中的局部变量,则只是将变量的副本传递给了内部类对象)。另外,js 中允许外部方法对本层提供给内部方法的自由变量进行修改,也就是本例中修改 a 的值,而这在 java 中是不被允许的。

  每次外部函数的调用都会形成一个新的作用域,在此作用域中被声明的匿名内部方法因为持有该作用域中变量的引用而导致了该作用域未被垃圾回收,js 中的闭包虽然可以借此来帮助我们实现命名空间的隔离,但也会带来内存泄漏问题。

  执行结果:

  我们再看一段修改函数定义上下文中自由变量的例子:

var fun=function(){
    var a=‘我是外部函数的变量a‘;
    var getA = function(){console.log(a);}
    var setA = function(){a+=‘,我被改了‘;}
    return {
        getA:getA,
        setA:setA
    }
}

var result=fun();
result.getA();
result.setA();
result.getA();  

  执行结果:

  可以看到 js 中对传递给内部函数的自由变量的修改没有限制。实际上,在JavaScript中,一个新函数维护一个指向它所定义的封闭范围的指针。这个基本机制允许创建闭包,这保存了自由变量的存储位置 - 这些可以由函数本身以及其他函数修改。JavaScript使用闭包作为创建“类”实例:对象的基本机制。这就是为什么在JavaScript中,类似的函数 MyCounter 称为“构造函数”。相反,Java已经有类,我们可以以更优雅的方式创建对象。

  在 js 中当闭包函数调用时,它会动态开辟出自己的作用域,在它之上的是父函数的永恒作用域,在父函数作用域之上的,是window永恒的全局作用域。闭包函数调用完了,它自己的作用域关闭了,从内存中消失了,但是父函数的永恒作用域和window永恒作用域还一直在内存是打开的。闭包函数再次调用时,还能访问这两个作用域,可能还保存了它上次调用时候产生的数据。只有当闭包函数的引用被释放了,它的父作用域才会最终关闭(当然父函数可能创建了多个闭包函数,就需要多个闭包函数全部释放后,父函数作用域才会关闭)。

  与之相比,java 对闭包的支持显得并不是那么完善。当然,硬来的话我们也可以用 java 模拟出类似 js 的闭包,比如:

    public Consumer<Integer> getCosumer(){
        StringBuilder strBuilder=new StringBuilder("原自由变量");
        Consumer<Integer> f=(inConsumer)->{
            System.out.println(strBuilder.toString());
        };
        strBuilder.append(",被外层函数修改了");
        return f;
    }

  我们不能改变 strBuilder 指向的地址,但我们可以修改该地址中对象的内容。但并没有什么必须要使用这种不怎么优雅的写法的场景,所以我们很少见到它。

原文地址:https://www.cnblogs.com/niuyourou/p/12249521.html

时间: 2024-08-30 04:20:31

从 λ 演算看 JS 与 JAVA8 闭包的相关文章

终于理解JS中的闭包了

之前看到一个观点是  闭包是走向高级Javascript的必经之路,之前看过很多关于闭包的讲解帖子,一直没有理解透彻,模棱两可. 现在终于可以讲出来了. 检验自己有没有掌握一个知识,最好的方式是讲给一个不懂的人 ,给Ta讲懂了.我做到了. 请有心读者检阅我的知识点有么有错误. 一:什么闭包 首先要理解 js特殊的作用域机制:只能按照作用域链向上访问,而不能访问Ta下级域中的变量. 闭包:就是能够读取其他函数内部变量的函数. (*^__^*) 一切函数某种环境下都可以当做闭包. 手写一个demo:

js中的闭包和c#中的闭包

我们 关于闭包,一个老僧长谈的话题:js的闭包俺将的比较多了,而且很详细,俺就不说了,可以看看之前的文章: 我们来对比一下c#中的闭包和js中的闭包: 先看我们的c#代码: static List<Action> fn0() { int result = 0; List<Action> list = new List<Action>(); for (int i = 0; i < 10; i++) { result = i + 1; //这样result相当于一个全

通俗易懂地解释JS中的闭包

1. "闭包就是跨作用域访问变量." [示例一] ? 1 2 3 4 5 6 7 8 9 var name = 'wangxi' function user () {  // var name = 'wangxi'  function getName () {  console.log(name)  }  getName() } user() // wangxi 在 getName 函数中获取 name,首先在 getName 函数的作用域中查找 name,未找到,进而在 user 函

详解js中的闭包

前言 在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以<javascript高级程序设计>里面的理论为基础.用拆分的方式,深入讲解一下对于闭包的理解,如果有不对请指正. 写在闭包之前 闭包的内部细节,依赖于函数被调用过程所发生的一系列事件为基础,所以有必要先弄清楚以下几个概念: 1. 执行环境和活动对象 ** - 执行环境(execution context)定义了变量或者函数有权访问的其他数据,每个执行环境都有一个与之关联的

js中的闭包之我理解

闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的理解以及js内部解释器的运作方式的描述,都是可以看出你js实际水平的.即使你没答对,也能让考官对你的水平有个评估.那么我先来说说我对js中的闭包的理解. 闭包是很多语言都具备的特性,在js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等. 在理解闭包以前.最好能

(转)js中的闭包问题

闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的理解以及js内部解释器的运作方式的描述,都是可以看出你js实际水平的.即使你没答对,也能让考官对你的水平有个评估.那么我先来说说我对js中的闭包的理解. 闭包是很多语言都具备的特性,在js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等. 在理解闭包以前.最好能

js中的闭包理解

闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的理解以及js内部解释器的运作方式的描述,都是可以看出你js实际水平的.即使你没答对,也能让考官对你的水平有个评估.那么我先来说说我对js中的闭包的理解. 闭包是很多语言都具备的特性,在js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等. 在理解闭包以前.最好能

js匿名函数闭包

函数声明: function functionName(arg0,arg1){ //函数体 } 函数表达式: var functionName = function(arg0,arg1){ //函数体 } 函数声明和函数表达式之间的主要区别是前者会在代码执行前被加载到作用域中,而后者是在代码执行到那一行的时候才会有定义.另一个区别是函数声明会给函数指定一个名字,而函数表达式则创建一个匿名函数,然后将这个函数赋给一个变量 1 递归 递归函数是在一个函数通过名字调用自身的情况下构成的,例如: fun

js中的闭包理解一

闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的理解以及js内部解释器的运作方式的描述,都是可以看出你js实际水平的.即使你没答对,也能让考官对你的水平有个评估.那么我先来说说我对js中的闭包的理解. 闭包是很多语言都具备的特性,在js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等. 在理解闭包以前.最好能