JS `typeof` 与 `instanceof` 的底层原理与类型判断的局限性

各位朋友,大家好!今天咱们来聊聊JavaScript里两个常用的类型判断小能手:typeofinstanceof。听起来是不是挺简单的?但你要是觉得它们俩“人如其名”,那可就大错特错了!它们背后藏着不少玄机,用不好,可是会掉坑里的。准备好,咱们发车啦!

第一站:typeof 的“真面目”

typeof,顾名思义,是用来判断变量类型的。但它判断的,其实是操作数的类型,而不是对象实例的类型。这一点很重要,一定要记住!

简单来说,typeof 会返回一个字符串,告诉你这个变量是啥“底子”。它能识别以下几种基本类型:

  • "undefined":未定义
  • "boolean":布尔值 (true 或 false)
  • "number":数值 (整数或浮点数)
  • "string":字符串
  • "bigint":BigInt
  • "symbol":Symbol
  • "object":对象 (包含 null、数组、对象等)
  • "function":函数

看,种类还挺多的。咱们来举几个例子:

console.log(typeof undefined);   // "undefined"
console.log(typeof true);        // "boolean"
console.log(typeof 42);          // "number"
console.log(typeof "Hello");       // "string"
console.log(typeof 12345678901234567890n); // "bigint"
console.log(typeof Symbol("foo"));  // "symbol"
console.log(typeof {});          // "object"
console.log(typeof []);          // "object"
console.log(typeof null);        // "object"  <- 这是一个历史遗留问题!
console.log(typeof function() {}); // "function"

看起来挺靠谱的,对吧?但注意到了吗?数组和 nulltypeof 判断,都变成了 "object"! 这就是 typeof 的第一个坑:它无法区分对象和数组,也无法区分 null 和对象

至于 null 被判断为 "object",这其实是JavaScript的一个历史遗留的 bug。最初的 JavaScript 实现中,null 被当做是一个指向不存在对象的指针,所以 typeof 把它判断为 "object"。这个 bug 一直留到了现在,为了兼容性,无法修复。

更让人头疼的是,typeof 对所有对象类型(除了函数)都返回 "object"。这意味着,自定义的类实例、Date 对象、正则表达式等等,都会被 typeof 误判。

class MyClass {}
const myInstance = new MyClass();

console.log(typeof myInstance); // "object"
console.log(typeof new Date());   // "object"
console.log(typeof /abc/);      // "object"

总结一下 typeof 的优点和缺点:

特性 优点 缺点
基础类型 可以准确判断 undefinedbooleannumberstringbigintsymbol
对象类型 可以判断是否为对象 无法区分对象、数组、null,也无法区分自定义类实例、Date 对象、正则表达式等。 对所有对象类型(除了函数)都返回 "object"。
函数类型 可以判断是否为函数
速度 很快

第二站:instanceof 的“套路”

instanceof 运算符用来检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 换句话说,它检查的是 对象是否是某个构造函数的实例

instanceof 的语法是:

object instanceof constructor

其中,object 是要检查的对象,constructor 是构造函数。

举个例子:

function Person(name) {
  this.name = name;
}

const john = new Person("John");

console.log(john instanceof Person); // true
console.log(john instanceof Object); // true (因为 Person 的 prototype 继承自 Object)
console.log(john instanceof Array);  // false

从上面的例子可以看出,instanceof 不仅会检查对象是否是直接由构造函数创建的,还会沿着原型链向上查找。如果 object 的原型链上存在 constructor.prototype,那么 instanceof 就会返回 true

这也就意味着,instanceof 可以用来判断对象是否是某个类的实例,或者是否继承自某个类。

但是,instanceof 也有它的局限性。

局限性一:跨 Frame 或 iFrame 的问题

如果对象是在不同的 Frame 或 iFrame 中创建的,那么 instanceof 可能会失效。因为不同的 Frame 或 iFrame 有不同的全局环境,构造函数也会有所不同。

<!DOCTYPE html>
<html>
<head>
<title>instanceof 问题</title>
</head>
<body>
  <iframe id="myFrame" src="iframe.html"></iframe>
  <script>
    const myFrame = document.getElementById('myFrame');
    const myArray = myFrame.contentWindow.Array; // 获取 iFrame 中的 Array 构造函数
    const arr = new myArray(1, 2, 3); // 在 iFrame 中创建一个数组

    console.log(arr instanceof Array); // false (因为这里的 Array 是当前页面的 Array,而不是 iFrame 中的 Array)
    console.log(arr instanceof myArray); // true
  </script>
</body>
</html>
<!-- iframe.html -->
<!DOCTYPE html>
<html>
<head>
<title>iFrame</title>
</head>
<body>
</body>
</html>

在这个例子中,arr 是在 iFrame 中创建的,它的构造函数是 iFrame 中的 Array。而 arr instanceof Array 返回 false,是因为这里的 Array 是当前页面的 Array,而不是 iFrame 中的 Array

局限性二:手动修改原型链

如果手动修改了对象的原型链,那么 instanceof 的结果可能会出人意料。

function Person(name) {
  this.name = name;
}

const john = new Person("John");

// 修改 john 的原型链
Object.setPrototypeOf(john, null);

console.log(john instanceof Person); // false (因为 john 的原型链上已经没有 Person.prototype 了)
console.log(john instanceof Object); // false (因为 john 的原型链上已经没有 Object.prototype 了)

在这个例子中,我们通过 Object.setPrototypeOf(john, null)john 的原型链设置为了 null,导致 john instanceof Personjohn instanceof Object 都返回 false

局限性三:基本类型和包装对象

instanceof 只能用于判断对象是否是某个构造函数的实例,不能用于判断基本类型。

console.log(123 instanceof Number);   // false
console.log("abc" instanceof String);   // false
console.log(true instanceof Boolean);   // false

虽然基本类型可以使用包装对象来创建,但是 instanceof 仍然会返回 false

const num = new Number(123);
const str = new String("abc");
const bool = new Boolean(true);

console.log(num instanceof Number);   // true
console.log(str instanceof String);   // true
console.log(bool instanceof Boolean);  // true

console.log(123 instanceof Number);   // false
console.log("abc" instanceof String);   // false
console.log(true instanceof Boolean);   // false

这是因为 instanceof 检查的是对象的原型链,而基本类型没有原型链。

总结一下 instanceof 的优点和缺点:

特性 优点 缺点
对象类型 可以判断对象是否是某个构造函数的实例,可以沿着原型链向上查找。 无法判断基本类型,跨 Frame 或 iFrame 可能失效,手动修改原型链可能导致结果不准确。
基本类型 无法判断基本类型
原型链 可以沿着原型链向上查找 手动修改原型链可能导致结果不准确。
跨 Frame/iFrame 跨 Frame 或 iFrame 可能失效

第三站:类型判断的“正确姿势”

既然 typeofinstanceof 都有各自的局限性,那我们该如何进行类型判断呢?别慌,这里有一些“正确姿势”供你参考:

  1. 判断基本类型: 使用 typeof 可以准确判断 undefinedbooleannumberstringbigintsymbol

  2. 判断 null 由于 typeof null 返回 "object",所以需要单独判断 null

    function isNull(value) {
      return value === null;
    }
    
    console.log(isNull(null));      // true
    console.log(isNull(undefined)); // false
    console.log(isNull({}));       // false
  3. 判断数组: 可以使用 Array.isArray() 方法来判断是否为数组。

    console.log(Array.isArray([]));       // true
    console.log(Array.isArray({}));       // false
    console.log(Array.isArray(new Array())); // true

    Array.isArray() 的兼容性很好,几乎所有现代浏览器都支持。

  4. 判断对象: 如果需要判断一个变量是否为对象,并且排除 null 和数组,可以使用以下方法:

    function isObject(value) {
      return typeof value === 'object' && value !== null && !Array.isArray(value);
    }
    
    console.log(isObject({}));       // true
    console.log(isObject(null));       // false
    console.log(isObject([]));         // false
    console.log(isObject("hello"));    // false
  5. 判断自定义类实例: 使用 instanceof 可以判断对象是否是某个构造函数的实例。但要注意跨 Frame 或 iFrame 的问题,以及手动修改原型链的影响。

    class MyClass {}
    const myInstance = new MyClass();
    
    console.log(myInstance instanceof MyClass); // true
  6. 终极方案:Object.prototype.toString.call()

    这个方法可以说是类型判断的瑞士军刀,几乎可以判断所有类型。

    function getType(value) {
       return Object.prototype.toString.call(value).slice(8, -1);
    }
    
    console.log(getType(undefined));   // "Undefined"
    console.log(getType(null));        // "Null"
    console.log(getType(123));         // "Number"
    console.log(getType("abc"));       // "String"
    console.log(getType(true));        // "Boolean"
    console.log(getType({}));          // "Object"
    console.log(getType([]));          // "Array"
    console.log(getType(new Date()));   // "Date"
    console.log(getType(/abc/));      // "RegExp"
    console.log(getType(function() {})); // "Function"
    console.log(getType(new Error())); // "Error"
    console.log(getType(Symbol('foo'))); // "Symbol"
    console.log(getType(123n));      // "BigInt"
    
    class MyClass {}
    const myInstance = new MyClass();
    console.log(getType(myInstance)); // "Object" (无法区分自定义类)
    

    Object.prototype.toString.call(value) 会返回一个形如 "[object Type]" 的字符串,其中 Type 就是对象的类型。我们通过 slice(8, -1)Type 提取出来。

    这个方法之所以强大,是因为 Object.prototype.toString 是一个通用的方法,可以应用于任何对象。通过 call 方法,我们可以将 this 指向要判断的对象,从而获取对象的类型信息。

    注意: 即使使用 Object.prototype.toString.call(),也无法区分自定义类,因为它会把所有自定义类实例都判断为 "Object"

总结:类型判断的“葵花宝典”

类型 判断方法 备注
undefined typeof value === "undefined"
null value === null typeof null 返回 "object",需要单独判断。
boolean typeof value === "boolean"
number typeof value === "number"
string typeof value === "string"
bigint typeof value === "bigint"
symbol typeof value === "symbol"
array Array.isArray(value)
object (排除null和数组) typeof value === "object" && value !== null && !Array.isArray(value)
自定义类实例 value instanceof MyClass 注意跨 Frame/iFrame 问题,以及手动修改原型链的影响。
所有类型 (终极方案) Object.prototype.toString.call(value).slice(8, -1) 可以判断几乎所有类型,但无法区分自定义类。

最后一站:实战演练

现在,咱们来做几个小练习,巩固一下今天所学的知识:

  1. 判断一个变量是否为字符串,且长度大于 5。

    function isLongString(value) {
      return typeof value === "string" && value.length > 5;
    }
    
    console.log(isLongString("Hello"));     // false
    console.log(isLongString("Hello World")); // true
    console.log(isLongString(123));         // false
  2. 判断一个变量是否为数组,且第一个元素是数字。

    function isNumberArray(value) {
      return Array.isArray(value) && typeof value[0] === "number";
    }
    
    console.log(isNumberArray([1, 2, 3]));    // true
    console.log(isNumberArray(["a", "b", "c"])); // false
    console.log(isNumberArray({}));          // false
  3. 判断一个变量是否为 Error 类型的实例。

    function isError(value) {
      return value instanceof Error;
    }
    
    console.log(isError(new Error()));   // true
    console.log(isError({}));          // false
    console.log(isError("error"));        // false

好了,今天的讲座就到这里。希望通过今天的学习,你对 typeofinstanceof 有了更深入的了解,也能在实际开发中更加准确地进行类型判断。记住,选择合适的类型判断方法,才能避免掉进坑里哦! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注