lodash 是如何做类型检测的

7,371 阅读8分钟

欢迎Star:github.com/FatGe/FatGe…

js 基本数据类型

JS 的基本数据类型有 NumberStringBooleanSymbolNullUndefined,六种数据类型。一种引用类型 object

基本数据类型

Number

数值,根据 ECMAScript 标准,JavaScript 中只有一种数字类型:基于 IEEE 754 标准的双精度 64 位二进制格式的值(-(263 -1) 到 263 -1)。它并没有为整数给出一种特定的类型

除了能够表示浮点数外,还有一些带符号的值:+Infinity-InfinityNaN (非数值,Not-a-Number)。

对应 lodash 中的检测函数有

  • isNumber 检查 value 是否是原始 Number 数值型 或者 对象;
  • isInteger 检查 value 是否为一个整数;
  • isNaN 检测 value 是否为 NaN
  • isFinite 检测 value 是否是原始有限数值。
isNumber
function isNumber(value) {
  return typeof value == 'number' ||
    (isObjectLike(value) && getTag(value) == '[object Number]')
}

typeof 操作符可以返回一个字符串,表示未经计算的操作数的类型。对于 Number、String、Boolean、Undefined、String 可以很明确的得到它的类型。

那么 lodash 为什么还要添加 (isObjectLike(value) && getTag(value) == '[object Number]')

原因在于,JS 中也允许我们以如下形式创建一个数值

const value = new Number(1)
console.log(value) // log 1
console.log(typeof value) // log "object"

这时,单单只是使用 typeof 操作符就没法判断 value 的类型是否为数值。所以要结合以下两个函数来判断,value 是否为 object 然后再通过过 toString() 来获取每个对象的类型。

function getTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  return Object.prototype.toString.call(value)  
}

function isObjectLike(value) {
  return typeof value == 'object' && value !== null
}

Object.prototype.toString.call 每个对象都有一个toString()方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。

isInteger
function isInteger(value) {
    return 
    	typeof value == 'number' 
    	&& value == toInteger(value);
}

检查 value 是否为一个整数,判断是否 value 的类型是否为数值,并且是否与 Int 型相同。其取整过程如下

function toInteger(value) {
    var result = toFinite(value),
        remainder = result % 1;

    return result === result ? 
        (remainder ? result - remainder : result) : 0;
}
isNaN

检查 value 是否是 NaN

function isNaN(value) {
    return isNumber(value) && value != +value;
}

与 ES 2015 的 isNaN 不同的是,对于 undefined{},原生的结果是 true,而 lodashfalse。这是因为如果isNaN函数的参数不是Number类型, isNaN函数会首先尝试将这个参数转换为数值,然后才会对转换后的结果是否是NaN进行判断。

// js native isNaN
var isNaN = function(value) {
    var n = Number(value);
    return n !== n;
};

但是无论是 ES 2015 还是 lodash,它们本质上都是利用 x != x 来判断 NaN

isFinite

检查 value 是否是原始有限数值。

function isFinite(value) {
    return typeof value == 'number' 
    	&& nativeIsFinite(value);
}

利用原生的 isFinite 结合 typeof 判断数字是否为有限值。

String

String 类型用于表示由零或多个16 位Unicode 字符组成的字符序列,即字符串。用于保存可以以文本形式表示的数据非常有用。

值得注意的是,不单单要注意基本字符串,还需要注意字符串对象,字符串字面量 (通过单引号或双引号定义) 和 直接调用 String 方法(没有通过 new 生成字符串对象实例)的字符串都是基本字符串。

JavaScript会自动将基本字符串转换为字符串对象,只有将基本字符串转化为字符串对象之后才可以使用字符串对象的方法。

与之前的 number 类似,利用构造函数 String 创建的字符串是一个 object

const s_prim = "foo";
const s_obj = new String(s_prim);

console.log(typeof s_prim); // Logs "string"
console.log(typeof s_obj);  // Logs "object"

所以检测字符串,除了基本字符串以外还要注意字符串对象。

function isString(value) {
  const type = typeof value
  return 
    type == 'string' || 
        (type == 'object' 
         	&& value != null 
         	&& !Array.isArray(value) 
         	&& getTag(value) == '[object String]')
}

可以利用 typeof 检测基本字符串,对于模板字符串采用了之前介绍的方案 getTag 来获取 value 的类型。

Boolean

Boolean 类型是ECMAScript 中使用得最多的一种类型,该类型只有两个字面值:truefalse。同样也需要区分基本的 Boolean 类型以及 Boolean 对象。

function isBoolean(value) {
  return 
    value === true || value === false ||
    (isObjectLike(value) 
     && getTag(value) == '[object Boolean]')
}

大部分在之前都已经涉及到了,这里出现了 isObjectLike,那么它是做什么的。

function isObjectLike(value) {
  return typeof value == 'object' && value !== null
}

原来只是检测是否是一个非 null 的对象。

Symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。Symbol 值通过Symbol函数生成。

function isSymbol(value) {
  const type = typeof value
  return type == 'symbol' || 
      (isObjectLike(value) && 
       getTag(value) == '[object Symbol]')
}

会发现 (isObjectLike(value) && getTag(value) == '[object Symbol]'),也对 Symbol 对象进行检测,但是如果直接 new Symbol 会 log 出 TypeError

那么 lodash 为什么要对其进行检测,原来是创建一个显式包装器对象从 ECMAScript 6 开始不再被支持,现在可以利用如下代码来模拟,虽然没什么用。

const sym = Symbol("foo");
typeof sym;     // "symbol"
const symObj = Object(sym);
typeof symObj;  // "object"

Undefined

Undefined 类型只有一个值,即特殊的 undefined。在使用 letvar 声明变量但未对其加以初始化时,这个变量的值就是 undefined

function isUndefined(value) {
    return value === undefined;
}

Null

Null 类型是只有一个值的数据类型,这个特殊的值是 null 。与 undefined 不同的是,它是一个字面量,而 undefined 是全局对象的一个属性。

从逻辑角度来看,null 值表示一个空对象指针,null 是表示缺少的标识,指示变量未指向任何对象。而这也正是使用typeof 操作符检测null 值时会返回"object"的原因。

对其的判断也非常的简单,只需要

function isNull(value) {
  return value === null
}

当然你也可以使用

console.log(Object.prototype.toString.call(null))
// [object Null]

以上是基本数据类型的判断,总结一下,主要是利用 typeOf 以及 Object.prototype.toString ,还有一些特殊值的特性。下面开始分析引用类型 Object

引用类型

引用类型的值(对象)是引用类型的一个实例。在ECMAScript 中,引用类型是一种数据结构,用于将数据和功能组织在一起。具体的有 ObjectArrayDateErrorRegExpFunction,还有ES2015 引入 SetMapWeakSetWeakMap

Object

ECMAScript 中的对象其实就是一组数据和功能的集合。它有一个很重要的用途,就是在 JavaScript 中的所有对象都来自 Object;所有对象从Object.prototype继承方法和属性,尽管它们可能被覆盖。即在ECMAScript 中,Object 类型是所有它的实例的基础。

所以 Lodash 去判断 value 是否为 Object 时,只使用了 typeOf 操作即可。

function isObject(value) {
  const type = typeof value
  return value != null && 
      (type == 'object' || type == 'function')
}

Function

Function 构造函数 创建一个新的Function对象。 在 JavaScript 中, 每个函数实际上都是一个Function对象。

function isFunction(value) {
  if (!isObject(value)) {
    return false
  }

  const tag = getTag(value)
  return tag == '[object Function]' || 
      	tag == '[object AsyncFunction]' ||
    	tag == '[object GeneratorFunction]' || 
      	tag == '[object Proxy]'
}

有个问题,typeOf 可以检测 Function对象的类型为 Function, 那为什么还需要 Object.prototype.toString 呢?

// in Safari 9 which returns 'object' for typed arrays and other constructors.

Array

Array 在 ECMAScript 中代表数组,它的每一项可以保存任何类型的数据。

对它的常规检测就是 Array.isArrayLodash 也是使用这个 API,如果需要 Polyfill 方案的话,可以使用

// plan 1
Object.prototype.toString.call(value) === '[object Array]'
// plan 2
value.constructor === Array

之前还有 value instanceof Array 会什么问题么?

在存在不同全局变量的环境,通过语义 instanceof 检测数组的时候,value instanceof Array只有当 value 是由该页面的原始 Array 构造函数创建的数组时才能正常工作。

具体请见,web.mit.edu/jwalden/www…

Date

ECMAScript 中的 Date 类型是在早期Java 中的java.util.Date 类基础上构建的。

const nodeIsDate = nodeTypes && nodeTypes.isDate

const isDate = nodeIsDate
  ? (value) => nodeIsDate(value)
  : (value) => isObjectLike(value) && getTag(value) == '[object Date]'

Lodash 分为两个环境来处理这个问题,如果是 Node 就利用 util.types.isDate(value) 来检测,如果是在游览器,就还是通过 Object.prototype.toString 来判断。

Set

ES2015 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

const isSet = nodeIsSet
  ? (value) => nodeIsSet(value)
  : (value) => isObjectLike(value) && getTag(value) == '[object Set]'

同样的还有 Map

const isMap = nodeIsMap
  ? (value) => nodeIsMap(value)
  : (value) => isObjectLike(value) && getTag(value) == '[object Map]'

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

function isWeakSet(value) {
  return isObjectLike(value) && getTag(value) == '[object WeakSet]'
}

也是利用 Object.prototype.toString ,同样还有 WeakMap

function isWeakMap(value) {
  return isObjectLike(value) && getTag(value) == '[object WeakMap]'
}

Error

当运行时错误产生时,Error的实例对象会被抛出。

function isError(value) {
  if (!isObjectLike(value)) {
    return false
  }
    
  const tag = getTag(value)
  return tag == '[object Error]' || 
      tag == '[object DOMException]' ||
    (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value))
}

有之前一致的 Object.prototype.toString 依然可以用来判断对象是否是一个 Error,除此之外,如果对象满足以下条件,也可以被视为一个 Error

  • 具备 messagename 属性,且值为 string
  • 是普通对象。 也就是说该对象由 Object 构造函数创建,或者 [[Prototype]]null

那么如何检测普通对象呢?

function isPlainObject(value) {
  if (!isObjectLike(value) || getTag(value) != '[object Object]') {
    return false
  }
  if (Object.getPrototypeOf(value) === null) {
    return true
  }
  let proto = value
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }
  return Object.getPrototypeOf(value) === proto
}

主要是利用 Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值),同时和 value 本身的 [[Prototype]] 做判断。