这注定是一篇自取其辱的博客, 飞哥, 你们眼中的大神, 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);
- },100
- );
- }
- <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);
- },100
- )
- }
由此可见, 对于 IE 浏览器, event 失去值的过程发生在 setTimeout()中
那 setTimeout()中究竟发生了些什么? 我看了很多文章和书籍, 感觉确实提高了不少, 总结如下:
JavaScript 是非阻塞 (异步) 的比如, 上述代码执行的顺序是: 1-2-3-7-8-9-4-5JavaScript 执行器碰到 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 浏览器而言):
onclick 事件被触发,
事件相关的信息被存放进 window.event 对象, 并开始执行事件回调函数
碰到 setTimeout(), 通知其他线程, setTimeout()中回调函数被略过
程序继续执行
event 事件执行完毕, window.event 被清空 (这点很关键, 因为此时的 window.event 是全局的, 它不能被 setTimeout() 一直占用着)
100 毫秒以后 (准确的说, 和时间多少没关系, 哪怕是 0 毫秒也一样, 反正都得等 event 事件执行完毕), 继续执行 setTimeout() 的回调函数
这时候, window.event 当然就是 undefined 的啦!
写在最后
已经很久没有这么认真的写过技术博客了草稿是上上周周末写完的, 记得昨天晚上和今天上午又改了一遍, 真心累
现在前端 (JavaScript) 很火, 但个人觉得, JavaScript 真的是先天不足, 大型化的工程应用坑太多就像前几年火得一塌糊涂的 node.js, 看上去很美, 但真用起来你就知道厉害了
很多同学都因为简单一些而入坑前端, 其实我觉得前端一点都不简单 (应该是简陋吧?) 前端的复杂性在于 JavaScript(以及 CSS)各种奇葩特性, 以及不胜其烦的兼容性而且我很怀疑, 一入门就学这些东西, 会不会被带偏带坏? 至少, 通过 JavaScript 来理解工程化模块化, 还有四不像的面向对象反正我讲起来都特别累, 真不知道刚入门的同学能不能听得懂
来源: https://www.cnblogs.com/freeflying/p/8526058.html