作用域
讲到闭包就需要讲到作用域,闭包其实是 js 作用域的一个应用。
定义
广义上来说,计算机程序的基本功能就是存储一些值,并且读取、修改这些值。作用域就是定义如何读取、修改值的规则。
狭义上来说,作用域就是变量的集合,决定了变量的可见性。
为什么
知道了作用域是定义如何访问变量的规则,那么为什么要有作用域呢?没有不行吗?
还真不行,主要是几个方面:
- 防止覆盖变量:在复杂的程序中,变量的同名是不可避免的,变量同名时,应该如何处理呢?就是将他们放在不同的作用域下。
- 隐藏变量:定义的某个变量不想被外面的访问,就需要作用域的出现,来规定这个变量能够被哪些位置访问。
- 垃圾收集:有了作用域,也就知道了变量被使用的范围,这利于统计变量是否不需要再被使用,方便及时清理销毁。
JS 的作用域
那么JS中的作用域规则,是如何定义的呢?
JS 的作用域分为3类:
- 全局作用域:即window 或 global;能够访问所有变量
- 函数作用域:一个函数内能够访问的变量
- 块级作用域:一个块内能够访问的变量
作用域可以包含其他作用域,上层作用域不可以访问下层作用域中的变量,下层作用域可以访问上层作用域的变量。JS 通过作用域链来实现作用域。
首先,js 中使用 var 声明的变量,均存在于全局作用域中,因为
var a = 1
实质上是 window.a = 1
在 ES6 中,引入了 let、const,他们声明的变量保存在块级作用域中,并且没有变量提升。除了 let、const,其他的语法也会创建块级作用域:
- with
- try catch
例子
相信很多人遇到过这个case
for (var a = 1; a < 5; a++) { setTimeout(() =>console.log(a)) }
认为会得到 1 - 4 的输出,结果输出的全部都是5。
而解决方案是使用 let
for (let a = 1; a < 5; a++) { setTimeout(() =>console.log(a)) }
为什么会这样?
还记得上面说过的,var 声明的变量是绑定在全局作用域中的,而 let 会创建块级作用域。
即
var a
的时候,在 window
上创建了a,后续循环中,都在修改 window.a
;而最终输出的时候,a
已经变成了 5
。在使用
let a
的时候,由于 let
会创建块级作用域,所以每次for
循环执行时,let
会在当前循环的块里面创建一个新的 a
,并且这个 a
仅能被这个块访问;所以 a
就是 1 - 4
了。闭包
理解了作用域后,再来看看什么是闭包。
上面提到了,上层作用域不能访问下层作用域的变量,也就是说下面的例子中,a 是访问不到的:
function fun() { const a = 1; // 函数作用域 } console.log(a); // 全局作用域
得到的结果是
undefined
这个规则就不能被打破吗?假如我就要访问这个变量,该咋办?
答案是可以打破,只需要创建一个闭包:
function fun() { const a = 1; // 函数作用域1 return function getA() { return a; // 函数作用域2 } } const getA = fun(); // 全局作用域 console.log(getA());
上面的例子中,
函数作用域1
包含 函数作用域2
,也就是说函数作用域2
可以访问到上层作用域 即 函数作用域1
中的变量 a
。此时直接将
getA
返回,那么就能在外面获取到 getA
了。然后再调用
getA
,就能拿到变量 a
了,就实现了在全局作用域
中访问 函数作用域1
中的变量。上面就是创建了一个闭包。总结一下,闭包就是携带作用域的函数。能够打通外部作用域访问内部作用域的通道。
最后一句话总结一下闭包与类的区别:
类是有行为的数据(实体),闭包是有数据(作用域)的行为。