JS中数据类型检测汇总

148 阅读4分钟

javascript 中,我们四种检测数据类型的方法:

  1. typeof
  2. instanceof
  3. constructor
  4. Object.prototype.toString()

typeof

typeof 是一种用来检测数据类型的运算符,它返回一个字符串的数据类型。

优势:

typeof 检测数据类型是按照计算机底层存储的二进制值来检测的(性能好)

弊端:

  1. typeof 检测 null,返回 "object"
  2. typeof 检测原始值对应的对象类型的值,结果是 "object"

原理:

typeof 认为内存中以 000 开始的都是对象,而 null 全是 0,检测出对象后,它还会检测该对象是否实现了 call 方法,如果实现则返回 "function", 否则返回 "object"

// @1 因为 typeof 始终返回一个字符串,所以第一次检测得到 "object" 之后,后面都返回 string
// @2 多个 typeof 同时调用,会从右往左执行
// @3 typeof null -> 'object'
// @4 对象类型除了 function,其余都返回 'object'
// @5 typeof 检测一个未声明变量不会报错,结果为 'undefined'
typeof typeof typeof [10, 20, 30] // 'string' 

typeof new Number(1); // 'object'

instanceof

临时用来"拉壮丁"来检测数据类型,本意是检测当前实例是否属于这个类,返回一个布尔值。

优势:

细分对象数据类型值「但是不能因为结果是 true,就说他是标准普通对象(object)」

弊端:

  1. 不能检测原始值类型的值「但是原始值对应的对象格式实例可以检测」
  2. 因为原型链指向是可以肆意改动的,所以最后检测的结果不一定准确

原理:

  1. 按照原型链检测的,只要当前检测的构造函数,它的原型对象,出现在了实例的原型链上,则检测结果为 true,如果找到 Object.prototype 都没有找到,则结果是 false
  2. 实际调用的是 Function.prototype[Symbol.hasInstance],也就是说 [1, 2, 3] instanceof Array 等同于 Array[Symbol.hasInstance]([1, 2, 3])
var arr = [1, 2, 3];

console.log(arr instanceof Array); // true
console.log(arr instanceof RegExp); // false
console.log(arr instanceof Object); // true  -> 参照优势 1

// 弊端示例
console.log(new Number(1) instanceof Number); // true
console.log(1 instanceof Number); // false 参照弊端 1

定制 instanceof

ES5 构造函数不能修改 Symbol.hasInstance 方法

function Fn() {

}

var f = new Fn;

Fn[Symbol.hasInstance] = function() {}
Fn[Symbol.hasInstance]  // ƒ [Symbol.hasInstance]() { [native code] }

ES6 类声明可以通过静态属性修改 Symbol.hasInstance 方法

class Fn {
	static [Symbol.hasInstance] () {
		console.log('ok');
		return false
	}
}

var f = new Fn;

f instanceof Fn // false

实现 instanceof

// 前置知识
//   @1 应该避免使用 __proto__,IE 上不支持,使用 getPrototypeOf 来获取原型对象
//   @2 如果被检测的实例是一个值类型,直接返回 false,因为 getPrototypeOf(1) 
//      会拿到 Number 的原型对象(因为默认对原始值类型装箱),导致结果为 true
//   @3 右侧必须是个具有 prototype 的函数对象,否则报错
const _instanceof = function(instance, Fn) {
	let typeLeft = typeof instance;
	let typeRight = typeof Fn;

	// 不是对象类型
	if (typeRight === null || 
		(typeRight !== 'object' && typeRight !== 'function')) {
		throw new TypeError(`Right-hand side of 'instanceof' is not an object!`);
	}

	// 不是函数
	if (typeRight !== 'function') {
		throw new TypeError(`Right-hand side of 'instanceof' is not callable`);
	}

	// 没有原型对象
	if (!Fn.prototype) {
		throw new TypeError(`Function has non-object prototype 'undefined' in instanceof check`);
	}

	// 传入的实例不是对象类型 直接返回 false
	if (typeLeft === null || 
	   (typeLeft !== 'object' && typeLeft !== 'function')) {
		return false;
	}

	let proto = Object.getPrototypeOf(instance); 

	while(proto) {
		if (proto === Fn.prototype) {
			return true;
		}

		proto = Object.getPrototypeOf(proto);
	}

	return false;
}

测试下

console.log(_instanceof([], Array)); // true
console.log(_instanceof([], RegExp)); // false

// 原始值类型检测结果应该为 false
console.log(_instanceof(1, Number)); // false
console.log(_instanceof(Symbol(), Symbol)) // false

// 右侧不是一个具有 callable 特性的函数对象,或者函数对象没有 prototype 
// 属性(箭头函数或对象内 ES6 声明的函数) 则报错
console.log(_instanceof([] instanceof 2)) 
// Right-hand side of 'instanceof' is not an object

console.log(_instanceof([] instanceof {})); 
// Right-hand side of 'instanceof' is not callable

console.log(_instanceof([] instanceof (() => {}))); 
// Function has non-object prototype 'undefined' in instanceof check

constructor

也是"拉壮丁",本身是去找构造函数,不过它能弥补 instanceOf 的不足。

constructor 的修改比 instanceof 更"肆无忌惮"

let arr = [];
let n = 10;
let m = new Number(10);

console.log(arr.constructor === Array); // true

console.log(arr.constructor === RegExp); // false

// 下面这行代码,实例的 constuctor 和 Object 相等,说明它可能是个标准普通对象(可随意更改)。
console.log(arr.constructor === Object); // false 

console.log(n.constructor === Number); // true

console.log(m.constructor === Number);// true

理解一下

function Fn() {}

Fn.prototype = {};

let f = new Fn;

// Fn的原型对象变成了个空对象,自身没有 constructor 了
console.log(f.constructor === Fn); // false 
console.log(f.constructor === Object); // true

以上三种方法检测数据类型都有自己的局限性,那有没有一种比较完美的解决办法呢?

终极方案之 Object.prototype.toString

Object.prototype.toString.call([value]) 这是 JS 中唯一一个检测数据类型没有任何瑕疵的「最准确的」方法,除了性能比 typeof 略微差了一点,写法更复杂一点,其他都没问题。

ps: 大部分内置类的原型上都有 toString 方法,一般都是转换成字符串的,但是 Object.prototype.toString 是检测数据类型的,返回值中包含自己所属的构造函数信息.

原理:

一般都是返回当前实例所属的构造函数信息,但是如果实例对象拥有 Symbol.toStringTag 属性,属性值是什么,则返回什么。

// 内置有 toStringTag 属性的不能修改 
Math[Symbol.toStringTag]; // "Math"
Math[Symbol.toStringTag] = 'Object'
Math[Symbol.toStringTag]; // "Math"


function* func() {}
func[Symbol.toStringTag] // "GeneratorFunction"
func[Symbol.toStringTag] = 'wow';
func[Symbol.toStringTag] // "GeneratorFunction"



// 可以改的情况
var obj = {
    [Symbol.toStringTag]: 'wow'
}

({}).toString.call(obj); // "[object wow]"

// --------------------------------
class Fn {
	[Symbol.toStringTag] = 'wow';
}

var f = new Fn;

({}).toString.call(f) // "[object wow]"


// ---------------------------------
Array[Symbol.toStringTag] = 'wow'
({}).toString.call(Array); // "[object wow]"
Object.prototype.toString.call(1); // "[object Number]"
Object.prototype.toString.call(new Number(1)); // "[object Number]"
Object.prototype.toString.call('1'); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Symbol]"
Object.prototype.toString.call(1n); // "[object BigInt]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function() {}); // "[object Function]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(/a/); // "[object RegExp]"
Object.prototype.toString.call(new Date()); // "[object Date]"
// Math, Error 等