三、JavaScript专题之类型判断(上)

在项目开发中,我们经常会进行类型判断,这里对JS类型判断的方式进行一下总结。

MDN中根据最新的 ECMAScript 标准定义了 8 种数据类型:

基本类型(7种):String, Number, Boolean, null, undefined, Bigint, Symbol。
引用类型(1种):Object
下面我们来对这些类型进行检测。

typeof

语法

typeof 操作符的唯一目的就是检查数据类型,但是如果我们希望检查任何从 Object 派生出来的结构类型,使用 typeof 是不起作用的,因为总是会得到 “object”。

类型 结果
undefined “undefined”
Boolean “boolean”
Number “number”
String “string”
BigInt “bigint”
Symbol “symbol”
null “object”
Function “function”
其他任何对象 “object”

注意:

  • typeof Function: 结果是”function”,因为Function是一个构造函数,可以通过new操作符来构造对象
  • typeof null:结果是”object”,null是一个只有一个值的特殊类型。表示一个空对象引用。
  • typeof无法区分各种内置的对象,如Array, Date等。

综上,typeof操作符能能返回的类型:

  • string number boolean undefined symbol bigint
  • object 、(typeof null === ‘object’)
  • function

原理

JS是动态类型的变量,每个变量在存储时除了存储变量值外,还需要存储变量的类型。JS里使用32位(bit)存储变量信息。低位的1~3个bit存储变量类型信息,叫做类型标签(type tag)

1
2
3
4
5
.... XXXX X000 // object
.... XXXX XXX1 // int
.... XXXX X010 // double
.... XXXX X100 // string
.... XXXX X110 // boolean
  • 只有int类型的type tag使用1个bit,并且取值为1,其他都是3个bit, 并且低位为0。这样可以通过type tag低位取值判断是否为int数据;
  • 为了区分int,还剩下2个bit,相当于使用2个bit区分这四个类型:object, double, string, boolean;
  • 但是null,undefined和Function并没有分配type tag。

如何识别Function

函数并没有单独的type tag,因为函数也是对象。typeof内部判断如果一个对象实现了[[call]]内部方法则认为是函数。

如何识别undefined

undefined变量存储的是个特殊值JSVAL_VOID(0-2^30),typeof内部判断如果一个变量存储的是这个特殊值,则认为是undefined。

1
#define JSVAL_VOID INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))

如何识别null

null变量存储的也是个特殊值JSVAL_NULL,并且恰巧取值是空指针机器码(0),正好低位bit的值跟对象的type tag是一样的,这也是 typeof null === ‘object’ 的原因。

Object.prototype.toString

语法

前面已经知道typeof Object 只会返回’object’,无法区分 Object 下细分的类型呐,如 Array、Function、Date、RegExp、Error 等。
所以我们一般使用Object.prototype.toString区分各种内置对象。

1
2
3
4
5
6
7
8
console.log(Object.prototype.toString.call(1)); // [object Number],隐式类型转换
console.log(Object.prototype.toString.call('')); // [object String],隐式类型转换
console.log(Object.prototype.toString.call(null)); // [object Null],特殊处理
console.log(Object.prototype.toString.call(undefined)); // [object Undefined],特殊处理
console.log(Object.prototype.toString.call(true)); // [object Boolean],隐式类型转换
console.log(Object.prototype.toString.call( {})); // [object Object]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
  1. 如果实参是个基本类型,会自动转成对应的引用类型;

    Object.prototype.toString不能区分基本类型的,只是用于区分各种对象;

  2. null和undefined不存在对应的引用类型,内部特殊处理了;

原理

每个对象都有个内部属性[[Class]],内置对象的[[Class]]的值都是不同的(”Arguments”, “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, “String”),并且目前[[Class]]属性值只能通过Object.prototype.toString访问。

Symbol.toStringTag属性

其实Object.prototype.toString内部先访问对象的Symbol.toStringTag属性值拼接返回值的。

1
2
3
4
5
6
7
8
9
10
11
var a = "hello"
console.log(Object.prototype.toString.call(a)); // "[object String]"

// 修改Symbol.toStringTag值
Object.defineProperty(String.prototype, Symbol.toStringTag, {
get() {
return 'MyString'
}
})

console.log(Object.prototype.toString.call(a)); // "[object MyString]"

所以不要轻易修改内置对象的Symbol.toStringTag属性值,否则Object.prototype.toString就不能正常使用。

Object.prototype.toString内部逻辑

ECMA-262规范中,第19.1.3.6 Object.prototype.toString ( )条介绍:

When the toString toString method is called, the following steps are taken:

  1. If the this value is undefined, return “[object Undefined]”.
  2. If the this value is null, return “[object Null]”.
  3. Let O be ToObject(this value).
  4. Let isArray be IsArray(O).
  5. ReturnIfAbrupt(isArray).
  6. If isArray is true, let builtinTag be “Array”.
  7. Else, if O is an exotic String object, let builtinTag be “String”.
  8. Else, if O has an [[ParameterMap]] internal slot, let builtinTag be “Arguments”.
  9. Else, if O has a [[Call]] internal method, let builtinTag be “Function”.
  10. Else, if O has an [[ErrorData]] internal slot, let builtinTag be “Error”.
  11. Else, if O has a [[BooleanData]] internal slot, let builtinTag be “Boolean”.
  12. Else, if O has a [[NumberData]] internal slot, let builtinTag be “Number”.
  13. Else, if O has a [[DateValue]] internal slot, let builtinTag be “Date”.
  14. Else, if O has a [[RegExpMatcher]] internal slot, let builtinTag be “RegExp”.
  15. Else, let builtinTag be “Object”.
  16. Let tag be Get (O, @@toStringTag).
  17. ReturnIfAbrupt(tag).
  18. If Type(tag) is not String, let tag be builtinTag.
  19. Return the String that is the result of concatenating “[object “, tag, and “]”.

Instanceof

语法

1
object instanceof constructorFunc

instanceof 操作符判断构造函数constructorFunc的prototype属性是否在对象object的原型链上。

1
2
3
4
5
6
Object.create({}) instanceof Object // true
Object.create(null) instanceof Object // false

Function instanceof Object // true
Function instanceof Function // true
Object instanceof Object // true
  1. 作为类型判断的一种方式,instanceof 操作符不会对变量object进行隐式类型转换
    1
    2
    "" instanceof String; // false,基本类型不会转成对象
    new String('') instanceof String; // true
  2. 对于没有原型的对象或则基本类型直接返回false
    1
    2
    1 instanceof Object // false
    Object.create(null) instanceof Object // false
  3. constructorFunc必须是个对象。并且大部分情况要求是个构造函数(即要具有prototype属性)
    1
    2
    3
    4
    5
    6
    7
    8
    // TypeError: Right-hand side of 'instanceof' is not an object
    1 instanceof 1

    // TypeError: Right-hand side of 'instanceof' is not callable
    1 instanceof ({})

    // TypeError: Function has non-object prototype 'undefined' in instanceof check
    ({}) instanceof (() => {})

原理

参照ECMA:

1
2
3
4
5
6
1. 如果constructorFunc不是个对象,或则是null,直接抛TypeError异常;
2. 如果constructorFunc[Symbole.hasInstance]方法,则返回!!constructorFunc[Symbole.hasInstance](object )
3. 如果constructorFunc不是函数,直接抛TypeError异常;
4. 遍历object的原型链,逐个跟constructorFunc.prototype属性比较:
- 如果object没有原型,则直接返回false;
- 如果constructorFunc.prototype不是对象,则直接抛TypeError异常。

内置的类型判断方法

Array.isArray

ES5引入了方法Array.isArray专门用于数组类型判断。Object.prototype.toString和instanceof都不够严格

1
2
3
4
5
6
7
8
9
10
11
12
var arr = []
Object.defineProperty(Array.prototype, Symbol.toStringTag, {
get() {
return 'myArray'
}
})

console.log(Object.prototype.toString.call(arr)); // [object myArray]
console.log(Array.isArray(arr)); // true

console.log(Array.prototype instanceof Array); // false
console.log(Array.isArray(Array.prototype)); // true

不过现实情况下基本都是利用Object.prototype.toString作为Array.isArray的polyfill:

1
2
3
4
5
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

参考文献