我手撕了 instanceof,结果发现了 JavaScript 继承的最大陷阱

50 阅读4分钟

手撕 instanceof:深入理解 JavaScript 原型链与继承机制

前言
在 JavaScript 面试中,“手写 instanceof”是一个高频考点。它不仅考察你对原型链的理解,还反映出你是否真正掌握面向对象编程(OOP)在 JS 中的实现方式。本文将带你从原理出发,一步步实现一个自己的 instanceof,并结合继承模式加深理解。


一、什么是 instanceof

instanceof 是 JavaScript 中用于判断一个对象是否是某个构造函数的实例的操作符。例如:

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

const p = new Person();

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

从上面的例子可以看到,p 不仅是 Person 的实例,也是 Animal 的实例。这是因为 Person.prototype 被设置为了 Animal 的一个实例,从而建立了原型链关系。


二、instanceof 的工作原理

A instanceof B 的本质是:检查 B.prototype 是否出现在 A 的原型链上

换句话说,JavaScript 引擎会沿着 A.__proto__ 向上查找,直到找到 B.prototype 或到达 null(即原型链尽头)。


三、动手实现 isInstanceof

下面是完整可运行的手写版本:

// 判断 right.prototype 是否出现在 left 的原型链上
function isInstanceOf(left, right) {
    let proto = left.__proto__;
    while (proto) {
        if (proto === right.prototype) {
            return true;
        }
        proto = proto.__proto__; // null 时结束循环
    }
    return false;
}

function Animal() {}
function Cat() {}
Cat.prototype = new Animal();

function Dog() {}
Dog.prototype = new Animal();

const dog = new Dog();
console.log(isInstanceOf(dog, Dog));     // true
console.log(isInstanceOf(dog, Animal));  // true
console.log(isInstanceOf(dog, Object));  // true
console.log(isInstanceOf(dog, Cat));     // false

四、为什么需要 instanceof?—— 大型项目中的意义

在多人协作或大型项目中,对象来源复杂,属性和方法可能来自多个继承层级。此时,instanceof 成为判断对象“血缘关系”的关键工具。

例如:

  • 判断一个变量是否为自定义组件实例;
  • 区分不同类型的错误对象(如 CustomError instanceof Error);
  • 在框架源码中做类型校验(如 Vue/React 内部逻辑)。

💡 没有 instanceof,我们就只能通过 constructortypeof 等不准确的方式猜测对象类型,容易出错。


五、继承与原型链:instanceof 的根基

要真正理解 instanceof,必须理解 JavaScript 的继承机制。常见的继承方式包括:

1. 构造函数绑定(call/apply)

function Animal() {
    this.species = '动物';
}
function Cat(name, color) {
    Animal.apply(this);
    this.name = name;
    this.color = color;
}
const cat = new Cat('小白', '黑色');

缺点:无法继承原型上的方法,只能继承实例属性。


2. 原型链继承(prototype 模式)

function Animal() {
    this.species = '动物'; // 每个实例都会创建一份,浪费内存
}
function Cat(name, color) {
    this.name = name;
    this.color = color;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 手动修复 constructor
const cat = new Cat('小白', '黑色');

⚠️ 注意Cat.prototype = new Animal() 会覆盖默认原型,导致 constructor 指向 Animal,需手动修正。


3. 原型继承变种:直接原型继承(⚠️ 反模式!)

function Animal() {}
Animal.prototype.species = '动物';

function Cat(name, color) {
    Animal.call(this);
    this.name = name;
    this.color = color;
}

// 危险操作!
Cat.prototype = Animal.prototype; // 共享同一个原型对象
Cat.prototype.constructor = Cat;  // 同时修改了 Animal.prototype.constructor!

Cat.prototype.sayHello = function() {
    console.log('hello');
};

console.log(Animal.prototype.constructor); // 输出:Cat ❌

🔥 严重副作用
Cat.prototypeAnimal.prototype同一个对象引用
修改 Cat.prototype.constructor污染父类
这是一种错误的继承方式,应避免使用。


4. 组合继承(✅ 推荐写法)

组合继承 = 构造函数继承(属性) + 原型链继承(方法) ,是 ES5 中最常用的继承模式。

function Animal() {
    this.species = '动物';
}
Animal.prototype.getSpecies = function() {
    return this.species;
};

function Cat(name, color) {
    Animal.call(this); // 继承实例属性
    this.name = name;
    this.color = color;
}

Cat.prototype = new Animal();      // 继承原型方法
Cat.prototype.constructor = Cat;   // 修复 constructor

Cat.prototype.meow = function() {
    console.log('Meow!');
};

const cat = new Cat('小白', '白色');
console.log(cat.species);        // '动物'
console.log(cat.getSpecies());   // '动物'
cat.meow();                      // 'Meow!'

优点

  • 实例属性不共享(避免引用类型污染)
  • 原型方法可复用
  • instanceofisPrototypeOf() 能正确识别

⚠️ 缺点

  • 父类构造函数被调用两次

    • 第一次:new Animal()(设置原型时)
    • 第二次:Animal.call(this)(创建实例时)
  • 导致父类实例属性在原型上冗余存在(通常无害,但不完美)

💬 尽管有冗余,组合继承仍是面试中最常考察、项目中最稳妥的 ES5 继承方式


六、总结

  • instanceof 的本质是 原型链查找
  • 手写 instanceof 考察的是对 __proto__prototype 关系的理解。
  • 在继承体系中,只有通过正确构建原型链的对象,才能被 instanceof 正确识别。
  • 掌握它,不仅能应对面试,更能写出更健壮、可维护的 OOP 代码。

🧠 记住:JavaScript 没有“类继承”,只有“原型委托”。
instanceof,就是这条委托链上的 “血缘检测仪”


希望这篇博客能帮你彻底吃透 instanceof!如果你觉得有用,欢迎点赞、收藏或分享给正在准备面试的朋友 💪。