javascript数据类型检测及检测方法封装

1,706 阅读9分钟

前言

数据类型检测在开发中有非常广泛的应用,常见的有四种方法,每种方法都有优缺点和使用场景。

  • typeof
  • instanceof & constructor
  • Object.prototype.toString.call([value])

数据类型

js 中数据类型分为基本类型和引用类型,基本类型有六种:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol (es6)

引用类型包括对象object、数组array、函数function等,统称对象类型:

  • object

typeof

我们最常用的莫过于 typeof,typeof 是一元操作符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。

特点

在检测基本类型值(null除外)和函数类型值的时候很方便。

在基本类型中,null返回了object。

引用类型中,除了函数外(如 Array、Function、Date、RegExp、Error 等)其他返回的object。

typeof返回值都是小写的字符串,如下。

  • undefined
  • object
  • boolean
  • number
  • string
  • object

场景

形参赋值默认值

function func(n,m){
	n === undefined ? n=0:null;
  typeof m === 'undefined' ? m=0:null;
  
  // 当 n=false 基于逻辑或和逻辑与 会有问题
  n=n||0
  
  // ES 2020 新增 空位合并运算符 ??
  //表示我们仅在第一项为 null 或 undefined 时设置默认值
  m = m ?? 0
}

instanceof

用来检测实例是否属于某个类的运算符

特点

不能处理基本数据类型

let arr =[],reg = /^$/;

arr instanceof Array // true
reg instanceof Array // false


let n=12,m= new Number('12')  // 分别是字面量和构造函数创建

n instanceof Array // false
m instanceof Array // true

不能正确处理继承中的类

只要在当前实例原型链中(proto)出现过的类,检测结果都是true。如果修改原型链或者检测预先类都会出现一点问题。

let arr =[];

arr instanceof Object // false

constructor

特点

在类的原型上一般都有constructor属性,存储当前类的本身,利用这一点,验证是否为所属类,从而进行类型判断。

但是constructor的值太容易被修改了。

let n = 12,arr=[];

n.constructor === Number // true
arr.constrctor === Array //true
arr.constrctor === Object //false

arr.constrctor = 11 // Func.prototype={} 或者修改原型链
arr.constrctor === Array //false

Obejct.prototype.toString.call()

首先来看英文版的定义:

When the toString method is called, the following steps are taken:

  1. If the this value is undefined, return "[object Undefined]".
  2. If the this value is null, return "[object Null]".
  3. Let O be the result of calling ToObject passing the this value as the argument.
  4. Let class be the value of the [[Class]] internal property of O.
  5. Return the String value that is the result of concatenating the three Strings "[object ", class, and "]".

当 toString 方法被调用的时候,下面的步骤会被执行:

  1. 如果 this 值是 undefined,就返回 [object Undefined]
  2. 如果 this 的值是 null,就返回 [object Null]
  3. 让 O 成为 ToObject(this) 的结果
  4. 让 class 成为 O 的内部属性 [[Class]] 的值
  5. 最后返回由 "[object " 和 class 和 "]" 三个部分组成的字符串

特点

调用Object原型上的方法时,方法中的this是要检测的数据类型,结果会返回一个由 "[object " 和 class 和 "]" 组成的字符串,而 class 是要判断的对象的内部属性。

这个方法很强大,能检测出几乎所有的基本类型和引用类型,但是无法检测自定义的类。

console.log([12,34].toString()) // "[12,34]"

所以我们可以识别至少 14 种类型。

// 以下是11种:
var number = 1;          // [object Number]
var string = '123';      // [object String]
var boolean = true;      // [object Boolean]
var und = undefined;     // [object Undefined]
var nul = null;          // [object Null]
var obj = {a: 1}         // [object Object]
var array = [1, 2, 3];   // [object Array]
var date = newDate();   // [object Date]
var error = newError(); // [object Error]
var reg = /a/g;          // [object RegExp]
var func = functiona(){}; // [object Function]

functioncheckType() {
    for (var i = 0; i < arguments.length; i++) {
        console.log(Object.prototype.toString.call(arguments[i]))
    }
}

checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func)

除了以上 11 种之外,还有:

console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]

functiona() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
a();

无法检测自定义的类

class myCat{}
Object.prototype.toString.call(myCat) // "[object Function]"

数据类型检测方法封装

简单的封装

function isType(obj, type) {
	return Object.prototype.toString.call(obj).includes(type);
}

let a = true;
console.log(isType(a, 'Boolean'));
console.log(isType(a, 'Object'));

使用高阶函数

虽然上面的函数能实现数据类型检测,但是手动书写字符串容易造成问题,接着使用高阶函数改造。

考虑到实际情况下并不会检测 Math 和 JSON,所以去掉这两个类型的检测。

// 函数柯里化:函数参数只有一个
function isType(type) {
	return function(obj) {
		return Object.prototype.toString.call(obj).includes(type);
	};
}

let types = [
	'Boolean',
	'Number',
	'String',
	'Function',
	'Array',
	'Date',
	'RegExp',
	'Object',
	'Error',
	'Null',
	'Undefined'
];
let fns = {};
types.forEach((type) => {
	fns['is' + type] = isType(type);
});

console.log(fns.isBoolean(a)); 

提升性能

Object.prototype.toString.call  性能不如 typeof,但胜在可通用。如果是基本类型,就使用 typeof,引用类型就使用 toString。

function isType(type) {
	return function(obj) {
		let res = typeof obj;
		return res === 'object' || res === 'function'
			? Object.prototype.toString.call(obj).toLowerCase().includes(type)
			: res.includes(type);
	};
}

let types = [
	'boolean',
	'number',
	'string',
	'function',
	'array',
	'date',
	'regExp',
	'object',
	'error',
	'null',
	'undefined'
];
let fns = {};
types.forEach((type) => {
	fns['is' + type[0].toUpperCase() + type.substr(1)] = isType(type);
});

console.log(fns.isBoolean(true));
console.log(fns.isBoolean([])); 

返回数据类型方法封装

方法封装

此外鉴于 typeof 的结果是小写,我也希望所有的结果都是小写。

// 第一版
var class2type = {};

// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error Null Undefined".split(" ").map(function(item, index) {
    class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj) {
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}

在 IE6 中,null 和 undefined 会被 Object.prototype.toString 识别成 [object Object]。

// 第二版
var class2type = {};

// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
    class2type["[object " + item + "]"] = item.toLowerCase();
})

function type(obj) {
    // 一箭双雕
  if (obj == null) {
        return obj + "";
    }
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[Object.prototype.toString.call(obj)] || "object" :
        typeof obj;
}

类型检测

有了 type 函数后,我们可以对常用的判断直接封装,比如 isFunction:

function isFunction(obj) {
    return type(obj) === "function";
}

jQuery 判断数组类型,旧版本是通过判断 Array.isArray 方法是否存在,如果存在就使用该方法,不存在就使用 type 函数。

var isArray = Array.isArray || function( obj ) {
    return type(obj) === "array";
}

但是在 jQuery v3.0 中已经完全采用了 Array.isArray。

其他类型检测

plainObject

plainObject 来自于 jQuery,可以翻译成纯粹的对象,所谓"纯粹的对象",就是该对象是通过 "{}" 或 "new Object" 创建的,该对象含有零个或者多个键值对。除了 {} 和 new Object 创建的之外,jQuery 认为一个没有原型的对象也是一个纯粹的对象。

实际上随着 jQuery 版本的提升,isPlainObject 的实现也在变化,我们今天讲的是 3.0 版本下的 isPlainObject,我们直接看源码。

// 上节中写 type 函数时,用来存放 toString 映射结果的对象
var class2type = {};

// 相当于 Object.prototype.toString
var toString = class2type.toString;

// 相当于 Object.prototype.hasOwnProperty
var hasOwn = class2type.hasOwnProperty;

function isPlainObject(obj) {
    var proto, Ctor;

    // 排除掉明显不是obj的以及一些宿主对象如Window
  	if (!obj || toString.call(obj) !== "[object Object]") {
        return false;
    }

    /**
     * getPrototypeOf es5 方法,获取 obj 的原型
     * 以 new Object 创建的对象为例的话
     * obj.__proto__ === Object.prototype
     */
    proto = Object.getPrototypeOf(obj);

    // 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true
  	if (!proto) {
        return true;
    }

    /**
     * 以下判断通过 new Object 方式创建的对象
     * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
     * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
     */
    Ctor = hasOwn.call(proto, "constructor") && proto.constructor;

    // 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数
  	return typeof Ctor === "function" 
  				&& hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}

我们判断 Ctor 构造函数是不是 Object 构造函数,用的是 hasOwn.toString.call(Ctor),这个方法可不是 Object.prototype.toString。

console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] }
console.log(Object.prototype.toString.call(Ctor)); // [object Function]

发现返回的值并不一样,这是因为 hasOwn.toString 调用的其实是 Function.prototype.toString,而且 Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。

函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function关键字,形参列表,大括号,以及函数体中的内容。

EmptyObject

jQuery提供了 isEmptyObject 方法来判断是否是空对象,代码简单,我们直接看源码:

functionisEmptyObject( obj ) {

        var name;

        for ( name in obj ) {
            return false;
        }

        return true;
}

isEmptyObject 就是判断是否有属性,for 循环一旦执行,就说明有属性,有属性就会返回 false。

但是根据这个源码我们可以看出isEmptyObject实际上判断的并不仅仅是空对象。

console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true

jQuery可能是因为考虑到实际开发中 isEmptyObject 用来判断 {} 和 {a: 1} 已经足够,如果真的是只判断 {},完全可以使用封装的type函数筛选掉不适合的情况。

Window对象

Window 对象作为客户端 JavaScript 的全局对象,它有一个 window 属性指向自身。我们可以利用这个特性判断是否是 Window 对象。

function isWindow( obj ) {
    return obj != null && obj === obj.window;
}

isArrayLike

isArrayLike,看名字可能会让我们觉得这是判断类数组对象的,其实不仅仅是这样,jQuery 实现的 isArrayLike,数组和类数组都会返回 true。

function isArrayLike(obj) {

    // obj 必须有 length属性
  	var length = !!obj && "length"in obj && obj.length;
    var typeRes = type(obj);

    // 排除掉函数和 Window 对象
  	if (typeRes === "function" || isWindow(obj)) {
        return false;
    }

    return typeRes === "array" || length === 0 ||
        typeof length === "number" && length > 0 && (length - 1) in obj;
}

重点分析 return 这一行,使用了或语句,只要一个为 true,结果就返回 true。

所以如果 isArrayLike 返回true,至少要满足三个条件之一:

  1. 是数组
  2. 长度为 0
  3. lengths 属性是大于 0 的数组,并且obj[length - 1]必须存在

第一个就不说了,看第二个,为什么长度为 0 就可以直接判断为 true 呢?

那我们写个对象:

var obj = {a: 1, b: 2, length: 0}

isArrayLike 函数就会返回 true,那这个合理吗?

回答合不合理之前,我们先看一个例子:

function a(){
    console.log(isArrayLike(arguments))
}
a();

如果我们去掉length === 0 这个判断,就会打印 false,然而我们都知道 arguments 是一个类数组对象,这里是应该返回 true 的。

所以是不是为了放过空的 arguments 时也放过了一些存在争议的对象呢?

第三个条件:length 是数字,并且 length > 0 且最后一个元素存在。

为什么仅仅要求最后一个元素存在呢?

让我们先想下数组是不是可以这样写:

var arr = [,,3]

当我们写一个对应的类数组对象就是:

var arrLike = {
    2: 3,
    length: 3
}

也就是说当我们在数组中用逗号直接跳过的时候,我们认为该元素是不存在的,类数组对象中也就不用写这个元素,但是最后一个元素是一定要写的,要不然 length 的长度就不会是最后一个元素的 key 值加 1。比如数组可以这样写

var arr = [1,,];
console.log(arr.length) // 2

但是类数组对象就只能写成:

var arrLike = {
    0: 1,
    length: 1
}

所以符合条件的类数组对象是一定存在最后一个元素的!

这就是满足 isArrayLike 的三个条件,其实除了 jQuery 之外,很多库都有对 isArrayLike 的实现,比如 underscore:

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

var isArrayLike = function(collection) {
    var length = getLength(collection);
    returntypeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};

isElement

isElement 判断是不是 DOM 元素。

var isElement = function(obj) {
    return !!(obj && obj.nodeType === 1);
};

并非全部原创,部分资料搜集于网络。因过于零散,很多参考文献无法考证。侵删