back back-top comments magnifier menu mobile right smile views

15分钟帮你轻松理解 JS闭包

Jean
Jean

记得几年前刚工作那时,听到高大上的 JS闭包 一词 让我一头雾水,很多初学者也许和我当时一样困惑,其实 闭包 也并没那么高深莫测。

今天我写了篇简单的学习笔记 希望能帮助大家轻松理解 JS闭包。
参考资料:1.《你所不知道的JavaScript-上卷》闭包和作用域章节、2. 阮一峰老师的《学习JavaScript闭包》
( 大家学习时 为了更好理解,最好跟着本文内容 在IDE或浏览器控制台中,敲一遍所有的示例代码 )

要彻底弄懂 闭包,必须先理解 JS的 变量作用域,变量分为: 全局变量局部变量,JS的特殊之处在于:每个函数都会创建一个新的作用域,函数内部可以读取函数外部的变量,相反 函数外部无法读取内部变量。

var a = 123;

function foo() {
  console.log(a);
}

foo(); // 123
function foo() {
  var a = 123;
}

console.log(a); // error,查找不到变量a的引用

为了更透彻的理解作用域,请思考以下代码:

function foo(a) {
  var b = a * 2;

  function bar(c) {
    console.log( a, b, c );
  }

  bar( b * 3 );
}

foo(2); // 2, 4, 12

上面代码中有三个逐级嵌套的作用域,我们可以将它们想象成几个逐级包含的 作用域气泡 如下图:

15min-closure-bubble

作用域气泡是逐级包含的,图中最里面的紫色气泡 被完全包在 最外层的foo所创建的淡蓝色气泡里。

气泡 1:包含着整个全局作用域,其中只有一个标识符 foo
气泡 2:包含着 函数foo 所创建的作用域,其中有三个标识符 abbar
气泡 3:包含着 函数bar 所创建的作用域,其中只有一个标识符 c

当JS引擎执行 console.log(a, b, c) 时,开始查找 abc 这3个变量的引用,引擎首先从最内部的作用域  即function bar(c) {…} 中开始查找,由于无法在这里找到 a,因此它会去上一级的 function foo() {…} 作用域中继续查找,结果在这里找到了 a ,引擎便使用了这个引用,对于变量b、c 的查找过程也是一样,引擎会逐级向上查找。

到这里,我们知道了作用域的规则是只能从内部向外部查找变量。

那如果想从作用域外部读取内部的变量呢
一般情况当然是不行的,但有个办法!我们可以在函数内部再定义一个函数,再将内部这个函数作为返回值,看下面代码你就明白了:

function foo() {
  var a = 2;

  function bar() {
    console.log( a ); // 2
  }

  return bar;
}

var todo = foo();
todo(); // 输出结果为2 说明在函数foo的外部访问到了内部的变量a,妈妈快看呀 这就是闭包!

函数 bar 就是 闭包 

没明白?
我来解释下原理,函数 bar 的作用域能访问 foo 的内部作用域,而函数 bar 被作为值返回,当 var todo =foo(); 执行了函数 foo 时,其实就将函数 bar 作为值引用类型传递给了 todo,当执行 todo() 时 等于执行了bar(),这样在 foo 外部访问到了内部的局部变量 a,所以我们看到输出结果为2。神奇的 闭包 让函数从外面作用域访问到了内部作用域的变量,原理其实就是 局部函数 bar 在它自己所定义的作用域之外被执行了。

JS 中,通常一个函数执行完后,内部的整个作用域都会被销毁,被JS引擎的垃圾回收器回收,但闭包的出现阻止了这件事,上个例子中函数 foo 的作用域就不会销毁,因为它内部的作用域依然还存在,原来是本身在使用变量 a 的引用,而 bar foo 的作用域之外被执行,当每次调用 todo() 便又访问到函数 foo 内部的变量 a 。
简而言之,这个例子中的函数 bar 就是一个闭包。

giahlama-431313388

各种专业文献上的”闭包“(closure)定义的非常抽象,晦涩难懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在 Javascript 语言中,只有函数内部的子函数才能读取该函数的局部变量,因此可以把闭包简单理解成 “定义在一个函数内部的函数”。

在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以在函数外部读取内部的变量,另一个就是让这些变量的值始终保持在内存中。

 其实在你写过的代码中 到处都有闭包的身影,如果将函数当作第一级的值类型并到处传递时,你就会看到闭包在这些函数中的应用,如:定时器事件监听器Ajax请求跨窗口通信Web Workers 或任何其他异步或同步的任务中,只要使用了回调函数,那就是在使用闭包,下面举了几个闭包的例子:

// 闭包1
function wait(message) {
  setTimeout(function timer() {
    console.log(message);
  }, 1000);
}

wait("Hello, closure!");

// 闭包2
function setupBot(name, selector) {
  $(selector).click(function () {
    console.log("Activating" + name);
  });
}

setupBot("Closure Bot", "#bot");

// 等……
// 原来闭包无处不在啊

使用闭包的注意点:

(1) 由于闭包会使得函数中的变量都被保存在内存中,内存会消耗很大,如果滥用闭包,会造成网页的性能问题,在IE中还可能导致内存泄露。可以在退出函数之前,将不使用的局部变量全部删除 (设为null)。

(2) 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

本文由 前端先生 原创,欢迎转载,但请注明出处。

1条评论

1条评论

  1. 修正:
    气泡 3:包含着整个全局作用域,其中只有一个标识符 foo
    气泡 2:包含着 函数foo 所创建的作用域,其中有三个标识符 a、b、bar
    气泡 1:包含着 函数bar 所创建的作用域,其中只有一个标识符 c

发表评论

电子邮件地址不会被公开。 必填项已用*标注

扫描二维码分享到微信