JavaScript 中的闭包

146 阅读5分钟

前言

闭包是一个晦涩难懂的概念,很难确定到底是一个什么东西,本文探究 JavaScript 中的闭包

《你不知道的 JavaScript》: 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前此法作用域之外执行

《JavaScript 高级程序设计》: 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的

《JavaScript 权威指南》: 函数对象与作用域(即一组变量绑定)组合起来解析函数变量的机制,在计算机科学文献中被称作闭包

通过这些说明,我们可以大概理解闭包就是能够读取其他作用域变量,这个作用域与其函数的组合

作用域

在仔细分析闭包之前,我们要先了解作用域,作用域就是用于存储并方便之后查找变量的规则,在 JavaScript 中的作用域为词法作用域,简单来如何查找变量是由你的代码写在哪里来决定的,而作用域链就是当在你自身的作用域找不到该变量便会向上级作用域查找,如果还找不到就逐级向上查找,直到在全局作用域也找不到的话就抛出 ReferenceError 异常

var a = 1;
function fun1() {
  var a = 2; 
  console.log(a);
};
function fun2() {
  console.log(a);
};
fun1() // 2
fun2() // 1

全局作用域

在浏览器中,全局作用域就是 window 对象,我们在全局作用域声明一个变量,会自动添加到 window 属性中

window.a // undefined
var a = 1;
window.a // 1
1

函数作用域

在 JavaScript 函数是有自己的作用域的,在函数作用域的外部无法直接访问函数作用域内部的变量

var a = 1;
function fun1() {
  var a = 2;
  cosnole.log(a) // 2
}
fun1();
console.log(a) // 1

块作用域

ES5 中的块作用域

一般来说,在 ES5 中是没有块作用域的

var a = 1;
{
  var a = 2;
  console.log(a); // 2
}
console.log(a); // 2

但也不是完全没有,其中 withtry/catch 语法是拥有块级作用域

可以看出例子中 catch 中的 a 是块级作用域,只在 catch 代码块内,而全局作用域的 a 并没有被覆盖

var a = 1;
try {
  throw 2;
} catch(a) {
  console.log(a); // 2 
}
console.log(a); // 1

ES6 中的块作用域

在 ES6 中,增加了 letconst 关键字用于声明块级作用域

let a = 1;
const b = 2
{
  let a = 3;
  const b = 4
  console.log(a); // 3
  console.log(b); // 4
}
console.log(a) // 1
console.log(b); // 2

闭包

闭包长什么样子

我们了解了作用域的概念后,就可以理解下面这个代码了

function fun1() {
  var a = 1;
  return function fun2() {
    console.log(a);
  }
}
var a = fun1()
a(); // 1

我们在立即执行函数表达式 fun2() 中通过使用了 fun1() 的变量 a ,则此时便产生了闭包

在作用域内部直接调用函数是否是闭包

JavaScript 中闭包的基本概念其实还是比较简单的,但是还有一些细节问题本人还没有理解

例子中我们在 fun1() 中我们定义了 fun2() ,然后我们在 fun1() 作用域中又直接调用了 fun2() ,那么此时是否产生了闭包呢

function fun1() {
  var a = 1;
  function fun2() {
    console.log(a);
  }
  fun2();
}
fun1();

对于这个问题我更倾向于他是闭包(仅代表个人理解,本人菜鸟,还在继续学习),有三个原因

  1. 《JavaScript 高级程序设计》 中通过对 [[Scope]] 的分析来理解闭包,我们也以 [[Scope]] 分析来理解闭包,可以看出,此时 fun1() 的作用域是被添加进了 fun2() 内部的作用域的,所以是闭包
  2. 《JavaScript 权威指南》 中说"严格来讲,所有 JavaScript 函数都是闭包",可以看出,也是闭包
  3. 上例子中, fun2() 引用了作用域以外的变量,其存在的争议就是此时调用栈中的 fun1() 并没有出栈,作用域并没有本该被销毁,但是对于闭包的理解并没有说一定是 fun1() 中作用域本应该被销毁,而由于闭包作用域依然保存在内存中这种情况才是闭包

闭包的应用

模块

模块主要就是对逻辑进行分块,各自封装,相互独立,然后自行决定对外暴露什么,同时自行决定引入执行那些外部代码.

而通过闭包,我们即可以很简单的完成最基本的模块的封装

hellow.js

function hellow() {
  var name = 'a';
  var helloStr = 'Hello World';
  function sayHellow() {
    console.log(helloStr, name);
  }
  return {
    sayHellow: sayHellow,
  }
}

bye.js

function bye() {
  var name = 'b';
  var byeStr = 'Bye';
  function sayBye() {
    console.log(byeStr, name);
  }
  return {
    sayBye: sayBye,
  }
}

index.html

<script src="hellow.js"></script>
<script src="bye.js"></script>
<script>
  var a = hellow();
  var b = bye();
  a.sayHellow(); // Hello World a
  b.sayBye(); // Bye b
</script>

例子中,我们封装了 hellowbye 两个模块,我们通过执行 hellow()bye() 获取模块实例,而内部的数据 namehelloStrbyeStr 则由于闭包未被销毁,我们调用 sayHellow() 或者 sayBye() 时,还可以继续访问其属性,并且两个模块中虽然都有 name 变量,但是由于在各自的函数作用域中,彼此独立,互不影响

函数科里化

对于函数柯里化我之前有个文章已经写过了,虽然理解还很浅薄,但是里面的一些例子也可以用来理解闭包的应用, 函数柯里化

除了柯里化还有与其类似的还有偏函数和高阶函数,其中都常借助于闭包来实现

  • 偏函数: 柯里化不同的是可以不止传入一个参数,而是传入一部分参数

  • 高阶函数: 简单来说,输入或输出为函数即为高阶函数

总结

闭包就是在函数内部访问外部作用域,是其代码块和作用域的组合