从 "猜盲盒" 到 "读心术":JavaScript 类型判断的进化史

1,171 阅读7分钟

为什么要做类型判断?

你可能会疑惑: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 就像一把基础的瑞士军刀,能帮我们快速识别大部分类型,但它也有自己的小脾气

它的特点是:

  1. 能准确判断除了 null 以外的所有原始类型
  2. 判断引用类型时,只有 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" 只有函数被正确识别

image.png

你肯定会问:那为什么 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 原始类型不适用

image.png

有意思的是,因为原型链的特性一个数组同时也是 Object 的实例,所以就会出现这种情况:

console.log(arr instanceof Object); // true 
// 因为 Array.prototype.__proto__ === Object.prototype

image.png

instanceof 也有局限性,它无法判断原始类型,而且在跨 iframe 等场景下可能会失效。

四、终极利器:Object.prototype.toString.call ()

这是 JavaScript 中最精准的类型判断方法,就像一个高精度的光谱分析仪,能识别出每种类型的独特 "光谱"

它的工作原理是:

原文:

image.png

翻译过来就是:

  1. 如果值是 undefined,返回 "[object Undefined]"
  2. 如果值是 null,返回 "[object Null]"
  3. 将 toString 中 this 值作为参数传递给 ToObject,设 O 为调用 ToObject 的结果
  4. 设一个变量 class 为 O 的 [[class]] 内部属性值
  5. 返回一个字符串,这个字符串由 '[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)); 

image.png

很好,就连null都有了答案Nullfn也输出了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

image.png

这个方法几乎能正确识别所有类型,是很多 JavaScript 库的首选类型判断方案

五、小试牛刀:slice 的妙用

在刚刚的通用的类型判断函数中我们用到了slice,因为输出结果是以[object ' + class + ']的形式输出,那如果我只想要后面的类型呢?那就让我们看看字符串方法 slice 在类型判断中的小技巧。slice 可以接受负数作为参数,表示从末尾开始计算位置

一个例子秒懂:

const str = 'hello world';
// 从下标 6开始,到倒数第 1个字符结束
const newStr = str.slice(6, -1);
console.log(newStr); // 输出"worl"

image.png

那么回到我们刚刚的函数:

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

image.png

再来实现一个简化版的 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

image.png

总结

JavaScript 的类型判断就像一场侦探游戏:

  • typeof 是我们的初步线索,但有局限性
  • instanceof 帮助我们追踪引用类型的 "家族谱系"
  • Object.prototype.toString.call () 则是最终的 "指纹鉴定"

掌握这些方法,你就能轻松应对各种类型判断场景,写出更健壮的 JavaScript 代码。记住:

没有万能的方法,根据具体场景选择合适的工具才是王道!