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

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

概览

  • 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)]
相关推荐
光影少年14 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
Rattenking19 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
别拿曾经看以后~3 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
川石课堂软件测试3 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
problc4 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
Gavin_9154 小时前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼5 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
待磨的钝刨6 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json