手写 instanceof 运算符

388 阅读4分钟

初探 instanceof

在一般情况下,判断一个变量或常量的类型会使用 typeof 运算符,那它返回值的类型有哪些呢?让我们来写个例子瞧瞧:

typeof '';                 // string
typeof 1;                  // number
typeof 9007199254740991n;  // bigint
typeof true;               // boolean
typeof Symbol();           // symbol
typeof {};                 // object
typeof [];                 // object
typeof null;               // object
// 声明一个变量
let u;
typeof u;                  // undefined

// 声明一个构造函数
function User(name, age) {
    this.name = name;
    this.age = age;
}
typeof User;               // function

// new 一个实例对象
const user1 = new User('rose', 20);
typeof user1               // object

它返回的类型值有 numberbigintstringbooleanobjectfunctionundefinedsymbol,对于 arraynull,它们的 typeof 运算符返回的类型值也是 object

JavaScript有两种数据类型,一种是原始数据类型,一种是引用数据类型。原始数据类型有:numberbigintstringbooleanundefinednullsymbol,引用数据类型有:objectarrayfunction

至于 null 是原始数据类型,但被 typeof 归为了 object,这是 JavaScript 的历史遗留问题,在这里就不做深究了。

那怎么去判断变量的类型是不是数组呢?以及怎么去判断一个对象是否是另一个对象或函数的实例呢?那就要用到本篇文章的主角——instanceof,那怎么去使用它呢?请看如下例子:

[] instanceof Array;      // true
user1 instanceof User;    // true

[] instanceof Object;     // true
user1 instanceof Object;  // true

左边操作数是需要判断的对象,右边操作数是我们希望该对象的实际构造函数。Object 构造函数是所有对象的实例,如果右边是 Object,那么左边无论是什么对象,instanceof 的返回值都是 true,但这样就失去了 instanceof 的意义了。对于它的用法,我们需要注意两点:

  1. instanceof 的返回值只有两个— truefalse
  2. 一般情况下,instanceof 只能用于引用数据类型的判断。

在下文「自定义 instanceof 返回的结果」可以实现使用 instanceof 判断基本数据类型。

对于第2点,我们可以通过代码来验证一下:

'' instanceof String;                // false
1 instanceof Number;                 // false
9007199254740991n instanceof BigInt  // false
true instanceof Boolean;             // false
Symbol() instanceof Symbol;          // false
undefined instanceof Object;         // false
null instanceof Object;              // false

可以看到 instanceof 的返回值永远是 false。不过人家 JavaScript 官方规定了,instanceof 左边必须是对象,如果硬要左边是非对象类型的值,那就只好返回 false 咯~

至于为什么只能用于引用数据类型的判断,这就要清楚地知道 instanceof 内部的实现原理了。

实现原理

其实 instanceof 是用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。因此,它只能正确判断引用数据类型,而不能判断基本数据类型。

那到底是什么意思呢?让我们来逐一分析一下:

  1. [] instanceof Array; 等同于 [].__proto__ === Array.prototype
  2. user1 instanceof Object 等同于 user1.__proto__ === User.prototype

instanceof 就是做了这样类似的判断,为什么说是类似?因为 instanceof 判断的是对象的原型链,如果对象的第一层原型不等于构造函数的 prototype属性,那么再找对象原型的原型,一直往下一层的原型找,直到原型链的终点 Object.prototype.__proto__,它的值为 null,最终才返回 false,否则如果在对象的原型链上任何一层原型等于构造函数的 prototype属性,就返回 true

有细心的朋友可能已经发现一个问题,如果 instancof 是按照上述规则来判断的,那么基本数据类型是可以进行判断的,比如说 ''.__proto__ === String.prototype 返回值是 true

是的,单单按照上述的规则确实是可以用来判断的,但 instanceof 内部首先对左边操作数进行判断,如果它是基本数据类型或者 null,直接返回 false

function myInstanceof(instance, constructorFn) {
  const typeIns = typeof instance;
  // 如果实例是基本数据类型或者 null,直接返回 false
  if (
    typeIns === null ||
    (typeIns !== "object" && typeIns !== "function")
  ) {
    return false;
  }
  // 获取对象的原型
  let proto = Object.getPrototypeOf(instance)
  // 获取构造函数的 prototype 对象
  const fnPrototype = constructorFn.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (proto) {
    if (proto === fnPrototype) return true;
    // 如果没有找到,就继续从其原型上找
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

myInstanceof('', String);   // false
myInstanceof([], Array);    // true
myInstanceof(user1, User);  // true

总的来说,原理代码的实现可分为3步:

  1. 使用 Object.getPrototypeOf 方法获取对象的原型
  2. 获取构造函数的 prototype 对象
  3. 循环判断构造函数的 prototype 对象是否等于对象的原型

自定义 instanceof 返回的结果

对象中有个叫 Symbol.hasInstance 的属性,它是一个方法,当其他对象使用 instanceof 判断是否为该对象的实例时,就会调用该对象的 Symbol.hasInstance 方法。因此我们可以自定义Symbol.hasInstance 方法的内部逻辑,从而改变 instanceof 返回的结果:

class myNumber {
    static [Symbol.hasInstance](x) {
        console.log(x); // 123
        return typeof x === "number";
    }
}
console.log(123 instanceof myNumber);  // true

可以看到我们实现了自定义 instanceof 的行为,控制了它的返回结果。

但一般情况下,很少使用这种方式去判断基本数据类型,这里当作一个扩展知识来了解一下吧。