JS数据类型判断及Object.prototype.toString.call( )的内部机制

95 阅读5分钟

typeof 运算符

typeof 是最常用的类型判断运算符,用于返回一个字符串,表示操作数的类型。

示例

console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" 
console.log(typeof true); // "boolean"
console.log(typeof 42); // "number"
console.log(typeof "Hello"); // "string"
console.log(typeof Symbol()); // "symbol"
console.log(typeof 123n); // "bigint"
console.log(typeof function(){}); // "function"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"

typeof 的问题

  • typeof null 返回 "object" 。 这是因为在 JavaScript 的早期版本中,一个值的类型信息是通过二进制表示的前几位来存储的。在这种表示法中,前几位为 000 的值被识别为对象类型。而 null 的二进制表示法是全零,因此它被误识别为对象类型。这种行为被认为是一个历史性的错误,但由于兼容性问题,这个错误一直保留至今。
  • 除了 function ,typeof 判断引用类型都会返回 "object"。 因为函数是一种特殊的对象,可以调用或实例化,在 ECMAScript 规范中,函数对象 Function Objects 有一个 [[ Call ]] 内置方法。而 typeof 进行判断时,会通过是否有 [[ Call ]] 内置方法来判断是否是函数,返回 "function"。

1720357582049-925636df-ae45-4cd5-b49a-7b2523b7abc2.webp

instanceof 运算符

instanceof 操作符通过判断一个对象是否属于某个构造函数的实例来进行类型判断。它会检查对象的原型链,判断原型链中是否有该构造函数的原型对象。

示例

let arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true

let func = function(){};
console.log(func instanceof Function); // true
console.log(func instanceof Object); // true

let date = new Date();
console.log(date instanceof Date); // true
console.log(date instanceof Object); // true

let regex = /regex/;
console.log(regex instanceof RegExp); // true
console.log(regex instanceof Object); // true

let map = new Map();
console.log(map instanceof Map); // true
console.log(map instanceof Object); // true

let set = new Set();
console.log(set instanceof Set); // true
console.log(set instanceof Object); // true

instanceof 的问题

instanceof 用于判断引用类型,而且实际上 instanceof 操作符不是准确判断对象的具体类型,而是判断对象是否属于某个构造函数的实例。

Object.prototype.toString.call()

使用 Object.prototype.toString.call() 方法可以获取更精确的类型信息。

示例

console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(42)); // "[object Number]"
console.log(Object.prototype.toString.call("Hello")); // "[object String]"
console.log(Object.prototype.toString.call(Symbol())); // "[object Symbol]"
console.log(Object.prototype.toString.call(123n)); // "[object BigInt]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
console.log(Object.prototype.toString.call(new RegExp(''))); // "[object RegExp]"
console.log(Object.prototype.toString.call(new Map())); // "[object Map]"
console.log(Object.prototype.toString.call(new Set())); // "[object Set]"
console.log(Object.prototype.toString.call(new WeakMap())); // "[object WeakMap]"
console.log(Object.prototype.toString.call(new WeakSet())); // "[object WeakSet]"

原理

Object.prototype.toString.call(val) 的意思就是用 val 调用 Object 原型对象上的 toString 方法

Function.prototype.call()

Function.prototype.call 是 JavaScript 中一个常用的方法,用于立即调用一个函数,并且可以为该函数指定 this 关键字的值和参数。 Function.prototype.call 的语法:

func.call(thisArg, arg1, arg2, ...)
  • func:要调用的函数。
  • thisArg:调用 func 时,this 关键字的值。
  • arg1, arg2, ...:调用 func 时的参数列表。
Object.prototype.toString()

Object.prototype.toString 内部会对调用者的值进行操作或判断,如图

1720354345410-81c437e0-b41e-4f8f-b39e-3fb6eb781363.webp 具体步骤:

  1. 如果方法调用者是 undefined,返回 "[object Undefined]"。
  2. 如果方法调用者是 null,返回 "[object Null]"。
  3. 将调用对象转换为对象类型,即 ToObject(this value),基本类型会被转为包装对象。
  4. 判断该对象是否是数组,如果是,builtinTag 记录为 "Array"。
  5. 如果对象具有 [[ParameterMap]] 内部槽,builtinTag 记录为 "Arguments"。
  6. 如果对象具有 [[Call]] 内部方法,则 builtinTag 记录为 "Function"。
  7. 如果对象具有 [[ErrorData]] 内部槽,builtinTag 记录为 "Error"。
  8. 如果对象具有 [[BooleanData]] 内部槽,builtinTag 记录为 "Boolean"。
  9. 如果对象具有 [[NumberData]] 内部槽,builtinTag 记录为 "Number"。
  10. 如果对象具有 [[StringData]] 内部槽,builtinTag 记录为 "String"。
  11. 如果对象具有 [[DateValue]] 内部槽,builtinTag 记录为 "Date"。
  12. 如果对象具有 [[RegExpMatcher]] 内部槽,builtinTag 记录为 "RegExp"。
  13. 如果以上情况均不符合,builtinTag 记录为 "Object"。
  14. 获取对象的 %Symbol.toStringTag% 属性作为 tag。
  15. 如果 tag 不是字符串类型(即不存在),将标签设为之前记录的的 builtinTag。
  16. 返回形如 "[object tag]" 的字符串。

简单理解就是,先判断特殊值 undefined 与 null,然后根据内部槽、内部方法或 Symbol.toStringTag 判断类型。那内部槽、内部方法或 Symbol.toStringTag 是什么?

内部槽、内部方法

内部槽(internal slots)和内部方法(internal methods)是 ECMAScript 规范中定义的概念,它们用于描述对象的内部状态和行为。这些概念对于实现 JavaScript 引擎至关重要,大部分在普通编程中并不直接可见。

可以理解为 JS 底层实现用到的属性和方法,大部分对开发者不可见。 部分对象中会有一个特殊的内部槽,通过此内部槽可以判断其类型,比如 String 的包装对象中具有 [[StringData]] 内部槽。

1720365632712-6e076179-e05f-4c23-afd6-6d57caf70bf7.webp

Symbol.toStringTag

并不是所有对象的类型都是通过内部槽判断的,具体来说一些早于 ES6 的对象是通过内部槽判断的,而 ES6 引入了 Symbol.toStringTag ,其值是一个字符串,用于创建对象的默认字符串描述,许多ES6 之后的内置对象,如 Map 和 Symbol ,其都有 Symbol.toStringTag 。Object.prototype.toString 内部会优先根据 Symbol.toStringTag 判断类型。

1720366659902-fde81487-2bc7-46e0-85f8-4f460a3f9628.webp

自定义 Symbol.toStringTag 属性

我们可以通过修改对象的 Symbol.toStringTag 属性来自定义对象的类型标签,从而更改 Object.prototype.toString() 的行为。 包括一些没有 Symbol.toStringTag 的对象,我们也可以设置 Symbol.toStringTag 属性,因为Object.prototype.toString 内部判断时 Symbol.toStringTag 的优先级比较高。

示例

// 自定义对象
let myObject = {
  [Symbol.toStringTag]: 'MyCustomObject'
};

// 使用 Object.prototype.toString 方法
console.log(Object.prototype.toString.call(myObject)); // [object MyCustomObject]

// 内置对象使用 Symbol.toStringTag
let myArray = [];
myArray[Symbol.toStringTag] = 'MyArray';
console.log(Object.prototype.toString.call(myArray)); // [object MyArray]

// 自定义类
class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyClassType';
  }
}

let myInstance = new MyClass();
console.log(Object.prototype.toString.call(myInstance)); // [object MyClassType]

其他

Array.isArray()

Array.isArray(arr); // 判断是否是数组