一周一章前端书·第5周:《你不知道的JavaScript(上)》S01E05
第5章:作用域闭包
到底什么是闭包
本章讲解
闭包(Closures)
,它与作用域工作原理息息相关。首先我用自己总结的三句话,简单说明什么是闭包:
(1)首先我们要知道,变量的查找 规则 是由内到外的;
(2)所以 子函数可以访问外部作用域 的变量;
(3)如果 把子函数赋值给外部变量 时,此时外部变量就 拥有 了可以 访问封闭数据包的能力 ;
一个简单的闭包示例
function foo(){ var a = 2; function bar(){ console.log(a); //2 } return bar; }var baz = foo();
分析上述代码:
bar
函数能访问外部foo
函数的作用域,将bar
传递给外部变量baz
来执行;此时
bar
函数在原来定义的词法作用域之外执行,同时持有foo
函数作用域的引用,这就叫作闭包。
并且通过闭包的执行方式,
foo
函数在执行后,其作用域不会被立即销毁(毕竟bar
函数还要用的啊)
闭包暴露的方式不止一种
闭包函数除了可以直接赋值给外部变量,也可以通过执行外部函数,将闭包函数以参数传递的方式暴露出去。
var foo(){ var a = 2; //闭包函数 function baz(){ console.log(a);; //2 } //执行外部函数,将闭包函数通过参数的方式传递进去 bar(baz) }var bar(fn){ fn(); }
闭包是最熟悉的陌生人
虽然闭包比较隐晦,但它绝不仅仅是一个好玩的玩具而已,在我们的代码中到处都有它的身影。
比如常见的定时函数
setTimeout()
:
function wait(message){ setTimeout(function timer(){ console.log(message); },1000) }
上述代码等价于:
//全局的setTimout函数准备就绪var setTimout = function(invokeFn){}function wait(message){ //timer内部函数拥有对mesage的访问权 var timer = function(){ console.log(message); } //执行setTimeout()函数,并将timer以参数方式传递进去 setTimout(timer); }
是不是有似曾相似的感觉?内部函数
timer()
具有外部函数wait()
作用域中的message
变量的引用,它就是闭包。除了定时函数之外,jQuery代码也普遍的在使用闭包,不信你看下面的代码:
//参数传递一个name字符串,选择器字符串function setupBot(name,selector){ //通过选择器字符串初始化成jQuery对象 //绑定点击事件 $(selector).click(function activator(){ //打印外部函数的name console.log('Activating:' + name); }) } setupBot('Closure Bot 1','#bot_1'); setupBot('Closure Bot 2','#bot_2');
其实无论何时何地,只要将函数当做参数进行传递,就有闭包的应用。
什么定时器、事件监听函数、Ajax请求回调函数、跨窗口通信、Web Workers等代码中,都普遍应用到了闭包。
它每天与我们擦肩而过,就好像那个最熟悉的陌生人。
闭包解决了什么问题
1. 用闭包造块级作用域
我们先看问题: 我只是想依次输出循环的
i
(1 ~ 5)
for(var i=1;i<=5;i++){ setTimtou(function timer(){ console.log(i); },0) }
见鬼了,然而输出的结果却是五次6,这是为什么呢?
其根本原因是,定时器的回调函数永远在循环结束后才执行。
那你可能会想,哦,那我岂不是永远都不能在
for
循环中用定时器了?JavaScript真垃圾!诶诶,先别慌,我们来分析一下。我们不是预期循环的每个迭代中,都有一个
i
的副本,然后输出它吗?通过闭包就能实现。
for(var i=1;i<=5;i++){ (function(index){ setTimtou(function timer(){ console.log(index); },0) })(i); }
我们通过IIFE构造了一个块级作用域将
i
存了起来。提到块级作用域,其实ES6的语法里还有一种更便捷的解决方式——
let
声明
for(let i=1;i<=5;i++){ setTimtou(function timer(){ console.log(i); },0) }
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 } }
上述代码演示了JavaScript模块暴露。通过调用
CoolModule
函数创建一个模块实例,CoolModule
返回的对象中包含内部函数的引用,就相当于模块的公共API。当然上述代码可以任意调用多次,重复返回新的模块实例,我们可以改成单例模式:
//通过一个IIFE函数来包装var foo = (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 } })(); foo.doSomething(); //coolfoo.doAnother(); //1,2,3
模块的公共API不仅可以是内部私有变量的访问,也可以是修改私有变量的方法:
var foo = (function CoolModule(id){ var moduleId = id; function showId(){ console.log(moduleId); } function uppcaseId(){ moduleId = moduleId.toUpperCase(); } return{ showId : showId, uppcaseId : uppcaseId } })('fooModule'); foo.showId(); foo.uppcaseId(); foo.showId();
其实大多数模块管理机制本质是也是通过类似的方式来实现的,我们来尝试写一个简版的模块管理器:
/** * 定义牛批哄哄的超级模块管理器 */var SuperModules = (function(){ //所有的模块集合,以name作为key var moduleMap = {}; function define(name,deps,impl){ //获取依赖 for(var i=0;i<deps.length;i++){ deps[i] = moduleMap[deps[i]] } //执行引入的模块,并以deps作为参数 moduleMap[name] = impl.apply(impl,deps); } function get(name){ reutrn moduleMap[name]; } //暴露公共API return{ define : define, get : get } })()/** * 先定义一个bar模块 * 没有依赖,impl是一个执行函数 */SuperModules.define('bar',[],function(){ function hello(name){ return 'let me introduce:' + name; } return { hello : hello } })/** * 再定义一个foo模块 */SuperModules.difine('foo',['bar'],function(bar){ var hungry = 'hippo'; function awesome(){ console.log(bar.hello(hungry).toUpperCase()); } return { awesome : awesome } })/** * 调用测试 */var bar = SuperModules.get('bar');var foo = SuperModules.get('foo');//let me introduce: hippoconsole.log(bar.hello('hippo'));//let me introduce: HIPPOfoo.awesome();
是不是看起来比较复杂……不过不用担心,ES6以及添加了模块的语法支持!
ES6会将每个js文件当做独立的模块来处理,每个模块可以通过
import
关键字导入依赖的模块,或者通过export
关键字导出API。你需要做的,只是拥抱ES6!bar.js
function hello(name){ return 'let me introduce:' + name; }export hello;
foo.js
import hello from 'bar';var hungry = 'hippo';function awesome(){ console.log(bar.hello(hungry).toUpperCase()); }export awesome;
baz.js
module foo from 'foo';module bar from 'bar';//let me introduce: hippoconsole.log(bar.hello('hippo'));//let me introduce: HIPPOfoo.awesome();
小结
内部函数可以访问外部函数的作用域,如果将内部函数暴露给外部变量时,或者说内部函数在定义的词法作用域之外执行时,就产生了闭包。
闭包是个非常强大的工具,可以用来实现模块模式。
模块的特征:
为创建内部作用域而调用一个包装函数;
包装函数的返回值必须至少包含一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包;
作者:梁同学de自言自语
链接:https://www.jianshu.com/p/7741dd5f6485
共同学习,写下你的评论
评论加载中...
作者其他优质文章