介绍

  1. 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。
  2. 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
  3. 闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

使用

下面在全局上下文中,定义了一个 showName 函数。

1
2
3
4
5
6
7
8
9
10
11
12
function showName() {
const company = "Bytedance";
const dep = "边缘云";
const name = "zhangqi";
console.log('company', company);
return function() {
console.log(dep);
return name;
}
}
const getName = showName();
console.log(getName());

上面代码会依次打印:

1
2
3
company Bytedance
边缘云
zhangqi

第 11 行,showName 函数已被执行完成,按照通常逻辑,里面的变量应该被回收,但是我们还是通过 getName 函数访问到了它里面的变量 depname

因为 showName 函数返回了一个函数,这个函数产生了一个闭包,这个闭包是一个地址,它指向了堆上的一块内存,这块内存存储了闭包要访问的外部函数的两个变量 depname

1
2
3
4
{
dep: "边缘云",
name: "zhangqi"
}

这个闭包会一直跟随通过 showName 函数返回的函数赋值的 getName 函数的生命周期结束。所以即使 showName 函数执行结束,它里面的这个闭包也是没有被回收的。

image.png

性能问题

  1. 在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 GC 回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数 a 被 b 引用,b 又被 a 外的 c 引用,这就是为什么函数 a 执行后不会被回收的原因。
  2. 通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
  3. 如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
    this.getName = function() {
    return this.name;
    };

    this.getMessage = function() {
    return this.message;
    };
    }
    在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
    }
    MyObject.prototype = {
    getName() {
    return this.name;
    },
    getMessage() {
    return this.message;
    }
    };
    但我们不建议重新定义原型,可以对原型进行更新操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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;
    };

总结

理解 JavaScript 的闭包是迈向高级 JS 程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。

参考资料

  1. 闭包 - JavaScript | MDN