1.2 闭包 Closure
Closures 闭包
闭包的定义:从实现上讲,闭包是一条记录,它存储了一个函数与其环境的上下文信息。这个上下文主要记录了:函数的每个自由变量(在内层函数使用,但在外层函数处被定义的变量)与其被绑定的值或引用之间的关联(a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created)。主要是用来实现信息封装。
闭包是指那些能够访问自由变量(既不是本地定义也不作为参数的那些变量)的函数。换句话说,这些函数可以“记住”它被创建时候的环境。 -- MDN definition
都说闭包神奇,神奇的地方在哪里呢?在于它为什么能记住定义在 enclosing closure 上的变量。这要求变量的存活期至少跟闭包函数一样长,但是通常外部函数 enclosing function 执行完退出,它的变量就随着执行上下文的出栈而再见了,这样,闭包如何能获取到外部函数的变量呢?下面会详细拆解,但 MDN 上的这个回答给出了一些线索。
A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time that the closure was created. -- MDN definition
函数执行结束后,如何使在其中定义的函数/变量仍能被获得?
setTimeout()
- 返回该创建的函数对象
- 将其值设置到更高的变量中去(比如全局作用域)
- 闭包
如上图,匿名函数的执行上下文是在 newSaga
中的,也即,执行上下文永远会在它被定义处的函数上下文内被创建,即便该函数已经结束运行而退出。这合乎逻辑。但从实现上看,即便 newSaga()
已经执行完毕退出了,其中红色部分的执行上下文仍然留存于内存中,是否如此?会否导致大量的内存浪费?
闭包如何实现 How Closure is implemented
学习时我有个问题,即闭包也是普通函数,普通函数执行时会创建其 EC,而由于 JavaScript 引擎是单线程执行,决定了任一时刻只能有一个 active EC,这也决定了它使用栈作为实现结构。那么,函数执行完其 EC 就消失了,其引用的自由变量也就不存在了,如此何以能实现闭包呢?
要回答这个问题,可能需要比较艰深的理论知识和回答,前端早读课上有一篇很不错的闭包文章。不过总而言之,两点最重要:
- 关于函数 EC 及其栈实现的假设基本是对的。但 EC 只是实现闭包的其中一个工具,不是全部。单靠它确实不能实现闭包,这基于栈会被弹出的事实。但闭包能访问到的自由变量并非存在栈,而是存在堆里
- 它其实不是直接访问外部变量(因为外部变量的执行上下文已出栈),而是访问外部变量的一个引用。它怎么知道这个引用的呢?通过作用域链,作用域链又是通过静态的词法作用域在“编译”时就能确定的
merveilleux。
为什么需要闭包 / 闭包解决了什么问题
- 保存私有变量
- 惰性求值
使用例子
ES2015 之前,如果在循环中使用闭包变量,很可能就会不小心出错,比如:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
绑定到 onfocus
上面去的是个闭包函数,其中引用了一个自由变量 item
。这个变量在 setupHelp()
执行完后执行上下文就出栈了,在执行时,引擎会使用作用域链来查找这个变量的值,因此执行时实际使用的都是最后一次调用剩下的值「我不是很理解这个」。修复办法:
- 绑定到
onfocus
上的办法改成两层闭包函数。这样里面那层闭包的词法作用域内就有了help
这个变量 - 使用 ES2015 提供的
let
关键字。它会生成一个块作用域,因此let item = helpText[i]
能存住值
参考资料
最佳
- Princess analogy。看到这个故事,满分一定得给了
- What was the book your father just told you。这个比喻也是很赞,看了都快笑死了
- 前端早读课:深入学习JavaScript闭包。这个文章把词法环境、动态作用域、作用域链、执行上下文与闭包的实现原理结合起来讲得通通透透,酣畅淋漓啊,彻底理解了闭包的概念实现
其他
- Wikipedia: Closure(Computer Science)。Implementation and Theory 一节很多有价值的介绍。可惜太高端,没看懂
- JavaScript Closures as a Metaphor。讲的是作者向多人寻求一个关于 JS 闭包的比喻,比较贴切的有:
- 镜子比喻。镜子里面的人可以看到镜子外面的人,而外面的人看不到里面的人。我真的也想过这个比喻
- 工匠装箱比喻。你对工匠耳语几句要求,然后就把他装进箱子扔进海里。箱子飘到大洋彼岸,有人捡起箱子把工匠放出来,此时他再根据你之前耳语的内容继续完成你要的手工。这里,箱子就是闭包的环境,你的耳语就是传入给闭包保存的“私有变量”,工匠就是那个实际做事的内部闭包“函数”。胜在喜感
- https://github.com/yang-wei/definalogy#closure 这个仓库都是关于计算机科学的比喻,很好的创意。闭包一节的比喻均不生动,但是仍然是很好的刻意学习、理解方法
- http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html 阮一峰的这篇被喷惨了。建议不要看
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 1.3 this 关键字
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论