一位初級進階中級 JavaScript 工做者的自我修養(二)

前言

最近的前端面試已經的飛起了😂,從計算機原理、編譯原理、數據結構、算法、設計模式、編程範式編譯工具、格式工具、Git、NPM、單元測試、Nginx、PM二、CI / CD 瞭解和使用javascript

這隨便挑選一個部分,知識點均可以深刻挖掘,深不見底那種。css

前兩天發佈了 JS 基礎系列第一篇文章,獲得了同窗們比較好的反饋。❤️❤️❤️html

讓咱們繼續學習這個系列其餘有意思的內容,但願能夠給你們帶來一點點🤏幫助。前端

舒適提示:本文適用於前端入門的同窗和最近在準備想要系統化溫習 JS 基礎的朋友。已經工做多年的中高級前端大佬能夠直接跳過本文哈~java

相關文章:node

第四章 「重學 JavaScript」執行機制

1、try 和 finally

爲什麼 try 裏面放 return,finally 還會執行,理解其內部機制es6

1.1 Completion 類型

// return 執行了可是沒有當即返回,而是先執行了 finally
function kaimo() {
  try {
    return 0;
  } catch (err) {
    console.log(err);
  } finally {
    console.log("a");
  }
}

console.log(kaimo()); // a 0
複製代碼
// finally 中的 return 覆蓋了 try 中的 return。
function kaimo() {
  try {
    return 0;
  } catch (err) {
    console.log(err);
  } finally {
    return 1;
  }
}

console.log(kaimo()); // 1
複製代碼

Completion Record Completion Record 用於描述異常、跳出等語句執行過程。表示一個語句執行完以後的結果,它有三個字段。面試

[[type]]:表示完成的類型,有 break、continue、return、throw、normal 幾種類型正則表達式

[[value]]:表示語句的返回值,若是語句沒有,則是 empty算法

[[target]]:表示語句的目標,一般是一個 JavaScript 標籤

JavaScript 使用 Completion Record 類型,控制語句執行的過程。

1.2 普通語句

在 JavaScript 中,把不帶控制能力的語句稱爲普通語句。種類能夠參考引言的圖片。

一、這些語句在執行時,從前到後順次執行(這裏先忽略 var 和函數聲明的預處理機制),沒有任何分支或者重複執行邏輯。

二、普通語句執行後,會獲得 [[type]] 爲 normal 的 Completion Record,JavaScript 引擎遇到這樣的 Completion Record,會繼續執行下一條語句。

三、在 Chrome 控制檯輸入一個表達式,能夠獲得結果,可是在前面加上 var,就變成了 undefined。Chrome 控制檯顯示的正是語句的 Completion Record 的 [[value]]。

1.3 語句塊

語句塊就是拿大括號括起來的一組語句,它是一種語句的複合結構,能夠嵌套。

語句塊內部的語句的 Completion Record 的 [[type]] 若是不爲 normal,會打斷語句塊後續的語句執行。

1.3.1 內部爲普通語句的一個語句塊

// 在每一行的註釋中爲 Completion Record
{
  var i = 1; // normal, empty, empty
  i++; // normal, 1, empty
  console.log(i); //normal, undefined, empty
} // normal, undefined, empty
複製代碼

在這個 block 中都是 normal 類型的話,該程序會按順序執行。

1.3.2 加入 return

// 在每一行的註釋中爲 Completion Record
{
  var i = 1; // normal, empty, empty
  return i; // return, 1, empty
  i++;
  console.log(i);
} // return, 1, empty
複製代碼

在 block 中產生的非 normal 的完成類型能夠穿透複雜的語句嵌套結構,產生控制效果。

1.4 控制型語句

控制型語句帶有 if、switch 關鍵字,它們會對不一樣類型的 Completion Record 產生反應。

控制類語句分紅兩部分:

對其內部形成影響:如 if、switch、while/for、try。 對外部形成影響:如 break、continue、return、throw。

穿透就是去上一層的做用域或者控制語句找能夠消費 break,continue 的執行環境,消費就是在這一層就執行了這個 break 或者 continue

這兩類語句的配合,會產生控制代碼執行順序和執行邏輯的效果。

1.5 帶標籤的語句

一、任何 JavaScript 語句是能夠加標籤的,在語句前加冒號便可:。

firstStatement: var i = 1;
複製代碼

二、相似於註釋,基本沒有任何用處。惟一有做用的時候是:與完成記錄類型中的 target 相配合,用於跳出多層循環。

outer: while (true) {
  console.log("outer");
  inner: while (true) {
    console.log("inner1");
    break outer;
    console.log("inner2");
  }
}
console.log("finished");
// outer inner1 finished
複製代碼

2、宏任務和微任務

宏任務和微任務分別有哪些

宏任務主要有:script(總體代碼)、setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 環境)。

微任務主要有:Promise.then、 MutationObserver、 process.nextTick(Node.js 環境)。

3、異步編程

JavaScript 如何實現異步編程,能夠詳細描述 EventLoop 機制

在 js 中,任務分爲宏任務(macrotask)和微任務(microtask),這兩個任務分別維護一個隊列,均採用先進先出的策略進行執行!同步執行的任務都在宏任務上執行。

具體的操做步驟以下:

  • 從宏任務的頭部取出一個任務執行;
  • 執行過程當中若遇到微任務則將其添加到微任務的隊列中;
  • 宏任務執行完畢後,微任務的隊列中是否存在任務,若存在,則挨個兒出去執行,直到執行完畢;
  • GUI 渲染;
  • 回到步驟 1,直到宏任務執行完畢;

前 4 步構成了一個事件的循環檢測機制,即咱們所稱的 eventloop。

4、分析異步嵌套

能夠快速分析一個複雜的異步嵌套邏輯,並掌握分析方法

能夠先把複雜的異步寫法轉換爲簡單寫法。好比 async、await 異步的這種寫法,其原理就是回調函數。

而後按照事件的循環機制進行分析。

5、使用 Promise 實現串行

5.1 概述

最經常使用的隊列操做就是 Array.prototype.reduce()

let result = [1, 2, 5].reduce((accumulator, item) => {
  return accumulator + item;
}, 0); // <-- Our initial value.

console.log(result); // 8
複製代碼

最後一個值 0 是起始值,每次 reduce 返回的值都會做爲下次 reduce 回調函數的第一個參數,直到隊列循環完畢,所以能夠進行累加計算。

那麼將 reduce 的特性用在 Promise 試試:

function runPromiseByQueue(myPromise) {
  myPromise.reduce(
    (previousPromise, nextPromise) => previousPromise.then(() => nextPromise()),
    Promise.resolve()
  );
}
複製代碼

當上一個 Promise 開始執行(previousPromise.then),當其執行完畢後再調用下一個 Promise,並做爲一個新 Promise 返回,下次迭代就會繼續這個循環。

const createPromise = (time, id) => () =>
  new Promise(
    setTimeout(() => {
      console.log("promise", id);
      solve();
    }, time)
  );

runPromiseByQueue([
  createPromise(3000, 1),
  createPromise(2000, 2),
  createPromise(1000, 3),
]);
複製代碼

5.2 精讀

Reduce 是同步執行的,在一個事件循環就會完成,但這僅僅是在內存快速構造了 Promise 執行隊列,展開以下:

new Promise((resolve, reject) => {
  // Promise #1

  resolve();
})
  .then((result) => {
    // Promise #2

    return result;
  })
  .then((result) => {
    // Promise #3

    return result;
  }); // ... and so on!
複製代碼

Reduce 的做用就是在內存中生成這個隊列,而不須要把這個冗餘的隊列寫在代碼裏!

5.3 更簡單的方法

在 async/await 的支持下,runPromiseByQueue 函數能夠更爲簡化:

async function runPromiseByQueue(myPromises) {
  for (let value of myPromises) {
    await value();
  }
}
複製代碼

多虧了 async/await,代碼看起來如此簡潔明瞭。

不過要注意,這個思路與 reduce 思路不一樣之處在於,利用 reduce 的函數總體是個同步函數,本身先執行完畢構造 Promise 隊列,而後在內存異步執行;而利用 async/await 的函數是利用將本身改造爲一個異步函數,等待每個 Promise 執行完畢。

6、EventLoop

Node 與瀏覽器 EventLoop 的差別

6.1 與瀏覽器環境有何不一樣

在 node 中,事件循環表現出的狀態與瀏覽器中大體相同。不一樣的是 node 中有一套本身的模型。node 中事件循環的實現是依靠的 libuv 引擎。咱們知道 node 選擇 chrome v8 引擎做爲 js 解釋器,v8 引擎將 js 代碼分析後去調用對應的 node api,而這些 api 最後則由 libuv 引擎驅動,執行對應的任務,並把不一樣的事件放在不一樣的隊列中等待主線程執行。 所以實際上 node 中的事件循環存在於 libuv 引擎中。

6.2 事件循環模型

下面是一個 libuv 引擎中的事件循環的模型:

┌───────────────────────┐
┌─>│       timers          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │    I/O callbacks      │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │    idle, prepare      │
│  └──────────┬────────────┘ ┌───────────────┐
│  ┌──────────┴────────────┐ │ incoming:     │
│  │        poll           │<──connections───│
│  └──────────┬────────────┘ │ data, etc.    │
│  ┌──────────┴────────────┐ └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
複製代碼

注:模型中的每個方塊表明事件循環的一個階段

這個模型是 node 官網上的一篇文章中給出的,我下面的解釋也都來源於這篇文章。我會在文末把文章地址貼出來,有興趣的朋友能夠親自與看看原文。

6.3 事件循環各階段詳解

從上面這個模型中,咱們能夠大體分析出 node 中的事件循環的順序:

外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O 事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段...

以上各階段的名稱是根據我我的理解的翻譯,爲了不錯誤和歧義,下面解釋的時候會用英文來表示這些階段。

這些階段大體的功能以下:

  • timers: 這個階段執行定時器隊列中的回調如  setTimeout()  和  setInterval()。
  • I/O callbacks: 這個階段執行幾乎全部的回調。可是不包括 close 事件,定時器和 setImmediate()的回調。
  • idle, prepare: 這個階段僅在內部使用,能夠沒必要理會。
  • poll: 等待新的 I/O 事件,node 在一些特殊狀況下會阻塞在這裏。
  • check: setImmediate()的回調會在這個階段執行。
  • close callbacks: 例如 socket.on('close', ...)這種 close 事件的回調。

下面咱們來按照代碼第一次進入 libuv 引擎後的順序來詳細解說這些階段:

6.3.1 poll 階段

當個 v8 引擎將 js 代碼解析後傳入 libuv 引擎後,循環首先進入 poll 階段。poll 階段的執行邏輯以下: 先查看 poll queue 中是否有事件,有任務就按先進先出的順序依次執行回調。 當 queue 爲空時,會檢查是否有 setImmediate()的 callback,若是有就進入 check 階段執行這些 callback。但同時也會檢查是否有到期的 timer,若是有,就把這些到期的 timer 的 callback 按照調用順序放到 timer queue 中,以後循環會進入 timer 階段執行 queue 中的 callback。 這二者的順序是不固定的,收到代碼運行的環境的影響。若是二者的 queue 都是空的,那麼 loop 會在 poll 階段停留,直到有一個 i/o 事件返回,循環會進入 i/o callback 階段並當即執行這個事件的 callback。

值得注意的是,poll 階段在執行 poll queue 中的回調時實際上不會無限的執行下去。有兩種狀況 poll 階段會終止執行 poll queue 中的下一個回調:1.全部回調執行完畢。2.執行數超過了 node 的限制。

6.3.2 check 階段

check 階段專門用來執行 setImmediate()方法的回調,當 poll 階段進入空閒狀態,而且 setImmediate queue 中有 callback 時,事件循環進入這個階段。

6.3.3 close 階段

當一個 socket 鏈接或者一個 handle 被忽然關閉時(例如調用了 socket.destroy()方法),close 事件會被髮送到這個階段執行回調。不然事件會用 process.nextTick()方法發送出去。

6.3.4 timer 階段

這個階段以先進先出的方式執行全部到期的 timer 加入 timer 隊列裏的 callback,一個 timer callback 指得是一個經過 setTimeout 或者 setInterval 函數設置的回調函數。

6.3.5 I/O callback 階段

如上文所言,這個階段主要執行大部分 I/O 事件的回調,包括一些爲操做系統執行的回調。例如一個 TCP 鏈接生錯誤時,系統須要執行回調來得到這個錯誤的報告。

6.4 推遲任務執行的方法

在 node 中有三個經常使用的用來推遲任務執行的方法:process.nextTick,setTimeout(setInterval 與之相同)與 setImmediate

這三者間存在着一些很是不一樣的區別:

process.nextTick()

儘管沒有說起,可是實際上 node 中存在着一個特殊的隊列,即 nextTick queue。這個隊列中的回調執行雖然沒有被表示爲一個階段,當時這些事件卻會在每個階段執行完畢準備進入下一個階段時優先執行。當事件循環準備進入下一個階段以前,會先檢查 nextTick queue 中是否有任務,若是有,那麼會先清空這個隊列。與執行 poll queue 中的任務不一樣的是,這個操做在隊列清空前是不會中止的。這也就意味着,錯誤的使用 process.nextTick()方法會致使 node 進入一個死循環。。直到內存泄漏。

那麼合適使用這個方法比較合適呢?下面有一個例子:

const server = net.createServer(() => {}).listen(8080);

server.on("listening", () => {});
複製代碼

這個例子中當,當 listen 方法被調用時,除非端口被佔用,不然會馬上綁定在對應的端口上。這意味着此時這個端口能夠馬上觸發 listening 事件並執行其回調。然而,這時候 on('listening)尚未將 callback 設置好,天然沒有 callback 能夠執行。爲了不出現這種狀況,node 會在 listen 事件中使用 process.nextTick()方法,確保事件在回調函數綁定後被觸發。

setTimeout()和 setImmediate() 在三個方法中,這兩個方法最容易被弄混。實際上,某些狀況下這兩個方法的表現也很是類似。然而實際上,這兩個方法的意義卻大爲不一樣。

setTimeout()方法是定義一個回調,而且但願這個回調在咱們所指定的時間間隔後第一時間去執行。注意這個「第一時間執行」,這意味着,受到操做系統和當前執行任務的諸多影響,該回調並不會在咱們預期的時間間隔後精準的執行。執行的時間存在必定的延遲和偏差,這是不可避免的。node 會在能夠執行 timer 回調的第一時間去執行你所設定的任務。

setImmediate()方法從意義上將是馬上執行的意思,可是實際上它倒是在一個固定的階段纔會執行回調,即 poll 階段以後。有趣的是,這個名字的意義和以前提到過的 process.nextTick()方法纔是最匹配的。node 的開發者們也清楚這兩個方法的命名上存在必定的混淆,他們表示不會把這兩個方法的名字調換過來---由於有大量的 ndoe 程序使用着這兩個方法,調換命名所帶來的好處與它的影響相比不值一提。

setTimeout()和不設置時間間隔的 setImmediate()表現上及其類似。猜猜下面這段代碼的結果是什麼?

setTimeout(() => {
  console.log("timeout");
}, 0);

setImmediate(() => {
  console.log("immediate");
});
複製代碼

實際上,答案是不必定。沒錯,就連 node 的開發者都沒法準確的判斷這二者的順序誰前誰後。這取決於這段代碼的運行環境。運行環境中的各類複雜的狀況會致使在同步隊列裏兩個方法的順序隨機決定。可是,在一種狀況下能夠準確判斷兩個方法回調的執行順序,那就是在一個 I/O 事件的回調中。下面這段代碼的順序永遠是固定的:

const fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log("timeout");
  }, 0);
  setImmediate(() => {
    console.log("immediate");
  });
});
複製代碼

答案永遠是:

immediate

timeout

由於在 I/O 事件的回調中,setImmediate 方法的回調永遠在 timer 的回調前執行。

7、處理海量數據

如何在保證頁面運行流暢的狀況下處理海量數據

若是要在前端呈現大量的數據,通常的策略就是分頁。前端要呈現百萬數據,這個需求是不多見的,可是展現千條稍微複雜點的數據,這種需求仍是比較常見,只要內存夠,javascript 確定是吃得消的,計算幾千上萬條數據,js 效率根本不在話下,可是 DOM 的渲染瀏覽器扛不住,CPU 稍微搓點的電腦必然會卡爆。

策略:顯示三屏數據,其餘的移除 DOM。

7.1 策略

下面是我簡單勾畫的一個草圖,咱們把一串數據放到一個容器當中,這串數據的高度(Data List)確定是比 Container 的高度要高不少的,若是咱們一次性把數據都顯示出來,瀏覽器須要花費大量的時間來計算每一個 data 的位置,而且依次渲染出來,整個過程當中 JS 並無花費太多的時間,開銷主要是 DOM 渲染。

爲了解決這個問題,咱們讓數據是顯示一部分,這一部分是 Container 可視區域的內容,以及上下各一屏(一屏指的是 Container 高度所能容納的區域大小)的緩存內容。若是 Container 比較高,也但是隻緩存半屏,緩存的緣由是,在咱們滾動滾動條的時候,js 須要時間來拼湊字符串(或者建立 Node ),這個時候瀏覽器還來不及渲染,因此會出現臨時的空白,這種體驗是至關很差的。

7.2 Demo

<title>百萬數據前端快速流暢顯示</title>
<style type="text/css"> #box {position: relative; height: 300px; width: 200px; border:1px solid #CCC; overflow: auto} #box div { position: absolute; height: 20px; width: 100%; left: 0; overflow: hidden; font: 16px/20px Courier;} </style>

<div id="box"></div>

<script type="text/javascript"> var total = 1e5 , len = total , height = 300 , delta = 20 , num = height / delta , data = []; for(var i = 0; i < total; i++){ data.push({content: "item-" + i}); } var box = document.getElementById("box"); box.onscroll = function(){ var sTop = box.scrollTop||0 , first = parseInt(sTop / delta, 10) , start = Math.max(first - num, 0) , end = Math.min(first + num, len - 1) , i = 0; for(var s = start; s <= end; s++){ var child = box.children[s]; if(!box.contains(child) && s != len - 1){ insert(s); } } while(child = box.children[i++]){ var index = child.getAttribute("data-index"); if((index > end || index < start) && index != len - 1){ box.removeChild(child); } } }; function insert(i){ var div = document.createElement("div"); div.setAttribute("data-index", i); div.style.top = delta * i + "px"; div.appendChild(document.createTextNode(data[i].content)); box.appendChild(div); } box.onscroll(); insert(len - 1); </script>
複製代碼

7.3 算法說明

  • 計算 start 和 end 節點

    image Container 能夠容納的 Data 數目爲 num = height / delta,Container 頂部第一個節點的索引值爲

    var first = parseInt(Container.scrollTop / delta);
    複製代碼

    因爲咱們上下都有留出一屏,因此

    var start = Math.max(first - num, 0);
    var end = Math.min(first + num, len - 1);
    複製代碼
  • 插入節點

    經過上面的計算,從 start 到 end 將節點一次插入到 Container 中,而且將最後一個節點插入到 DOM 中。

    // 插入最後一個節點
    insert(len - 1);
    // 插入從 start 到 end 之間的節點
    for (var s = start; s <= end; s++) {
      var child = Container.children[s];
      // 若是 Container 中已經有該節點,或者該節點爲最後一個節點則跳過
      if (!Container.contains(child) && s != len - 1) {
        insert(s);
      }
    }
    複製代碼

    這裏解釋下爲何要插入最後一個節點,插入節點的方式是:

    function insert(i){
    var div = document.createElement("div");
    div.setAttribute("data-index", i);
    div.style.top = delta \* i + "px";
    div.appendChild(document.createTextNode(data[i].content));
    Container.appendChild(div);
    }
    複製代碼

    能夠看到咱們給插入的節點都加了一個 top 屬性,最後一個節點的 top 是最大的,只有把這個節點插入到 DOM 中,才能讓滾動條拉長,讓人感受放了不少的數據。

  • 刪除節點

    爲了減小瀏覽器的重排(reflow),咱們能夠隱藏三屏以外的數據。我這裏爲了方便,直接給刪除掉了,後續須要再從新插入。

    while ((child = Container.children[i++])) {
      var index = child.getAttribute("data-index");
      // 這裏記得不要把最後一個節點給刪除掉了
      if ((index > end || index < start) && index != len - 1) {
        Container.removeChild(child);
      }
    }
    複製代碼

    當 DOM 加載完畢以後,觸發一次 Container.onscroll(),而後整個程序就 OK 了。

第五章 「重學 JavaScript」語法和 API

1、ECMAScript 和 JavaScript

理解 ECMAScript 和 JavaScript 的關係

一個常見的問題是,ECMAScript 和 JavaScript 究竟是什麼關係?

要講清楚這個問題,須要回顧歷史。1996 年 11 月,JavaScript 的創造者 Netscape 公司,決定將 JavaScript 提交給標準化組織 ECMA,但願這種語言可以成爲國際標準。次年,ECMA 發佈 262 號標準文件(ECMA-262)的初版,規定了瀏覽器腳本語言的標準,並將這種語言稱爲 ECMAScript,這個版本就是 1.0 版。

該標準從一開始就是針對 JavaScript 語言制定的,可是之因此不叫 JavaScript,有兩個緣由。一是商標,Java 是 Sun 公司的商標,根據受權協議,只有 Netscape 公司能夠合法地使用 JavaScript 這個名字,且 JavaScript 自己也已經被 Netscape 公司註冊爲商標。二是想體現這門語言的制定者是 ECMA,不是 Netscape,這樣有利於保證這門語言的開放性和中立性。

所以,ECMAScript 和 JavaScript 的關係是,前者是後者的規格,後者是前者的一種實現(另外的 ECMAScript 方言還有 JScript 和 ActionScript)。平常場合,這兩個詞是能夠互換的。

2、ES6

熟練運用 es五、es6 提供的語法規範

JavaScript 教程

ECMAScript 6 入門

3、setInterval

setInterval 須要注意的點,使用 settimeout 實現 setInterval

3.1 setInterval 須要注意的點

在使用 setInterval 方法時,每一次啓動都須要對 setInterval 方法返回的值作一個判斷,判斷是不是空值,若不是空值,則要中止定時器並將值設爲空,再從新啓動,若是不進行判斷並賦值,有可能會形成計時器循環調用,在同等的時間內同時執行調用的代碼,並會隨着代碼的運行時間增長而增長,致使功能沒法實現,甚至佔用過多資源而卡死奔潰。所以在每一次使用 setInterval 方法時,都須要進行一次判斷。

let timer = setInterval(func, 1000);
// 在其餘地方再次用到setInterval(func, 1000)
if (timer !== null) {
  clearInterval(timer);
  timer = null;
}
timer = setInterval(func, 1000);
複製代碼

3.2 使用 settimeout 實現 setInterval

setIntervalFunc = () => {
  console.log(1); //使用遞歸
  setTimeout(setIntervalFunc, 1000);
};
setInterval();
複製代碼

4、正則表達式

JavaScript 提供的正則表達式 API、可使用正則表達式(郵箱校驗、URL 解析、去重等)解決常見問題

RegExp 對象

5、錯誤處理

JavaScript 異常處理的方式,統一的異常處理方案

當 JavaScript 引擎執行 JavaScript 代碼時,有可能會發生各類異常,例如是語法異常,語言中缺乏的功能,因爲來自服務器或用戶的異常輸出而致使的異常。

而 Javascript 引擎是單線程的,所以一旦遇到異常,Javascript 引擎一般會中止執行,阻塞後續代碼並拋出一個異常信息,所以對於可預見的異常,咱們應該捕捉並正確展現給用戶或開發者。

5.1 Error 對象

throw 和 Promise.reject() 能夠拋出字符串類型的異常,並且能夠拋出一個 Error 對象類型的異常。

一個 Error 對象類型的異常不只包含一個異常信息,同時也包含一個追溯棧這樣你就能夠很容易經過追溯棧找到代碼出錯的行數了。

因此推薦拋出 Error 對象類型的異常,而不是字符串類型的異常。

建立本身的異常構造函數

function MyError(message) {
  var instance = new Error(message);
  instance.name = "MyError";
  Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
  return instance;
}

MyError.prototype = Object.create(Error.prototype, {
  constructor: {
    value: MyError,
    enumerable: false,
    writable: true,
    configurable: true,
  },
});

if (Object.setPrototypeOf) {
  Object.setPrototypeOf(MyError, Error);
} else {
  MyError.__proto__ = Error;
}

export default MyError;
複製代碼

在代碼中拋出自定義的異常類型並捕捉

try {
  throw new MyError("some message");
} catch (e) {
  console.log(e.name + ":" + e.message);
}
複製代碼

5.2 Throw

throw expression;
複製代碼

throw 語句用來拋出一個用戶自定義的異常。當前函數的執行將被中止(throw 以後的語句將不會執行),而且控制將被傳遞到調用堆棧中的第一個 catch 塊。若是調用者函數中沒有 catch 塊,程序將會終止。

try {
  console.log("before throw error");
  throw new Error("throw error");
  console.log("after throw error");
} catch (err) {
  console.log(err.message);
}

// before throw error
// throw error
複製代碼

5.3 Try / Catch

try {
try_statements
}
[catch (exception) {
catch_statements
}][finally {
  finally_statements
}]
複製代碼

try/catch 主要用於捕捉異常。try/catch 語句包含了一個 try 塊, 和至少有一個 catch 塊或者一個 finally 塊,下面是三種形式的 try 聲明:

  • try...catch
  • try...finally
  • try...catch...finally

try 塊中放入可能會產生異常的語句或函數

catch 塊中包含要執行的語句,當 try 塊中拋出異常時,catch 塊會捕捉到這個異常信息,並執行 catch 塊中的代碼,若是在 try 塊中沒有異常拋出,這 catch 塊將會跳過。

finally 塊在 try 塊和 catch 塊以後執行。不管是否有異常拋出或着是否被捕獲它老是執行。當在 finally 塊中拋出異常信息時會覆蓋掉 try 塊中的異常信息。

try {
  try {
    throw new Error("can not find it1");
  } finally {
    throw new Error("can not find it2");
  }
} catch (err) {
  console.log(err.message);
}

// can not find it2
複製代碼

若是從 finally 塊中返回一個值,那麼這個值將會成爲整個 try-catch-finally 的返回值,不管是否有 return 語句在 try 和 catch 中。這包括在 catch 塊裏拋出的異常。

function test() {
  try {
    throw new Error("can not find it1");
    return 1;
  } catch (err) {
    throw new Error("can not find it2");
    return 2;
  } finally {
    return 3;
  }
}

console.log(test()); // 3
複製代碼

Try / Catch 性能

有一個你們衆所周知的反優化模式就是使用 try/catch。

在 V8(其餘 JS 引擎也可能出現相同狀況)函數中使用了 try/catch 語句不可以被 V8 編譯器優化。

5.4 window.onerror

經過在 window.onerror 上定義一個事件監聽函數,程序中其餘代碼產生的未被捕獲的異常每每就會被 window.onerror 上面註冊的監聽函數捕獲到。而且同時捕獲到一些關於異常的信息。

window.onerror = function(message, source, lineno, colno, error) {};
複製代碼
  • message:異常信息(字符串)
  • source:發生異常的腳本 URL(字符串)
  • lineno:發生異常的行號(數字)
  • colno:發生異常的列號(數字)
  • error:Error 對象(對象)

注意:Safari 和 IE10 還不支持在 window.onerror 的回調函數中使用第五個參數,也就是一個 Error 對象並帶有一個追溯棧

try/catch 不可以捕獲異步代碼中的異常,可是其將會把異常拋向全局而後 window.onerror 能夠將其捕獲。

try {
  setTimeout(() => {
    throw new Error("some message");
  }, 0);
} catch (err) {
  console.log(err);
}
// Uncaught Error: some message
複製代碼
window.onerror = (msg, url, line, col, err) => {
  console.log(err);
};
setTimeout(() => {
  throw new Error("some message");
}, 0);
// Error: some message
複製代碼

在 Chrome 中,window.onerror 可以檢測到從別的域引用的 script 文件中的異常,而且將這些異常標記爲 Script error。若是你不想處理這些從別的域引入的 script 文件,那麼能夠在程序中經過 Script error 標記將其過濾掉。然而,在 Firefox、Safari 或者 IE11 中,並不會引入跨域的 JS 異常,即便在 Chrome 中,若是使用 try/catch 將這些討厭的代碼包圍,那麼 Chrome 也不會再檢測到這些跨域異常。

在 Chrome 中,若是你想經過 window.onerror 來獲取到完整的跨域異常信息,那麼這些跨域資源必須提供合適的跨域頭信息。

5.5 Promise 中的異常

  • Promise 中拋出異常

    new Promise((resolve, reject) => {
      reject();
    });
    Promise.resolve().then((resolve, reject) => {
      reject();
    });
    Promise.reject();
    throw expression;
    複製代碼
  • Promise 中捕捉異常

    promiseObj.then(undefined, (err) => {
      catch_statements;
    });
    promiseObj.catch((exception) => {
      catch_statements;
    });
    複製代碼

    在 JavaScript 函數中,只有 return / yield / throw 會中斷函數的執行,其餘的都沒法阻止其運行到結束的。

    在 resolve / reject 以前加上 return 能阻止往下繼續運行。

    without return:

    Promise.resolve()
      .then(() => {
        console.log("before excute reject");
        reject(new Error("throw error"));
        console.log("after excute reject");
      })
      .catch((err) => {
        console.log(err.message);
      });
    
    // before excute reject
    // throw error
    // after excute reject
    複製代碼

    use return:

    Promise.resolve()
      .then(() => {
        console.log("before excute reject");
        return reject(new Error("throw error"));
        console.log("after excute reject");
      })
      .catch((err) => {
        console.log(err.message);
      });
    
    // before excute reject
    // throw error
    複製代碼
  • Throw or Reject

    不管是 try/catch 仍是 promise 都能捕獲到的是「同步」異常

    reject 是回調,而 throw 只是一個同步的語句,若是在另外一個異步的上下文中拋出,在當前上下文中是沒法捕獲到的。

    所以在 Promise 中使用 reject 拋出異常。不然 catch 有可能會捕捉不到。

    Promise.resolve()
      .then(() => {
        setTimeout(() => {
          throw new Error("throw error");
        }, 0);
      })
      .catch((err) => {
        console.log(err);
      });
    
    // Uncaught Error: throw error
    複製代碼
    Promise.resolve()
      .then(() => {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            reject(new Error("throw error"));
          }, 0);
        });
      })
      .catch((err) => {
        console.log(err);
      });
    
    // Error: throw error
    複製代碼

5.6 window.onunhandledrejection

window.onunhandledrejection 與 window.onerror 相似,在一個 JavaScript Promise 被 reject 可是沒有 catch 來捕捉這個 reject 時觸發。而且同時捕獲到一些關於異常的信息。

window.onunhandledrejection = (event) => {
  console.log(event.reason);
};
複製代碼

event 事件是 PromiseRejectionEvent 的實例,它有兩個屬性:

  • event.promise:被 rejected 的 JavaScript Promise
  • event.reason:一個值或 Object 代表爲何 promise 被 rejected,是 Promise.reject() 中的內容。

5.7 window.rejectionhandled

由於 Promise 能夠延後調用 catch 方法,若在拋出 reject 時未調用 catch 進行捕捉,但稍後再次調用 catch,此時會觸發 rejectionhandled 事件。

window.onrejectionhandled = (event) => {
  console.log("rejection handled");
};

let p = Promise.reject(new Error("throw error"));

setTimeout(() => {
  p.catch((e) => {
    console.log(e);
  });
}, 1000);

// Uncaught (in promise) Error: throw error
// 1 秒後輸出
// Error: throw error
// rejection handled
複製代碼

5.8 統一異常處理

代碼中拋出的異常,一種是要展現給用戶,一種是展現給開發者。

對於展現給用戶的異常,通常使用 alert 或 toast 展現;對於展現給開發者的異常,通常輸出到控制檯。

在一個函數或一個代碼塊中能夠把拋出的異常統一捕捉起來,按照不一樣的異常類型以不一樣的方式展現,對於。

須要點擊確認的異常類型:

ensureError.js

function EnsureError(message = "Default Message") {
  this.name = "EnsureError";
  this.message = message;
  this.stack = new Error().stack;
}
EnsureError.prototype = Object.create(Error.prototype);
EnsureError.prototype.constructor = EnsureError;

export default EnsureError;
複製代碼

彈窗提示的異常類型:

toastError.js

function ToastError(message = "Default Message") {
  this.name = "ToastError";
  this.message = message;
  this.stack = new Error().stack;
}
ToastError.prototype = Object.create(Error.prototype);
ToastError.prototype.constructor = ToastError;

export default ToastError;
複製代碼

提示開發者的異常類型:

devError.js

function DevError(message = "Default Message") {
  this.name = "ToastError";
  this.message = message;
  this.stack = new Error().stack;
}
DevError.prototype = Object.create(Error.prototype);
DevError.prototype.constructor = DevError;

export default DevError;
複製代碼

異常處理器:

拋出普通異常時,能夠帶上 stackoverflow 上問題的列表,方便開發者查找緣由。

errorHandler.js

import EnsureError from "./ensureError.js";
import ToastError from "./toastError.js";
import DevError from "./devError.js";
import EnsurePopup from "./ensurePopup.js";
import ToastPopup from "./toastPopup.js";

function errorHandler(err) {
  if (err instanceof EnsureError) {
    EnsurePopup(err.message);
  } else if (err instanceof ToastError) {
    ToastPopup(err.message);
  } else if (err instanceof DevError) {
    DevError(err.message);
  } else {
    error.message += `https://stackoverflow.com/questions?q=${encodeURI( error.message )}`;
    console.error(err.message);
  }
}

window.onerror = (msg, url, line, col, err) => {
  errorHandler(err);
};

window.onunhandledrejection = (event) => {
  errorHandler(event.reason);
};

export default errorHandler;
複製代碼
相關文章
相關標籤/搜索