2
头图

闭包是在JavaScript中常见的概念,不过各类其它语言也都模拟实现了闭包的行为,包括Java,C++,Objective-C,C#,Golang等等(不过还是和传统闭包有所区别)。之前对这个概念一直不是很清晰,希望能通过阅读网络材料并贯通学习掌握闭包的原理和应用场景。

定义与初衷

根据维基百科的溯源,闭包(closure)概念最早是在1964年由Peter Landin定义,用于表述在他的SECD机器上求解表达式时的“环境部分”+“控制部分”,这个术语用来指代某些开放绑定(自由变量)已被其周围的词法环境闭合(close,或绑定)的Lambda表达式[1]。这个概念还是有些抽象,更明确一些的表述是MDN Web社区对于闭包的说明:一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合称为闭包[2]。进一步的理解每个人也有不同的看法[3]。

从定义来看,闭包最大的用途其实是把一个函数,和一组它“私有”的变量看捆绑在一起。在这个函数被多次调用的过程中,这些变量都可以保留变化,并且又不会被其它函数改变。保留变量的值被多次使用不难,核心价值在于同时保证这个变量的私密性,对外隐藏相关信息。这里就涉及到变量的作用域问题,也是JavaScript中的一大知识点。绝大部分材料也都以JS来应用和解释闭包。

闭包实例与应用

官网教程中举了一个清晰的例子来说明对闭包的定义:

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}
 
var myFunc = makeFunc();
myFunc();

首先,这段代码可以运行。直观上var myFunc = makeFunc()已经运行完了makeFunc()函数,但随后的myFunc()调用能够正常处理的原因就在于形成了闭包。这里形成闭包的函数是displayName实例,与其绑定的变量就是name,准确的说变量name被保存在了displayName函数实例的词法环境中,共同形成了闭包。因此调用myFunc时,变量name仍然可以被alert出来。

这是一个用于说明概念的实例,真实的应用场景中,闭包的使用方式繁多,举一些收集到的案例:

JavaScript用闭包模拟私有方法

https://developer.mozilla.org...

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();
 
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

独特的是能够使用这种方法来实现类似Java封装私有函数的场景,多个函数能够共享相同的词法环境(操作相同的私有变量)

Golang闭包和协程的使用

带我重新认识闭包的例子,是学习golang时在segmentfault回答的一个问题(https://segmentfault.com/q/10...),以下代码中的协程用法:

for i := 0; i < 100; i++ {
    go func(i int) {
        fmt.Println(i)
    }(i)
}

以及Golang学习网站相似的例子:https://books.studygolang.com...

在Golang中,闭包体现为一个嵌套的匿名函数,它主要的用途包括[8]:

  1. 和JS一样,函数私有变量,延长变量的生命周期,并把变量隔离起来不让外界访问。
  2. 回调,和其它语言一样。
  3. 包装函数,并制作“中间件”(middleware,Golang中的概念为可重用的函数)。在Golang中,函数是一等公民,可以把函数作为参数放到另一个函数中,那么一些通用的处理逻辑,比如打印日志,计时器等等都可以实现为闭包。
  4. 在不少库函数中,你可以使用闭包传入函数来充分利用库函数带来的便捷性,例如在sort包中,可以通过闭包中的函数确定搜索对象的筛选条件。

Java中的闭包与Lambda函数

Java 8之后,语言生态不再只盯着对象了,很多时候函数式编程的方法显得更加简洁轻巧,也因此引入了Lambda表达式。而它又经常和“匿名函数”及“闭包”一起被提及。首先需要明确的是Lambda函数和匿名函数基本相等,但和闭包并不等价,从起源也能看出,闭包是在计算Lambda表达式时被引入的概念,而不等于Lambda表达式本身。针对Lambda表达式和闭包区别本身的解读可以参考[10],有非常详细的说明。简单来说Lambda表达式是编写程序的一种简洁方式,而其中涉及到开放表达式——表达式中的变量在函数外部时,就需要使用闭包的方式把函数和外部参数所处的词法环境绑定起来进行计算。这一点其实对任何语言也都通用。

int n = 0;
final int k = n; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
    int i = k;
    // do something
};
n++;      // Now will not generate an error
r.run();  // Will run with i = 0 because k was 0 when the lambda was created

其中变量k超出了Lambda表达式之外,需要使用闭包来引入使用。值得注意的一点是,Java本身最好的写法就是封装一个对象包含私有变量和私有函数,不需要像JavaScript一样去模拟。

闭包实现

根据闭包的定义和想要达到的功能表现,我们可以看出闭包背后实现是通常的做法就是使用一个数据结构,这个结构中首先需要保存一个指向函数代码的指针,其次需要保存闭包创建时的词法环境,最典型的就是保存创建时所有可用的变量。

理想的可以实现闭包的语言,它在运行时的内存模型中,所有原子变量应该放在一个线性栈里。而在这种语言场景下,如果创建闭包,那么对应词法环境中的变量就不能随函数执行完毕被回收,此时典型的做法是把变量放在堆中(能和Java例子中闭包使用的变量需要用final修饰结合起来),直到所有的闭包引用都使用完毕再进行回收。我想这也间接说明了网上广泛流传的“IE中使用闭包会有内存泄露”问题的由来。闭包也更加适合于那些会进行“垃圾回收”的语言。

而对于只操作栈的语言来说,实现闭包就不那么容易了,这些原子开发变量会出现野指针等问题。典型的以栈为基础的编程语言包括C和C++。

而专门针对JavaScript来说,闭包的实现有赖于其变量作用域的相关机制,我也还需要进一步学习JavaScript语言背后的内存模型。

优势和劣势

面对闭包,我们需要考虑的是要不要用,以及不用闭包的情况下是否还有其它的替代方式。

首先闭包的最大优势已经体现在其应用场景中,避免使用全局变量,让变量被函数所“私有”,并且长期留存供多次使用。

不过闭包也有其劣势,从原理和实现的角度:

  1. 闭包主动延长了它所绑定的词法空间中相关变量的生命周期,而这部分变量会再用完前始终放在内存里,占用资源,一旦处理不佳(比如IE)还有可能出现内存泄露问题。
  2. 其次,闭包的性能并不算好,一些特殊的场景需要注意写法,例如[2]中所举的例子:
// bad case,每次构造器被调用时会对方法进行赋值
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };
     
  this.getMessage = function() {
    return this.message;
  };
}     
// good case,拆分出来
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

参考资料

  1. https://en.wikipedia.org/wiki...(computer_programming)
  2. https://developer.mozilla.org...
  3. https://segmentfault.com/q/10...
  4. https://www.liaoxuefeng.com/w...
  5. https://zhuanlan.zhihu.com/p/...
  6. https://www.runoob.com/w3cnot...
  7. https://books.studygolang.com...
  8. https://www.calhoun.io/5-usef...
  9. https://riptutorial.com/java/...
  10. https://stackoverflow.com/que...

Hotlink
340 声望7 粉丝

Stay hungry, stay foolish.