3

详细解读JavaScript中类型检测方案

 3 years ago
source link: https://zhuanlan.zhihu.com/p/126291383
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

详细解读JavaScript中类型检测方案

Enjoy what you are doing!

数据类型检测

v2-49a1e2dba902c9f9e4b9c64265a4e82e_720w.jpg

JavaScript中,数据类型检测有如下方法:

  • typeof
  • instanceof
  • constructor
  • Object.prototype.toString.call
  • Array.isArray

接下来我们一一介绍每种方式的使用方式及使用技巧

typeof

首先看一下mdn中关于typeof返回值的介绍:

从上图中可以看到,typeof对于简单数据类型(Number,String,Boolean,Undefined)的判断是准确的。

对于复杂数据类型,typeof会将function进行单独处理。而function之外的数据类型,不管是字面量对象,还是Date, Regexp, Math都会统一返回object。当typeof用来处理null时也会返回object

总结一下:

  • typeof function === 'function'
  • typeof null === 'object'
  • typeof execeptFunctionObject === 'object'

当我们使用typeof来判断一个未声明的变量时,会返回undefined,并不会报错:

const a = 1
console.log(typeof a) // 'number'
console.log(typeof b) // 'undefined' (不会报错)

需要注意的是,typeof的返回值为string类型,我们看一下下边的代码:

console.log(typeof typeof []) // typeof 'object' => 'string'
console.log(typeof typeof {}) // typeof 'object' => 'string'

instanceof

参考:
* 类检查:"instanceof"

我们先看下mdn对于instanceof的介绍:

instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

对于instanceof我们可以这样理解:

  • 检测某个实例是否属于某个类
  • 所有出现在实例原型链上的类都会返回true

我们看下面一个例子:

console.log([] instanceof Array); // true
console.log([] instanceof Object); // true

instanceof的执行过程就是判断实例的__proto__是否为instanceof后边的类的prototype

是的话就会返回true,如果不是的话,继续从原型链向上查找,即实例的__proto__.__proto__是否为instanceof后边的类的prototype,直到找最后Object.prototype都没有找到的话,返回false,否则返回true

这里我们用代码模拟一下instanceof的查找过程,方便理解:

const _instanceof = function (instance, Class) {
  // let proto = instanceof.__proto__
  let proto = Object.getPrototypeOf(instance);
  while (Class.prototype) {
    if (proto === Class.prototype) {
      return true;
    }
    // proto = proto.__proto__
    proto = Object.getPrototypeOf(proto);
  }
  return false;
};

console.log(_instanceof([], Array));  // true

console.log(_instanceof([], Object)); // true

根据instanceof的原理可以做出如下推论:

  • 由于[].__proto__ === Array.prototype,所以[] instanceof Array返回true
  • 由于Array.prototype.__proto__ === Object.prototype,即[].__proto__.__proto === Object.prototype,所以[] instanceof Object返回true

由于instanceof是根据原型链来进行检测的,所以我们可以得出以下俩个结论:

  • 原型链的指向可以改变,所以instanceof用来检测数据类型并不完全准确
  • 由于简单数据类型没有__proto__属性,所以无法使用instanceof来检测类型

我们看下instanceof无法准确进行检测的例子:

const fn = function() { };

// 更改原型和原型链指向
fn.__proto__ = Array.prototype;
fn.prototype = Array.prototype;
const fn1 = new fn();

console.log(fn instanceof Function); // false
console.log(fn instanceof Array); // true

console.log(fn1 instanceof fn); // true
console.log(fn1 instanceof Array); // true

// 简单数据类型检测不准确
console.log(1 instanceof Number); // false

constructor

每一个函数都有一个prototype属性,该属性的值为对象。而prototype相比于普通对象,其天生就带有constructor属性,属性值为原型所属的函数:

const arr = [];
console.log(arr.constructor); // Array

上边我们定义了一个数组arr,在获取arrconstructor属性时:

  • 从本身的属性中查找并没有找到constructor属性
  • arr.__proto__中查找
  • 由于arrArray的实例,所以arr.__proto__ === Array.prototype
  • Array.prototype.constructor === Array,所以arr.constructorArray

constructorinstanceof类似,都是基于原型链来进行数据类型检测,所以当我们修改原型指向后,检测结果并不准确。

const arr = [];
// 当然,一般情况下我们并不会这样做,这里仅仅举例说明
Array.prototype.constructor = null;
console.log(arr.constructor); // null

Array.isArray

Array.isArray是构造函数Array上的一个属性,它可以判断一个值的类型是否为数组:

Array.isArray([1,2,3]); // true
Array.isArray(1); // false
Array.isArray('abc'); // false

当然,该方法只能用于判断数组,并不能判断是否为其它数据类型

Object.prototype.toString.call

在上文中介绍的数据类型检测方法都有一定的不足,这里我们介绍的Object.prototype.toString.call方法可以很好的判断数据类型,并且弥补了之前几种方法出现的问题。

下面我们看下如何使用该方法来进行数据类型检测:

const toString = Object.prototype.toString
// 判断简单数据类型
console.log(toString.call(1)); // '[object Number]'
console.log(toString.call('a')); // '[object String]'
console.log(toString.call(true)); // '[object Boolean]'
console.log(toString.call(null)); // '[object Null]'
console.log(toString.call(undefined)); // '[object Undefined]'

// 判断复杂数据类型,可以详细区分不同的对象
console.log(toString.call([])); // '[object Array]'
console.log(toString.call({})); // '[object Object]'
console.log(toString.call(new Date)); // '[object Date]'

即使我们更改原型链,也可以准确检测数据类型:

const arr = []
arr.__proto__ = Function.prototype
console.log(arr instanceof Array); // false
console.log(toString.call(arr)); // '[object Array]'

Object.prototype.toString并不会将对象转换为字符串,而是将对象的信息作为[object Type]的格式输出。

我们通过call方法来借用Object.prototype.toString,让其它非对象数据类型也能调用该方法,获取到其信息来进行数据检测。

如果你不知道选择哪种方式来检测数据类型的话,使用Object.prototype.toString.call方法准没错!

jQuery源码数据类型检测

在掌握了JavaScript中关于数据类型检测的相关知识后,我们看一下在jQuery中是如何进行数据类型检测的。

先看下如何使用jQuery中的数据类型检测方法:

jQuery.type( true ) === "boolean"
jQuery.type( 3 ) === "number"
jQuery.type( "test" ) === "string"
jQuery.type( function(){} ) === "function"
jQuery.type( [] ) === "array"
jQuery.type( undefined ) === "undefined"

下面是笔者整理的jQuery中的类型检测相关代码:

var class2type = {};
var toString = class2type.toString; // Object.prototype.toString
var hasOwn = class2type.hasOwnProperty; // Object.prototype.hasOwnProperty
var fnToString = hasOwn.toString; // Function.prototype.toString
// Function.prototype.toString.call(Object)
// Object是一个类,也属于函数,可以调用函数的toString方法
var ObjectFunctionString = fnToString.call(Object);

// 一些常见数据类型
'Boolean Number String Function Array Date RegExp Object Error Symbol'.split(' ').forEach(function anonymous (item) {
  class2type['[object ' + item + ']'] = item.toLowerCase();
  // 拼接成如下格式 class2type[object Type] = type
});

function toType (obj) {
  // null/undefined == null
  // 若果obj是null或者undefined,返回其对应的字符串('null'或者'undefined')
  if (obj == null) {
    return obj + ''; // return 'null' / return 'undefined'
  }
  // 复杂数据类型: typeof object = 'object' ; typeof function = 'function'
  // 1. typeof obj === 'object' || typeof obj === 'function' , 这个逻辑表示obj为对象或者null,null之前已经进行了处理
  // 2. class2type[toString.call(obj)) || 'object',
  //    这个逻辑表示会根据obj调用Object.prototype.toString方法后是否是class2type的属性,
  //    是的话返回其类型,否则返回'object'
  // 3. 对象 ? class2type中的属性值类型或'object' : typeof obj,
  //    即复杂数据类型通过class2type中的属性值判断或者返回'object',简单数据类型直接使用typeof进行判断
  return typeof obj === 'object' || typeof obj === 'function' ? class2type[toString.call(obj)] || 'object' : typeof obj;
}

jQuery会将类型经过Object.prototype.toString方法转换后的结果作为key放到class2type中,并将类型的小写值作为属性值,最终在调用时返回

jQuery还为我们封装了一些工具方法方便进行类型判断:

var isWindow = function isWindow (obj) {
  // null和undefined返回false, window = window.window
  return obj != null && obj === obj.window;
};

// 普通对象的逻辑:
//    1. toString.call(obj) === '[object Object]'
//    2. Object.create(null) 如果没有原型并且满足条件1就是普通对象
//    3. 如果对象的原型有constructor属性,并且置为Object的话,就是普通对象
var isPlainObject = function isPlainObject (obj) {
  var proto, Ctor;
  if (!obj || toString.call(obj) !== '[object Object]') {
    return false;
  }
  // 获取obj的原型
  proto = Object.getPrototypeOf(obj);

  // 如果一个对象没有原型,那么是plain object
  // Objects with no prototype (`Object.create( null )`)
  if (!proto) {
    return true;
  }

  // proto原型自身有constructor属性,赋值给Ctor,否则Ctor为false
  // Objects with prototype are plain if they were constructed by a global Object function
  Ctor = hasOwn.call(proto, 'constructor') && proto.constructor;
  // 如果Ctor是一个函数,
  // 那么Ctor调用Function.prototype.toString方法是否和Object调用Function.prototype.toString方法结果相同
  return typeof Ctor === 'function' && fnToString.call(Ctor) === ObjectFunctionString;
};

// 空对象
var isEmptyObject = function isEmptyObject (obj) {
  var name;
  // 如果obj是空对象不会执行该循环
  // in 关键字会遍历原型上的一些属性和方法,需要与hasOwnProperty进行结合使用
  for (name in obj) {
    return false;
  }
  return true;
};

在日常开发中,我们可以结合jQuery源码封装自己的类型判断函数。

JavaScript中的类型检测方法各有特点,对于其用法我们做一下小结:

  • 简单数据类型可以使用typeof来判断,语法简单快捷
  • 可以直接使用Array.isArray来检测数组
  • instanceofconstructor可以用来检测对象的具体类型,但是在修改了原型链后,结果会不准确
  • Object.prototype.toString.call是一个万能公式,基本上可以用来检测JavaScript中所有的数据类

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK