终于到了神话破灭的时刻……

这注定是一篇“自取其辱”的博客,飞哥,你们眼中的大神,Duang,这次脸朝下摔地上了。

故事得从这个求助开始:e.returnValue 报错:未定义,“一起帮”现在人气还不够旺,碰到了我勉勉强强能够解决的问题,硬着头皮也得上啊!远程一看,问题不是e.returnValue没值,是e本身就没值。而更核心的问题是:这段代码,是被放在setTimeout()里面的。(这里插一句:很多问题,就得远程,求助人贴出来的代码,根本就没抓住重点。话说,很多时候,要是能抓住问题的核心,问题也已经解决了一大半了。)

把代码简单化一下,代码大致应该是这样的:

 <script>
function showEvent(){
setTimeout(
function(){
alert('in setTimeout:'+ event);
},100
);
}
</script> <input type="submit" name="Submit" value="提交" onclick="showEvent()" />

点击提交按钮,就会看到:event 为 undefined。

凭着直觉,真的就是坑踩得太多的直觉,我飞快的解决了这个问题:在setTimeout()之前加一行代码,如下所示:

     function showEvent(){
var e = event; //把event赋值给e
setTimeout(
function(){
alert('in setTimeout:'+ e);
},100
);
}

问题就解决了:

欧耶,\(^o^)/

但是,但是求助人要问:为什么呢

是啊,为什么呢?我敷衍他:三言两语说不清楚,等我写“总结”吧一起帮上每个求助搞定之后都可以写总结),结果这一弄啊,就是一周给混过去了……

花了这么多的心血,趁热打铁,干脆写篇博客,总结一下,运气好,园子里的同学还能给点指教呢!

++++++++++++++++++++

以下是试图维护偶像光环无力的自我辩护:

习惯了C#的优雅严谨,我承认:灰常灰常不喜欢JavaScript!所以JS一直是我的弱项,我的一贯原则是:能不用,就不用;就是要用,够用就行!“深入研究JavaScript”在我看来,纯粹就是找抽。我一直在等待JavaScript死掉的那一天,让我好结束苦逼的JavaScript开发工作……但微软不给力,看来是等不到那一天了。

以及无耻的推卸责任:

说句题外话,微软走到今天,犯的最大的错误:把开发者往外推。IE6不知道是多少开发人员的噩梦,各种不兼容,拥抱一个通用标准就那么难么?当初IE几乎是一统江湖啊!不管是CSS,还是JavaScript,要是IE能全面支持标准,哪有之前什么Firefox,现在什么Chrome的事?!包括.NET,现在才开源跨平台,早干什么去了?让Java这种古董级语言死灰复燃,全是自己作出来的。

++++++++++++++++++++

回到主题,这个问题,凭直觉,能想到的就这几方面的问题:

  • 作用域
  • 回调函数
  • 异步

我们一个一个的整起来吧。

说明一下,这篇博客的写作思路:紧紧围绕上面提出来的问题进行分析讲解。这比较适合像我这样的半吊子,很多概念有接触,但又理解不深,始终云里雾里的同学。借助这个具体问题的深入分析,把之前的“夹生饭”掰细了蒸熟了!

都是自己的一些理解,欢迎JavaScript大神批评指正。

作用域

JavaScript变量的作用域分为两种:全局的,和局部的。

全局的,非常好理解,但同时,这一特性,可以说是JavaScript万恶之源。《JavaScript语言精粹》一书附录“糟粕”A.1首当其冲的就是“全局变量”。很多你不理解的“为什么呢”之流的问题(比如:函数声明理解执行、闭包、模拟名称空间,等等),都可以一直逆推到“避免全局污染”上面来。

有同学问过我,这么恶劣的一个语法特性,为什么会一直存在呢?这就得从JavaScript的发展历史说起了。JavaScript的发展,深刻的证明了雷军的那句话:风口上面,猪都飞得起来。

1995年5月,作为Netscape公司实习生的Brendan Eich只用了10来天的时间,就设计完成了JavaScript的第一版,最初的定位是一个“嵌入html网页功能简单、易于学习的”脚本语言。因为当时Java正如日中天,所以Netscape很“鸡贼”的取名为Javascript,而实际上,这玩意儿和Java半毛钱的关系也没有,而是一个粗制滥造的大杂烩…… 参考:http://javascript.ruanyifeng.com/introduction/history.html

我们可以想象,当作为一个简短的、内嵌于html页面的脚本语言,全局变量其实是一个非常方便的东西(尤其是JavaScript的局部变量同时还有很多问题)。然而,后来随着前端的不断发展,JavaScript代码量不断增加,模块化工程化的要求越来越高,全局变量“重名”的概率越来越大,大量滥用全局变量,最终变成了一场灾难。所以前端开发人员,想出了很多办法来解决这一问题。而非常不幸的是,这些hack方法,又进一步的加剧了JavaScript代码理解上的难度……

就前几天,一个网友告诉我“一起帮”上面的验证码失效了,而错误在我本地无法重现。远程到他电脑上一看,错误提示:找不到$。看他用的Chrome,马上问他:是不是装了插件?果然,卸载了插件就OK了。

这说明,即使今天,当web应用面向的是不特定人群时,我们仍然不能完全信任JavaScript,不应该把核心的功能交给JavaScript,因为客户端的情况,是你无法预知的。讲真,我真不知道那种整个页面都是JavaScript加载渲染的web应用,是如何保证其“健壮性”(甚至是“可用性”)的。

那我们今天这个问题,涉不涉及到全局变量?

看看我们使用的event变量,不是参数传递进来的局部变量。那就只能是全局变量,相当于window.event;而window.event,是存在版本兼容性问题的,大体上来说,只有IE支持(各种乱七八糟的细节,大家可以参考:e = e || window.event用法细节讨论

在我的Firefox上测试,event只能通过参数传递,所以代码应该改写为:

     function showEvent(event){  //event作为参数传入
setTimeout(
function(){
alert('in setTimeout:'+ event);
},100
);
}

相应的,html上事件绑定为:

<input type="submit" name="Submit" value="提交" onclick="showEvent(event)" />

这样一测试,( ⊙ o ⊙ )啊!event有值,再也不是undefined了。

如果就这样结尾,你会不会艹, ヽ(`Д´)ノ︵ ┻━┻ ┻━┻ (掀桌子)?

好吧,我们假装这个问题没有解决。因为即使到这里,我们还是不能解释:为什么通过参数传递(或者var e = event;再赋值)的event能一直存在,作为全局变量的event怎么就变成了undefined呢?

再多说两句,这也是细抠JavaScript就容易变“玄学”的又一个原因。JavaScript代码是在不同的宿主环境(浏览器)上编译执行的。而直到今天,各个浏览器都还没有严格的遵守ECMAScript规范,所以存在大量的兼容性问题,让人晕头转向不知所措……

我们还是继续吧,顺带复习/捋清很多JavaScript的基础概念。

再看局部变量,当event作为参数传入,它就类似于一个局部变量。局部变量也有很多坑爹的“特性”(是的,JavaScript到处都是“bug用久了就变特性”的例子),大致的:

  • 函数块内的变量可以“先使用后声明”,换成特性就是:变量声明提前
  • 没有“块级作用域”,典型的就是for循环里的i可以被用于循环体外(ES6引入了let解决这一问题)。而这个历史遗留问题,换成特性表述就是:词法作用域。我的理解:JavaScript的作用域不是基于花括号{},而是基于函数的;是一个函数定义一个作用域,而不是一个{}定义一个作用域。

所以我们现在遇到的这个问题,必须把函数也引入进来,继续分析。

函数

JavaScript号称“面向对象”,我觉得啊,还不如说它是“面向函数”。

函数在JavaScript中是一个非常特殊的存在。它又有一个特性:函数里面可以再嵌套函数,于是玄而又玄的“闭包”问题就产生了。关于闭包问题的文章,汗牛充栋,根据我之前“零基础课程”的反馈,我就简单的说几点,看能不能帮助大家。

首先,闭包产生的前提条件,是两个语法特征:

  • 函数里面还可以嵌套函数
  • 嵌套的函数可以调用外部函数中的变量

闭包本质上是一个“作用域”问题,或者说变量的生命周期问题。被C#和VisualStudio宠惯了,对于这个问题我们会觉得非常陌生。因为在VisualStudio里面写代码,如果一个变量不在作用域内,就不能使用,就使用不了智能提示,而且会立即报错。(这就是“强类型”语言的好处,唉~~JavaScript的槽点无处不在啊!)

而JavaScript这种所谓的“弱类型”“动态”语言,很容易就一团浆糊。

如果仅仅从概率上理解,做“名词解释”,我个人觉得,闭包就是这么回事了:(一个函数内部)嵌套的函数可以调用(嵌套它的)外部函数中的变量。

这样就完了?那衣物(naive)啊……

为什么我说JavaScript是面向函数的?因为在JavaScript中,函数也是一个变量。(个人觉得,理解到这一层就够了,深究下去“对象继承自函数,函数也继承自对象”会把你逼疯的……JavaScript,能用就行,能用就行!唉~~)

回调

函数是一个变量,你们就可以作为方法的参数,是不是?当函数作为参数进行传递,就产生了JavaScript另一个特性:回调。回调其实也不难理解,类似于C#中delegate,已经衍生出来的Aciton<T>,Func<T>等,函数作为方法参数嘛。问题在于,当回调和闭包同时出现时,问题就复杂了。

我们再看一遍我们的问题代码:

         setTimeout(
//该匿名函数就被作为setTimeout的第一个参数了
function(){
alert('in setTimeout:'+ event); //event是哪里来的?
},100
);

回调表现得很清晰:整个function()匿名函数作为setTimeout()的第一个参数。再仔细看看,在该匿名函数中:alert('in setTimeout:'+ event); 咦,这个event是哪里来的?(说明:以下讨论都建立在非IE浏览器中运行,使用onclick="showEvent(event)",排除window.event的影响

凭直觉或者习惯,我会写成这样:

         setTimeout(
function(event){ //把event作为参数传入
alert('in setTimeout:'+ event);
},100
);

然而,在这里,这样写就会出问题:这样写event会是undefined。ʅ(‾◡◝)ʃ 为什么呢?

当我们在setTimeout()调用的匿名函数中声明参数event,匿名函数中的event就会“就近”的使用传入的参数event,但是这个参数event是没有赋值的(undefined)。

这又涉及到回调函数的参数传递问题。注意,不是回调函数作为参数被传递,是回调函数自己的参数问题。

setTimeout()函数是window自带的,其声明和实现我们(好吧,至少飞哥我)不知道。但我们查看其MDN文档,可以看到:

setTimeout()delay之后还可以带参数param1,param2,……,所以理论上(为什么是“理论上”?因为老版IE又不支持,艹)我们还可以这样:

     function showEvent(event){
setTimeout(
function(event){ //把event作为参数传入
alert('in setTimeout:'+ event);
}, 100, event //event作为匿名回调函数的参数
);
}

根据上述setTimeout()函数的调用,大家能不能猜到setTimeout()的大致实现?我想应该是这样的:

     function mockSetTimeout(callback, delay){
//JavaScript很有意思的一个特性:可以直接通过arguments取得传入的参数(实参)
callback(arguments[2], arguments[3]);
} mockSetTimeout(function(param1, param2){
alert(param1+" , "+param2);
},1, "hello","world");

好啦,不跑题太远了。

其实把event作为setTimeout()的参数传递是比较好理解的 ,这符合一般的编程语言的处理逻辑,参数得一层一层的传递:showEvent()把参数event传递给setTimeout(),setTimeout()再用参数把event传递匿名回调函数function(),因为event是局部变量啊。但是我们看一下我们的代码:

 function showEvent(event){  //event作为参数传入
setTimeout(
function(){
alert('in setTimeout:'+ event);
},
);
} <input type="submit" name="Submit" value="提交" onclick="showEvent(event)" />

没有这种传递!

没有这种传递!

没有这种传递!

第4行代码中使用的event是直接地使用调用它的匿名函数function()之外的setTimeout()之外的showEvent()中的变量——这句话非常拗口,但我想习惯了C#之类语言的同学应该能明白我的意思:都特么的多少“级”(作用域)之外了,怎么这scope还能用?

其实,这就是JavaScript没有“块级作用域”,或者说只有“词法作用域”的体现。我看到过最经典最直白的解释:

你不要管JavaScript运行起来的时候是怎么样的,你就看它源代码书写起来是怎么样的就行了。

我觉得说得非常……嗯,非常简单,是不是绝对正确?唉!我也就不操这个心了。JavaScript里面太多诡异的地方,谁说得准呢?

所以,只要event出现在第4行,不管是函数的定义,还是函数的调用,只要包裹在第1行和第7行的函数之间,它就能使用第1行和第7行之间声明的变量。注意这里的“能使用”,准确的表述应该是:

当执行到第4行代码时,仍然能够获得event的值(不会是undefined),哪怕此时其外部函数showEvent()已运行完毕

这就是闭包的精髓

闭包的复杂性(容易把开发人员弄晕的地方)就体现在这里。

闭包

我自己写代码,总是尽量避免产生闭包,忒反人性了,一不留神就是bug,而且是非常难以发现的bug。

然而,很多时候你得调用别人的类库,稍不注意(甚至不用不行),闭包就来了。

写草稿的时候,想到setTimeout()这是一个函数调用,不是函数声明,脑子里又捣糨糊了,突然怀疑这是不是闭包?

结果查到这个:阮一峰关于 Javascript 中闭包的解读是否正确

里面的高赞答案显然认为setTimeout()里对外部变量的引用,就是一个闭包。

所以还是得记牢前面所说的JavaScript的“词法作用域”:JavaScript的变量作用域基于函数的声明,而不是函数的运行

好了,非IE浏览器下通过参数传递event的情形似乎已经OK了?但还有一个问题,使用window.event时,为什么在setTimeout()的回调函数里就undefined呢?

setTimeout()

我们首先看一看,这锅该不该setTimeout()背?因为setTimeout()是一种“特殊的”函数,它的回调函数要在一定时间后才执行。

为了验证这个问题,我自己写了一个“同步的”回调函数,如下所示:

    function showEvent(){
myFunc(
function(){
//仅适用于IE浏览器:event有值
alert('in setTimeout:'+ event);
}
);
} function myFunc(callback){
callback();
}

耶!运行的结果,event的值是能取到的。

此外,在能够正常运行的代码中、分别alert通过参数传递event,和全部变量的window.event,如下所示:

     function showEvent(event){  //event作为参数传入
alert('在setTime()之前的window.event: ' + window.event); //有值
setTimeout(
function(){
alert('在setTime()中的window.event: ' + window.event); //undefined
alert('in setTimeout:'+ event);
},
)
}

由此可见,对于IE浏览器,event失去值的过程发生在setTimeout()中。

那setTimeout()中究竟发生了些什么?我看了很多文章和书籍,感觉确实提高了不少,总结如下:

  • JavaScript是非阻塞(异步)的。比如,上述代码执行的顺序是:1-2-3-7-8-9-4-5……。JavaScript执行器碰到setTimeout()不会停留(阻塞),等上100毫秒,啥事不做,而是会直接执行后面的代码,直到100毫秒过后,再回头来执行setTimeout()里的回调函数。这比较好理解,因为我们经常调试,能发现这个现象。但接下来,
  • JavaScript是单线程的。这可能就会冲击有些同学的世界观了,单线程怎么能异步呢?这涉及到两个概念:JavaScript引擎线程和其他线程。简而言之,JavaScript引擎线程,负责进行JavaScript解释执行的线程,始终只有一个线程,浏览器无论什么时候都只有一个JS线程在运行JS程序;但浏览器的内核是多线程的,JS引擎线程碰到setTimeout(),就召唤其他线程,“嗨,哥们,定时这活交给你了”,说完JS引擎继续干它自个的活去了(非阻塞)。那100毫秒过去了,其他线程怎么办?通知JS线程,停止执行手头上的代码,马上执行setTimeout()的回调函数?错!这里特别要注意:JS引擎线程不会停下手头的活儿(仍然是非阻塞),而是让setTimeout()的回调函数排队去,等着,等我把手头的活干完——这就是所谓的JavaScript的event loop机制。(详细的、规范的解释可以参考:以setTimeout来聊聊Event Loop

知道了这些之后,不知道大家有没有什么启发。我能够想象出来(真的只能是“想象”啊,没找到实锤,如果有大神直到真相,欢迎赐教)的解释就是(仅对IE浏览器而言)

  1. onclick事件被触发,
  2. 事件相关的信息被存放进window.event对象,并开始执行事件回调函数
  3. 碰到setTimeout(),通知其他线程,setTimeout()中回调函数被略过
  4. 程序继续执行
  5. ……
  6. event事件执行完毕,window.event被清空(这点很关键,因为此时的window.event是全局的,它不能被setTimeout()一直占用着)
  7. ……
  8. 100毫秒以后(准确的说,和时间多少没关系,哪怕是0毫秒也一样,反正都得等event事件执行完毕),继续执行setTimeout()的回调函数

这时候,window.event当然就是undefined的啦!

写在最后

已经很久没有这么认真的写过技术博客了。草稿是上上周周末写完的,记得。昨天晚上和今天上午又改了一遍,真心累。

现在前端(JavaScript)很火,但个人觉得,JavaScript真的是先天不足,大型化的工程应用坑太多。就像前几年火得一塌糊涂的node.js,看上去很美,但真用起来你就知道厉害了。

很多同学都因为“简单一些”而入坑前端,其实我觉得前端一点都不简单(应该是简陋吧?)前端的复杂性在于JavaScript(以及CSS)各种奇葩“特性”,以及不胜其烦的兼容性。而且我很怀疑,一入门就学这些东西,会不会被“带偏”“带坏”?至少,通过JavaScript来理解“工程化”“模块化”,还有四不像的“面向对象”……反正我讲起来都特别累,真不知道刚入门的同学能不能听得懂。

说这些可能很多人不爱听,就这样吧,最后一句忠告:不要把自己局限在“前端”,后端也很精彩,而且比前端简单——至少是“清晰”多了。O(∩_∩)O~

+++++++++++++++++

好了,继续撸我的“一起帮”代码了。争取4月10日上线新版本,这个版本就应该定型了。写到现在,都整整一年了……

Javascript的那些硬骨头:作用域、回调、闭包、异步……的更多相关文章

  1. 【译】学习JavaScript中提升、作用域、闭包的终极指南

    这似乎令人惊讶,但在我看来,理解JavaScript语言最重要和最基本的概念是理解执行上下文.通过正确学习它,你将很好地学习更多高级主题,如提升,作用域链和闭包.考虑到这一点,究竟什么是"执 ...

  2. 你不知道的JavaScript(上)作用域与闭包

    第一部分 作用域与闭包 第一章 作用域是什么 1.作用域 变量赋值操作会执行两个动作:首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后会在运行时引擎会在作用域中查找该变量,找到就会 ...

  3. JavaScript递归中的作用域问题

    需求是这样的,从子节点寻找指定className的父节点,一开始就想到递归(笨!),Dom结构如下: <div class="layer_1"> <div cla ...

  4. 《你不知道的JavaScript》第一部分:作用域和闭包

    第1章 作用域是什么 抛出问题:程序中的变量存储在哪里?程序需要时,如何找到它们? 设计 作用域 的目的:为了更好地存储和访问变量. 作用域:根据名称查找变量的一套规则,用于确定在何处以及如何查找变量 ...

  5. javascript 函数和作用域(闭包、作用域)(七)

    一.闭包 JavaScript中允许嵌套函数,允许函数用作数据(可以把函数赋值给变量,存储在对象属性中,存储在数组元素中),并且使用词法作用域,这些因素相互交互,创造了惊人的,强大的闭包效果.[upd ...

  6. (60)Wangdao.com第十天_JavaScript 函数_作用域_闭包_IIFE_回调函数_eval

    函数        实现特定功能的 n 条语句封装体. 1. 创建一个函数对象 var myFunc = new Function(); // typeof myFunc 将会打印 function ...

  7. 《你必须知道的javascript(上)》- 1.作用域和闭包

    1 作用域是什么 1.1 编译原理 分词/词法分析(Tokenizing/Lexing) 将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token). 解析/语 ...

  8. 你不知道的JavaScript(作用域和闭包)

    作用域和闭包 ・作用域 引擎:从头到尾负责整个JavaScript的编译及执行过程. 编译器:负责语法分析及代码生成等. 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非 ...

  9. JavaScript从作用域到闭包

    目录 作用域 全局作用域和局部作用域 块作用域与函数作用域 作用域中的声明提前 作用域链 函数声明与赋值 声明式函数.赋值式函数与匿名函数 代码块 自执行函数 闭包  作用域(scope) 全局作用域 ...

随机推荐

  1. 1.2 Python开发环境

    1.2.1 百家争鸣的繁荣景象 工欲善其事,必先利其器.学习编程也是同样的道理,熟悉开发环境应该是学习一门编程语言的第一步. IDLE是Python的官方标准开发环境,从官网www.python.or ...

  2. wss 协议传送过来的数据是经过 gzip 压缩过的,如何使用 qt 解压该数据呢?

    #include <QtZlib/zlib.h> QByteArray qGzipUncompress(const QByteArray& data) { if (!data.da ...

  3. SharedPreferences封装类

    最近一直在读马伟奇老师的简书,给人以不一样的感觉,接下来的时间会做做笔记,毕竟好东西变成自己的才有用 原文地址SharedPreferencesUtils 依赖 dependencies { comp ...

  4. css y轴溢出滚动条,x轴溢出显示

    这个是我工作中遇到的一个问题,困扰了我好几天,彻底理解了什么叫思路很重要. 黄色盒子里的内容是要超出出现滚动条的,红色的方块是根据另外的元素去定位的,于是呢 我就加上了 overflow-y:auto ...

  5. nginx启动停止

    nginx -s reload :修改配置后重新加载生效 nginx -s reopen :重新打开日志文件 nginx -t -c /path/to/nginx.conf 测试nginx配置文件是否 ...

  6. 关于HC04超声波模块测距的思考(51版)

    之前写过一篇HC04的使用文章,当时是使用stm32来实现的,原文链接. 后来又多次使用51来驱动这个模块,有时候有测距需要,使用了几次,总是感觉我上次那个程序不是很好, 所以这次对它进行了改进.虽然 ...

  7. DAVINCI DM6446 开发攻略——V4L2视频驱动和应用分析

     针对DAVINCI DM6446平台,网络上也有很多网友写了V4L2的驱动,但只是解析Montavista linux-2.6.10 V4L2的原理.结构和函数,深度不够.本文决定把Montavis ...

  8. vxworks下文件读写示例

    dev 1.create file on floopy disk and write contents: -> pdev=fdDevCreate(0,0,0,0)     /* A:,1.44M ...

  9. JSP标签c:forEach报错(二)

    1.今天,我在用c标签写一些样例,结果出现一些错误,写下作为记录 具体错误如下: 三月 31, 2014 9:46:28 下午 org.apache.catalina.core.StandardWra ...

  10. Flex和Servlet结合上传文件

    Flex和Servlet结合上传文件 1.准备工作 (1)下载文件上传的组件,commons-fileupload-1.3.1.jar (2)下载文件输入输出jar,commons-io-2.4.ja ...