🧠 手写 `instanceof`:深入理解原型链与继承机制

105 阅读6分钟

在现代前端开发中,尤其是在大型项目、多人协作的场景下 💼,开发者常常会面对复杂的对象结构和继承关系。如何判断一个对象是否“属于”某个构造函数?JavaScript 提供了 instanceof 运算符来解决这个问题。

本文将从 原型与原型链 出发 🔄,结合多种继承方式 🔗,深入剖析 instanceof 的工作原理,并最终手写实现一个自定义版本的 isInstanceOf 函数 ✍️,帮助你彻底掌握这一核心概念。


🎯 一、什么是 instanceof

instanceof 是 JavaScript 中的一个二元运算符,用于检测构造函数的 prototype 是否出现在某个实例对象的原型链上。

📝 语法

A instanceof B

返回值为布尔值

  • true:表示 A 是由 B 构造出来的(或在其原型链上有 B.prototype)
  • false:否则

📌 示例:

const arr = [];
console.log(arr instanceof Array);   // true ✅
console.log(arr instanceof Object);  // true ✅

❓ 为什么 arr instanceof Object 也是 true?这就引出了我们接下来要讲的核心——原型链


🔗 二、原型与原型链(Prototype & Prototype Chain)

每个 JavaScript 对象都有一个内部属性 [[Prototype]],可以通过 __proto__ 访问(不推荐直接使用)或者通过 Object.getPrototypeOf() 获取。

🔍 当访问一个对象的属性时,如果该对象本身没有这个属性,JS 引擎就会沿着 [[Prototype]] 向上查找,直到找到该属性或到达原型链顶端(即 null)。这就是所谓的原型链机制

🧪 原型链示例分析

<script>
const arr = []; // 相当于 new Array()
console.log(
    arr.__proto__ === Array.prototype,           // true ✅
    arr.__proto__.constructor === Array,         // true ✅
    arr.constructor === Array                    // true ✅
);

console.log(
    arr.__proto__.__proto__ === Object.prototype, // true ✅
    arr.__proto__.__proto__.__proto__ === null     // true ✅
);
</script>

我们可以画出原型链如下:

arr 
→ arr.__proto__ (Array.prototype) 
→ arr.__proto__.__proto__ (Object.prototype)
→ arr.__proto__.__proto__.__proto__ (null)

所以 arr instanceof Object 返回 true,因为 Object.prototypearr 的原型链上。

💡 小贴士:所有对象最终都继承自 Object,因此绝大多数对象都是 Object 的“实例”。


🧬 三、面向对象中的“血缘”关系

在传统 OOP 语言如 Java/C++ 中,我们用 is-a 关系描述继承,例如 “Dog is an Animal” 🐶。

JavaScript 虽然是基于原型的语言,但 instanceof 实现了类似的语义判断功能。它不是看对象有没有某些方法或属性,而是看是否有血缘关系——也就是原型链上的连接。

A instanceof B 的本质是:
B 的 prototype 是否存在于 A 的原型链之中?

🧠 换句话说:你是不是我祖宗?


✍️ 四、实现自己的 isInstanceOf

现在我们来手动实现一个等价于原生 instanceof 的函数。

🔍 思路分析:

  1. 从实例对象 A 开始,获取其 __proto__
  2. 循环遍历原型链,逐层比对当前原型是否等于 B.prototype
  3. 如果找到则返回 true
  4. 如果走到 null 还没找到,则返回 false

💻 实现代码:

function isInstanceOf(A, B) {
    // 确保 A 是对象且 B 是函数
    if (typeof A !== 'object' || A === null || typeof B !== 'function') {
        return false;
    }

    let proto = Object.getPrototypeOf(A); // 推荐使用 getPrototypeOf 而非 __proto__

    while (proto) {
        if (proto === B.prototype) {
            return true;
        }
        proto = Object.getPrototypeOf(proto);
    }

    return false;
}

✅ 测试验证:

function Animal() {}
function Cat() {}
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 ❌

🎉 完全符合预期!

💡 建议:这里我们使用 Object.getPrototypeOf() 替代 __proto__,因为后者已被视为过时(尽管仍广泛支持),更推荐标准 API。


🏗️ 五、JavaScript 中常见的继承方式回顾

为了更好地理解 instanceof 的应用场景,我们需要了解 JS 中几种典型的继承方式。

1️⃣ 构造函数绑定继承(借用构造函数)🔧

利用 callapply 在子类中调用父类构造函数,实现属性继承。

function Animal() {
    this.species = "动物";
}

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

const cat = new Cat('小黑', '黑色');
console.log(cat.species); // "动物" ✅
优点缺点
✅ 可传递参数,避免引用共享❌ 无法继承父类原型上的方法

2️⃣ 原型模式继承(经典原型链)🔁

将父类实例作为子类原型。

function Animal() {
    this.species = "动物"; // 每次都会重复创建
}

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

// 继承:Cat.prototype 指向 Animal 实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修正 constructor 指向

const cat = new Cat('小黑', '黑色');
console.log(cat.species); // "动物" ✅
优点缺点
✅ 可以继承父类原型方法⚠️ 父类构造函数被多次执行;引用类型可能造成共享问题

3️⃣ 利用空对象作为中介(圣杯模式 / 寄生组合式继承)🛡️

解决上述两种方式的问题,推荐做法 ✅。

function inherit(Child, Parent) {
    const F = function() {};        // 创建空函数
    F.prototype = Parent.prototype; // 空函数原型指向父类原型
    Child.prototype = new F();      // 子类原型为中介实例
    Child.prototype.constructor = Child;
}

这样既不会执行父类构造函数,又能正确建立原型链,是目前最安全高效的继承方式。


4️⃣ 直接继承 prototype(❌ 不推荐)

Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;

⚠️ 危险! 这样做会让 Cat.prototypeAnimal.prototype 指向同一个对象。修改 Cat.prototype 会影响 Animal,造成副作用。

Cat.prototype.eat = function() { console.log('吃'); }
// 此时 Animal.prototype 也有了 eat 方法!🚨

🚫 结论:不要这样做!容易引发全局污染。


🏢 六、为什么 instanceof 在大型项目中有必要?🚀

在多人协作的复杂系统中 👥,常会出现以下问题:

  • ❓ 不清楚某个变量到底是什么类型的对象
  • 📦 第三方库返回的对象结构不透明
  • 🧩 类型判断依赖字符串比较(如 toString())容易出错

此时 instanceof 就显得尤为重要:

if (data instanceof Request) {
    // 发起网络请求 🌐
} else if (data instanceof Response) {
    // 处理响应 📥
} else if (data instanceof Error) {
    // 错误处理 ⚠️
}

它提供了一种安全、可靠、语义清晰的方式来判断对象类型,尤其适用于插件系统、表单校验、事件处理器等场景。


⚠️ 七、边界情况与注意事项

1. 基本类型不能使用 instanceof

"hello" instanceof String // false ❌
new String("hello") instanceof String // true ✅

📌 因为基本类型没有原型链(除了包装对象),所以结果为 false


2. 跨窗口/iframe 问题 🖼️

不同全局环境下的对象会有不同的构造器:

const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const IframeArray = window.frames[0].Array;
const arr = new IframeArray();

arr instanceof Array;        // false ❌!
arr instanceof IframeArray;  // true ✅

🚨 此时 instanceof 会失效,应改用 Array.isArray() 等方法。


3. 自定义行为:Symbol.hasInstance 🎛️

你可以重写 instanceof 的判断逻辑:

class MyArray {
    static [Symbol.hasInstance](obj) {
        return Array.isArray(obj);
    }
}

console.log([1,2,3] instanceof MyArray); // true ✅

这说明 instanceof 的行为是可以被定制的 —— 更加灵活但也需谨慎使用。


📊 八、总结对比

内容说明
instanceof 本质🔍 检查构造函数的 prototype 是否在实例的原型链上
核心机制🔗 原型链查找
手写关键点🔁 遍历 __proto__ 链,对比 B.prototype
应用场景🏗️ 类型判断、继承体系调试、框架设计
替代方案🧰 Array.isArray()Object.prototype.toString.call() 更通用

🧩 九、拓展练习 💪

尝试完成以下任务加深理解:

  1. 使用 Object.create(null) 创建一个无原型的对象,测试 isInstanceOf 行为。
  2. 改进你的 isInstanceOf 函数,使其支持 Symbol.hasInstance
  3. 实现一个通用类型判断工具函数 typeOf(value),能准确识别数组、日期、正则等。

🎯 目标:让你写出的代码不仅“能跑”,还能“扛得住”。


🎉 结语

instanceof 不只是一个简单的操作符,它是理解 JavaScript 原型继承体系的一把钥匙 🔑。通过手写其实现,我们不仅掌握了底层原理,也为日后阅读源码、设计架构打下了坚实基础。

在实际开发中,请合理使用 instanceof,并注意其局限性。结合 typeofArray.isArray()Object.prototype.toString() 等方法,才能构建出健壮的类型判断系统 ✅。

🌟 记住一句话
“一切对象皆源于原型 🌀,一切判断终归于链 🔗。”


📌 参考资料

  • 📘 MDN: instanceof
  • 📚 《JavaScript高级程序设计》第6章
  • 📖 《你不知道的JavaScript(上卷)》第二部分

祝你在前端世界的探索之旅中,越走越远,越学越明!