javascript 数据类型 -- 检测

一、前言

  在上一篇博文中 Javascript 数据类型 -- 分类 中,我们梳理了 javascript 的基本类型和引用类型,并提到了一些冷知识。大概的知识框架如下:

     

  这篇博文就讲一下在写代码的过程中,通常怎么检测这些类型。

二、检测

  总的来说,我们有4种可检测数据类型的方法, typeof 运算符、  constructor 属性、 instanceof 运算符、 prototype.isPrototypeOf 方法、 Object.prototype.toString.call 方法、 in 操作符、 hasOwnProperty 方法及 isNaN() 、 Array.isArray() 等几种特殊检测方式。每种方法各有优劣,实际运用时还要结合着使用。

 

  typeof 运算符

    在上一篇博文中,我们已经用到了 typeof 运算符来检测类别,这里就简单得总结一下它的结果和结论。

typeof '123'                    // string
typeof String('123')            // string
typeif new String('123')        // string

typeof 123                      // number
typeof Number(23.4)             // number
typeof new Number(2.3e4)        // number
typeof NaN                      // number
typeof Infinity                 // number

typeof false                    // boolean
typeof Boolean(false)           // boolean
typeof new Boolean(false)       // boolean

var mySymbol = Symbol()
typeof mySymbol                 // symbol

var a;
typeof a                        // undefined,定义但未初始化
typeof b                        // undefined,未定义

var div = document.getElementById('abc')
console.log(div)                // null
typeof div                      // object

var Fn = function () {}
var classC = class C {}
typeof Fn                       // function
typeof classC                   // function,类在本质上还是函数

typeof {}                       // object
typeof []                       // object
typeof /\w+/                    // object
typeof new Map()                // object
typeof new Set()                // object

(function () {
  return typeof arguments
})()                 // object

    如上所示, typeof 运算符的语法是 typeof variable 。因是运算符的原由,所以不推荐使用加括号的写法,即( typeof (variable) ) ,以免与方法混淆。运算后的返回值有:

    • "boolean"     :布尔型;
    • "string"      :字符型;
    • "number"      :数值型(整数/浮点数值/NaN);
    •  "symbol"     :Symbol 型
    • "undefined" :变量未定义,或未初始化;
    • "object"     :引用型或null;
    • "function"  :函数型;

    由上可知, typeof 运算符对于大部分基本类型的判断还是准确的,特别是 string 、 number 、 boolean 和 symbol ,对于大部分的引用类似会返回 object ,函数虽然也属于引用类型,但由于其具有很对有别于一般引用类型的特性,所以单独判断为 "function" 。若是针对 NaN 进行检测,可以使用全局方法 isNaN(variable) 更为精准。由此,我们可以编写一个统一的方法,先将 undefined 和 null 类型检测出来,再检测其他基本类型。

function is(value, type) {
    if (value == null) {
        return value === type
    } else if (typeof value === 'number' && type === 'isNaN') {
        return isNaN(value)
    }
    return typeof value === type
}

var x;
is(x, undefined)                // true

var div = document.getElementById('div')
is(div, null)                   // true

var y = 2 + undefined
is(y, 'isNaN')                  // true

is(3, 'number')                 // true
is('3', 'string')               // true
is(true, 'boolean')             // true
is(Symbol(), 'symbol')          // true
is(function(){}, 'function')    // true

is(new String('s'), 'object')   // true
is(new Map(), 'object')         // true
is({}, 'object')                // true

  

  constructor 属性

    由于 typeof 运算符并不能准确地检测出引用类型的值,所以我们可以试试 constructor 属性。

    当一个函数  foo 被定义是,javascript 会给 foo 函数添加 prototype 原型,然后再在 prototype 上添加一个 contructor 属性,并让其指向 foo 的引用,如下所示:

    当执行 var bar = new foo() 时, foo 被当成了构造函数, bar 是 foo 的实例对象,此时 foo 原型上的 constructor 传递到了 bar 上,因此 bar.constructor == foo (返回的是一个true)

    可以看出,JavaScript在函数 foo 的原型上定义了 constructor ,当 foo 被当作构造函数用来创建对象时,创建的新对象就被标记为 foo 类型,使得新对象有名有姓,可以追溯。所以 constructor 属性可以用来检测自定义类型。同理,JavaScript中的数据类型也遵守这个规则:

'3'.constructor === String                 // true,由于包装对象的缘故,而拥有了 construtor 属性
String('').constructor === String          // true
new String('').constructor === String      // true

(3).constructor === Number                 // true,与 string 同理
Number(3).constructor === Number           // true
new Number(3).constructor === Number       // true

false.constructor === Boolean              // true,与 string 同理
Boolean(false).constructor === Boolean     // true
new Boolean().constructor === Boolean      // true

Symbol().constructor === Symbol            // true

({}).constructor == Object                 // true
[].constructor === Array                   // true
(function(){}).constructor === Function    // true
new Date().constructor === Date            // true
new Error().constructor === Error          // true
new RegExp().constructor === RegExp        // true
new Map().constructor === Map              // true
new Set().constructor === Set              // true

document.constructor === HTMLDocument      // true
window.constructor === Window              // true

null.constructor === Object                // Uncaught TypeError: Cannot read property 'constructor' of null
undefined.constructor === Object           // Uncaught TypeError: Cannot read property 'constructor' of undefined

    由上可见, constructor 属性对于大部分值都是能准确得出其类型的,特别是几个基本类型,也得益于包装对象的缘故,可以被准确地检测出来。只有 undefined 和 null 由于没有包装对象,不能进行转换,所以不能被检出。因此,上面的判断函数可以改进为:

function is(value, type) {
    if (value == null) {
        return value === type
    // } else if (value.constructor === 'number' && type === 'isNaN') {
    } else if (value.constructor === Number && type === 'isNaN') {
        return isNaN(value)
    }
    // return typeof value === type
    return value.constructor === type
}

var x;
is(x, undefined)                // true

var div = document.getElementById('div')
is(div, null)                   // true

var y = 2 + undefined
is(y, 'isNaN')                  // true

is(3, Number)                 // true
is('3', String)               // true
is(true, Boolean)             // true
is(Symbol(), Symbol)          // true
is({}, Object)                // true
is([], Array)                 // true
is(new Map(), Map)            // true
is(new Set(), Set)            // true
is(new Date(), Date)          // true
is(new Error(), Error)        // true
is(new RegExp(), RegExp)      // true
is(function(){}, Function)    // true
is(document, HTMLDocument)    // true
is(window, Window)            // true

  当然,根据 construtor 方法的原理可以知道,在判断自定义类型时,由于 prototype 是可以被认为修改的,原有的 construtor 也就会被指向新 prototype 的构造器上。因此,为了规范,在重新定义原型时,一定要给 constructor 重新赋值,以保证构造器不被改变。除非是有预期的希望它改变。

function Animal(){}

var cat = new Animal()
is(cat, Animal)                  // true

Animal.prototype = {}
var dog = new Animal()
is(dog, Animal)                 // false
is(dog, Object)                 // true

Animal.prototype.constructor = Animal
var tiger = new Animal()
is(tiger, Animal)               // true
is(tiger, Object)               // false

 

  instanceof 运算符

    在上一节中我们说到,如果手动更改构造函数的 prototpye 的话, constructor 方法就会失效。这种情况下,除了手动为构造函数指定 constructor 外,还有一种方法就是使用 instanceof 运算符。

cat instanceof Animal         // false
dog instanceof Animal         // true
tiger instanceof Animal       // true

    在上面的代码中, cat instanceof Animal 输出的是  false ,为什么呢?

    这是因为 instanceof 检测是是对象的原型链中是否包含某个构造函数的 prototype 属性。即 instanceof 检测的是原型。在上面的代码中, Animal 在 cat 实例化之后,将 prototype 属性改成 {},所以  cat._proto_ 中,已经不包括 Animal.prototype 了,所以输出为 false 。所以  instanceof 运算符也存在自定义类型的继承问题。但对于没被修改过 prototype 属性的内置对象而言, instanceof 方法还是可以判断的。

({}) instanceof Object;               // true
[] instanceof Array;                  // true
(function(){}) instanceof Function;   // true
/\w+/ instanceof RegExp;              // true
new Date instanceof Date;             // true
new Error instanceof Error;           // true
new Map instanceof Map;               // true
new Set instanceof Set;               // true

// 由于默认情况下,对象都是继承自 Object,所以引用类型都是 Object 的实例。
[] instanceof Object                  // true

    由于 instanceof  是根据原型链来进行检查的,所以适用于任何引用类型。而基础类型并没有原型,所以并不能检测出来。对于有包装对象的几个继承类型, instanceof 也不会隐性转换,除非用包装对象进行显性转换才可检测出来。

3 instanceof Number;                  // false
Number(3) instanceof Number           // false
new Number(3) instanceof Number       // true

'3' instanceof String;                // false
String('3') instanceof String         // false
new String('3') instanceof String     // true

true instanceof Boolean;              // false
Boolean(true) instanceof Boolean      // false
new Boolean(true) instanceof Boolean  // true

Symbol() instanceof Symbol;           // false
Object(Symbol()) instanceof Symbol    // true

// 特别的,虽然 typeof null 等于 object,但 null 并不是 object 的实例
null instanceof Object;               // false

 

  prototype.isPrototypeOf() 方法

    方法用于测试一个对象是否存在于另一个对象的原型链上,用法为 XXX.prototype.isPrototypeOf(instance) 。

Object.prototype.isPrototypeOf({});                  // true
Array.prototype.isPrototypeOf([]);  ;                // true
Function.prototype.isPrototypeOf(function(){});      // true
RegExp.prototype.isPrototypeOf(/\w+/);               // true
Date.prototype.isPrototypeOf(new Date);              // true
Error.prototype.isPrototypeOf(new Error);            // true
Map.prototype.isPrototypeOf(new Map);                // true
Set.prototype.isPrototypeOf(new Set);                // true

// 由于默认情况下,对象都是继承自 Object,所以引用类型都是 Object 的实例。
Object.prototype.isPrototypeOf(function(){})         // true

Number.prototype.isPrototypeOf(3);                   // false
Number.prototype.isPrototypeOf(Number(3));           // false
Number.prototype.isPrototypeOf(new Number(3));       // true

String.prototype.isPrototypeOf('3');                 // false
String.prototype.isPrototypeOf(String('3'));         // false
String.prototype.isPrototypeOf(new String('3'));     // true

Boolean.prototype.isPrototypeOf(true);               // false
Boolean.prototype.isPrototypeOf(Boolean(true);       // false
Boolean.prototype.isPrototypeOf(new Boolean(true));  // true

Symbol.prototype.isPrototypeOf(Symbol());            // false
Symbol.prototype.isPrototypeOf(Object(Symbol()));    // true

// 特别的,虽然 typeof null 等于 object,Object 并不在 null 的原型链上
Object.prototype.isPrototypeOf(null);                // false

    由上可知, prototype.isPrototypeOf() 方法与 instanceof 操作符走的是互相逆向的两条路, instanceof 是从实例出发,查找实例上是否有某构造函数的 prototype 属性,而 prototype.isPrototypeOf() 则是从构造函数出发,寻找该构造函数是否在某个已存在的实例的原型链上。

    故而,如果 A instanceof B 为真,则 B.prototype.isPrototypeOf(A) 也一定为真。

 

  Object.prototype.toString.call() 方法

    在最新的ES6规范中,关于 Object.prototype.toString() 方法是这么规定的:

    也就是说,如果上下文对象为 null 和 undefined ,返回 "[object Null]" 和 "[object Undefined]" ,如果是其他值,先将其转为对象,然后依次检测数组、字符串、arguments对象、函数及其它对象,得到一个内建的类型标记,最后拼接成 "[object Type]" 这样的字符串。由此可见, Object.prototype.toString.call() 方法是根正苗红的用来检测内置对象类型的方法。

var _toString = Object.prototype.toString;

function is(value, typeString) {
    // 获取到类型字符串
    var stripped = _toString.call(value).replace(/^\[object\s|\]$/g, '');
    if (stripped === 'Number' && typeString === 'isNaN') {
        return isNaN(value);
    }
    return stripped === typeString;
}

var x;
is(x, 'Undefined')              // true

var div = document.getElementById('div')
is(div, 'Null')                 // true

var y = 2 + undefined
is(y, 'isNaN')                  // true

is(3, 'Number')                 // true
is('3', 'String')               // true
is(true, 'Boolean')             // true
is(Symbol(), 'Symbol')          // true
is({}, 'Object')                // true
is([], 'Array')                 // true
is(new Map(), 'Map')            // true
is(new Set(), 'Set')            // true
is(new Date(), 'Date')          // true
is(new Error(), 'Error')        // true
is(new RegExp(), 'RegExp')      // true
is(function(){}, 'Function')    // true
is(document, 'HTMLDocument')    // true
is(window, 'Window')            // true

    但是由于定义的原因, Object.prototype.toString.call() 对于自定义类型就心有余而力不足了。所以,对于自定义类型就只能借助上面所提到的 contructor 属性或者 instanceof 运算符来检测了。

 

  特殊方式

    isNaN():如上所述,专门用来判断 NaN 的数据,用法是 isNaN(variable) ;

    Array.isArray():用于确定传递的值是不是 Array。

// 当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes。
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

//在兼容性方面,Array.isArray在IE9-浏览器上没有这个方法,所以我们可以改写下方法以兼容低版本浏览器

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

// or
var isArray = function(arr){
   return typeof Array.isArray === "function" ? Array.isArray(arr) : Object.prototype.toString.call(arr) === "[object Array]";
};

var arr = [1, 2, 3, 4];
console.log(isArray(arr));                 // true
console.log(Array.isArray(arr));        // true

  

三、总结及知识结构图

  由此,简单总结下:

    •  Object.prototype.toString.call() :覆盖范围广,特别是对于JS内置数据类型,无论基本类似还是引用类型都适用。缺点是不能检测自定义类型,需要配合用到 instanceof 方法。
    •  constructor :通过原型链的继承实现检测,对大部分基本类似和引用类型适用,特别是改进后的 is 函数。但也是原型继承的原因,对于自定义类型的检测不太稳定,可配合 instanceof 方法使用。
    •  instanceof :因为是通过原型链检测的,所以仅适用于引用类型的检测。
    •  prototype.isPrototypeOf() :  instanceof 操作符的逆向操作,结论与 instanceof 操作符相同,同样仅适用于引用类型的检测。
    •  typeof :适用于基本类型的检测,对于 null 的检测结果是 object ,对于函数的检测结果是 function 。
    •  isNaN :检测传入值是不是 NaN ,也是该类数据的唯一精准检测方法。
    •  Array.isArray :检测传入值是不是 Array ,在跨 iframe 时,使用优先级高于 instanceof 操作符。

 

四、参考及拓展资料

  JavaScript系列文章:不能不看的数据类型检测

  JavaScript中数据类型转换

  JavaScript检测原始值、引用值、属性

posted @ 2019-02-26 15:24  橙橙的麦田  阅读(219)  评论(0编辑  收藏  举报