JS `Object.prototype.toString.call()` 的精确类型判断原理

大家好!今天我们来聊聊 Object.prototype.toString.call() 这位“类型判断大师”的独门秘籍!

要说 JavaScript 里类型判断,那真是一场“雾里看花”的游戏。typeof 有时候靠不住,instanceof 又容易认错干爹,直到 Object.prototype.toString.call() 出场,才算有了个相对靠谱的裁判。

我们先来热个身,回顾一下 JavaScript 里都有哪些类型:

类型 描述 例子
Undefined 表示未定义,声明了变量但未赋值。 let a;
Null 表示空值,是一个只有一个值的特殊类型。 let b = null;
Boolean 表示真或假。 let c = true;
Number 表示数字,包括整数和浮点数。 let d = 123;
String 表示字符串。 let e = "hello";
Symbol ES6 新增,表示独一无二的值。 let f = Symbol();
BigInt ES2020 新增,表示任意精度的整数。 let g = 123n;
Object 表示对象,包括普通对象、数组、函数等。 let h = {};

为什么 typeof 不靠谱?

typeof 虽然简单好用,但它有几个致命的缺陷:

  1. null 的误判: typeof null 返回 "object",这简直是 JavaScript 历史遗留问题,没办法。

  2. 对所有对象类型的简化: 除了函数,typeof 对所有对象类型都返回 "object"。比如数组、Date 对象、RegExp 对象,统统都是 "object"。这让我们很难区分它们。

console.log(typeof null); // "object"
console.log(typeof []);   // "object"
console.log(typeof {});   // "object"
console.log(typeof new Date()); // "object"
console.log(typeof function(){}); // "function"

instanceof 的局限性

instanceof 用来判断一个对象是否是某个构造函数的实例。它通过原型链来查找,如果对象的原型链上存在该构造函数的 prototype,就返回 true

但是,instanceof 有两个问题:

  1. 多框架问题: 在有多个框架的页面中,不同的框架有自己的构造函数和原型链,instanceof 可能会出错。

  2. 原型链修改问题: 如果原型链被修改过,instanceof 的结果可能不准确。

let arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true  // 因为 Array.prototype 继承自 Object.prototype

function MyArray() {}
MyArray.prototype = []; // 故意修改原型链
let myArray = new MyArray();
console.log(myArray instanceof Array);   // true  // 看起来像是 Array 的实例
console.log(myArray instanceof MyArray); // false // 但实际上不是 MyArray 的实例

Object.prototype.toString.call() 的原理

Object.prototype.toString 方法原本是用来返回对象的字符串表示的。比如 ({}).toString() 会返回 "[object Object]"。 关键在于,这个方法内部会根据 this 的指向来确定返回的类型信息。

重点来了: call() 方法可以改变 this 的指向。 所以,我们可以用 call() 方法把 Object.prototype.toString 方法的 this 指向我们要判断类型的变量。 这样,Object.prototype.toString 就能返回该变量的准确类型信息了。

具体来说,Object.prototype.toString.call(value) 会返回一个形如 "[object Type]" 的字符串,其中 Type 就是 value 的类型。

console.log(Object.prototype.toString.call(null));        // "[object Null]"
console.log(Object.prototype.toString.call(undefined));   // "[object Undefined]"
console.log(Object.prototype.toString.call(123));         // "[object Number]"
console.log(Object.prototype.toString.call("hello"));       // "[object String]"
console.log(Object.prototype.toString.call(true));        // "[object Boolean]"
console.log(Object.prototype.toString.call([]));          // "[object Array]"
console.log(Object.prototype.toString.call({}));          // "[object Object]"
console.log(Object.prototype.toString.call(new Date()));   // "[object Date]"
console.log(Object.prototype.toString.call(/abc/));        // "[object RegExp]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"
console.log(Object.prototype.toString.call(Symbol()));    // "[object Symbol]"
console.log(Object.prototype.toString.call(123n));       // "[object BigInt]"

拆解一下 Object.prototype.toString.call()

  1. Object.prototype.toString 这是 Object 原型上的一个方法,所有对象都可以继承使用。

  2. .call() 这是函数的一个方法,用来改变函数执行时的 this 指向。

  3. call(value) call() 方法的第一个参数是要绑定的 this 值。在这里,我们将 this 绑定为我们要判断类型的变量 value

  4. 返回值: Object.prototype.toString 方法根据 this 的类型,返回一个字符串,形如 "[object Type]"

为什么它比 typeofinstanceof 更准确?

  • null 的正确判断: Object.prototype.toString.call(null) 返回 "[object Null]",而不是 "object"

  • 区分对象类型: Object.prototype.toString.call([]) 返回 "[object Array]"Object.prototype.toString.call({}) 返回 "[object Object]"Object.prototype.toString.call(new Date()) 返回 "[object Date]", 能够区分不同的对象类型。

  • 不受原型链修改的影响: Object.prototype.toString.call() 直接读取对象的内部 [[Class]] 属性,不受原型链的影响。

[[Class]] 是什么?

[[Class]] 是一个内部属性,它存储了对象的类型信息。 这个属性是 JavaScript 引擎内部使用的,我们无法直接访问它。 Object.prototype.toString 方法就是通过读取这个 [[Class]] 属性来确定对象的类型。

如何封装一个类型判断函数?

为了方便使用,我们可以把 Object.prototype.toString.call() 封装成一个类型判断函数:

function typeOf(obj) {
  const toString = Object.prototype.toString;
  const map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object',
    '[object Symbol]': 'symbol',
    '[object BigInt]': 'bigint',
  };
  return map[toString.call(obj)];
}

console.log(typeOf(null));        // "null"
console.log(typeOf(undefined));   // "undefined"
console.log(typeOf(123));         // "number"
console.log(typeOf("hello"));       // "string"
console.log(typeOf(true));        // "boolean"
console.log(typeOf([]));          // "array"
console.log(typeOf({}));          // "object"
console.log(typeOf(new Date()));   // "date"
console.log(typeOf(/abc/));        // "regExp"
console.log(typeOf(function(){})); // "function"
console.log(typeOf(Symbol()));    // "symbol"
console.log(typeOf(123n));       // "bigint"

这个 typeOf 函数接收一个参数 obj,然后使用 Object.prototype.toString.call(obj) 获取类型字符串,最后从 map 对象中查找对应的类型名称并返回。

兼容性考虑

Object.prototype.toString.call() 的兼容性非常好,几乎所有浏览器都支持。 可以放心使用。

总结

Object.prototype.toString.call() 是 JavaScript 中一种非常可靠的类型判断方法。 它能准确地区分各种数据类型,不受 typeofinstanceof 的局限性影响。 通过理解它的原理,我们可以更好地掌握 JavaScript 的类型系统,编写更健壮的代码。

让我们用一张表格来总结一下三种类型判断方式的优缺点:

方法 优点 缺点
typeof 简单易用 null 误判,对所有对象类型(除了函数)都返回 "object"
instanceof 可以判断对象是否是某个构造函数的实例 受多框架和原型链修改的影响
Object.prototype.toString.call() 准确区分各种数据类型,不受 typeofinstanceof 的局限性影响,兼容性好 稍微复杂一些

记住,没有万能的工具,只有适合的工具。 在实际开发中,我们需要根据具体情况选择合适的类型判断方法。 对于需要准确区分对象类型的情况,Object.prototype.toString.call() 绝对是你的不二之选!

好了,今天的讲座就到这里。 希望大家对 Object.prototype.toString.call() 有了更深入的了解。 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注