基本数据类型
截至 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'
}
baseGetTag
为 Object.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 提供的方法,就用 Node 提供的方法,如
isMap
。 - 如果没有 Node 提供的方法,就用
Object.prototype.toString.call()
,如isWeakMap
。
最后
该说的都说了,就到这里吧。