说明
《你不知道的JavaScript》学习笔记。
定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
例子:
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); //2 (这就是闭包的效果。)
本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
循环和闭包
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); // 每秒一次的频率输出五次 6。 }, i*1000 ); }
对上面的例子进行改造:
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); // 1 , 2 , 3 , 4 , 5 }, j*1000 ); })( i ); }
在迭代内使用 IIFE
会为每个迭代都生成一个新的作用域, 使得延迟函数的回调可以将新的作用域封闭在每个迭代内部, 每个迭代中都会含有一个具有正确值的变量供我们访问。
重返块作用域
上面的例子使用 IIFE 在每次迭代时都创建一个新的作用域。
换句话说, 每次迭代我们都需要一个块作用域。
前面的学习我们了解到 let 声明, 可以用来劫持块作用域, 并且在这个块作用域中声明一个变量。
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
模块
模块有两个主要特征:
(1)、为创建内部作用域而调用了一个包装函数
(2)、包装函数的返回值必须至少包括一个对内部函数的引用, 这样就会创建涵盖整个包装函数内部作用域的闭包。
先看一个例子:
function CoolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join( " ! " ) ); } return { doSomething: doSomething, doAnother: doAnother }; } var foo = CoolModule(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3
这个模式在JavaScript中被称为模块。
代码分析:
1、CoolModule() 只是一个函数, 必须要通过调用它来创建一个模块实例。 如果不执行外部函数, 内部作用域和闭包都无法被创建。
2、CoolModule() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。 这个返回的对象中含有对内部函数而不是内部数据变量的引用。 我们保持内部数据变量是隐藏且私有的状态。 可以将这个对象类型的返回值看作本质上是模块的公共 API。
3、doSomething() 和 doAnother() 函数具有涵盖模块实例内部作用域的闭包( 通过调用 CoolModule() 实现)。
现代的模块机制
例子:
// 1、公共 API 的函数 var MyModules = (function Manager() { var modules = {}; function define(name, deps, impl) { for (var i=0; i<deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply( impl, deps ); // 核心代码 } function get(name) { return modules[name]; } return { define: define, get: get }; })(); // 2、使用公共 API 来定义bar模块: MyModules.define( "bar", [], function() { function hello(who) { return "Let me introduce: " + who; } return { hello: hello }; } ); // 3、"foo" 接受 "bar" 的示例作为依赖参数, 并能相应地使用它。 MyModules.define( "foo", ["bar"], function(bar) { var hungry = "hippo"; function awesome() { console.log( bar.hello( hungry ).toUpperCase() ); } return { awesome: awesome }; } ); var bar = MyModules.get( "bar" ); var foo = MyModules.get( "foo" ); console.log( bar.hello( "hippo" ) ); // Let me introduce: hippo foo.awesome(); // LET ME INTRODUCE: HIPPO
特点: 为函数定义引入包装函数, 并保证它的返回值和模块的 API 保持一致。
换句话说, 模块就是模块, 即使在它们外层加上一个友好的包装工具也不会发生任何变化。
未来的模块机制
ES6 中为模块增加了一级语法支持。 但通过模块系统进行加载时, ES6 会将文件当作独立的模块来处理。 每个模块都可以导入其他模块或特定的 API 成员, 同样也可以导出自己的API 成员。
ES6 的模块没有“行内” 格式, 必须被定义在独立的文件中(一个文件一个模块)。
1、bar.js 模块
function hello(who) { return "Let me introduce: " + who; } export hello;
2、foo.js 模块
// 从 "bar" 模块导入 hello() import hello from "bar"; var hungry = "hippo"; function awesome() { console.log( hello( hungry ).toUpperCase() ); } export awesome;
// 导入完整的 "foo" 和 "bar" 模块 module foo from "foo"; module bar from "bar"; console.log( bar.hello( "rhino" ) ); // Let me introduce: rhino foo.awesome(); // LET ME INTRODUCE: HIPPO
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理, 就和前面介绍的函数闭包模块一样。