一位初级进阶中级 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;
复制代码
相关文章
相关标签/搜索