一夜彻底掌握数据类型检测原理
概览
- 4 种判断数据类型方法的底层原理和优缺点
instanceof
的手写实现「标准版」- 类型检测方法封装
for in
循环存在的问题- 如何获取对象的 key
数据类型判断原理
typeof
使用方式: typeof value
**底层机制:**按照数据在计算机底层存储的"二进制"值进行检测,效率比较高
局限性:
-
typeof null
-->object
null的二进制值是64个0,而typeof认为前三位是零的都是object -
typeof 除了能够区分函数对象,其余对象无法细分 +
typeof 函数
-->function
typeof []
-->object
typeof /^\d+$/
-->object
**应用场景:**检测除null
以外的其他原始值类型、笼统的检测是否为对象
instanceof
本意是检测某个实例是否属于这个类,"临时"拉来检测数据类型,可以用于"细分对象"
使用方式: 实例 instanceof 构造函数
底层机制:
1、先检测 构造函数 是否拥有 Symbol.hasInstance
方法(ES6+之后,Function.prototype设置了Symbol.hasInstance这个方法,所以函数都具备这个属性),如果有这个方法,构造函数[Symbol.hasInstance](实例)
返回的值就是我们要的结果。
2、如果没有这个方法,则按照原型链进行查找:按照实例的__proto__
一直向上找,直到找到Object.prototype
为止,只要在原型链上出现了 构造函数.prototype
,说明当前实例属于它,结果返回true
,如果没找到,结果就是false
。
⚠️ 我们正常情况下重写是无效的, Array[Symbol.hasInstance]=function(){...}
js
const arr = [10, 20];
Array[Symbol.hasInstance] = function (arr) {
return false;
};
console.log(arr instanceof Array); // true
console.log(Array[Symbol.hasInstance](arr)); // true
console.dir(Array);
Array[Symbol.hasInstance]
如下图
但是基于class创建的自定义类,可以重写其Symbol.hasInstance
方法
js
class Fn {
constructor(name) {
if (name) {
this.name = name;
}
}
sayName() {
console.log(this.name);
}
// 静态私有属性方法
static [Symbol.hasInstance](obj) {
return obj.hasOwnProperty('name');
}
}
const f1 = new Fn('dahuang');
// 等价于 Fn[Symbol.hasInstance](f1)
console.log(f1 instanceof Fn, f1); // true Fn {name: 'dahuang'}
const f2 = new Fn;
// 等价于 Fn[Symbol.hasInstance](f2)
console.log(f2 instanceof Fn, f2); // false Fn {}
我们可以用来判断某个实例是不是该类的标准实例
局限性:
- 无法处理原始值类型,返回结果都是false
1 instanceof Number
-->false
new Number(1) instanceof Number
-->true
null instanceof Number
--> 只要检测原始值类型,返回都是false
- 任何对象基于
instanceof
检测是否为Object
实例,结果都是true
,所以无法区分是否为"标准普通对象"arr instanceof Array
-->true
arr instanceof Object
-->true
obj instanceof Object
-->true
总结:
基于instanceof
检测数据类型,不是很靠谱;
- 无法检测原始值类型
- 无法区分是否为"标准普通对象"
- 一但原型链被重构,检测的结果是不准确
项目中,偶尔用于初步检测是否为特殊对象,例如:检测是否为正则、日期对象等...
手写实现:
原instanceof
表现
js
var instance_of = function instance_of(obj, ctor) {
// 必须保证右侧是个对象,然后判断是不是一个函数对象、原型是否存在
// 检测值类型的校验:校验ctor的格式
if (ctor === null || !/^(object|function)$/.test(typeof ctor)) throw new TypeError('Right-hand side of instanceof is not an object');
if (typeof ctor !== 'function') throw new TypeError('Right-hand side of instanceof is not callable');
if (!ctor.prototype) throw new TypeError('Function has non-object prototype undefined in instanceof check');
// 检测值类型的校验:不支持原始值类型
if (obj === null || !/^(object|function)$/.test(typeof obj)) return false;
// 首先检测ctor是否拥有Symbol.hasInstance方法
if (typeof Symbol !== "undefined") {
var hasInstance = ctor[Symbol.hasInstance];
if (typeof hasInstance === "function") {
// 等价于 ctor[Symbol.hasInstance](obj)
return hasInstance.call(ctor, obj);
}
}
// 按照原型链进行查找,看是否会出现ctor.prototype
var proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === ctor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
};
var arr = [10, 20];
console.log(instance_of(arr, Array)); // true
console.log(instance_of(arr, RegExp)); // false
console.log(instance_of(arr, Object)); // true
constructor
使用方式: if(对象.constructor === 构造函数){...}
**底层机制:**原型链
局限性:
不准确,因为constructor可以被"肆意"重写
相比于instanceof
,可以检测原始值类型,也可以判断是否为"标准普通对象"
js
const arr=[], obj={}, num=10;
console.log(arr.constructor); // ƒ Array() { [native code] }
console.log(obj.constructor); // ƒ Object() { [native code] }
console.log(num.constructor); // ƒ Number() { [native code] }
console.log(arr.constructor === Array); // true
console.log(obj.constructor === Object); // true
console.log(num.constructor === Number); // true
Object.prototype.toString.call
内置构造函数的原型对象上,基本上都有toString这个方法,基本都是用来把值转换为字符串的,除Object.prototype.toString
外,它是用来检测数据类型的;只需要把Object.prototype.toString
执行,方法中的this
是谁,就是检测谁的数据类型,返回结果 [object ?]
, ?
是自己所属的构造函数
使用方式: Object.prototype.toString.call({})
==> [object Object]
底层机制:
以Object.prototype.toString.call(value)
为例:
1、首先会看value
值是否有 Symbol.toStringTag
属性,有这个属性,属性值是什么,检测出来的类型就是什么;
2、如果没有这个属性,才一般是按照自己所属的构造函数返回
具备Symbol.toStringTag
这个属性的值:
-
Math[Symbol.toStringTag]:'Math'
-
Promise.prototype[Symbol.toStringTag]:'Promise'
-
Generator函数原型链上有
-
Set.prototype[Symbol.toStringTag]:'Set'
-
Map.prototype[Symbol.toStringTag]:'Map'
-
...
**优势:**属于检测最准确、最全面的方式,能够区分null、能够检测原始值类型、能够细分对象、即便重构原型对象检测也是准确的...
其他方法
isNaN([value])
检测是否为有效数字
Array.isArray([value])
检测是否为数组
类型检测方法封装
参考 jQuery 的源码
js
const class2type = {};
const toString = class2type.toString; // Object.prototype.toString 检测数据类型的
const hasOwn = class2type.hasOwnProperty; // Object.prototype.hasOwnProperty 检测是否为私有属性
// 检测数据类型的通用方法:返回所属类型的字符串
const toType = function toType(obj) {
let reg = /^\[object ([\w\W]+)\]$/;
if (obj == null) return obj + "";
return typeof obj === "object" || typeof obj === "function" ?
reg.exec(toString.call(obj))[1].toLowerCase() :
typeof obj;
};
// 检测是否为函数
const isFunction = function isFunction(obj) {
// Support: Chrome <=57, Firefox <=52
// In some browsers, typeof returns "function" for HTML <object> elements
// (i.e., `typeof document.createElement( "object" ) === "function"`).
// We don't want to classify *any* DOM node as a function.
// Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5
// Plus for old WebKit, typeof returns "function" for HTML collections
// (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756)
return typeof obj === "function" &&
typeof obj.nodeType !== "number" &&
typeof obj.item !== "function";
};
// 检测是否为window对象 window.window === window 为 true
const isWindow = function isWindow(obj) {
return obj != null && obj === obj.window;
};
// 检测是否为数组或者类数组
const isArrayLike = function isArrayLike(obj) {
let length = !!obj && "length" in obj && obj.length,
type = toType(obj);
// 函数有 length 属性,表示 形参的个数
// window 有 length 属性,表示 当前页面嵌套 iframe 的数量
if (isFunction(obj) || isWindow(obj)) return false;
// length === 0 表示为空的类数组
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
};
// 检测是否为"纯粹对象/标准普通"对象: obj.__proto__ === Object.prototype
const isPlainObject = function isPlainObject(obj) {
let proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true; // 匹配 Object.create(null)
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
};
// 检测是否为空对象 JSON.stringify(obj) === '{}'
const isEmptyObject = function isEmptyObject(obj) {
let keys = Object.getOwnPropertyNames(obj);
if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
return keys.length === 0;
};
// 检测是否为有效数字:支持数字字符串,支持 16进制的
const isNumeric = function isNumeric(obj) {
var type = toType(obj);
// obj - parseFloat(obj) 正常情况下结果应该是 0
return (type === "number" || type === "string") &&
!isNaN(obj - parseFloat(obj));
};
拓展
for in 问题
for in
循环非常"恶心",项目中尽可能不用它
- 优先迭代数字属性,按照从小到大「对象本身的特征,我们解决不了这个问题」
- 会迭代"私有"以及"原型链上(公有)"所有"可枚举"的属性,非常消耗性能
- 只能迭代"可枚举"的属性
- 不能迭代"Symbol类型"的属性
js
Object.prototype.AA = 100;
let obj = {
0: 10,
name: 'xxx',
[Symbol('@1')]: 200,
1: 20
};
Object.defineProperty(obj, 'age', {
enumerable: false,
value: 13
});
for (let key in obj) {
// if (!obj.hasOwnProperty(key)) break;
console.log(key); //都是私有属性
}
打印如下:
如何获取对象的 key
Object.keys(obj)
获取对象 "非Symbol类型"、"可枚举的"、"私有属性" 「结果:包含属性名的数组」Object.getOwnPropertyNames(obj)
获取对象 "非Symbol类型"、"私有属性",不论是否是可枚举的Object.getOwnPropertySymbols(obj)
获取对象 "Symbol类型"、"私有属性",不论是否是可枚举的
Reflect.ownKeys(obj)
ES6中新增Reflect对象,其中ownKeys就是获取obj所有私有属性,不论类型或者是否可枚举
方法 | Symbol类型 | 私有属性 | 可枚举的 | 不可枚举的 |
---|---|---|---|---|
Object.keys(obj) |
❌ | ✅ | ✅ | ❌ |
Object.getOwnPropertyNames(obj) |
❌ | ✅ | ✅ | ✅ |
Object.getOwnPropertySymbols(obj) |
✅ | ❌ | ❌ | ❌ |
Reflect.ownKeys(obj) |
✅ | ✅ | ✅ | ✅ |
js
Object.prototype.AA = 100;
let obj = {
0: 10,
name: 'xxx',
[Symbol('@1')]: 200,
1: 20
};
Object.defineProperty(obj, 'age', {
enumerable: false,
value: 13
});
console.log(obj);
console.log(Object.keys(obj)); // ['0', '1', 'name']
console.log(Object.getOwnPropertyNames(obj)); // ['0', '1', 'name', 'age']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(@1)]
console.log(Reflect.ownKeys(obj)); // ['0', '1', 'name', 'age', Symbol(@1)]