这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
本文详细介绍了 javascript 闭包,十分的详尽,推荐给有需要的小伙伴参考下。
在上一篇文章我们对预解释作了概述,在写这篇博文前打算写几个经典案例,考虑到那些案例综合性比较强,也就循序渐进的有了这篇博文,这样对于学习和深入 JavaScript 也更加容易入手。
序
一同事去面试,面试官问了一道题:你写一个闭包我看下?于是同事火速写出如下代码:
- function fn(){
- alert('Hello JavaScript Closure!!!');//妈蛋,E文本来就不好,找翻译才把闭包单词写出来
- }
- fn();
然后面试官摇摇头说道:"这怎么能叫闭包呢?",最终两人争执不下,同事果断走人,面试官什么玩意儿?(本故事纯属虚构,如有雷同纯属巧合)
闭包可能在很多人眼中都是 "高大不好上" 的技术,可能在很多人眼中只有这样才算得上闭包:
示例 1:
- function fn() {
- return function () {
- alert('示例1');
- }
- }
- fn()();
示例 1 PS:这个看起来不怎么高级,看样子这人水平不咋地哦!
示例 2:
- ;(function () {
- alert('示例2');
- })();
示例 2 PS:这个看起来比上一个要高级,而且第一个括号前还加了一个分号,为何加一个分号,好吧我们先把这个疑问留这儿,后面会讲到。
示例 3:
- ~function fn() {
- alert('示例3')
- }();
示例 3 PS:这个最高级了,简直吊炸天,我读书少,你们别骗我!
撸主读书不多,仅能写出这三种 "闭包",相信博友们能写出更多更优秀的 "闭包";到此请先暂停我的瞎掰,接下来研究下函数运行的机制,貌似有人已经知道了,肯定是作用域,我真的很不想在标题上再加上这个作用域,这样总感觉差点儿意思,这个几个东西本来都是一起的,为何要重复呢?老习惯,先上代码:
- var n = 10;
- function fn(){
- alert(n);
- var n = 9;
- alert(n);
- }
- fn();
好简单的说,我们画图(撸主只会用 Windows 自带的画图软件,若有更好的请博友推荐)来分析下:
分析 1
从图中我们看到了两个作用域,一个是 window 作用域(顶级作用域),一个是 fn 调用的时候形成的一个私有作用域;那什么是作用域,作用域其实就是代码执行的环境。举个栗子,一个学生他的学习环境是学校,相当于他的作用域是学校,假如这个学生很调皮,晚上经常 FanQiang 去网吧打游戏,相当于形成了一个私有环境,这个作用域就是网吧。好吧!这个栗子太 TM 像撸主本人了,不由感叹一句:"少壮不努力,长大干挨踢"。还是回到正题,其实函数 fn 的定义就是指向一段代码的描述(图中红框),当这个 fn 调用(图中的绿框)的时候,就会形成一个作用域,当然这个作用域中的代码执行前也会预解释,我是不会告诉你这个作用域是当它执行完毕后会被销毁,这个 fn 再次调用也会形成一个新的作用域,然后执行前预解释,然后代码执行,最后执行完毕销毁。
理解闭包
我们知道函数被调用在执行的时候会形成一个私有作用域(执行环境),这个私有作用域就是闭包。回头再看看闭包还是传说中的 "高大不好上" 吗?我们再回头看看第一个面试故事,还有我写的三个示例,它们其实都是闭包,确切的说那三个示例都是闭包的常用形式。
应用场景
现在有这样一个需求:html 页面中有一个 ul 标签,ul 下面有 5 个 li 标签,要求任意点击一个 li,弹出被点击的这个 li 所在的索引(索引从 0 开始)位置,HTML 结构如下:
- <ul id="ul">
- <li>
- 列表1
- </li>
- <li>
- 列表2
- </li>
- <li>
- 列表3
- </li>
- <li>
- 列表4
- </li>
- <li>
- 列表5
- </li>
- </ul>
机智的我火速写出如下代码:
- var lis = document.getElementById('ul').getElementsByTagName('li');
- for (var i = 0, len = lis.length; i < len; i++) {
- lis[i].onclick = function () {
- alert(i);
- };
- }
最终测试,看是否完美实现这个需求:
发现无论点击多少次,最终都弹出这个结果,而需求期望的结果是:点击列表 1 弹出 0,点击列表 2 弹出 1,点击列表 3 弹出 2…… 此时此刻只想用这幅图来形容现在的心情:
(当原型在演示时没能按设计的要求运行时的样子)
这可如何才好,为何总是弹出 5 呢?理论上很正确呀!我们不妨画图来分析下:
其实我们只是给每一个 li 的 onclick 其实就是保存的一段函数的描述字符串,这个字符串内容就是上图红框中的内容,如果您还是不信,我有图有真相:
在 Chrome 控制台下输入:lis[4].onclick,其值就是函数的描述。当我们在点击第 5 个列表时,其实就是相当于 lis[4].onclick(),调用了这段函数描述,我们知道函数在被调用执行的时会形成一个私有作用域,在这个私有作用域下也是先预解释,然后代码执行,此时会去找 i,在当前私有作用域下没有 i,然后去 window 作用域下找到了 i,因此每次点击都弹出 5。
显然上面的代码无法满足这个需求,我们代码那么写是不正确的,我们思考一下出现问题的原因是什么?其实原因就是每次点击的时候都是读取的 window 下的 i,此时这个 i 的值已经是 5 了,于是有了如下代码:
方式一:
- var lis = document.getElementById('ul').getElementsByTagName('li');
- function fn(i) {
- return function () {
- alert(i);
- }
- }
- for (var i = 0, len = lis.length; i < len; i++) {
- lis[i].onclick = fn(i);
- }
方式二:
- var lis = document.getElementById('ul').getElementsByTagName('li');
- for (var i = 0, len = lis.length; i < len; i++) {
- ;(function (i) {
- lis[i].onclick = function () {
- alert(i);
- };
- })(i);
- }
方式三:
- var lis = document.getElementById('ul').getElementsByTagName('li');
- for (var i = 0, len = lis.length; i < len; i++) {
- lis[i].onclick = function fn(i) {
- return function () {
- alert(i);
- }
- }(i);
- }
一口气写了三种方式,其思想都是一样的,就是将这个变量 i 用一个私有变量存储起来,这里我就只讲方式二,当然明白其中一个其余也就都明白了。按照惯例,我们画图来一步步分析下:
我详细的对整个代码执行做了描述,需要注意的是:每个 li 的 onclick 属性都要占用 (function(i){…})(i) 作用域,当这个函数执行完毕后不会被销毁,因为它被外面的 li(这个 li 是 window 作用域下的)占用着,因此这个作用域不会被销毁。当点击任意一个 li 时,function(){ alert(i); }会被执行,也会形成一个作用域,这个作用域没有 i,它会去 (function(){…})(i) 作用域找 i,最终在形参找到 i,这个形参 i 的值就是 for 循环时传进去的;这个例子巧妙地使用闭包来贮存值,完美解决问题。
PS:刚刚说 (function(i){…})(i) 为什么在前面加一个分号,其原因就是防止前面的语句忘记加分号,这样导致 JavaScript 在解析时出错,仅此而已。当然上面的一个应用场景就是 Tabs 实现原理,可以有其他实现方式,比如自定义属性方式、通过 DOM 节点关系找到索引,而撸主采用这样一种方式只是为了加深对闭包的理解。
总结
闭包并不是传说中的高大不好上,其核心就是理解函数定义、调用,函数调用时会形成一个新的私有作用域,当某个作用域被外面占用,那么这个作用域将不会被销毁。撸主读书甚少,有说得不对的地方请博友们指正,同时也感谢大家对撸主文章的支持。
来源: http://www.phperz.com/article/17/0421/272957.html