JS 中原生错误类型总结

Hi 你们好,我是张小猪。此次咱们来聊聊 JS 中的那些原生错误类型。git

初衷

可能会有小伙伴好奇,为何小猪要写这么一篇文章呢?这件事情说来话长了。github

小猪的从业时间并不长,四舍五入也就刚毕业(哈哈哈,永远 22 岁)。不过坦白说,以前在一些不一样的地方,小猪时常见到一些明明能够给出更明确的错误类型,不过都无论三七二十一统统 throw new Error("xxxx") 或者 throw "xxxx" 这样的代码。就像江南皮革厂的那些统统 20 块同样,印象深入的刻进了 DNA 里(不要什么奇怪的东西都往 DNA 里刻啊喂shell

并且,这也让我想到了另一个时不时就碰见的情况,那就是无论四七二十八,全部 API 统统返回 200 OK。反正先 200 了再说,而后 body 里再给个鉴权失败的消息。什么?还有除了 200 之外的状态码?不存在的!json

每次遇到这样的 API,小猪都是黑人问号脸...EXM?王德发?不过那都是另一个故事了。咱们仍是说回今天的主题吧,JS 中原生的错误类型。segmentfault

原生错误类型

如上文提到,在 JS 中存在着一个全局对象 Error。因为它是继承自 Function,因此小伙伴们的那些对于函数的骚操做均可以对它使用。不过它更常见的仍是用做构造函数,产生错误实例,例如:浏览器

try {
  throw new Error('小猪才是最萌哒!')
} catch (e) {
  console.error('本题正解:' + e.message)
}

在错误实例中,咱们常常访问的通用属性有 namemessage,其中 name 表示这个错误类型的名字,而 message 则是错误信息。除此以外,还有诸如 fileNamelineNumbercolumnNumberstack 这些常见的也很容易理解的属性。还有就是一些,不一样浏览器本身实现的属性,这里就不作过多的介绍啦。下面举个简单的栗子:函数

try {
  throw new Error('你猜')
} catch (e) {
  console.error(`错误类型名称:${e.name} 错误信息:${e.message}`)
}

最后须要说的一点是,咱们接下来要介绍的几种原生的错误类型,其实都是来自于 Error 这个基类。那么,咱们就按照字母序来逐个说明吧。测试

EvalError

一上来就遇到一个不常见的错误类型。不过不用担忧,其实咱们能够忽略这个类型。优化

EvalError 这个错误类型原本的做用是用来表示在全局 eval 函数中产生的错误,不过如今的 JS 引擎都不会再抛出这个错误了。在 ECMAScript 规范里,也说明了保留这个对象只是为了向前兼容而已。this

RangeError

RangeError 这个错误类型自己是表示一个值超出了它能够被容许的范围。这个值有多是一个枚举值,超出了集合范围;也有多是一个数值,超出了有效范围。这里针对以上两种状况分别举一个栗子:

try {
  String.prototype.normalize.call('\u0061', 'HELLO')
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // RangeError: The normalization form should be one of NFC, NFD, NFKC, NFKD.
}

try {
  const arr = new Uint8Array(233 ** 33)
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // RangeError: Invalid typed array length: 1.3266164977310088e+78
}

固然,咱们也能够根据实际需求,自行抛出这个错误。例如这里例子中用一个 checkNum 方法来检测数字是否在咱们须要的范围内:

function checkNum(num) {
  if (num < -500 || num > 500) {
    throw new RangeError("The argument must be between -500 and 500.")
  }
  // Your logic
}

try {
  check(2000)
} catch(e) {
  if (e instanceof RangeError) {
    // Handle the error
  }
}

ReferenceError

ReferenceError 这个错误类型自己表示检测到了一个无效的引用,最多见的状况就是尝试访问一个不存在的变量。例如:

try {
  const value = undefinedVariable
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // ReferenceError: undefinedVariable is not defined
}

不过这里须要注意的是,一个变量的值是 undefined 与这个变量不存在是不同的。咱们能够看下面这个栗子:

try {
  const undefinedVariable = undefined;
  const value = undefinedVariable;
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // Won't occur
}

这里背后具体的缘由会和咱们 JS 标准中的两个概念有关,一个是执行上下文(Execution Context),一个是环境记录(Environment Record)。这里小猪并不打算展开来讲这两个概念,只是先作一个简单的比喻便于理解。

function intro() {
  const name = '张小猪'
  console.log(name)
}
intro()

当咱们执行上面这样的代码的时候,咱们能够想象为在 intro 函数里面有一个咱们看不见的对象,它用键值对的形式记录着咱们申明的变量。当前它的状态多是:

{
  "name": "张小猪"
}

因此咱们接下来访问 name 变量的时候能够正常的找到它的值。而后咱们把代码再变为下面这样:

function intro() {
  const name = '张小猪'
  const age = undefined
  console.log(name, age)
  console.log(hobbies)
}
try {
  intro()
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
}

这时候咱们的这个看不见的对象能够想象为:

{
  "name": "张小猪",
  "age": undefined
}

因为并无一个名叫 hobbies 的键,因此咱们会获得下面这个结果:

张小猪 undefined
ReferenceError: hobbies is not defined

SyntaxError

SyntaxError 这个错误类型自己表示 JS 引擎在处理代码的时候碰见了非法的 JS 代码。为了更容易的可以明白这个场景,咱们先简单的描述一下 JS 引擎的解析处理过程吧。

大致上看,这个过程能够分红 3 个部分:

  1. 词法分析
  2. 语法分析
  3. 解释执行

词法分析

其中第一个过程 - 词法分析,简单的说就是把咱们输入的代码字符串转换为一个个的 token,获得的结果以供下一步语法分析使用。这里一个个的 token 能够理解为就是一个个的小单元,咱们仍是结合一个具体的栗子吧。

例如对于 a = (2 + b 这个字符串,咱们能够获得这样一些小单元:

  • 标识符 a
  • 赋值运算符
  • 开括号
  • 数字 2
  • 加法运算符
  • 标识符 b

这里其实能够注意到,咱们若是人为的看这串代码,会明显的发现缺乏了闭合括号。不过在进行词法分析的时候,只是按照规则拆分和标记 token,并不会作语法相关的处理。

语法分析

第二个过程 - 语法分析,简单的说就是基于上一步获得的 token,按照 JS 的语法规则进行判断和分析,并生成抽象语法树(AST)之类的东西。若是看回上面的那个栗子的话,因为缺乏闭合括号,因此是不符合 JS 的语法规范的。这时候 JS 引擎就会抛出一个 SyntaxError 的实例了。至于什么是 AST,咱们等到之后的相关专题再来聊吧,这里就不展开啦。

解释执行

这个过程简单的理解就是让咱们的代码跑起来。不过因为 JS 是解释型语言,因此这一步会须要借助解释器来不停的解释咱们的代码,并翻译成字节码以供后续执行。固然这中间还存在着比较复杂的过程并伴随着大量的优化。

报错举例

简单介绍完这 3 个过程后,咱们能够发现,SyntaxError 一般会来自于前两个过程。那么咱们仍是举几个报错的栗子吧:

try {
  const a b = 3
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // Uncaught SyntaxError: Missing initializer in const declaration
}
try {
  const a = 1 * % 2
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // Uncaught SyntaxError: Unexpected token '%'
}

TypeError

TypeError 这个错误类型自己表示由于一个变量或者参数并非合法的或者符合指望的类型,从而致使咱们的操做行为没法成功的完成。一般可能会有如下几种状况:

  • 咱们给一个函数或者一个操做符传递了一个不符合预期类型的值
  • 尝试修改一个不可更改的变量
  • 尝试对一个值作一些不符合它类型的事情

这里仍是举几个栗子更直观一点:

try {
  const a = 1
  a = 2
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // TypeError: Assignment to constant variable.
}
try {
  const a = 1;
  a();
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // TypeError: a is not a function
}

URIError

URIError 这个错误类型自己表示全局的 URI 处理函数在使用中产生了问题。例如当咱们传入了一些非法参数的时候,就会抛出这个错误。这里列举几个咱们经常使用的全局 URI 处理函数:

  • encodeURI
  • decodeURI
  • encodeURIComponent
  • decodeURIComponent

一样咱们举个会报错的栗子:

try {
  decodeURI('abc%abc')
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // URIError: URI malformed
}

自定义错误类型

上面我介绍了各类原生的错误类型它们自己的含义。固然,在具体开发的时候,咱们也能够根据自行的实际需求抛出这些错误。不过它们终究仍是没法表明全部的错误,难道对于其它的错误咱们都只能够用基础的 Error 类型了么?

固然不是的,咱们其实能够按照自身的需求建立一些自定义的错误类型。这里咱们就基于 ES2015+ 的 class 语法来作一个实现吧:

class SoundError extends Error {
  constructor(yell = 'bark', vol = 10, ...params) {
    super(...params);
    this.name = 'SoundError';
    this.voice = yell;
    this.volume = vol;
  }
}

try {
  throw new SoundError('嘤嘤嘤', 1, '小拳拳锤你胸口');
} catch (e) {
  console.error(`${e.name}: ${e.voice} at volume ${e.volume} since ${e.message}`);
  // SoundError: 嘤嘤嘤 at volume 1 since 小拳拳锤你胸口
}

应用

错误信息的应用场景,天然是须要抛出错误的地方啦(不要打我...

不太小猪我的感受是,对于须要给其余人使用或者维护的代码,若是咱们能把报错信息作的比较清楚易懂,对于使用者和维护者都是很舒服的事情。除了这种体验上的感觉外,在另一些具体的场景,小猪也常用到各类错误类型。

首先是,自己在咱们本身的代码中,涉及到捕获错误的地方,就能够根据错误类型来作针对的逻辑处理。

再好比,自动化测试。一方面是,在测试用例中,咱们可能须要根据不一样的错误类型进行针对的判断和处理;另外一方面,咱们也能够方便的维护一份公用的错误信息,用以同时给代码中的断言、测试用例中的判断等须要的地方来使用。

又或者,咱们若是作一些错误上报收集之类的事情,也能够根据不一样错误的类型进行对应的数据获取、序列化与反序列化、展现等等。

总结

上面依次介绍了 JS 中的原生错误类型,以及咱们如何自定义错误类型。其中 ReferenceErrorSyntaxError 稍微作了一点小扩展。

最后,小小的说了一下应用场景。固然,我也只是举了几个栗子,核心仍是那句话,须要抛出错误的地方,尽可能抛出相对准确和清晰的错误就好啦。

但愿能帮助到有须要的小伙伴。若是你以为不错的话,不要忘记三连支持小猪哦。小猪爱大家哟~

相关连接

qrcode_green.jpeg

相关文章
相关标签/搜索