編程時我們往往拿到的是業務流程正確的業務說明文檔或規範,但實際開發中卻佈滿荊棘和例外情況,而這些例外中包含業務用例的例外,也包含技術上的例外。對於業務用例的例外我們別無它法,必須要求實施人員與使用者共同提供合理的解決方案;而技術上的例外,則必須由我們碼農們手刃之,而這也是我想記錄的內容。
我打算分成《前端魔法堂-異常不只是try/catch》和《前端魔法堂-呼叫棧,異常實例中的寶藏》兩篇分別敘述內建/自訂異常類,捕獲運行時異常/語法異常/網路請求異常/PromiseRejection事件,什麼是呼叫堆疊和如何取得呼叫堆疊的相關資訊。
是不是未出發就已經很期待呢?好吧,大家捉緊扶手,老司機要開車了^_^
本篇將敘述如下內容:
異常還是錯誤?它會如何影響我們的程式碼?
內建異常類型有哪些?
動手寫自己的異常類型吧!
捕捉「同步程式碼」中的"執行時期異常",用try/catch
就夠了。
"萬能"異常捕獲者window.onerror
,真的萬用嗎?
Promise.reject也拋異常,怎麼辦?
404等網路請求異常真心要後之後覺嗎?
在學習Java時我們會被告知異常(Exception)和錯誤(Error)是不一樣的,異常是不會導致進程終止從而可以被修復(try/catch),但錯誤將會導致進程終止因此不能被修復。當對於JavaScript而言,我們要面對的僅有異常(雖然異常類別名為Error或含Error字樣),異常的出現不會導致JavaScript引擎崩潰,最多就是讓目前執行的任務終止而已。
上面說到異常的出現最多就是讓目前執行的任務終止,到底是什麼意思呢?這裡就牽涉到Event Loop的原理了,下面我試著用程式碼大致說明吧。
<script> // 1.当前代码块将作为一个任务压入任务队列中,JavaScript线程会不断地从任务队列中提取任务执行; // 2.当任务执行过程中报异常,且异常没有捕获处理,则会一路沿着调用栈从顶到底抛出,最终终止当前任务的执行; // 3.JavaScript线程会继续从任务队列中提取下一个任务继续执行。 function a(){throw Error("test")} function b(){a()} b() console.log("永远不会执行!") </script> <script> // 下一个任务 console.log("你有你抛异常,我照样执行!") </script>
說到內建例外類別那麼必先提到的就是Error
這個祖先類型了,其他所有的內建異常類別和自訂類別都必須繼承它。而它的標準屬性和方法就以下這寥寥幾個而已
@prop {String} name - 异常名称 @prop {String} message - 供人类阅读的异常信息 @prop {Function} constructor - 类型构造器 @method toString():String - 输出异常信息
由於標準屬性實在太少,無法提供更有效的信息供開發者定位異常發生的位置和重現事故現場,因此各瀏覽器廠商均手多多的自己增加些屬性,然後逐漸成了事實標準。
@prop {String} fileName - 异常发生的脚本URI @prop {number} lineNumber - 异常发生的行号 @prop {number} columnNumber - 异常发生的列号 @prop {String} stack - 异常发生时的调用栈信息,IE10及以上才支持 @method toSource():String - 异常发生的脚本内容
另外巨硬還新增以下兩個屬性
@prop {String} description - 和message差不多 @prop {number} number - 异常类型的编号,巨硬为每个异常设置了一个唯一的编号
那麼現在我要實例化一個Error對象,只需呼叫Error()
或 new Error()
即可;若想同時設定message,則改為Error("test")
或new Error("test")
。其實Error的建構函式簽章是這樣的
@constructor @param {String=} message - 设置message属性 @param {String=} fileName - 设置fileName属性 @param {number=} lineNumber - 设置lineNUmber属性
現在我們來看看具體有哪些內建的異常型別吧!
EvalError,呼叫eval()
時發生的異常,已被廢棄只用於向後相容而已
InternalError,JavaScript引擎內部異常,FireFox獨門提供的!
RangeError,當函數實參越界時發生,如Array
,Number.toExponential
, Number.toFixed
和Number.toPrecision
時入參非法時。
ReferenceError,當引用未宣告的變數時發生
SyntaxError
#發生語法錯誤
,當值不是所期待的型別時,
null.f()
URIErrordecodeURIComponent('%'),即
decodeURIComponent
decodeURI
,###encodeURIComponent###,###encodeURI###############三.動手寫自己的例外類型吧! ###### 關於在StackOverflow上早有人討論如何自訂異常類型了參考###於是我們順手拈來即可###function MyError(message, fileName, lineNumber){ if (this instanceof MyError);else return new MyError(message, fileName, lineNumber) this.message = message || "" if (fileName){ this.fileName = fileName } if (lineNumber){ this.lineNumber = lineNumber } } var proto = MyError.prototype = Object.create(Error.prototype) proto.name = "MyError" proto.constructor = MyError###cljs實現如下###
(defn ^export MyError [& args] (this-as this (if (instance? MyError this) (let [ps ["message" "fileName" "lineNumber"] idxs (-> (min (count args) (count ps)) range)] (reduce (fn [accu i] (aset accu (nth ps i) (nth args i)) accu) this idxs)) (apply new MyError args)))) (def proto (aset MyError "prototype" (.create js/Object (.-prototype Error)))) (aset proto "name" "MyError") (aset proto "constructor" MyError)###四.捕獲「同步代碼」中的"運行時異常",用###try/catch###就夠了####### 為了防止由於異常的出現,導致正常代碼被略過的風險,我們習慣採取###try/catch###來捕捉並處理例外狀況。 ###
try{ throw Error("unexpected operation happen...") } catch (e){ console.log(e.message) }###cljs寫法###
(try (throw (Error. "unexpected operation happen...") (catch e (println (.-message e)))))
很多时我们会以为这样书写就万事大吉了,但其实try/catch
能且仅能捕获“同步代码”中的"运行时异常"。
1."同步代码"就是说无法获取如setTimeout
、Promise
等异步代码的异常,也就是说try/catch
仅能捕获当前任务的异常,setTimeout
等异步代码是在下一个EventLoop中执行。
// 真心捕获不到啊亲~! try{ setTimeout(function(){ throw Error("unexpected operation happen...") }, 0) } catch(e){ console.log(e) }
2."运行时异常"是指非SyntaxError,也就是语法错误是无法捕获的,因为在解析JavaScript源码时就报错了,还怎么捕获呢~~
// 非法标识符a->b,真心捕获不到啊亲~! try{ a->b = 1 } catch(e){ console.log(e) }
这时大家会急不可待地问:“异步代码的异常咋办呢?语法异常咋办呢?”在解答上述疑问前,我们先偏离一下,稍微挖挖throw
语句的特性。
throw
后面可以跟什么啊? 一般而言我们会throw
一个Error或其子类的实例(如throw Error()
),其实我们throw
任何类型的数据(如throw 1
,throw "test"
,throw true
等)。但即使可以抛出任意类型的数据,我们还是要坚持抛出Error或其子类的实例。这是为什么呢?
try{ throw "unexpected operation happen..." } catch(e){ console.log(e) } try{ throw TypeError("unexpected operation happen...") } catch(e){ if ("TypeError" == e.name){ // Do something1 } else if ("RangeError" == e.name){ // Do something2 } }
原因显然易见——异常发生时提供信息越全越好,更容易追踪定位重现问题嘛!
window.onerror
,真的万能吗? 在每个可能发生异常的地方都写上try/catch
显然是不实际的(另外还存在性能问题),即使是罗嗦如Java我们开发时也就是不断声明throws
,然后在顶层处理异常罢了。那么,JavaScript中对应的顶层异常处理入口又在哪呢?木有错,就是在window.onerror
。看看方法签名吧
@description window.onerror处理函数 @param {string} message - 异常信息" @param {string} source - 发生异常的脚本的URI @param {number} lineno - 发生异常的脚本行号 @param {number} colno - 发生异常的脚本列号 @param {?Error} error - Error实例,Safari和IE10中没有这个实参
这时我们就可以通过它捕获除了try/catch
能捕获的异常外,还可以捕获setTimeout
等的异步代码异常,语法错误。
window.onerror = function(message, source, lineno, colno, error){ // Do something you like. } setTimeout(function(){ throw Error("oh no!") }, 0) a->b = 1
这样就满足了吗?还没出大杀技呢——屏蔽异常、屏蔽、屏~~
只有onerror函数返回true
时,异常就不会继续向上抛(否则继续上抛就成了Uncaught Error了)。
// 有异常没问题啊,因为我看不到^_^ window.onerror = function(){return true}
现在回到标题的疑问中,有了onerror就可以捕获所有异常了吗?答案又是否定的(我的娘啊,还要折腾多久啊~0~)
Chrome中对于跨域脚本所报的异常,虽然onerror能够捕获,但统一报Script Error
。若要得到正确的错误信息,则要配置跨域资源共享CORS才可以。
window.onerror
实际上采用的事件冒泡的机制捕获异常,并且在冒泡(bubble)阶段时才触发,因此像网络请求异常这些不会冒泡的异常是无法捕获的。
Promise.reject产生的未被catch的异常,window.onerror
也是无能为力。
通过Promise来处理复杂的异步流程控制让我们得心应手,但倘若其中出现异常或Promise实例状态变为rejected时,会是怎样一个状况,我们又可以如何处理呢?
Promise实例的初始化状态是pending,而发生异常时则为rejected,而导致状态从pending转变为rejected的操作有
调用Promise.reject
类方法
在工厂方法中调用reject
方法
在工厂方法或then回调函数中抛异常
// 方式1 Promise.reject("anything you want") // 方式2 new Promise(function(resolve, reject) { reject("anything you want") }) // 方式3 new Promise(function{ throw "anything you want" }) new Promise(function(r) { r(Error("anything you want" ) }).then(function(e) { throw e })
当Promise实例从pending转变为rejected时,和之前谈论到异常一样,要么被捕获处理,要么继续抛出直到成为Uncaught(in promise) Error
为止。
catch
掉 若在异常发生前我们已经调用catch
方法来捕获异常,那么则相安无事
new Promise(function(resolve, reject){ setTimeout(reject, 0) }).catch(function(e){ console.log("catch") return "bingo" }).then(function(x){ console.log(x) }) // 回显 bingo
若在异常发生前我们没有调用catch
方法来捕获异常,还是可以通过window
的unhandledrejection
事件捕获异常的
window.addEventListener("unhandledrejection", function(e){ // Event新增属性 // @prop {Promise} promise - 状态为rejected的Promise实例 // @prop {String|Object} reason - 异常信息或rejected的内容 // 会阻止异常继续抛出,不让Uncaught(in promise) Error产生 e.preventDefault() })
catch
由于Promise实例可异步订阅其状态变化,也就是可以异步注册catch处理函数,这时其实已经抛出Uncaught(in promise) Error
,但我们依然可以处理
var p = new Promise(function(resolve, reject){ setTimeout(reject, 0) }) setTimeout(function(){ p.catch(function(e){ console.log("catch") return "bingo" }) }, 1000)
另外,还可以通过window
的rejectionhandled
事件监听异步注册catch处理函数的行为
window.addEventListener("rejectionhandled", function(e){ // Event新增属性 // @prop {Promise} promise - 状态为rejected的Promise实例 // @prop {String|Object} reason - 异常信息或rejected的内容 // Uncaught(in promise) Error已经抛出,所以这句毫无意义^_^ e.preventDefault() })
注意:只有抛出Uncaught(in promise) Error
后,异步catch才会触发该事件。
也许我们都遇到482c199bb16757e6448cd883ac614811
报404网络请求异常的情况,然后测试或用户保障怎么哪个哪个图标没有显示。其实我们我们可以通过以下方式捕获这类异常
window.addEventListener("error", function(e){ // Do something console.log(e.bubbles) // 回显false }, true)
由于网络请求异常不会冒泡,因此必须在capture阶段捕获才可以。但还有一个问题是这种方式无法精确判断异常的HTTP状态是404还是500等,因此还是要配合服务端日志来排查分析才可以。
以上是有關前端異常try/catch的問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!