- A+
作用域的概念
同级作用域
在一个作用域中声明相同名称的变量会发生变量名冲突的问题。假如在作用域 A 中声明一个变量 a,作用域 B 也声明一个变量 a,两个作用域的变量都互不影响。
// 作用域 A { let a = 0; console.log(a); } // 作用域 B { let a = 10; console.log(a); }
第一个打印 0,第二个打印 10。
嵌套作用域
作用域是可以嵌套的,作用域 A 嵌套作用域 B,此时两个作用域分别声明变量 a,也是不冲突的。
{// 作用域 A let a = 0; console.log(a); { // 作用域 B let a = 10; console.log(a); } }
第一个打印 0,第二个打印 10。
在嵌套作用域中,子作用域 B 可以访问父作用域 A 的变量,也可以影响它的父作用域 A。
{ let a = 0; { a = 10; console.log(a); } }
最终打印的结果是 10。
函数的闭包
JS 的类实际上就是在使用函数的闭包。每执行一次函数就是在生成一个新的作用域,这些新的作用域就像是上面提到的,它们互不影响,只有它们的子作用域可以影响父作用域,即闭包。
闭包缓存数据
function Counter(x) { return { add: y => { return (x = x + y); }, del: y => { return (x = x - y); } }; }
add 和 del 都是父函数 Counter 的子函数,它们之间是一个闭包关系。创建两个 Counter 的实例:
let c1 = Counter(10); console.log(c1.add(20)); console.log(c1.del(40));
当执行c1.add()
时,左边有一个 Closure,说明已经形成了一个闭包:
Closure 中只有 x 是受闭包影响被缓存下来的数据,也就是父函数 Counter 的变量。Local 代表 add 函数自身的变量,没有与其他函数之间(或作用域)形成关系,也就不符合闭包的存在条件,只能由 add 函数自己来使用。
再继续往下执行,可以看到此时 Closure 中的 x 已经是 30:
再往下执行,调用 del 函数,再执行函数的相减操作之间,我们可以看到 Closure 中的 x 还是上一次的结果:30。
再接着往下执行一次,del 函数相减之后,Closure 中的 x 的结果是 -10:
本节小结
受闭包的影响,父函数的数据被缓存下来,子函数可以自由地使用,而且再内存中也不会被销毁。
闭包的好处
仔细观察上面的例子,add 和 del 函数都依赖了相同的变量 x,而这个 x 是父函数给的,再闭包中被缓存起来。add 和 del 只需要传递新的参数就可以参与运算,也就是说,闭包可以减少我们函数的参数传递,使得我们一个计算操作更加连贯,且降低代码耦合度。
假如不使用闭包,通过函数的参数传递来计算,替代上面的闭包函数:
function add(x, y) { return (x = x + y); } function del(x, y) { return (x = x - y); } let res = add(10, 20); let ser = del(res, 40);
就很没有必要,何不如把 x 抽离出来呢?变成一个全局变量:
let x = 10; function add(y) { return (x = x + y); } function del(y) { return (x = x - y); } let res = add(20); let ser = del(40);
可以,但是不推荐,全局作用域中,变量 x 被声明一次,假如代码越写越多,变量是不是会冲突,代码是不是变得难以维护?闭包可以把 add 和 del 以及 x 都囊括在一个作用域里,也不影响其他的作用域。
本节小节
闭包可以把一块代码容纳在一个里面,形成一个整体,一个不受其他作用域影响的作用域。是不是很像模块开发?没错,我猜测 CommonJS 就是使用的闭包。
总结
-
闭包可以让我们使用模块开发思想来写代码,把一系列代码揉进闭包里,是一个有机的结合。类就是使用的闭包,在早期通过闭包来实现模块的开发。
-
闭包可以缓存父函数的变量,子函数可以使用,子函数修改父函数的变量,其他子函数也跟着改变。