类型判断 typeof 、instanceof 与 Object.prototype.toString.call()

242 阅读6分钟

1. JS 数据类型

最新的 ECMAScript 标准定义了 9 种数据类型:

  1. 7 种基本数据类型(新增了 Symbol 和 BigInt),使用 typeof 运算符进行检查
  • string
  • number
  • boolean
  • null
  • undefined
  • Symbol
  • BigInt
  1. 引用数据类型:ObjectFunction

2. typeof 操作符

  1. 检查数据类型,返回一个表示类型的字符串
  • string: typeof instance; // 'string'
  • number: typeof instance; // 'number'
  • boolean: typeof instance; // 'boolean'
  • null: typeof instance; // 'object' (特殊)
  • undefined: typeof instance; // undefined'
  • Symbol: typeof instance; // 'symbol'
  • BigInt: typeof instance; // 'bigint'
  1. 除 Function 外的所有构造函数通过 typeof 判断都返回 object
  • Object: typeof instance; // "object"
  • Function: typeof instance; // "function"
  • typeof []; // "object"
  • typeof {}; // "object"
var str = new String("String");
var num = new Number(100);
typeof str; // 返回 'object'
typeof num; // 返回 'object'

var func = new Function();
typeof func; // 返回 'function'
  1. 特殊
  • typeof NaN === 'number',尽管它是 Not-A-Number (非数值) 的缩写
  • typeof null === 'object',因为计算机底层是二进制表示的类型标签,对象的类型标签是 0,而空指针 null 类型标签也是 0。
  1. 块级作用域与暂时性死区
  • 在 ECMAScript 2015 之前,使用 typeof 进行类型判断总是安全的。
  • 但 ES6 开始,在变量被声明之前对块中的 let 和 const 变量使用 typeof 会抛出一个 ReferenceError。块作用域变量在块的头部处于“暂存死区”,直至其被初始化,在这期间,访问变量将会引发错误。

3. instanceof 运算符

用于检测构造函数的 prototype 是否出现在某个实例对象的原型链上。(也就是判断实例对象的 __proto__ 属性是否指向了构造函数的 prototype 原型对象,或者是否在一条原型链上!)

语法: object instanceof constructor

  • object 是实例对象
  • constructor 是构造函数
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car("Honda", "Accord", 1998);

auto instanceof Car; // true, auto.__proto__ == Car.prototype
auto instanceof Object; // true, Car.prototype.__proto__ == Object.prototype,所以 auto.__proto__.__proto__ == Object.prototype

tips: 原型链相关可以参考这篇文章~

再来几个 🌰

var str = "abc";
str instanceof String; // false!! 非实例对象

var myString = new String();
myString instanceof String; // true
myString instanceof Object; // true

var myDate = new Date();
myDate instanceof Date; // true
myDate instanceof Object; // true
myDate instanceof String; // false

var myObj = {};
var myNonObj = Object.create(null);
myObj instanceof Object; // true
myNonObj instanceof Object; // false!! 创建非 Object 实例的对象

Symbol.hasInstance 属性

ES6 提供了 11 个 内置的 Symbol 值,指向语言内部使用的方法来实现。ES6 之前并没有暴露给开发者。

instanceof 运算符,其实底层调用的就是 Symbol.hasInstance 属性,用于判断某对象是否为某构造器的实例。

  • 当使用 instanceof 运算符时,会调用内部的 Symbol.hasInstance属性 ,然后执行语言内部的方法来判断某对象是否为某构造器的实例。
  • 比如执行 foo instanceof Foo ,在语言内部实际调用的是 Foo[Symbol.hasInstance](foo) 方法
// Demo1
function func() {
  this.a = 123;
}
let f = new func();
f instanceof func; // true
func[Symbol.hasInstance](f); // true

// Demo2
class MyFunc {}
MyFunc[Symbol.hasInstance]; // ƒ [Symbol.hasInstance]() { [native code] }

// Demo3
let obj = {};
obj instanceof Object; // true
Object[Symbol.hasInstance]; // ƒ [Symbol.hasInstance]() { [native code] }

实现一个自定义的 instanceof

// 重写Even[Symbol.hasInstance]的静态方法
class Even {
  // 静态方法,直接在类上调用 Even[Symbol.hasInstance],而不是在实例上调用。不会被实例继承,但是会被子类继承
  static [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
  }
}

// 自动调用 Even[Symbol.hasInstance]()
1 instanceof Even; // false
let e = new Even();
e instanceof Even; // false 因为被重写了 !

4. Object.prototype.toString.call()

为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 call() 的形式来调用,传递要检查的对象作为第一个参数,它的返回值代表该对象的 [object 数据类型] 字符串表示。

Object.prototype.toString.call("foo"); // "[object String]"
Object.prototype.toString.call(3); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(new Array()); // "[object Array]"
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
Object.prototype.toString.call(Math); // [object Math]
Object.prototype.toString.call(document); // [object HTMLDocument]
Object.prototype.toString.call(window); //[object global]
// ...

那么,为什么 Object.prototype.toString.call() 可以判断类型?

回顾 Object.prototype.toString()

该方法返回 表示该对象的字符串。所有对象都有 toString() 方法,因为它是 Object 原型上的方法,被每个 Object 对象继承。 toString() 默认返回 [object type],其中 type 是对象的类型。

// 先来看下 toString() 方法的效果
Number(123).toString(); // "123"
[(1, 2, 3)].toString(); // "1,2,3"
new Date().toString(); // "Thu Mar 25 2021..."

let obj = { a: 1 };
obj.toString(); // "[object Object]"

function Dog(name) {
  this.name = name;
}
let dog = new Dog("Gabby");
dog.toString(); // "[object Object]"
Dog.toString(); // "function Dog(name) {this.name = name;}"

// 可以重写 toString 来获取我们想要的值
Dog.prototype.toString = function dogToString() {
  return `${this.name}`;
};
dog.toString(); // "Gabby"

为什么 obj.toString() 和 Object.prototype.toString.call(obj) 的结果不一样?

var a = {};
var b = [];
var c = 1;
Object.prototype.toString.call(a); //[object,Object]
Object.prototype.toString.call(b); //[object,Array]
Object.prototype.toString.call(c); //[object,Number]
Object.prototype.toString.call(a) === "[object Object]"; // true

因为 toStringObject 的原型方法,ArrayFunction 等类型作为 Object 的实例,都 重写的了 toString 方法。 因此在调用时,是调用了重写后的方法,而不是原型链上的 toString()方法。 举个例子验证一下 ⬇️

let arr = [1, 2, 3];
Array.prototype.hasOwnProperty("toString"); // true
arr.toString(); // "1,2,3"
delete Array.prototype.toString; // delete 操作符可以删除实例属性
Array.prototype.hasOwnProperty("toString"); // false
arr.toString(); // "[object Array]"

如果 toString 方法没有被重写的话,会返回 [Object type],是可以判断出类型的。但目前除了 Object 类型的对象外,其他类型的 toString 方法都会直接返回都是内容的字符串了。所以我们需要使用 call 或者 apply 方法来改变 toString 方法的执行上下文,来达到调用 Object 原型上面 toString() 方法的目的

Object.prototype.toString.call() 原理分析

那么 toString 方法又是如何获取到不同数据的类型的呢?

这就涉及到了 Symbol 的内置属性 Symbol.toStringTag

Symbol.toStringTag 属性

对象的 Symbol.toStringTag 属性,指向一个方法。在该对象上面调用 Object.prototype.toString() 时,如果这个属性存在,它的返回值会出现在 toString 方法返回的字符串之中,表示对象的类型。也就是 [object xxx]xxx 部分。

  • 通常只有内置的 Object.prototype.toString() 方法会去读取这个标签,并把它包含在自己的返回值里。
  • 也就是说,当调用 Object 原型上面的 toString() 方法时,会默认读取 Symbol.toStringTag 属性,给属性指向的语言内置方法可以用来判断不同的数据类型。

参考这张图是 toString 内部定义的规则,来判断不同的数据类型。

  • 也可以通过重写 Symbol.toStringTag,来自定义的类型标签
class Test {
  get [Symbol.toStringTag]() {
    return "my type";
  }
}
let t = new Test();
t.toString(); // "[object my type]"

对比 Object.prototype.toString.call() 和 instanceof

  • Object.prototype.toString.call()

    • 优点:对于所有基本的数据类型都能进行判断,包括 nullundefined,也可以检测出 iframes
    • 缺点:不能精准判断自定义对象,对于自定义对象只会返回 [object Object]
  • instanceof

    • 优点:可以弥补 Object.prototype.toString.call() 不能判断自定义实例化对象的缺点
    • 缺点:只能用来判断对象类型,原始类型不可以;并且所有对象类型 instanceof Object 都是 true,不能检测出 iframes

总的来说就是,Object.prototype.toString.call() 常用于判断浏览器内置对象,而instanceof常用来判断对象实例。

END!!! 撒花✿✿ヽ(°▽°)ノ✿ ❀❀❀❀


参考

内置的 Symbol 值

Object.prototype.toString