揭开 `instanceof` 的底层面纱:JavaScript 原型链的魔法

57 阅读5分钟

揭开 instanceof 的底层面纱:JavaScript 原型链的魔法

大家好!今天咱们来揭开 JavaScript 中 instanceof 这个神秘运算符的底层面纱。别被它"实例判断"的名头骗了——在 JavaScript 中,它可不是像 Java 那样简单地检查类型,而是基于原型链的深度扫描!(笑)

一、什么是 instanceof

在传统 OOP 语言中(比如 Java、C#),instanceof 是一个简单的类型检查运算符,用来判断一个对象是否是某个类的实例。但在 JavaScript 这个"原型链语言"里,instanceof 会沿着对象的原型链一路向上查找,直到找到匹配的构造函数。

📌 关键区别:其他 OOP 语言是"实例判断",JS 是"原型链判断"。

二、instanceof 的工作原理

A instanceof B 的工作原理是:

  1. 检查 B.prototype 是否在 A 的原型链上
  2. 如果在,返回 true;否则返回 false

JavaScript 中每个对象都有一个内部属性 [[Prototype]](在浏览器中可以通过 __proto__ 访问),这个属性指向它的原型对象。instanceof 就是沿着这个链一路向上查找。

三、用代码看个明白

让我们用下面这串代码来演示:

function Animal(){}
function Person(){}
Person.prototype = new Animal();
const p = new Person();

// OOP instanceof 关键字
// 基于血缘关系
console.log(p instanceof Person); // true
console.log(p instanceof Animal); // true

解释

  • p 是 Person 的实例,所以 p instanceof Person 为 true
  • Person.prototype 指向 Animal 的实例,所以 p 的原型链上包含了 Animal.prototype,因此 p instanceof Animal 也为 true

💡 重点:p 不是 Animal 的直接实例,而是通过 Person 继承了 Animal 的属性和方法。

四、数组也是有原型对象的!

别以为只有自定义对象才有原型链。数组也是对象,所以它们也有原型链:

console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log([] instanceof String); // false

光看判断可能不够,那就来看这段代码在浏览器中F12一下!

const arr = [];// new Array()
console.log(arr.__proto__,
    arr.__proto__.constructor,
    arr.constructor);
    console.log(arr.__proto__.__proto__,
    arr.__proto__.__proto__.constructor,
    arr.__proto__.__proto__.__proto__,
    );

结果如下:

image.png 结果证明了数组的原型链是 Array.prototypeObject.prototype,所以它既是 Array 的实例,也是 Object 的实例。

五、面试官:"手写一下 instanceof 关键字"

这可是个高频面试题!面试官想看看你是否真正理解了原型链。手写版本如下:

function myInstanceof(obj, constructor) {
  let proto = obj.__proto__;
  while (proto) {
    if (proto === constructor.prototype) {
      return true;
    }
    proto = proto.__proto__;
  }
  return false;
}

这个函数会沿着 obj 的原型链一直向上查找,直到找到 constructor.prototype 或者到达原型链的尽头(null)。快去试一下吧!

六、从 instanceof 到继承:JS 的魔法

instanceof 和原型链的关系,直接引出了 JavaScript 的核心机制——继承。在 JS 中,继承不是"复制",而是"原型链的链接"。

1. 构造函数中使用 callapply 绑定继承

这是最简单的继承方式,但只继承了属性,不继承方法

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

function Person(name) {
  Animal.call(this, name); // 绑定 this,继承属性
}

const p = new Person("小明");
console.log(p.name); // "小明"
console.log(p instanceof Person); // true
console.log(p instanceof Animal); // false

问题p 不是 Animal 的实例,因为 Person 没有设置原型链。

🤦‍♂️ 这就像你从父母那里继承了名字,但没有继承家庭地址——你还是"你",不是"父母"。

2. prototype 模式:父类的实例作为子类的原型对象

这是最常用的继承方式,但有一个副作用

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

Animal.prototype.speak = function() {
  console.log("Animal sound");
};

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

// 关键:父类的实例作为子类的原型对象
Person.prototype = new Animal();

const p = new Person("小明");
p.speak(); // "Animal sound"

console.log(p instanceof Person); // true
console.log(p instanceof Animal); // true

// 问题:子类的 constructor 被覆盖了
console.log(Person.prototype.constructor === Animal); // true
console.log(Person.prototype.constructor);

结果如下:

image.png

副作用Person.prototype.constructor 现在指向 Animal,而不是 Person

那我们该如何修复他呢?只需要将子类的原型对象的constructor 再指回指向子类即可:

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

Animal.prototype.speak = function() {
  console.log("Animal sound");
};

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

// 关键:父类的实例作为子类的原型对象
Person.prototype = new Animal();
//子类的原型对象的`constructor` 再指回指向子类
Person.prototype.constructor = Person;

const p = new Person("小明");
console.log(Person.prototype.constructor);

结果如下:

image.png

3. 利用空对象作为中介

这是最优雅的继承方式,避免了 constructor 的问题:

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

Animal.prototype.speak = function() {
  console.log("Animal sound");
};

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

// 关键:利用空对象作为中介
function inherit(Child, Parent) {
  let F = function() {};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
}

inherit(Person, Animal);

const p = new Person("小明");
p.speak(); // "Animal sound"

console.log(p instanceof Person); // true
console.log(p instanceof Animal); // true
console.log(Person.prototype.constructor === Person); // true

为什么好

  • 不会污染父类的原型
  • 修复了 constructor 问题
  • 保持了原型链的纯净

🎯 这就像在父母和孩子之间加了一个"中介人",既继承了父母的优点,又保持了自己独立的身份。

七、总结:instanceof 和继承的奥义

  • instanceof 是 JS 原型链的"导航仪",不是简单的类型检查

  • 继承是 JS 的核心,但方式独特——不是复制,而是链接

  • 三种继承方式各有优劣:

    1. call/apply:简单,但只继承属性
    2. prototype 模式:常用,但需修复 constructor
    3. 空对象中介:优雅,推荐使用

💡 终极理解:在 JavaScript 中,instanceof 不是"你是谁",而是"你从哪里来"。你的原型链决定了你的身份。

想了解更多关于原型链的细节?那就来到我的往期文章中“ JS中原型式面向对象的精髓” 探索一番吧~

下次面试被问到 instanceof,别再傻乎乎地回答"判断实例类型"了——你可以说:"它是基于原型链的深度扫描,就像在家族谱系里找祖先一样!" 😎

希望这篇解释能帮你揭开 instanceof 的神秘面纱!如果还有疑问,欢迎在评论区讨论~