異常不僅僅是try/catch

来源:http://www.cnblogs.com/historylyt/archive/2017/10/18/7688248.html
-Advertisement-
Play Games

前言 編程時我們往往拿到的是業務流程正確的業務說明文檔或規範,但實際開發中卻佈滿荊棘和例外情況,而這些例外中包含業務用例的例外,也包含技術上的例外。對於業務用例的例外我們別無它法,必須要求實施人員與用戶共同提供合理的解決方案;而技術上的例外,則必須由我們碼農們手刃之,而這也是我想記錄的內容。 我打算 ...


前言

 編程時我們往往拿到的是業務流程正確的業務說明文檔或規範,但實際開發中卻佈滿荊棘和例外情況,而這些例外中包含業務用例的例外,也包含技術上的例外。對於業務用例的例外我們別無它法,必須要求實施人員與用戶共同提供合理的解決方案;而技術上的例外,則必須由我們碼農們手刃之,而這也是我想記錄的內容。
 我打算分成《前端魔法堂——異常不僅僅是try/catch》和《前端魔法堂——調用棧,異常實例中的寶藏》兩篇分別敘述內置/自定義異常類,捕獲運行時異常/語法異常/網路請求異常/PromiseRejection事件,什麼是調用棧和如何獲取調用棧的相關信息。
 是不是未出發就已經很期待呢?好吧,大家捉緊扶手,老司機要開車了^_^

概要

 本篇將敘述如下內容:

  1. 異常還是錯誤?它會如何影響我們的代碼?
  2. 內置異常類型有哪些?
  3. 動手寫自己的異常類型吧!
  4. 捕獲“同步代碼”中的"運行時異常",用try/catch就夠了。
  5. "萬能"異常捕獲者window.onerror,真的萬能嗎?
  6. Promise.reject也拋異常,怎麼辦?
  7. 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屬性

現在我們看看具體有哪些內置的異常類型吧!

  1. EvalError,調用eval()時發生的異常,已被廢棄只用於向後相容而已
  2. InternalError,JavaScript引擎內部異常,FireFox獨門提供的!
  3. RangeError,當函數實參越界時發生,如ArrayNumber.toExponential,Number.toFixedNumber.toPrecision時入參非法時。
  4. ReferenceError,當引用未聲明的變數時發生
  5. SyntaxError,解析時發生語法錯誤
  6. TypeError,當值不是所期待的類型時,null.f()也報這個錯
  7. URIError,當傳遞一個非法的URI給全局URI處理函數時發生,如decodeURIComponent('%'),即decodeURIComponentdecodeURI,encodeURIComponentencodeURI

三.動手寫自己的異常類型吧!

 關於在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."同步代碼"就是說無法獲取如setTimeoutPromise等非同步代碼的異常,也就是說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~)

  1. Chrome中對於跨域腳本所報的異常,雖然onerror能夠捕獲,但統一報Script Error。若要得到正確的錯誤信息,則要配置跨域資源共用CORS才可以。
  2. window.onerror實際上採用的事件冒泡的機制捕獲異常,並且在冒泡(bubble)階段時才觸發,因此像網路請求異常這些不會冒泡的異常是無法捕獲的。
  3. Promise.reject產生的未被catch的異常,window.onerror也是無能為力。

六.Promise.reject也拋異常,怎麼辦?

 通過Promise來處理複雜的非同步流程式控制制讓我們得心應手,但倘若其中出現異常或Promise實例狀態變為rejected時,會是怎樣一個狀況,我們又可以如何處理呢?

Promise是如何標識異常發生的?

 Promise實例的初始化狀態是pending,而發生異常時則為rejected,而導致狀態從pending轉變為rejected的操作有

  1. 調用Promise.reject類方法
  2. 在工廠方法中調用reject方法
  3. 在工廠方法或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

專屬於Promise的頂層異常處理

 若在異常發生前我們沒有調用catch方法來捕獲異常,還是可以通過windowunhandledrejection事件捕獲異常的

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)

 另外,還可以通過windowrejectionhandled事件監聽非同步註冊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才會觸發該事件。

七.404等網路請求異常真心要後之後覺嗎?

 也許我們都遇到<img src="./404.png">報404網路請求異常的情況,然後測試或用戶保障怎麼哪個哪個圖標沒有顯示。其實我們我們可以通過以下方式捕獲這類異常

window.addEventListener("error", function(e){
  // Do something
  console.log(e.bubbles) // 回顯false
}, true)

 由於網路請求異常不會冒泡,因此必須在capture階段捕獲才可以。但還有一個問題是這種方式無法精確判斷異常的HTTP狀態是404還是500等,因此還是要配合服務端日誌來排查分析才可以。

總結

 對異常和如何捕獲異常僅僅是前端智能監控中的一小撮知識點,敬請期待後續另一小撮知識點《前端魔法堂——調用棧,異常實例中的寶藏》吧:D
 尊重原創,轉載請註明來自:http://www.cnblogs.com/fsjohnhuang/p/7685144.html ^_^肥仔John


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 面向對象的基本特征 1.封裝性 封裝性就是把對象的屬性和服務結合成一個獨立的相同單位,並儘可能隱蔽對象的內部細節,包含兩個含義: ◇ 把對象的全部屬性和全部服務結合在一起,形成一個不可分割的獨立單位(即對象)。 ◇ 信息隱蔽,即儘可能隱蔽對象的內部細節,對外形成一個邊界〔或者說形成一道屏障〕,只保留 ...
  • 什麼是中介者模式? 在現實生活中,有很多中介者模式的身影,例如QQ游戲平臺,聊天室、QQ群、簡訊平臺和房產中介。不論是QQ游戲還是QQ群,它們都是充當一個中間平臺,QQ用戶可以登錄這個中間平臺與其他QQ用戶進行交流,如果沒有這些中間平臺,我們如果想與朋友進行聊天的話,可能就需要當面才可以了。電話、短 ...
  • 繼承:當前對象沒有的屬性和方法,別人有,拿來給自己用,就是繼承 1 混入式繼承 2.原型繼承 a) 給原型對象添加新成員(通過對象的動態特性),不是嚴格意義上的繼承 ,,,,實例對象繼承了原型 b) 直接替換原型對象 c) 利用混入的方式給原型對象添加成員 3.經典繼承 js var 對象1 = O ...
  • 運算符用於執行程式代碼運算,會針對一個及以上操作數項目來進行運算。2+3,其操作數是2和3,而運算符則是“+”。上一篇我們說過變數用來存儲數據,而同一個變數中的數據在不同的時刻可以不同,在程式的運行過程中,我們根據需要將數據進行相應規則的運算以得到預期的結果,運算符則是其中運算的規則。 運算符在js ...
  • 移動端用zepto做的頁面,突然發現on綁定的click事件並沒有觸發,代碼如下: 我把此寫法改成非事件委托的方式,發現該click事件就能觸發 最後$(document)換成$('body')或者$('ul')試試,實驗結果是: $(document) 不行 $(‘body') 不行 $(‘ul' ...
  • 本文是Javascript高級程式設計 第1章的筆記,主要介紹了: Javascript和ECMAScript的關係; 宿主環境; DOM和API的相關概念 ...
  • 要考慮函數可被可重覆使用(調用),需要將可變化的變為參數封裝起來 table載入成功寫的函數,是因為我自己需要才寫的。把table里的數據放在全局變數後,查詢詳細信息就不用再做ajax。這個'load-success.bs.table'api我還有個問題,當這個table被多次load-succes ...
  • 絕對地址和相對地址是網站開發基礎知識中很重要的知識點。1.在網頁中插入文件時,不可以使用硬碟的絕對路徑 舉個例子,在網頁中插入圖片,代碼如下: <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/ht ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...