一夜彻底掌握数据类型检测原理

一夜彻底掌握数据类型检测原理

概览

  • 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)]
相关推荐
爱怪笑的小杰杰5 分钟前
Cesium中的倒立四棱锥:从几何结构到交互式3D可视化
javascript·3d·arcgis·1024程序员节
不会算法的小灰33 分钟前
JavaScript基础详解
开发语言·javascript·udp
十一吖i5 小时前
vue3表格显示隐藏列全屏拖动功能
前端·javascript·vue.js
徐同保6 小时前
tailwindcss暗色主题切换
开发语言·前端·javascript
生莫甲鲁浪戴7 小时前
Android Studio新手开发第二十七天
前端·javascript·android studio
细节控菜鸡9 小时前
【2025最新】ArcGIS for JS 实现随着时间变化而变化的热力图
开发语言·javascript·arcgis
拉不动的猪10 小时前
h5后台切换检测利用visibilitychange的缺点分析
前端·javascript·面试
桃子不吃李子10 小时前
nextTick的使用
前端·javascript·vue.js
Devil枫12 小时前
HarmonyOS鸿蒙应用:仓颉语言与JavaScript核心差异深度解析
开发语言·javascript·ecmascript
惺忪979812 小时前
回调函数的概念
开发语言·前端·javascript