多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
## 1.5 作用域闭包 当函数可以**记住并访问**所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 ~~~ function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2 ~~~ 无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。 ~~~ function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // 这就是闭包! } ~~~ 把内部函数baz 传递给bar,当调用这个内部函数时(现在叫作fn),它涵盖的foo() 内部作用域的闭包就可以观察到了,因为它能够访问a。 传递函数当然也可以是间接的。 ~~~ var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; // 将baz 分配给全局变量 } function bar() { fn(); //这就是闭包! } foo(); bar(); // 2 ~~~ 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。 ### 1.5.1 深入理解 ~~~ function wait(message) { setTimeout( function timer() { console.log( message ); }, 1000 ); } wait( "Hello, closure!" ); ~~~ 将一个内部函数(名为timer)传递给setTimeout(..)。timer 具有涵盖wait(..) 作用域的闭包,因此还保有对变量message 的引用。 wait(..) 执行1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有wait(..)作用域的闭包。 **本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!** 通常认为IIFE 是典型的闭包例子,但根据先前对闭包的定义,这不是严格意义上的闭包。 ~~~ var a = 2; (function IIFE() { console.log( a ); })(); ~~~ 虽然这段代码可以正常工作,但严格来讲它并不是闭包。为什么?因为函数(示例代码中的IIFE)并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a 是通过普通的词法作用域查找而非闭包被发现的。 ### 1.5.2 循环与闭包 要说明闭包,for 循环是最常见的例子。 ~~~ for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } ~~~ 正常情况下,我们对这段代码行为的预期是分别输出数字1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次6。 改进: ~~~ for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); } ~~~ IIFE 也不过就是函数,因此我们可以将i 传递进去,如果愿意的话可以将变量名定为j,当然也可以还叫作i。无论如何这段代码现在可以工作了。 在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。 #### 重返块作用域 1.3节中说过,let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。 本质上这是将一个块转换成一个可以被关闭的作用域。 ~~~ for (var i=1; i<=5; i++) { let j = i; // 是的,闭包的块作用域! setTimeout( function timer() { console.log( j ); }, j*1000 ); } ~~~ 上面代码还不完美,进一步改进 ~~~ for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } ~~~ for 循环头部的let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。 ### 1.5.3 模块 考虑以下代码: ~~~ 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 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体。 **模块模式需要具备两个必要条件:** * 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。 * 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。 一个具有函数属性的对象本身并不是真正的模块。一个从函数调用所返回的,只有数据属性而没有闭包函数的对象也并不是真正的模块。 模块也是普通的函数,因此可以接受参数: ~~~ function CoolModule(id) { function identify() { console.log( id ); } return { identify: identify }; } var foo1 = CoolModule( "foo 1" ); var foo2 = CoolModule( "foo 2" ); foo1.identify(); // "foo 1" foo2.identify(); // "foo 2" ~~~ 模块模式另一个简单但强大的变化用法是,命名将要作为公共API 返回的对象: ~~~ var foo = (function CoolModule(id) { function change() { // 修改公共API publicAPI.identify = identify2; } function identify1() { console.log(id); } function identify2() { console.log(id.toUpperCase()); } var publicAPI = { change: change, identify: identify1 }; return publicAPI; })("foo module"); foo.identify(); // foo module foo.change(); foo.identify(); // FOO MODULE ~~~ 通过在模块实例的内部保留对公共API 对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。 **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 }; })(); ~~~ 这段代码的核心是modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的API,储存在一个根据名字来管理的模块列表中。 下面展示了如何使用它来定义模块: ~~~ MyModules.define("bar", [], function () { function hello(who) { return "Let me introduce: " + who; } return { hello: hello }; }); 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 ~~~ "foo" 和"bar" 模块都是通过一个返回公共API 的函数来定义的。"foo" 甚至接受"bar" 的示例作为依赖参数,并能相应地使用它。 **2. 未来的模块机制** ES6 中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6 会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API 成员,同样也可以导出自己的API 成员。 基于函数的模块并不是一个能被稳定识别的模式(编译器无法识别),它们的API 语义只有在运行时才会被考虑进来。因此可以在运行时修改一个模块的API(参考前面关于公共API 的讨论)。 相比之下,ES6 模块API 更加稳定(API 不会在运行时改变)。由于编辑器知道这一点,因此可以在(的确也这样做了)编译期检查对导入模块的API 成员的引用是否真实存在。如果API 引用并不存在,编译器会在运行时抛出一个或多个“早期”错误,而不会像往常一样在运行期采用动态的解决方案。 ES6 的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”(可以被重载)可以在导入模块时异步地加载模块文件。 考虑以下代码: ~~~ bar.js ---------------------------- function hello(who) { return "Let me introduce: " + who; } export hello; foo.js ---------------------------- // 仅从"bar" 模块导入hello() import hello from "bar"; var hungry = "hippo"; function awesome() { console.log( hello(hungry).toUpperCase() ); } export awesome; baz.js ---------------------------- // 导入完整的"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 ~~~ 需要用前面两个代码片段中的内容分别创建文件foo.js 和bar.js。然后如第三个代码片段中展示的那样,bar.js 中的程序会加载或导入这两个模块并使用它们。 * `import `可以将一个模块中的一个或多个API 导入到当前作用域中,并分别绑定在一个变量上(例子里是hello)。 * `module `会将整个模块的API 导入并绑定到一个变量上(在我们的例子里是foo 和bar)。 * `export `会将当前模块的一个标识符(变量、函数)导出为公共API。这些操作可以在模块定义中根据需要使用任意多次。 模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面的函数闭包模块一样。