1. typeof
typeof 是 Javascript 的一个操作符,可以表示未经计算的操作数的类型。
如下是摘录自 MDN 的 typeof 可能输出的值的列表:
| 类型 | 结果 |
|---|---|
| Undefined | "undefined" |
| Null | "object" |
| Boolean | "boolean" |
| Number | "number" |
| BigInt | "bigint" |
| String | "string" |
| Symbol (ECMAScript 2015 新增) | "symbol" |
| 宿主对象(由 JS 环境提供) | 取决于具体实现 |
| Function 对象 (按照 ECMA-262 规范实现 [[Call]]) | "function" |
| 其他任何对象 | "object" |
使用 typeof 时要特别注意以下两个非预想的结果:
typeof null输出为 “object”typeof NaN输出为 “number” (NaN:Not-A-Number)
此外 MDN 附加了以下 typeof 的诡异特性,在此进行摘录:
null
1 | // JavaScript 诞生以来便如此 |
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。(参考来源)
曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了。该提案会导致 typeof null === 'null'。
使用 new 操作符
1 | // 除 Function 外的所有构造函数的类型都是 'object' |
语法中的括号
1 | // 括号有无将决定表达式的类型。 |
正则表达式
对正则表达式字面量的类型判断在某些浏览器中不符合标准:
1 | typeof /s/ === 'function'; // Chrome 1-12 , 不符合 ECMAScript 5.1 |
错误
在 ECMAScript 2015 之前,typeof 总能保证对任何所给的操作数返回一个字符串。即便是没有声明的标识符,typeof 也能返回 'undefined'。使用 typeof 永远不会抛出错误。
但在加入了块级作用域的 let 和 const 之后,在其被声明之前对块中的 let 和 const 变量使用 typeof 会抛出一个 ReferenceError。块作用域变量在块的头部处于“暂存死区”,直至其被初始化,在这期间,访问变量将会引发错误。
1 | typeof undeclaredVariable === 'undefined'; |
例外
当前所有的浏览器都暴露了一个类型为 undefined 的非标准宿主对象 document.all。
1 | typeof document.all === 'undefined'; |
尽管规范允许为非标准的外来对象自定义类型标签,但它要求这些类型标签与已有的不同。document.all 的类型标签为 'undefined' 的例子在 Web 领域中被归类为对原 ECMA JavaScript 标准的“故意侵犯”。
2. instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,换句话说,instanceof 可以帮助我们来判断一个对象是否是否继承与另一个对象。
内部原理
其实这里可以看一下 instanceof 运算符代码:
1 | function instance_of(L, R) {//L 表示左表达式,R 表示右表达式 |
可以很容易发现,instance_of 方法取出了右边对象 R 的 prototype 属性,然后使用了 while 循环一层一层的去调出左边对象 L 的 __proto__ 隐式原型,按照原型链的调用规则,如果 L 继承与 R ,那 L 在某一层的隐式原型一定与 R 的显示原型完全相等。
举个例子:
1 | console.log(Number instanceof Number);//false |
上面第一组的解析我们很容易就能明白,因为一个对象本身的隐式原型与其显示原型不相等,那么肯定返回 false;第二组,Foo 函数构造于 Function 这是一个很标准的原型继承;而第三组似乎有些特殊,但是仔细看一下原型继承图我们就很容易看明白,对于 Object 来说 Object.__proto__.proto__ === Object.prototype,对于 Function 来说 Function.__proto__ === Function.prototype。
特殊情况
对于字面量而非对象的元素,不可以使用 instanceof 来判断类型,因为其本身并非是 Object,因此会出现如下的情况:
1 | let num = 123 |
1 | let num = new Number(123) |
但有趣的一点是,我们直接对比变量的隐式原型,其指向的正是这些内置对象的显式原型:
1 | let str = "string" |
按照 instance 的判断规则,可以判断出 str instanceof String 返回结果是 true,因此说明其底层对字面量元素进行了屏蔽,使其直接返回了 false 。因此对于字面量的 Number 或者 String,只能使用 typeof 来判断或者 Object.prototype.toString() 来判断。
3.Object.prototype.toString()
被改写的 toString()
由于 Object 对象的原型上挂载了一个 toString() 方法,因此根据原型链的调用规则,在 Javascript 中每个对象都可以调用 toString() 方法,其本身原意为返回一个表示该对象的字符串。
我们可以通过创建一个对象来调用该方法:
1 | let obj = new Object() |
但是当我们调用 Number 类型或者 Array 类型时,其结果为:
1 | let num = 123 |
这是由于在 Number.prototype 与 Array.prototype 上已经改写了 toString() 方法,123.toString() 执行的其实是 Number.prototype.toString() 而并非 Object.prototype.toString()。对于大部分的 Javascript 内建类型来说,都改写了 toString() 方法,用户自行创建的构造函数也可以通过在 prototype 上挂载 toString() 方法达到改写的目的。以下的表格列举了常见的类型对象调用 toString() 方法所输出的结果:
| 数据类型 | 例子 | return |
|---|---|---|
| 字符串 | “foo”.toString() | “foo” |
| 数字 | 1.toString() | Uncaught SyntaxError: Invalid or unexpected token |
| 布尔值 | false.toString() | “false” |
| undefined | undefined.toString() | Uncaught TypeError: Cannot read property ‘toString’ of undefined |
| null | null.toString() | Uncaught TypeError: Cannot read property ‘toString’ of null |
| String | String.toString() | “function String() { [native code] }” |
| Number | Number.toString() | “function Number() { [native code] }” |
| Boolean | Boolean.toString() | “function Boolean() { [native code] }” |
| Array | Array.toString() | “function Array() { [native code] }” |
| Function | Function.toString() | “function Function() { [native code] }” |
| Date | Date.toString() | “function Date() { [native code] }” |
| RegExp | RegExp.toString() | “function RegExp() { [native code] }” |
| Error | Error.toString() | “function Error() { [native code] }” |
| Promise | Promise.toString() | “function Promise() { [native code] }” |
| Obejct | Object.toString() | “function Object() { [native code] }” |
| Math | Math.toString() | “[object Math]“ |
那如果我们想强制让某一对象调用 Object.prototype.toString() 方法会发生什么呢?我们使用 call 来改写方法中的 this 可以达到这一效果:
1 | let num = 123 |
可以发现通过借助 Object.prototype.toString() 我们可以获取到调用对象的类型,这一点非常有用,可以帮助我们接下来进行类型判断。
内部原理
在编写类型判断的方法之前,我们不妨来看一下 Object.prototype.toString 到底做了什么,在不同的 ES 版本中,该方法会有一定的区别:
ES5 环境下:
- 如果this的值为undefined,则返回
"[object Undefined]". - 如果this的值为null,则返回
"[object Null]". - 让O成为调用ToObject(this)的结果.
- 让class成为O的内部属性[[Class]]的值.
- 返回三个字符串“[object “, class, 以及 “]“连接后的新字符串.
ES6 环境下:
- 如果this的值为undefined,则返回
"[object Undefined]". - 如果this的值为null,则返回
"[object Null]". - 让O成为调用ToObject(this)的结果.
- 如果O有[[NativeBrand]]内部属性,让tag成为表29中对应的值.
- 否则
- 让hasTag成为调用O的[[HasProperty]]内部方法后的结果,参数为@@toStringTag.
- 如果hasTag为false,则让tag为
"Object". - 否则,
- 让tag成为调用O的[[Get]]内部方法后的结果,参数为@@toStringTag.
- 如果tag是一个abrupt completion,则让tag成为NormalCompletion(
"???"). - 让tag成为tag.[[value]].
- 如果Type(tag)不是字符串,则让tag成为
"???". - 如果tag的值为
"Arguments","Array","Boolean","Date","Error","Function","JSON","Math","Number","Object","RegExp",或者"String"中的任一个,则让tag成为字符串"~"和tag当前的值连接后的结果.
- 返回三个字符串”[object “, tag, and “]“连接后的新字符串.
封装一个类型判断的方法
1 | function type (data){ |