扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
前端工程师都知道 JavaScript 有基本的异常处理能力。我们可以 throw new Error(),浏览器也会在我们调用 API 出错时抛出异常。但估计绝大多数前端工程师都没考虑过收集这些异常信息。反正只要 JavaScript 出错后刷新不复现,那用户就可以通过刷新解决问题,浏览器不会崩溃,当没有发生过好了。这种假设在 Single Page App 流行之前还是成立的。现在的 Single Page App 运行一段时间后状态复杂无比,用户可能进行了若干输入操作才来到这里的,说刷新就刷新啊?之前的操作岂不要完全重做?所以我们还是有必要捕获和分析这些异常信息的,然后我们就可以修改代码避免影响用户体验。
创新互联自2013年创立以来,是专业互联网技术服务公司,拥有项目成都网站设计、做网站网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元嵩县做网站,已为上家服务,为嵩县各地企业和个人服务,联系电话:13518219792
捕获异常的方式
我们自己写的 throw new Error() 想要捕获当然可以捕获,因为我们很清楚 throw 写在哪里了。但是调用浏览器 API 时发生的异常就不一定那么容易捕获了,有些 API 在标准里就写着会抛出异常,有些 API 只有个别浏览器因为实现差异或者有缺陷而抛出异常。对于前者我们还能通过 try-catch 捕获,对于后者我们必须监听全局的异常然后捕获。
try-catch
如果有些浏览器 API 是已知会抛出异常的,那我们就需要把调用放到 try-catch 里面,避免因为出错而导致整个程序进入非法状态。例如说 window.localStorage 就是这样的一个 API,在写入数据超过容量限制后就会抛出异常,在 Safari 的隐私浏览模式下也会如此。
try { localStorage.setItem('date', Date.now());} catch (error) { reportError(error);}
另一个常见的 try-catch 适用场景是回调。因为回调函数的代码是我们不可控的,代码质量如何,会不会调用其它会抛出异常的 API,我们一概不知道。为了不要因为回调出错而导致调用回调后的其它代码无法执行,所以把调用回到放到 try-catch 里面是必须的。
listeners.forEach(function(listener) { try { listener(); } catch (error) { reportError(error); }});
window.onerror
对于 try-catch 覆盖不到的地方,如果出现异常就只能通过 window.onerror 来捕获了。
window.onerror = function(errorMessage, scriptURI, lineNumber) { reportError({ message: errorMessage, script: scriptURI, line: lineNumber });}
注意不要耍小聪明使用 window.addEventListener 或 window.attachEvent 的形式去监听 window.onerror。很多浏览器只实现了 window.onerror,或者是只有 window.onerror 的实现是标准的。考虑到标准草案定义的也是 window.onerror,我们使用 window.onerror 就好了。
属性丢失
假设我们有一个 reportError 函数用来收集捕获到的异常,然后批量发送到服务器端存储以便查询分析,那么我们会想要收集哪些信息呢?比较有用的信息包括:错误类型(name)、错误消息(message)、脚本文件地址(script)、行号(line)、列号(column)、堆栈跟踪(stack)。如果一个异常是通过 try-catch 捕获到的,这些信息都在 Error 对象上(主流浏览器都支持),所以 reportError 也能收集到这些信息。但如果是通过 window.onerror 捕获到的,我们都知道这个事件函数只有 3 个参数,所以这 3 个参数以外的信息就丢失了。
序列化消息
如果 Error 对象是我们自己创建的话,那么 error.message 就是由我们控制的。基本上我们把什么放进 error.message 里面,window.onerror 的第一个参数(message)就会是什么。(浏览器其实会略作修改,例如加上 'Uncaught Error: ' 前缀。)因此我们可以把我们关注的属性序列化(例如 JSON.Stringify)后存放到 error.message 里面,然后在 window.onerror 读取出来反序列化就可以了。当然,这仅限于我们自己创建的 Error 对象。
第五个参数
浏览器厂商也知道大家在使用 window.onerror 时受到的限制,所以开始往 window.onerror 上面添加新的参数。考虑到只有行号没有列号好像不是很对称的样子,IE 首先把列号加上了,放在第四个参数。然而大家更关心的是能否拿到完整的堆栈,于是 Firefox 说不如把堆栈放在第五个参数吧。但 Chrome 说那还不如把整个 Error 对象放在第五个参数,大家想读取什么属性都可以了,包括自定义属性。结果由于 Chrome 动作比较快,在 Chrome 30 实现了新的 window.onerror 签名,导致标准草案也就跟着这样写了。
window.onerror = function( errorMessage, scriptURI, lineNumber, columnNumber, error) { if (error) { reportError(error); } else { reportError({ message: errorMessage, script: scriptURI, line: lineNumber, column: columnNumber }); }}
属性正规化
我们之前讨论到的 Error 对象属性,其名称都是基于 Chrome 命名方式的,然而不同浏览器对 Error 对象属性的命名方式各不相同,例如脚本文件地址在 Chrome 叫做 script 但在 Firefox 叫做 filename。因此,我们还需要一个专门的函数来对Error 对象进行正规化处理,也就是把不同的属性名称都映射到统一的属性名称上。具体做法可以参考这篇文章。尽管浏览器实现会更新,但人手维护一份这样的映射表并不会太难。
类似的是堆栈跟踪(stack)的格式。这个属性以纯文本的形式保存一份异常在发生时的堆栈信息,由于各个浏览器使用的文本格式不一样,所以也需要人手维护一份正则表达,用于从纯文本中提取每一帧的函数名(identifier)、文件(script)、行号(line)和列号(column)。
安全限制
如果你也遇到过消息为 'Script error.' 的错误,你会明白我在说什么的,这其实是浏览器针对不同源(origin)脚本文件的限制。这个安全限制的理由是这样的:假设一家网银在用户登录后返回的 HTML 跟匿名用户看到的 HTML 不一样,一个第三方网站就能把这家网银的 URI 放到 script.src 属性里面。HTML 当然不可能被当做 JS 解析啦,所以浏览器会抛出异常,而这个第三方网站就能通过解析异常的位置来判断用户是否有登录。为此浏览器对于不同源脚本文件抛出的异常一律进行过滤,过滤得只剩下 'Script error.' 这样一条不变的消息,其它属性统统消失。
对于有一定规模的网站来说,脚本文件放在 CDN 上,不同源是很正常的。现在就算是自己做个小网站,常见框架如 jQuery 和 Backbone 都能直接引用公共 CDN 上的版本,加速用户下载。所以这个安全限制确实造成了一些麻烦,导致我们从 Chrome 和 Firefox 收集到的异常信息都是无用的 'Script error.'。
CORS
想要绕过这个限制,只要保证脚本文件和页面本身同源即可。但把脚本文件放在不经 CDN 加速的服务器上,岂不降低用户下载速度?一个解决方案是,脚本文件继续放在 CDN 上,利用 XMLHttpRequest 通过 CORS 把内容下载回来,再创建 script 标签注入到页面当中。在页面当中内嵌的代码当然是同源的啦。
这说起来很简单,但实现起来却有很多细节问题。用一个简单的例子来说:
script src=""/scriptscript (function step2() {})();/scriptscript src=""/script
我们都知道这个 step1、step2、step3 如果存在依赖关系的话,则必须严格按照这个顺序执行,否则就可能出错。浏览器可以并行请求 step1 和 step3 的文件,但在执行时顺序是保证的。如果我们自己通过 XMLHttpRequest 获取 step1 和 step3 的文件内容,我们就需要自行保证其顺序正确性。此外不要忘记了 step2,在 step1 以非阻塞形式下载的时候 step2 就可以被执行了,所以我们还必须人为干预 step2 让它等待 step1 完成后再执行。
如果我们已经有一整套工具来生成网站上不同页面的 script 标签的话,我们就需要调整一下这套工具让它对 script 标签做出改动:
script scheduleRemoteScript('');/scriptscript scheduleInlineScript(function code() { (function step2() {})(); });/scriptscript scheduleRemoteScript('');/script
我们需要实现 scheduleRemoteScript 和 scheduleInlineScript 这两个函数,并且保证它们在第一个引用外部脚本文件的 script 标签之前就被定义好,然后余下的 script 标签都会被改写成上面这种形式。注意原本立即执行的 step2 函数被放到了一个更大的 code 函数里面了。code 函数并不会被执行,它只是一个容器而已,这样使得原本 step2 的代码不需要转义就能保留下来,但又不会被立即执行。
接下来我们还需要实现一套完整的机制,保证这些由 scheduleRemoteScript 根据地址下载回来的文件内容和由 scheduleInlineScript 直接获取到的代码能够按照正确的顺序一个接一个地执行。详细的代码我就不在这里给出了,大家有兴趣可以自己去实现。
行号反查
通过 CORS 获取内容再把代码注入页面能够突破安全限制,但会引入一个新的问题,那就是行号冲突。原本通过 error.script 可以定位到唯一的脚本文件,再通过 error.line 可以定位到唯一的行号。现在由于都是页面内嵌的代码,多个 script 标签并不能通过 error.script 来区分,然而每一个 script 标签内部的行号都是从 1 算起的,结果就导致我们无法利用异常信息定位错误所在的源代码位置。
为了避免行号冲突,我们可以浪费一些行号,使得每一个 script 标签中有实际代码所使用的行号区间互相不重叠。举个例子来说,假设每个 script 标签中的实际代码都不超过 1000 行,那么我可以让第一个 script 标签中的代码占用第 1–1000 行,让第二个 script 标签中的代码占用第 1001–2000 行(前面插入 1000 行空行),第三个 script 标签种的代码占用第 2001–3000 行(前面插入 2000 行空行),以此类推。然后我们使用 data-* 属性记录这些信息,便于反查。
script data-src="" data-line-start="1" // code for step 1/scriptscript data-line-start="1001" // '\n' * 1000 // code for step 2/scriptscript data-src="" data-line-start="2001" // '\n' * 2000 // code for step 3/script
经过这样处理后,如果一个错误的 error.line 是 3005 的话,那意味着实际的 error.script 应该是 '',而实际的 error.line 则应该是 5。我们可以在之前提到的 reportError 函数里面完成这项行号反查工作。
当然,由于我们没办法保证每一个脚本文件只有 1000 行,也有可能有些脚本文件明显小于 1000 行,所以其实不需要固定分配 1000 行的区间给每一个 script 标签。我们可以根据实际脚本行数来分配区间,只要保证每一个 script 标签所使用的区间互不重叠就可以了。
crossorigin 属性
浏览器对于不同源的内容进行的安全限制当然不仅限于 script 标签。既然 XMLHttpRequest 可以通过 CORS 来突破这个限制,为什么直接通过标签引用的资源就不可以呢?这当然是可以的。
针对 script 标签引用不同源脚本文件的限制同样作用于 img 标签引用不同源图片文件。如果一个 img 标签是不同源的话,一旦在 canvas 绘图时用到了,该 canvas 将变为只写状态,保证网站不能通过 JavaScript 窃取未授权的不同源图片数据。后来 img 标签通过引入 crossorigin 属性解决了这个问题。如果使用 crossorigin="anonymous",则相当于匿名 CORS;如果使用 `crossorigin=“use-credentials”,则相当于带认证的 CORS。
既然 img 标签能这样做,为什么 script 标签就不能这样做?于是浏览器厂商就为 script 标签加入了同样的 crossorigin 属性用于解决上述安全限制问题。现在 Chrome 和 Firefox 对这个属性的支持是完全没有问题的。Safari 则会把 crossorigin="anonymous" 当做 crossorigin="use-credentials" 处理,结果是如果服务器只支持匿名 CORS 则 Safari 会当做认证失败。由于 CDN 服务器出于性能的考虑被设计为只能返回静态内容,不可能动态的根据请求返回认证 CORS 所需的 HTTP Header,Safari 相当于不能利用此特性来解决上述问题。
总结
JavaScript 异常处理看起来很简单,跟其它语言没什么区别,但真的要把异常都捕获了然后对属性做分析,其实还不是那么容易的事情。现在尽管有一些第三方服务提供捕获 JavaScript 异常的类 Google Analytics 服务,但如果要弄明白其中的细节和原理还是必须自己亲手做一次。
咋看上去很头痛,不过你测试的结果是那样,就表示你的算法没有表述好,
我猜你for循环里面的结果依然是2,3,4依次加1为一个循环的吧. 起码我试的时候他们是一起left+=1的;
要他们轮流下落的话就要一个一个来,等2到了末尾再跳到3。
这代码看得难受的,我也只能猜测一下..
ECMAScript(即Javascript)变量包含两种不同类型的值,基本类型和引用类型。
基本类型:指的就是保存在栈内存中的简单数据值。
引用类型:指的是那些保存在堆内存中的对象,换句话说,就是变量名实际上是一个指针,而这个指针指向的位置,就是保存对象的位置。
两种不同的访问方式
基本类型:按值访问,操作的是它们实际的值。
引用类型:按引用访问,当查询时,我们需要先从栈中读取内存地址,然后按照指针所指向的地方,找到堆内存里面的值。
基本类型
基本的数据类型有:`undefined,boolean,number,string,null.基本类型的访问是按值访问的,就是说你可以操作保存在变量中的实际的值。
有以下几个特点:
基本类型的值是不可变得:
基本类型的比较是值的比较:
基本类型的变量是存放在栈区的(栈区指内存里的栈内存)
引用类型
javascript中除了上面的基本类型(number,string,boolean,null,undefined)之外就是引用类型了,也可以说是就是对象了。对象是属性和方法的集合。
引用类型的值是可变的 ,可为为引用类型添加属性和方法,也可以删除其属性和方法
引用类型的值是同时保存在栈内存和堆内存中的对象
引用类型的比较是引用的比较
引用类型和传统的面向对象程序设计中的类相似,但实现不同。
Object是一个基础类型,其他所有类型都是从Object继承基本的行为;
Array类型是一组值的有序列表,同事还提供了操作和转换这些值的功能;
Date类型提供有关日期和时间信息,包括当前日期和时间已经相关的计算功能;
RegExp类型是支持正则表达式的。
function,函数实际上是Function类型的实例,因此函数也是对象,函数也拥有方法,可以来增强其行为。
判断类型的最佳使用方法
1. Typeof操作符是检测基本类型的最佳工具;
2. 如果变量值是nul或者对象,typeof 将返回“object”;
3. Instanceof用于检测引用类型,可以检测到具体的,它是什么类型的实例;
4. 如果变量是给定引用类型的实例,instanceof操作符会返回true;
JQuery是一种JavaScript框架
Javascript 是一种脚本语言
JQuery只是一个人用javascript把JS里面的常用函数及自定义函数打包到一个文件而异,方便大家调用,只是这个人做的比较牛,这个函数包用着很方便,所以很多人都用,就记住了这个名字JQuery,相比大家做网站时都会专门有一个js文件,存放这个网站所调用的JS函数,那这个JQuery只是做的更专业,更牛B,更强大,更方便而异,你原来可能要几行代码来实现的功能,用JQuery可能只用一行就搞定了。
相关资料:
js,即JavaScript
在1995年时,由Netscape公司的Brendan Eich,在网景导航者浏览器上首次设计实现而成。因为Netscape与Sun合作,Netscape管理层希望它外观看起来像Java,因此取名为JavaScript
JavaScript一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型
jQuery
jQuery是一个兼容多浏览器的javascript库,核心理念是write less,do more(写得更少,做得更多)
其实只要学好了javascript,使用jquery将不会有什么问题
新手程序员必须花时间重点学习一下javascript;
至于jQuery可以直接在使用时参考jQuery参考文档,不需花太多时间在上面,当然如果读者时间充裕,也可以学习下jQuery源码,一边更好的了解jquery实现原理
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流