各位编程领域的同仁、学习者们,大家下午好!
今天,我们齐聚一堂,共同深入探讨JavaScript中一个看似基础,实则充满奥秘和挑战的话题:类型判断。在JavaScript这个灵活多变的动态语言世界里,准确地识别变量的类型,是编写健壮、可维护、少bug代码的关键。我们将从最常见的 typeof 操作符出发,一路探寻到被誉为“终极武器”的 Object.prototype.toString.call() 方法,剖析它们各自的精确性、局限性,并展望现代JavaScript中类型判断的演进与最佳实践。
JavaScript:动态类型的双刃剑
JavaScript是一种弱类型、动态语言。这意味着我们无需在声明变量时指定其类型,变量的类型会在运行时根据赋给它的值而自动确定。这种特性赋予了JavaScript极高的灵活性和开发效率,但也带来了潜在的陷阱:类型不确定性。
想象一下,你正在编写一个函数,它可能接收数字、字符串、甚至是一个对象作为参数。如果不对参数进行类型判断,直接对其执行特定操作,就可能导致运行时错误,比如对一个对象执行数学运算,或者试图调用一个不存在的方法。因此,类型判断是确保代码健壮性、避免运行时错误、提高程序可预测性的关键一环。
接下来,我们将逐一审视JavaScript中各种类型判断的方法。
一、typeof 操作符:快速但粗糙的起点
typeof 是JavaScript中最简单、最常用的类型判断工具。它是一个一元操作符,返回一个字符串,表示其操作数的类型。
1.1 typeof 的基本用法与返回值
typeof 操作符的语法非常直接:typeof operand。它会返回以下字符串之一:
"undefined":如果变量是undefined。"boolean":如果变量是布尔值。"number":如果变量是数字。"string":如果变量是字符串。"symbol":如果变量是 Symbol 值(ES6新增)。"bigint":如果变量是 BigInt 值(ES2020新增)。"function":如果变量是函数。"object":如果变量是对象,或者null。
让我们通过一些代码示例来观察 typeof 的行为:
// 原始类型
console.log(typeof 100); // "number"
console.log(typeof 'Hello'); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol('id')); // "symbol"
console.log(typeof 10n); // "bigint" (BigInt 字面量)
// 函数
console.log(typeof function() {}); // "function"
console.log(typeof (() => {})); // "function"
// 对象类型
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof new Date()); // "object"
console.log(typeof /abc/); // "object"
console.log(typeof new Map()); // "object"
console.log(typeof new Set()); // "object"
// 一个臭名昭著的特例
console.log(typeof null); // "object" -- 这是一个历史遗留的bug
1.2 typeof 的精确性与局限性
精确性(优点):
- 简单快捷:语法简洁,执行效率高,是判断原始类型(
number,string,boolean,symbol,bigint,undefined)的首选方法。 - 区分函数:能够准确地区分出
function类型,这在JavaScript中非常有用,因为函数在JS中是“一等公民”,本身也是一种特殊的对象。
局限性(缺点):
null的误判:这是typeof最广为人知且最致命的缺陷。typeof null返回"object"。这是JavaScript语言设计之初的一个错误,至今未能修复,因为它会破坏大量的现有代码。因此,如果你需要判断一个值是否为null,必须使用严格相等===。let myNull = null; console.log(typeof myNull); // "object" console.log(myNull === null); // true- 无法区分具体的对象类型:对于所有非函数、非原始类型的引用类型(如数组
[]、日期new Date()、正则表达式/abc/、普通对象{}等),typeof一律返回"object"。这使得它在需要区分这些具体对象类型时毫无用处。console.log(typeof []); // "object" console.log(typeof {}); // "object" console.log(typeof new Date()); // "object" // 如何判断它是数组还是日期?typeof 帮不了你。 - 对类实例的识别:对于ES6的类(Class)实例,
typeof也只会返回"object"。class MyClass {} const instance = new MyClass(); console.log(typeof instance); // "object"
typeof 返回值总结表:
| 值 | typeof 返回值 |
备注 |
|---|---|---|
123 |
"number" |
|
'hello' |
"string" |
|
true |
"boolean" |
|
undefined |
"undefined" |
|
Symbol('id') |
"symbol" |
ES6新增 |
10n |
"bigint" |
ES2020新增 |
function() {} |
"function" |
|
null |
"object" |
历史遗留的Bug,非常重要! |
{} |
"object" |
普通对象 |
[] |
"object" |
数组 |
new Date() |
"object" |
日期对象 |
/abc/ |
"object" |
正则表达式对象 |
new Map() |
"object" |
Map对象 |
new Set() |
"object" |
Set对象 |
new MyClass() |
"object" |
自定义类实例 |
鉴于 typeof 的这些局限性,尤其是在处理 null 和各种引用类型时,我们不得不寻找更精确的类型判断方法。
二、instanceof 操作符:原型链的探索者
instanceof 操作符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。简单来说,它用来判断一个对象是否是某个特定构造函数的实例。
2.1 instanceof 的基本用法
instanceof 的语法是:object instanceof Constructor。它返回一个布尔值。
// 判断日期对象
const myDate = new Date();
console.log(myDate instanceof Date); // true
console.log(myDate instanceof Object); // true (Date 的原型链上也有 Object.prototype)
// 判断数组
const myArray = [1, 2, 3];
console.log(myArray instanceof Array); // true
console.log(myArray instanceof Object); // true (Array 的原型链上也有 Object.prototype)
// 判断普通对象
const myObject = {};
console.log(myObject instanceof Object); // true
console.log(myObject instanceof Array); // false
// 判断自定义类实例
class Person {}
const john = new Person();
console.log(john instanceof Person); // true
console.log(john instanceof Object); // true
// 判断函数 (函数也是对象)
function greet() {}
console.log(greet instanceof Function); // true
console.log(greet instanceof Object); // true
2.2 instanceof 的精确性与局限性
精确性(优点):
- 识别实例类型:对于自定义的类或内置的构造函数(如
Date,RegExp,Array等),instanceof能准确判断一个对象是否是其某个构造函数的实例。这在处理特定类型的对象时非常有用。 - 继承关系判断:由于它检查的是原型链,因此也能正确判断继承关系。如果
SubClass继承自SuperClass,那么new SubClass()的实例subInstance会同时满足subInstance instanceof SubClass和subInstance instanceof SuperClass。
局限性(缺点):
-
不适用于原始类型:
instanceof只能用于判断对象,对于原始类型的值(number,string,boolean,symbol,bigint,undefined,null)它总是返回false。console.log(123 instanceof Number); // false (123是原始数字,不是Number对象) console.log('hello' instanceof String); // false console.log(true instanceof Boolean); // false console.log(null instanceof Object); // false (null不是对象) console.log(undefined instanceof Object); // false // 注意:如果使用包装对象,则会返回true const numObj = new Number(123); console.log(numObj instanceof Number); // true -
跨 Realm 问题 (iframe):这是
instanceof最严重的局限性之一。当一个对象在一个 JavaScript Realm (例如一个iframe) 中创建,并试图在另一个 Realm 中检测其类型时,instanceof可能会给出错误的结果。这是因为每个 Realm 都有自己独立的全局对象和一套内置构造函数。<!-- index.html --> <iframe id="myIframe" src="about:blank"></iframe> <script> const iframe = document.getElementById('myIframe'); const iframeWindow = iframe.contentWindow; const iframeArray = new iframeWindow.Array(1, 2, 3); console.log(iframeArray instanceof Array); // false (在主页面中,iframeArray 不是当前 Array 构造函数的实例) console.log(iframeArray instanceof iframeWindow.Array); // true </script>在上面的例子中,
iframeArray是由iframeWindow.Array构造的,而不是当前页面window.Array构造的,所以instanceof Array返回false。 -
原型链被修改:如果对象的原型链被手动修改,
instanceof可能会产生误导。class Animal {} class Dog extends Animal {} const puppy = new Dog(); console.log(puppy instanceof Dog); // true // 修改原型链 Object.setPrototypeOf(puppy, Animal.prototype); console.log(puppy instanceof Dog); // false (虽然它最初是Dog的实例,但原型链已断开) console.log(puppy instanceof Animal); // true null和undefined不会抛出错误:instanceof对于null或undefined会直接返回false,而不会像访问属性那样抛出错误。这本身不是坏事,但需要注意。
instanceof 返回值总结表:
| 值 | instanceof Date |
instanceof Array |
instanceof Object |
instanceof Function |
instanceof Number (原始值) |
|---|---|---|---|---|---|
new Date() |
true |
false |
true |
false |
false |
[] |
false |
true |
true |
false |
false |
{} |
false |
false |
true |
false |
false |
function() {} |
false |
false |
true |
true |
false |
null |
false |
false |
false |
false |
false |
undefined |
false |
false |
false |
false |
false |
123 |
false |
false |
false |
false |
false |
new Number(123) |
false |
false |
true |
false |
true |
由于 instanceof 的跨 Realm 缺陷和不适用于原始类型,它也不是一个完美通用的类型判断方案。
三、Object.prototype.toString.call():精确的内置类型识别器
Object.prototype.toString.call() 是JavaScript中用于精确判断内置对象类型(包括原始类型的包装对象)最可靠的方法。它利用了 Object.prototype.toString 方法的内部机制。
3.1 Object.prototype.toString 的内部机制
每个对象都有一个 toString() 方法。默认情况下,Object.prototype.toString() 返回一个表示该对象的字符串。这个字符串的格式是 "[object Class]",其中 Class 是对象的内部 [[Class]] 属性值(在ES5及之前规范中)。
在ES6及更高版本中,[[Class]] 内部槽已经被 Symbol.toStringTag 属性取代。当 Object.prototype.toString() 被调用时,它会检查对象的 Symbol.toStringTag 属性。如果该属性存在且是一个字符串,则使用它的值作为 Class 部分;否则,会回退到一些默认的内置值。
例如:
- 对普通对象调用
toString(),默认返回"[object Object]"。 - 对数组调用
toString(),默认返回"[object Array]"。 - 对函数调用
toString(),默认返回"[object Function]"。 - 对日期对象调用
toString(),默认返回"[object Date]"。
关键在于,我们可以通过 call() 方法来强制 Object.prototype.toString 在任何对象上下文上执行,从而获取其内部的 [[Class]] 或 Symbol.toStringTag 值。
3.2 Object.prototype.toString.call() 的用法
function checkType(value) {
return Object.prototype.toString.call(value);
}
// 原始类型
console.log(checkType(100)); // "[object Number]"
console.log(checkType('Hello')); // "[object String]"
console.log(checkType(true)); // "[object Boolean]"
console.log(checkType(undefined)); // "[object Undefined]"
console.log(checkType(null)); // "[object Null]" // 解决了 typeof 的痛点!
console.log(checkType(Symbol('id'))); // "[object Symbol]"
console.log(checkType(10n)); // "[object BigInt]"
// 对象类型
console.log(checkType({})); // "[object Object]"
console.log(checkType([])); // "[object Array]"
console.log(checkType(function() {})); // "[object Function]"
console.log(checkType(new Date())); // "[object Date]"
console.log(checkType(/abc/)); // "[object RegExp]"
console.log(checkType(new Map())); // "[object Map]"
console.log(checkType(new Set())); // "[object Set]"
console.log(checkType(new Promise(() => {}))); // "[object Promise]"
console.log(checkType(new WeakMap())); // "[object WeakMap]"
console.log(checkType(new WeakSet())); // "[object WeakSet]"
console.log(checkType(new ArrayBuffer(8))); // "[object ArrayBuffer]"
console.log(checkType(new DataView(new ArrayBuffer(8)))); // "[object DataView]"
我们可以封装一个更通用的函数来获取类型字符串:
function getType(value) {
if (value === null) {
return "Null";
}
const typeString = Object.prototype.toString.call(value);
return typeString.slice(8, -1); // 截取 "[object ???]" 中的 "???" 部分
}
console.log(getType(123)); // "Number"
console.log(getType('abc')); // "String"
console.log(getType(true)); // "Boolean"
console.log(getType(undefined)); // "Undefined"
console.log(getType(null)); // "Null"
console.log(getType({})); // "Object"
console.log(getType([])); // "Array"
console.log(getType(function(){})); // "Function"
console.log(getType(new Date())); // "Date"
console.log(getType(/xyz/)); // "RegExp"
console.log(getType(Symbol('foo'))); // "Symbol"
console.log(getType(100n)); // "BigInt"
3.3 Object.prototype.toString.call() 的精确性与局限性
精确性(优点):
- 准确识别所有内置类型:无论是原始类型(通过其包装对象的形式,如
"[object Number]")还是各种内置引用类型(Array,Date,RegExp,Map,Set,Promise等),它都能返回一个精确且唯一的字符串标识。 - 解决了
typeof null的问题:它能正确识别null为"[object Null]"和undefined为"[object Undefined]"。 -
解决了
instanceof的跨 Realm 问题:Object.prototype.toString.call()不依赖于构造函数的引用,因此即使对象是在不同的iframe中创建的,也能正确识别其内置类型。<!-- index.html --> <iframe id="myIframe" src="about:blank"></iframe> <script> const iframe = document.getElementById('myIframe'); const iframeWindow = iframe.contentWindow; const iframeArray = new iframeWindow.Array(1, 2, 3); console.log(Object.prototype.toString.call(iframeArray)); // "[object Array]" -- 正确! console.log(iframeArray instanceof Array); // false -- 错误! </script>
局限性(缺点):
- 无法区分自定义类实例:对于自定义的类实例,
Object.prototype.toString.call()默认情况下只会返回"[object Object]"。它无法像instanceof那样区分出具体的自定义类。class MyClass {} const instance = new MyClass(); console.log(Object.prototype.toString.call(instance)); // "[object Object]"但是,ES6引入了
Symbol.toStringTag,可以为自定义对象定制toStringTag:class MyCustomType { constructor() { this[Symbol.toStringTag] = 'MyCustomType'; } } const customInstance = new MyCustomType(); console.log(Object.prototype.toString.call(customInstance)); // "[object MyCustomType]"这使得我们可以为自定义类型提供更精确的
toStringTag标识,但它需要开发者主动设置。 - 相对繁琐:相比
typeof和instanceof,Object.prototype.toString.call()的语法稍显冗长,通常需要封装成一个辅助函数来简化使用。 - 返回字符串而非布尔值:它返回的是一个字符串,如果你需要布尔值结果,需要进行字符串比较。
Object.prototype.toString.call() 返回值总结表:
| 值 | Object.prototype.toString.call(value) 返回值 |
备注 |
|---|---|---|
123 |
"[object Number]" |
原始数字的包装类型 |
'hello' |
"[object String]" |
原始字符串的包装类型 |
true |
"[object Boolean]" |
原始布尔值的包装类型 |
undefined |
"[object Undefined]" |
|
null |
"[object Null]" |
解决了 typeof 的痛点 |
Symbol('id') |
"[object Symbol]" |
|
10n |
"[object BigInt]" |
|
function() {} |
"[object Function]" |
|
{} |
"[object Object]" |
普通对象 |
[] |
"[object Array]" |
数组 |
new Date() |
"[object Date]" |
日期对象 |
/abc/ |
"[object RegExp]" |
正则表达式对象 |
new Map() |
"[object Map]" |
Map对象 |
new Set() |
"[object Set]" |
Set对象 |
new Promise(() => {}) |
"[object Promise]" |
Promise对象 |
new MyClass() |
"[object Object]" |
默认情况下,自定义类实例 |
new MyCustomType() |
"[object MyCustomType]" |
设置了 Symbol.toStringTag 的自定义类实例 |
四、其他类型判断方法与辅助工具
除了上述三种主要方法,JavaScript还提供了其他一些特定场景下的类型判断手段。
4.1 Array.isArray():数组的专属判官
鉴于 typeof [] 返回 "object" 且 instanceof Array 存在跨 Realm 问题,ES5引入了 Array.isArray() 方法,专门用于判断一个值是否为数组。这是判断数组最可靠、最推荐的方法。
const arr = [1, 2, 3];
const obj = {};
const str = 'hello';
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false
console.log(Array.isArray(str)); // false
console.log(Array.isArray(null)); // false
console.log(Array.isArray(undefined)); // false
// 解决了跨 Realm 问题
// 假设 iframeArray 是在 iframe 中创建的数组
// console.log(Array.isArray(iframeArray)); // true
4.2 constructor 属性:一个不推荐的选择
每个对象实例都有一个 constructor 属性,指向创建该实例的构造函数。
const num = 123;
const str = 'abc';
const arr = [1, 2, 3];
const obj = {};
console.log(num.constructor === Number); // true
console.log(str.constructor === String); // true
console.log(arr.constructor === Array); // true
console.log(obj.constructor === Object); // true
console.log(obj.constructor === Number); // false
局限性:
- 原始类型问题:
null和undefined没有constructor属性,直接访问会报错。原始值虽然有,但它们在访问constructor时会进行隐式装箱,将其转换为包装对象。 - 可被修改:
constructor属性是可以被修改的,这使得它变得不可靠。function MyConstructor() {} const instance = new MyConstructor(); instance.constructor = Array; // 恶意或无意修改 console.log(instance.constructor === MyConstructor); // false console.log(instance.constructor === Array); // true (此时判断会出错) - 跨 Realm 问题:与
instanceof类似,constructor也存在跨 Realm 问题。// 假设 iframeArray 是在 iframe 中创建的数组 // console.log(iframeArray.constructor === Array); // false (主页面的 Array 构造函数)
因此,constructor 属性不适合作为通用的类型判断手段。
4.3 isNaN(), Number.isFinite(), Number.isInteger():数字的精细判断
对于数字类型,JavaScript提供了一些更细粒度的判断方法:
isNaN(value):判断一个值是否是NaN(Not-a-Number)。注意:isNaN会先尝试将参数转换为数字。isNaN('abc')为true。console.log(isNaN(NaN)); // true console.log(isNaN(123)); // false console.log(isNaN('hello'));// true (因为 'hello' 转换成数字是 NaN) console.log(isNaN('123')); // false (因为 '123' 转换成数字是 123)Number.isNaN(value)(ES6):更严格的isNaN,它不会进行类型转换,只有当参数严格等于NaN时才返回true。这是判断NaN的推荐方法。console.log(Number.isNaN(NaN)); // true console.log(Number.isNaN(123)); // false console.log(Number.isNaN('hello'));// falseNumber.isFinite(value)(ES6):判断一个值是否是有限的数字(非Infinity,-Infinity, 或NaN)。console.log(Number.isFinite(100)); // true console.log(Number.isFinite(Infinity)); // false console.log(Number.isFinite(-Infinity));// false console.log(Number.isFinite(NaN)); // false console.log(Number.isFinite('100')); // false (不进行类型转换)Number.isInteger(value)(ES6):判断一个值是否是整数。console.log(Number.isInteger(100)); // true console.log(Number.isInteger(100.5)); // false console.log(Number.isInteger(NaN)); // false console.log(Number.isInteger(Infinity));// false
4.4 检查 null 和 undefined
对于 null 和 undefined,最直接和准确的方法是使用严格相等比较:
let a = null;
let b = undefined;
let c = 0;
console.log(a === null); // true
console.log(b === undefined); // true
console.log(c === null); // false
如果你想判断一个值是否是 null 或 undefined(即“空值”),可以使用:
function isNullOrUndefined(value) {
return value === null || value === undefined;
}
console.log(isNullOrUndefined(null)); // true
console.log(isNullOrUndefined(undefined)); // true
console.log(isNullOrUndefined(0)); // false
console.log(isNullOrUndefined('')); // false
或者使用空值合并运算符 ?? 或逻辑或运算符 || 进行默认值处理,但这并非类型判断。
4.5 真值 (Truthiness) 和假值 (Falsiness)
虽然这不是严格意义上的类型判断,但在条件语句中经常会用到。JavaScript中,某些值在布尔上下文中会被视为 false,称为“假值”(Falsy values),包括:
false0(数字零)-0(负零)0n(BigInt零)""(空字符串)nullundefinedNaN
所有其他值都被视为“真值”(Truthy values)。
if (0) { console.log('This will not be logged'); }
if ('hello') { console.log('This will be logged'); }
if (null) { console.log('This will not be logged'); }
if ([]) { console.log('This will be logged'); } // 空数组是真值
if ({}) { console.log('This will be logged'); } // 空对象是真值
这种判断方式不能告诉你确切的类型,但可以用于判断一个值是否存在或有意义。
五、现代JavaScript与未来展望:TypeScript的崛起
尽管我们有多种运行时类型判断方法,但JavaScript作为一种动态语言,其类型检查始终发生在运行时。这意味着潜在的类型错误只有在代码执行时才会被发现,这在大型项目中可能导致生产环境的问题。
为了解决这个问题,社区发展出了像 TypeScript 这样的超集语言。TypeScript在JavaScript的基础上增加了静态类型系统,允许我们在开发阶段(编译时)就捕获大量的类型错误。
TypeScript 的核心思想:
- 静态类型声明:开发者可以为变量、函数参数、返回值等明确指定类型。
function add(a: number, b: number): number { return a + b; } let myString: string = "Hello"; // myString = 123; // 编译时报错! - 类型推断:即使不明确声明,TypeScript 也能根据赋值自动推断出变量的类型。
- 接口 (Interface) 和类型别名 (Type Alias):定义复杂的数据结构类型。
- 类型守卫 (Type Guards):在运行时缩小变量的类型范围,通常结合
typeof、instanceof或自定义函数来实现。
在 TypeScript 中,我们仍然会使用 typeof 和 instanceof 作为“类型守卫”来帮助编译器理解运行时代码的类型流:
function printLength(x: string | string[]) {
if (typeof x === 'string') {
console.log(x.length); // x 在这里被推断为 string
} else {
console.log(x.length); // x 在这里被推断为 string[]
}
}
class Animal { name: string = ''; }
class Dog extends Animal { breed: string = ''; }
function getAnimalName(animal: Animal | Dog) {
if (animal instanceof Dog) {
console.log(`Dog breed: ${animal.breed}`); // animal 在这里被推断为 Dog
} else {
console.log(`Animal name: ${animal.name}`); // animal 在这里被推断为 Animal
}
}
通过 TypeScript,我们可以在开发阶段就获得强大的类型安全保障,大大减少了运行时错误,提高了代码质量和开发效率。对于任何严肃的、大型的 JavaScript 项目,引入 TypeScript 几乎已成为行业标准。
六、类型判断的最佳实践与选择
面对众多的类型判断方法,如何选择最合适的一个呢?这取决于你的具体需求和要判断的类型。
-
判断原始类型 (除
null):- 使用
typeof。它简单、直接、高效。 - 示例:
typeof value === 'number',typeof value === 'string',typeof value === 'boolean',typeof value === 'symbol',typeof value === 'bigint',typeof value === 'undefined'。
- 使用
-
判断
null和undefined:- 使用严格相等
===。 - 示例:
value === null,value === undefined。
- 使用严格相等
-
判断数组:
- 使用
Array.isArray()。这是最可靠、最推荐的方法,解决了instanceof的跨 Realm 问题。 - 示例:
Array.isArray(value)。
- 使用
-
判断函数:
- 使用
typeof。 - 示例:
typeof value === 'function'。
- 使用
-
判断所有内置引用类型 (如
Date,RegExp,Map,Set,Promise等):- 使用
Object.prototype.toString.call()。这是最精确、最可靠的方法,能够区分各种内置对象,并且不受跨 Realm 影响。 - 示例:
Object.prototype.toString.call(value) === '[object Date]'。通常会封装成一个getType辅助函数。
- 使用
-
判断自定义类实例:
- 在同一 Realm 中,使用
instanceof。它能准确判断继承关系。 - 示例:
instance instanceof MyClass。 - 如果需要跨 Realm 或更灵活的自定义标识,考虑在类中设置
Symbol.toStringTag,然后使用Object.prototype.toString.call()。
- 在同一 Realm 中,使用
-
数字的特定判断:
Number.isNaN():判断是否为NaN。Number.isFinite():判断是否为有限数字。Number.isInteger():判断是否为整数。
综合判断函数示例:
function preciseTypeOf(value) {
if (value === null) {
return "null";
}
const type = typeof value;
if (type === 'object') {
if (Array.isArray(value)) {
return "array";
}
// 对于其他内置对象,使用 toStringTag
const tag = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
// 特殊处理一些 Object.prototype.toString.call() 默认返回 'object' 的情况
if (tag === 'object' && value.constructor && value.constructor.name !== 'Object') {
// 针对自定义类,可能返回类名
return value.constructor.name.toLowerCase();
}
return tag;
}
return type;
}
console.log(preciseTypeOf(123)); // "number"
console.log(preciseTypeOf('hello')); // "string"
console.log(preciseTypeOf(true)); // "boolean"
console.log(preciseTypeOf(undefined)); // "undefined"
console.log(preciseTypeOf(null)); // "null"
console.log(preciseTypeOf(Symbol('id'))); // "symbol"
console.log(preciseTypeOf(10n)); // "bigint"
console.log(preciseTypeOf(function(){})); // "function"
console.log(preciseTypeOf({})); // "object"
console.log(preciseTypeOf([])); // "array"
console.log(preciseTypeOf(new Date())); // "date"
console.log(preciseTypeOf(/abc/)); // "regexp"
console.log(preciseTypeOf(new Map())); // "map"
console.log(preciseTypeOf(new Set())); // "set"
class MyCustomClass {}
const myInstance = new MyCustomClass();
console.log(preciseTypeOf(myInstance)); // "mycustomclass" (这里利用了constructor.name,但有局限性)
// 具有 Symbol.toStringTag 的自定义类型
class AnotherCustomType {
constructor() {
this[Symbol.toStringTag] = 'MySpecialType';
}
}
console.log(preciseTypeOf(new AnotherCustomType())); // "myspecialtype"
这个 preciseTypeOf 函数结合了多种方法,尝试提供一个更全面的类型判断结果。但请注意,constructor.name 仍然有其局限性,例如在代码压缩后可能会丢失原始名称,或者在某些特殊情况下不可靠。
七、理解工具,驾驭语言
从 typeof 的简洁与粗糙,到 instanceof 对原型链的探索,再到 Object.prototype.toString.call() 的精确与通用,我们见证了JavaScript在类型判断方面逐步完善的旅程。每种方法都有其适用场景和固有限制。作为开发者,我们不能寄希望于“一招鲜吃遍天”,而是要深入理解每种工具的工作原理,明智地选择最适合当前需求的方案。
同时,我们也应认识到,运行时类型判断终究是“亡羊补牢”。对于大型复杂项目,拥抱如 TypeScript 这样的静态类型系统,将类型错误前置到开发阶段,无疑是构建更可靠、更易维护代码的康庄大道。理解这些运行时类型判断机制,不仅能帮助我们写出更严谨的 JavaScript 代码,也能更好地理解 TypeScript 类型守卫的底层逻辑。