热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:Javascript的那些硬骨头:作用域回调闭包异步……

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Javascript的那些硬骨头:作用域回调闭包异步……相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Javascript的那些硬骨头:作用域回调闭包异步……相关的知识,希望对你有一定的参考价值。



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

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

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

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


1
10
11

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

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


1 function showEvent(){
2 var e = event; //把event赋值给e
3 setTimeout(
4 function(){
5 alert(\'in setTimeout:\'+ e);
6 },100
7 );
8 }

问题就解决了:

欧耶,\\(^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只能通过参数传递,所以代码应该改写为:


1 function showEvent(event){ //event作为参数传入
2 setTimeout(
3 function(){
4 alert(\'in setTimeout:\'+ event);
5 },100
6 );
7 }

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



这样一测试,( ⊙ 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,Func等,函数作为方法参数嘛。问题在于,当回调和闭包同时出现时,问题就复杂了。

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


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

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

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


1 setTimeout(
2 function(event){ //把event作为参数传入
3 alert(\'in setTimeout:\'+ event);
4 },100
5 );

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

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

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

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

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


1 function showEvent(event){
2 setTimeout(
3 function(event){ //把event作为参数传入
4 alert(\'in setTimeout:\'+ event);
5 }, 100, event //event作为匿名回调函数的参数
6 );
7 }

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


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

好啦,不跑题太远了。

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


1 function showEvent(event){ //event作为参数传入
2 setTimeout(
3 function(){
4 alert(\'in setTimeout:\'+ event);
5 },100
6 );
7 }
8
9 "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,如下所示:


1 function showEvent(event){ //event作为参数传入
2 alert(\'在setTime()之前的window.event: \' + window.event); //有值
3 setTimeout(
4 function(){
5 alert(\'在setTime()中的window.event: \' + window.event); //undefined
6 alert(\'in setTimeout:\'+ event);
7 },100
8 )
9 }

由此可见,对于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  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • JavaScript和HTML之间的交互是经由过程事宜完成的。事宜:文档或浏览器窗口中发作的一些特定的交互霎时。能够运用侦听器(或处置惩罚递次来预订事宜),以便事宜发作时实行相应的 ... [详细]
  • 分享css中提升优先级属性!important的用法总结
    web前端|css教程css!importantweb前端-css教程本文分享css中提升优先级属性!important的用法总结微信门店展示源码,vscode如何管理站点,ubu ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • 【MEGA DEAL】Ruby on Rails编码训练营(97%折扣)限时特惠!
    本文介绍了JCG Deals商店提供的Ruby on Rails编码训练营的超值优惠活动,现在只需29美元即可获得,原价为$1,296。Ruby on Rails是一种用于Web开发的编程语言,即使没有编程或网页设计经验,也能在几分钟内构建专业的网站。该训练营共有6门课程,包括使用Ruby on Rails进行BDD的课程,使用RSpec 3和Capybara等。限时特惠,机会难得,赶快行动吧! ... [详细]
  • 如何在HTML中获取鼠标的当前位置
    本文介绍了在HTML中获取鼠标当前位置的三种方法,分别是相对于屏幕的位置、相对于窗口的位置以及考虑了页面滚动因素的位置。通过这些方法可以准确获取鼠标的坐标信息。 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • wpf+mvvm代码组织结构及实现方式
    本文介绍了wpf+mvvm代码组织结构的由来和实现方式。作者回顾了自己大学时期接触wpf开发和mvvm模式的经历,认为mvvm模式使得开发更加专注于业务且高效。与此同时,作者指出mvvm模式相较于mvc模式的优势。文章还提到了当没有mvvm时处理数据和UI交互的例子,以及前后端分离和组件化的概念。作者希望能够只关注原始数据结构,将数据交给UI自行改变,从而解放劳动力,避免加班。 ... [详细]
  • 2021最新总结网易/腾讯/CVTE/字节面经分享(附答案解析)
    本文分享作者在2021年面试网易、腾讯、CVTE和字节等大型互联网企业的经历和问题,包括稳定性设计、数据库优化、分布式锁的设计等内容。同时提供了大厂最新面试真题笔记,并附带答案解析。 ... [详细]
  • 本文整理了常用的CSS属性及用法,包括背景属性、边框属性、尺寸属性、可伸缩框属性、字体属性和文本属性等,方便开发者查阅和使用。 ... [详细]
  • 【爬虫】关于企业信用信息公示系统加速乐最新反爬虫机制
    ( ̄▽ ̄)~又得半夜修仙了,作为一个爬虫小白,花了3天时间写好的程序,才跑了一个月目标网站就更新了,是有点悲催,还是要只有一天的时间重构。升级后网站的层次结构并没有太多变化,表面上 ... [详细]
  • 巧用arguments在Javascript的函数中有个名为arguments的类数组对象。它看起来是那么的诡异而且名不经传,但众多的Javascript库都使用着它强大的功能。所 ... [详细]
author-avatar
-路人甲___
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有