作用域闭包
JavaScript中闭包无处不在,你只需要识别并拥抱它。
闭包是基于词法作用域书写代码时所产生的自然结果。
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
1function foo() {
2 let a = 2;
3 function bar() {
4 console.log(a);
5 }
6 // 将 bar() 函数本身当作一个值类型进行传递。
7 return bar;
8}
9
10const baz = foo();
11// 实际上只是通过不同的标识符引用调用了内部的函数 bar(), bar() 显然可以正常执行,这里它在自己定义的词法作用域以外被执行了。
12baz(); // 2
1function foo() {
2 let a = 2;
3 function baz() {
4 console.log(a);
5 }
6 bar(baz);
7}
8function bar(fn) {
9 fn(); // 这就是闭包
10}
11foo();
1let fn;
2function foo() {
3 let a = 2;
4 function baz() {
5 console.log(a);
6 }
7 fn = baz; // 把baz分配给全局变量
8}
9function bar() {
10 fn(); // 这就是闭包
11}
12foo();
13bar(); // 2
1function foo(message) {
2 // 将一个内部函数timer传递给setTimeout
3 // timer具有涵盖 foo作用域的闭包,还保有对变量message的引用
4 // 在引擎内部,内置的工具函数setTimeout持有对一个参数的引用,这个参数也叫做fn或者func,或者其他类似的名字。在这个函数中就是内部的 timer函数,而词法作用域在这个过程中保持完整。
5 setTimeout(function timer() {
6 console.log(message); // hello, closure
7 }, 1000);
8}
9foo("hello, closure");
:::
无论通过何种手段将内部函数传递到所在词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
本质上,无论何时何地,如果将(访问它们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
循环和闭包
1for (var i = 1; i <= 5; i++) {
2 // 以每隔一秒的频率输出五次6
3 setTimeout(function timer() {
4 console.log(i);
5 }, i * 1000);
6}
1for (var i = 1; i <= 5; i++) {
2 // 在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
3 (function (j) {
4 setTimeout(function timer() {
5 console.log(j);
6 }, j * 1000);
7 })(i);
8}
块级作用域和闭包
for循环头部的let声明还有有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每个迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
1for (let i = 1; i <= 5; i++) {
2 setTimeout(function timer() {
3 console.log(i);
4 }, i * 1000);
5}
模块
模块模式需要具备两个必要条件:
-
必需有外部的封闭函数,该函数至少被调用一次(每次调用都会创建一个新的模块实例)
-
封闭函数必需返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态
一个具有函数属性的对象本身并不是真正的模块
一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块