本文主要讲解如何准确判断JavaScript中出现的各种类型和对象。(基本类型、Object类、Window对象、纯对象plainObject、类数组)其中部分参考了jQuery的函数实现。

typeof

JavaScript定义的数据类型有UndefinedNullBooleanNumberStringObjectSymbol(ES6新增)。

其中typeof对大部分的数据类型都能够准确识别,如下:

typeof undefined // "undefined"
typeof null // "object"
typeof true // "boolean"
typeof 1 // "number"
typeof "s" // "string"
typeof {} // "object"
typeof function a() {} // "function"
typeof Symbol('2') // "symbol"

其中返回的字符串首字母都是小写的。

对于typeof null === 'object'来说,这其实是一个bug

在JavaScript中,Object下还有很多细分的类型,比如说DateRegExpErrorArrayFunction

typeof除了能够准确的判断出Function之外,对于其他细分类型均返回object

Object.prototype.toString()

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

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

该方法至少可以识别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 = new Date();   // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g;          // [object RegExp]
var func = function a(){}; // [object Function]

function checkType() {
    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)

// 还有不常见的Math、JSON
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]

// 还有一个arguments
function a() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
a();

type API

结合上面我们可以写一个type函数,其中基本类型值走typeof,引用类型值走toString
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;
}

通过toLowerCase()小写化和typeof的结果是小写一致。

注意IE6中toString()会把UndefinedNull都识别为[object Object],所以加了一个判断,直接调用+来隐式toString()-> "null"

这里之所以class2type[Object.prototype.toString.call(obj)] || "object"是考虑到ES6新增的SymbolMapSet在集合class2type中没有,直接把他们识别成object

这个type其实就是jQuery中的type

isFunction

之后可以直接封装:

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

数组

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

jQuery3.0中已经完全使用Array.isArray()

plainObject

plainObject翻译为中文即为纯对象,所谓的纯对象,就是该对象是通过{}new Object()创建的。

判断是否为“纯对象”,是为了和其他对象区分开比如说null、数组以及宿主对象(所有的DOMBOM都是数组对象)等。

jQuery中有提供了该方法的实现,除了规定该对象是通过{}new Object()创建的,且对象含有零个或者多个键值对外,一个没有原型(__proto__)的对象也是一个纯对象。

console.log($.isPlainObject({})) // true

console.log($.isPlainObject(new Object)) // true

console.log($.isPlainObject(Object.create(null))); // true

jQuery3.0版本的plainObject实现如下:

var toString = Object.prototype.toString;

var hasOwn = Object.prototype.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);
}

注意最后这一句非常的重要:

hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object)

hasOwn.toString调用的其实是Function.prototype.toString()而不是Object.prototype.toString(),因为hasOwnProperty是一个函数,它的原型是Function,于是Function.prototype.toString覆盖了Object.prototype.toString

Function.prototype.toString()会把整个函数体转换成一个字符串。如果该函数是内置函数的话,会返回一个表示函数源代码的字符串。比如说:

Function.prototype.toString(Object) === function Object() { [native code] }

所以如果此时对象不是由内置构造函数生成的对象,这个hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object)false

function Person(name) {
    this.name = name;
}
var person = new Person("Devin");
plainObject(person) === false; // true
// 其实就是`hasOwn.toString.call(Ctor) === "function Person(name) { this.name = name; }"

Window对象

Window对象有一个特性:Window.window指向自身。

Window.window === Window; //true

类数组对象

常见的类数组有函数的argumentsNodeList对象。

判断

对于类数组对象,只要该对象中存在length属性并且length为非负整数且在有限范围之内即可判断为类数组对象。

JavaScript权威指南中提供了方法:

function isArrayLike(o) {
    if (o && // o is not null, undefined, etc
        // o is an object
        typeof o === "object" &&
        // o.length is a finite number
        isFinite(o.length) &&
        // o.length is non-negative
        o.length >= 0 &&
        // o.length is an integer
        o.length === Math.floor(o.length) &&
        // o.length < 2^32
        o.length < 4294967296) //数组的上限值
          return true;
    else 
          return false;
}

以上的判断无论是真的数组对象或是类数组对象都会返回true,那我们如何区分到底是真的数组对象还是类数组对象?

其实只需要先判断是否为数组对象即可。

function utilArray(o) {
    if (Array.isArray(o)) {
        return 'array';
    }
    if (isArrayLike(o)) {
        return 'arrayLike';
    } else {
        return 'neither array nor arrayLike';
    }
}

类数组对象的特征

类数组对象并不关心除了数字索引和length以外的东西。

比如说:

var a = {"1": "a", "2": "b", "4": "c", "abc": "abc", length: 5};
Array.prototype.join.call(a, "+"); // +a+b++c

其中,'0''3'没有直接省略为两个undefined,同样的abc被忽略为undefined

如果length多出实际的位数会补undefined(空位也补充undefined),少位则截断后面的数组成员。

var a = {"1": "a", "2": "b", "4": "c", "abc": "abc", length: 6};
Array.from(a); // [undefined, "a", "b", undefined, "c", undefined]

var a = {"1": "a", "2": "b", "4": "c", "abc": "abc", length: 5};
Array.from(a); // [undefined, "a", "b", undefined, "c"]

var a = {"1": "a", "2": "b", "4": "c", "abc": "abc", length: 4};
Array.from(a); // [undefined, "a", "b", undefined]

类数组对象的转换

Array.from

该方法从一个类似数组或可迭代对象中创建一个新的数组实例。

Array.from('foo');
// ["f", "o", "o"]

Array.prototype.slice

该方法返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象。

var a = {"0":"a", "1":"b", "2":"c", length: 3};
Array.prototype.slice.call(a, 0); // ["a", "b", "c"]

ES6扩展运算符

var a = "hello";
[...a]; //["h", "e", "l", "l", "o"]

参考链接:

https://github.com/mqyqingfen...

https://github.com/mqyqingfen...


绪北
1.8k 声望159 粉丝

偏后前端开发攻城狮 Tencent FE