为什么要做类型判断?
你可能会疑惑:
let num = 123明摆着是数字,let str = 'hello'一看就是字符串,为啥写代码还要多此一举做类型判断?
这是因为你 “肉眼看到的”,和代码 “运行时遇到的”,根本不是一回事—— 代码的世界里,变量的类型经常会 “跑偏”,甚至超出你的预期。
比如你写了个 “计算年龄” 的功能,预期用户输入是数字(比如18),但实际用户可能输入:
- 字符串形式的数字:
'18'(输入框默认返回字符串) - 带汉字的内容:
'18岁' - 甚至是空值:
''
如果直接写 let age = 用户输入; let birthYear = 2025 - age;:
- 若
age是'18',会变成2025 - '18' = 2007(JS 自动隐式转换,看似没问题); - 若
age是'18岁',会得到NaN(无法计算); - 若
age是空值'',会得到2025 - 0 = 2025(明显错误)。
这时候类型判断就是 “守门员” :
function getBirthYear(ageInput) {
// 先判断是否是有效数字
if (typeof ageInput !== 'number' || isNaN(ageInput)) {
return '请输入正确的数字';
}
return 2025 - ageInput;
}
搞懂了为什么要有类型判断,我们开始今天的内容。
前言
在 JavaScript 的世界里,变量就像一个个神秘的包裹,你永远不知道里面装的是数字、字符串,还是复杂的对象。想要解开这个谜团,类型判断就是我们的 "透视眼"。今天我们就来聊聊 JavaScript 中那些五花八门的类型判断方法,从基础的 typeof 到自定义的 instanceof,最后到Object.prototype.toString.call (),让你轻松看透每个变量的 "真面目"。
一、变量的两大家族:原始类型和引用类型
我之前的文章已经写过了 JavaScript 的数据类型解析,大致可以分为两大家族:
- 原始类型:就像一个个不可分割的原子,包括 number、string、boolean、undefined、null、Symbol 和 Bigint
- 引用类型:更像一个个复杂的盒子,里面可以装很多东西,比如数组、对象、函数、日期等
看看下面这些例子,是不是很快就能分清谁属于哪个家族?
let n = 123; // 原始类型-number
let s = 'hello'; // 原始类型-string
let f = true; // 原始类型-boolean
let u = undefined; // 原始类型-undefined
let nu = null; // 原始类型-null
let sy = Symbol(1); // 原始类型-Symbol
let big = 123123123n; // 原始类型-Bigint
let arr = []; // 引用类型-数组
let obj = {}; // 引用类型-对象
let fn = function() {}; // 引用类型-函数
let date = new Date(); // 引用类型-日期对象
二、入门级判断工具:typeof
typeof 就像一把基础的瑞士军刀,能帮我们快速识别大部分类型,但它也有自己的小脾气。
它的特点是:
- 能准确判断除了 null 以外的所有原始类型
- 判断引用类型时,只有 function 能被正确识别,其他都会返回 "object"
看看实际效果:
console.log(typeof n); // "number"
console.log(typeof s); // "string"
console.log(typeof f); // "boolean"
console.log(typeof u); // "undefined"
console.log(typeof sy); // "symbol"
console.log(typeof big); // "bigint"
console.log(typeof nu); // 注意这里!返回"object"而不是"null"
console.log(typeof arr); // "object"
console.log(typeof obj); // "object"
console.log(typeof date); // "object"
console.log(typeof fn); // "function" 只有函数被正确识别
你肯定会问:那为什么 typeof 会把 null 识别为 object 呢?
这是 JavaScript 诞生之初的历史遗留设计:早期 JS 为了简化类型判断逻辑,采用二进制前三位标识数据类型 —— 前三位为 000 时统一判定为对象类型。而 null 被设计为 “空对象指针”,对应的二进制表示是全 0,自然就命中了对象的判定规则。这个设计在当时看似合理,却留下了这个经典的 “bug”,由于后续要兼容海量旧代码,这个特性便一直被保留至今,成为 JS 中一个广为人知的设计遗留问题。
不行啊有 bug ,那 null 怎么判断?别急,后面揭晓 ~
三、进阶工具:instanceof
如果你想判断一个引用类型具体属于哪个 "门派",instanceof 就是个好帮手。它的原理是通过查找对象的隐式原型链,看看是否能找到对应的构造函数原型。
直接打印看看结果:
console.log(arr instanceof Array); // true 数组是Array的实例
console.log(obj instanceof Object); // true 对象是Object的实例
console.log(date instanceof Date); // true 日期是Date的实例
console.log(fn instanceof Function); // true 函数是Function的实例
console.log(n instanceof Number); // false 原始类型不适用
有意思的是,因为原型链的特性,一个数组同时也是 Object 的实例,所以就会出现这种情况:
console.log(arr instanceof Object); // true
// 因为 Array.prototype.__proto__ === Object.prototype
但 instanceof 也有局限性,它无法判断原始类型,而且在跨 iframe 等场景下可能会失效。
四、终极利器:Object.prototype.toString.call ()
这是 JavaScript 中最精准的类型判断方法,就像一个高精度的光谱分析仪,能识别出每种类型的独特 "光谱"。
它的工作原理是:
原文:
翻译过来就是:
- 如果值是 undefined,返回 "[object Undefined]"
- 如果值是 null,返回 "[object Null]"
- 将 toString 中 this 值作为参数传递给 ToObject,设 O 为调用 ToObject 的结果
- 设一个变量 class 为 O 的 [[class]] 内部属性值
- 返回一个字符串,这个字符串由 '[object ' + class + ']' 组成
我们可以打印试试:
console.log(Object.prototype.toString.call(n));
console.log(Object.prototype.toString.call(nu));
console.log(Object.prototype.toString.call(arr));
console.log(Object.prototype.toString.call(fn));
很好,就连null都有了答案Null,fn也输出了Function。
我们可以封装一个通用的类型判断函数:
function getType(x) {
const val = Object.prototype.toString.call(x);
const valType = val.slice(8, -1); // 截取"[object Type]"中的Type部分
return valType;
}
console.log(getType(s)); // "String"
console.log(getType(arr)); // "Array"
console.log(getType(date)); // "Date"
console.log(getType(nu)); // "Null" 完美识别 null
这个方法几乎能正确识别所有类型,是很多 JavaScript 库的首选类型判断方案。
五、小试牛刀:slice 的妙用
在刚刚的通用的类型判断函数中我们用到了slice,因为输出结果是以[object ' + class + ']的形式输出,那如果我只想要后面的类型呢?那就让我们看看字符串方法 slice 在类型判断中的小技巧。slice 可以接受负数作为参数,表示从末尾开始计算位置。
一个例子秒懂:
const str = 'hello world';
// 从下标 6开始,到倒数第 1个字符结束
const newStr = str.slice(6, -1);
console.log(newStr); // 输出"worl"
那么回到我们刚刚的函数:
const valType = val.slice(8, -1);
// 截取"[object Type]"中的Type部分
因为前面一定是[object ,所以后面的Type就从8开始,但是后面还有一个],所以添一个-1,于是就有了val.slice(8, -1); ,这样就能直接拿到Type部分。
六、自己动手:实现 instanceof 和 call
理解原理的最好方式就是自己实现一遍。让我们来动手实现一下 instanceof:
function myinstanceof(L, R) {
// 如果是原始类型或null,直接返回false
if(typeof(L) != 'object' && typeof(L) != 'function' || L == null){
return false;
}
let proto = L.__proto__;
while(proto != null){
if(proto == R.prototype){
return true;
}
proto = proto.__proto__;
}
return false;
}
console.log(myinstanceof([], Array)); // true
console.log(myinstanceof([], Object)); // true
console.log(myinstanceof('hello', String)); // false
再来实现一个简化版的 call 方法,感受一下 this 绑定的魅力:
Function.prototype.myCall = function(context, ...args) {
context = context || window; // 默认上下文是window
const fn = Symbol('fn'); // 创建唯一的Symbol避免属性冲突
context[fn] = this; // 将当前函数设为上下文的属性
const res = context[fn](...args); // 调用函数,此时this指向context
delete context[fn]; // 清理添加的属性
return res;
};
function foo(x, y) {
console.log(this.a, x + y);
return 'hello';
}
const obj = {
a: 1
};
const res = foo.myCall(obj, 2, 3); // 输出1 5
console.log(res); // 输出hello
总结
JavaScript 的类型判断就像一场侦探游戏:
typeof是我们的初步线索,但有局限性instanceof帮助我们追踪引用类型的 "家族谱系"Object.prototype.toString.call ()则是最终的 "指纹鉴定"
掌握这些方法,你就能轻松应对各种类型判断场景,写出更健壮的 JavaScript 代码。记住:
没有万能的方法,根据具体场景选择合适的工具才是王道!