ES 基础 —— 数据类型检测

published

基本数据类型

截至 ES2015,ES 已经定义了 7 种数据类型:

  • 6 种基本数据类型
    • Null
    • Undefined
    • Boolean
    • Number
    • String
    • Symbol
  • Object(拥有众多派生类型)
    • Boolean(Boolean 数据类型的 Object Wrapper)
    • Number(Number 类型的 Object Wrapper)
    • String(String 类型的 Object Wrapper)
    • Array
    • Function
    • RegExp
    • Promise
    • ArrayBuffer
    • TypedArray
    • Map
    • WeakMap
    • Set
    • WeakSet
    • ……

Object Wrapper 的相关内容可阅读「ES 基础 —— 原始值的对象包装器」一文。

检测方法

typeof 操作符

typeof operand

typeof 操作符返回一个字符串,指示未经计算的操作数类型。

类型 结果
Null 'object'
Undefined 'undefined'
Boolean 'boolean'
Number 'number'
String 'string'
Symbol 'symbol'
Function Object 'function'
其他 Object 'object'

检测范围

typeof 检测能力有限,只能检测以下范围内的类型:

  • 除 Null 外的 5 种基本数据类型
  • Object
    • Function Object

陷阱 —— 关于 typeof null === 'object'

typeof null === 'object' 是个历史遗留问题。在 JavaScript 最初的实现中:

  • 一个值是由 类型标签 和 实际数据值 共同表示的
  • 对象类型的类型标签是 0

null代表的是空指针,而大多数平台下空指针该值为 0x00,因此,null 的类型标签也就成了 0,typeof null 就返回了 'object'。 Brendan Eich 曾尝试修复这个问题 —— 使 typeof null === 'null',不过,该提案最终被否决。

陷阱 —— 关于 typeof function() {} === 'function'

ECMA-262 规定任何在内部实现 call 方法的对象都应该在应用 typeof 运算符时返回 function

instanceof 操作符

object instanceof constructor

instanceof 操作符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

检测范围

只可用于 Object 及其派生类型,无法用于基本数据类型。

陷阱 —— instanceof 与多个上下文

在浏览器中,多个 frame 或多个 window 之间交互时,会存在多个上下文。 不同的上下文拥有不同的全局对象,也就拥有不同的内置类型构造函数。这种情形下,使用 instanceof 会遭遇一些问题,比如:

[] instanceof window.frame[0].Array // false

在这种情形下,可以使用更可靠的内置函数,比如 Array.isArray。或者下面提到的 Object.prototype.toString.call()

Object.prototype.toString.call() 方法

Object.prototype.toString.call(data)

例子:
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call([]) // '[object Array]'

另一个等价的方法(Reflect 于 ES2015 引入):

Reflect.apply(Object.prototype.toString, data, [])

例子:
Reflect.apply(Object.prototype.toString, null, []) // '[object Null]'
Reflect.apply(Object.prototype.toString, [], []) // '[object Array]'

以上两种方法的原理:在任意值上调用 toString() 方法,获得一个格式为 [object NativeConstructorName] 的字符串,用于进行类型检测。

每个类都有一个内部属性 [[Class]]。这个属性的值就是上述的 NativeConstructorName

检测范围

可用于任意原生数据类型的检测。

陷阱

Object.prototype.toString.call() 方法不能检测非原生构造函数生成的对象,因为开发人员定义的任何构造函数的 [[Class]] 内部属性都是 Object

方法论

  • 三种特殊的基本数据类型:Null 类型和 Undefined 类型分别都只有一个值,Boolean 类型只有两个值。它们可以直接通过严格相等 === 进行检测。
  • 其他数据类型:Boolean,Number,String,Symbol,Object(只限于 Object 与其派生类型 Function,不包含其它派生类型)通过 typeof 进行检测。 > 虽然 Object.prototype.toString.call() 也可以检测以上数据类型,但还是更倾向于使用 ===typeof,因为它们是运算符,使用它们,可以规避函数调用,拥有更快的运行速度。
  • Object 的派生类型:通过 Object.prototype.toString.call() 进行检测。
  • 非原生构造函数生成的对象使用 instanceof 进行检测。(不过,如果为构造函数添加了 Symbol.toStringTag 相关设置,也可以通过 Object.prototype.toString.call() 进行检测。)
// 代码来自 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
class ValidatorClass {
  get [Symbol.toStringTag]() {
    return 'Validator'
  }
}

Object.prototype.toString.call(new ValidatorClass()) // '[object Validator]'

这样就完美了。

Lodash 类型检测方法分析

  • Lodash 版本: 4.17.4

辅助代码

类型标记,用来与 Object.prototype.toString.call() 的调用结果比对:

/** `Object#toString` result references. */
const argsTag = '[object Arguments]',
  arrayTag = '[object Array]',
  asyncTag = '[object AsyncFunction]',
  boolTag = '[object Boolean]',
  dateTag = '[object Date]',
  domExcTag = '[object DOMException]',
  errorTag = '[object Error]',
  funcTag = '[object Function]',
  genTag = '[object GeneratorFunction]',
  mapTag = '[object Map]',
  numberTag = '[object Number]',
  nullTag = '[object Null]',
  objectTag = '[object Object]',
  promiseTag = '[object Promise]',
  proxyTag = '[object Proxy]',
  regexpTag = '[object RegExp]',
  setTag = '[object Set]',
  stringTag = '[object String]',
  symbolTag = '[object Symbol]',
  undefinedTag = '[object Undefined]',
  weakMapTag = '[object WeakMap]',
  weakSetTag = '[object WeakSet]'

const arrayBufferTag = '[object ArrayBuffer]',
  dataViewTag = '[object DataView]',
  float32Tag = '[object Float32Array]',
  float64Tag = '[object Float64Array]',
  int8Tag = '[object Int8Array]',
  int16Tag = '[object Int16Array]',
  int32Tag = '[object Int32Array]',
  uint8Tag = '[object Uint8Array]',
  uint8ClampedTag = '[object Uint8ClampedArray]',
  uint16Tag = '[object Uint16Array]',
  uint32Tag = '[object Uint32Array]'

判断是否为类 Object 对象(值的 typeof 结果为 object,且值不是 null):

function isObjectLike (value) {
  // 介绍 typeof 时,提到 typeof null === 'object',所以这里需要将其剔除。
  // 为什么使用了 != / == ,而不是 != / === ?个人觉得是可以替换的。
  return value != null && typeof value == 'object'
}

baseGetTagObject.prototype.toString.call() 的封装,以保证总是使用忽略 Symbol.toStringTag 属性值的 Object.prototype.toString.call()

// Symbol.toStringTag 的相关信息阅读。
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
const symToStringTag = Symbol ? Symbol.toStringTag : undefined

// 封装原生 Object.prototype.toString.call(),使其忽略 Symbol.toStringTag 属性值。
function getRawTag(value) {
  const isOwn = Object.prototype.hasOwnProperty.call(value, symToStringTag)
  const tag = value[symToStringTag]

  try {
    value[symToStringTag] = undefined
    var unmasked = true
  } catch (e) {}

  var result = Object.prototype.toString.call(value)
  if (unmasked) {
    if (isOwn) {
      value[symToStringTag] = tag
    } else {
      delete value[symToStringTag]
    }
  }
  return result
}

// 原生 Object.prototype.toString.call()
function objectToString (value) {
  return Object.prototype.toString.call(value)
}

function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? undefinedTag : nullTag
  }

  // 依据 Symbol.toStringTag 的存在与否,调用不同版本的 Object.prototype.toString.call(),
  // 以确保总是使用忽略 `Symbol.toStringTag` 属性值的 Object.prototype.toString.call()。
  return (symToStringTag && symToStringTag in Object(value))
    ? getRawTag(value)
    : objectToString(value)
}

类型检测方法

7 种数据类型

isNull
function isNull (value) {
  return value === null
}
isUndefined
function isUndefined (value) {
  return value === undefined
}
isBoolean

在检测 Boolean 基本数据类型的同时,也检测 Boolean 对象。

function isBoolean(value) {
  return value === true || value === false ||
    (isObjectLike(value) && baseGetTag(value) == boolTag)
}
isNumber

在检测 Number 基本数据类型的同时,也检测 Number 对象。

function isNumber(value) {
  return typeof value == 'number' ||
    (isObjectLike(value) && baseGetTag(value) == numberTag)
}
isString

在检测 String 基本数据类型的同时,也检测 String 对象。

function isNumber(value) {
  return typeof value == 'string' ||
    (isObjectLike(value) && baseGetTag(value) == stringTag)
}
isSymbol

在检测 Symbol 基本数据类型的同时,也检测 Symbol 对象。

因为 new Symbol() 会出现 TypeError,你可能会误以为 Symbol 类型不可能会有相应的对象的。事实是,真的有 Symbol 类型对应的对象。 new Symbol()TypeError 错误是为了防止开发者创建 Symbol 对象而不是 Symbol 值而故意为之的。如果你真的想创建的 Symbol 对象,可以通过 Object(Symbol('foobar')) 来完成。

function isSymbol(value) {
  return typeof value == 'symbol' ||
    (isObjectLike(value) && baseGetTag(value) == symbolTag)
}
isObject

检测是否为对象,要注意 Null 类型和 Function Object。

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

Object 衍生类型

isArray

ES2015 提供的 Array.isArray 方法。

var isArray = Array.isArray
isFunction

Function 除了包括常见的 Function,还有 Generator、Async Function、Proxy。

function isFunction (value) {
  if (!isObject(value)) {
    return false
  }
  // The use of `Object#toString` avoids issues with the `typeof` operator
  // in Safari 9 which returns 'object' for typed arrays and other constructors.
  var tag = baseGetTag(value)
  return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag
}
isMap
var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap

// The base implementation of `_.isMap` without Node.js optimizations.
function baseIsMap (value) {
  return isObjectLike(value) && getTag(value) == mapTag
}

如果不考虑 Node.js 提供的检测方法,其实就是方法论中提到的套路。

isWeakMap
function isWeakMap(value) {
  return isObjectLike(value) && getTag(value) == weakMapTag
}
其他

剩下的类型检测方法多是检测 Object 派生类型的方法了。大致的套路是:

  • 如果有 Node.js 提供的方法,就用 Node.js 提供的方法,如 isMap
  • 如果没有 Node.js 提供的方法,就用 Object.prototype.toString.call(),如 isWeakMap

最后

该说的都说了,就到这里吧。