深入理解 JavaScript 的 instanceof 与继承机制:从原型链到手写实现

42 阅读7分钟

 引言

在前端开发中,JavaScript 的面向对象特性一直是个既迷人又令人困惑的话题。特别是 instanceof 运算符和各种继承方式,常常让初学者摸不着头脑。今天,我们就来一起揭开它们的神秘面纱,通过原理剖析、代码示例以及亲手实现 instanceof,让你真正掌握这些核心概念!

你是否曾面对以下问题而感到迷茫?

  • 为什么 [] instanceof Array 返回 true,但 [] instanceof Object 也返回 true
  • 为什么有时候 instanceof 判断会“失灵”?比如跨 iframe 的对象判断。
  • 在 ES6 的 class 语法大行其道的今天,我们还需要理解原型链吗?
  • 如何在项目中安全、高效地使用 instanceof 来做类型判断?

如果你对这些问题感兴趣,那么恭喜你,这篇文章将带你从底层机制出发,彻底搞懂 instanceof 与 JavaScript 继承体系的来龙去脉。


一、原型(Prototype)与原型链:JavaScript 面向对象的基石

1.1 prototype 是函数的属性

每个函数(Function)都有一个 prototype 属性,它指向一个对象 —— 原型对象

function Person() {
  this.name = '张三';
}
console.log(Person.prototype); // { constructor: Person }

注意:虽然 Person.prototype 初始时没有 name 属性(因为 name 是实例属性),但它默认包含一个 constructor 属性,指向 Person 函数本身。

这个 prototype 对象的作用是:所有由该构造函数创建的实例,都会共享这个原型对象上的属性和方法

例如:

Person.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

const p1 = new Person();
p1.sayHello(); // "Hello, I am 张三"

这里 sayHello 方法并不属于 p1 自身,而是通过原型链查找到 Person.prototype 上的方法。

关键点prototype构造函数的属性;而 __proto__实例对象的属性。


1.2 __proto__ 是实例的“隐形指针”

当你用 new Person() 创建一个实例时,这个实例会自动获得一个内部属性 [[Prototype]],在大多数浏览器中可以通过 __proto__ 访问:

const person = new Person();
console.log(person.__proto__ === Person.prototype); // true

也就是说:

实例的 __proto__ 指向其构造函数的 prototype

这是 JavaScript 实现“继承”的基础机制。通过这个链接,实例可以访问构造函数原型上的方法和属性。

但要注意:__proto__ 并不是标准属性(尽管几乎所有现代浏览器都支持),更规范的方式是使用 Object.getPrototypeOf(obj)

console.log(Object.getPrototypeOf(person) === Person.prototype); // true

1.3 原型链:一层套一层的“家族谱系”

如果继续往上找:

  • Person.prototype.__proto__ 指向 Object.prototype
  • Object.prototype.__proto__null(原型链的终点)

这就构成了 原型链(Prototype Chain) —— JavaScript 实现继承的核心机制。

我们来看一个完整的例子:

const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr.__proto__.__proto__.__proto__ === null); // true

这意味着:

  • 数组 arr 可以调用 Array.prototype 上的方法(如 push, map
  • 也可以调用 Object.prototype 上的方法(如 toString, hasOwnProperty
  • 最终到达 null,查找终止

💡 原型链的本质:当访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 引擎会沿着 __proto__ 链向上查找,直到找到该属性或到达 null

原型与原型链详解请看:JavaScript 原型与原型链:从零到精通的深度解析 - 掘金


二、instanceof:判断“血缘关系”的魔法运算符

2.1 它到底在查什么?

A instanceof B 的本质是:

检查 A 的原型链上是否存在 B.prototype

换句话说,它沿着 A.__proto__ → A.__proto__.__proto__ → ... 一路向上查找,看是否能找到 B.prototype

这就像在问:“A 的祖先里有没有 B?”
如果有,返回 true;否则返回 false

2.2 看个经典例子

function Animal() {}
function Person() {}

// 原型链继承:Person 继承 Animal
Person.prototype = new Animal();

const p = new Person();

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

为什么 p instanceof Animaltrue
因为:

  • p.__proto__Person.prototype(即 new Animal()
  • new Animal()__proto__Animal.prototype
  • 所以 p 的原型链上确实包含了 Animal.prototype

这就是“血缘关系”成立的依据!

注意:这种继承方式会导致 Person.prototype.constructor 指向 Animal,需要手动修正:

Person.prototype.constructor = Person;

2.3 instanceof 的边界情况

情况1:基本类型

console.log(123 instanceof Number); // false
console.log(new Number(123) instanceof Number); // true

因为 123 是原始值,不是对象,没有 __proto__ 链。

情况2:跨窗口/iframe

// 在 iframe 中创建的数组
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const arrInIframe = iframe.contentWindow.Array.of(1, 2, 3);

console.log(arrInIframe instanceof Array); // false!

为什么?因为 arrInIframe.__proto__ 指向的是 iframe 内部的 Array.prototype,而当前上下文的 Array.prototype 是另一个对象。两者不相等,所以返回 false

解决方案:使用 Array.isArray() 判断数组,而不是 instanceof

情况3:Symbol.hasInstance 自定义行为

ES6 允许我们自定义 instanceof 的行为:

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

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

这说明 instanceof 并非完全不可控,它可以通过 Symbol.hasInstance 被“劫持”。


三、手写 instanceof:自己造轮子才真懂原理

既然知道了规则,那我们来手动实现一个 myInstanceof

function myInstanceof(instance, Constructor) {
  // 安全检查
  if (typeof Constructor !== 'function') {
    throw new TypeError('Right-hand side of instanceof is not callable');
  }

  // 获取构造函数的显式原型
  let proto = Constructor.prototype;
  
  // 获取实例的隐式原型(使用标准 API)
  let obj = Object.getPrototypeOf(instance);

  // 沿着原型链向上查找
  while (obj !== null) {
    if (obj === proto) {
      return true;
    }
    obj = Object.getPrototypeOf(obj);
  }
  return false;
}

测试用例

// 基础测试
console.log(myInstanceof(p, Person)); // true
console.log(myInstanceof(p, Animal)); // true

// 内置对象
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof(/regex/, RegExp)); // true

// 基本类型
console.log(myInstanceof(123, Number)); // false
console.log(myInstanceof(new Number(123), Number)); // true

// null 和 undefined
console.log(myInstanceof(null, Object)); // false(会抛出错误,因 null 没有原型)

真实 instanceof 会先检查右侧是否为函数,再检查左侧是否为对象。我们的实现已加入基础校验。


四、继承的多种姿势:从“直接赋值”到“寄生组合”

JavaScript 的继承方式五花八门,下面我们系统梳理主流方案,并分析其优劣。

4.1 原型链继承(prototype 模式)

function Animal() {
  this.species = 'animal';
  this.colors = ['red', 'blue'];
}

function Dog() {}
Dog.prototype = new Animal(); // 关键:子类原型 = 父类实例
Dog.prototype.constructor = Dog;

✅ 优点:简单直观,方法复用
❌ 缺点:

  • 所有 Dog 实例共享 colors 数组(引用类型问题)
  • 无法向 Animal 传参
const dog1 = new Dog();
const dog2 = new Dog();
dog1.colors.push('green');
console.log(dog2.colors); // ['red', 'blue', 'green']

4.2 构造函数绑定(call/apply)

function Dog(name) {
  Animal.call(this); // 借用父类构造函数
  this.name = name;
}

✅ 解决了引用共享和传参问题
❌ 缺点:方法无法复用(每次 new Dog 都会执行 Animal.call,但 Animal.prototype 上的方法无法继承)


4.3 组合继承(最常用)

结合前两种方式:

function Dog(name) {
  Animal.call(this); // 实例属性(解决引用共享)
  this.name = name;
}
Dog.prototype = new Animal(); // 原型方法(实现复用)
Dog.prototype.constructor = Dog;

✅ 兼顾传参、引用安全、方法复用
❌ 缺点:调用了两次 Animal 构造函数(一次在 new Animal(),一次在 Animal.call(this)


4.4 寄生组合继承(最优解)

避免重复调用父类构造函数:

function inheritPrototype(SubType, SuperType) {
  const prototype = Object.create(SuperType.prototype); // 创建空对象,原型指向 SuperType.prototype
  prototype.constructor = SubType;
  SubType.prototype = prototype;
}

function Dog(name) {
  Animal.call(this);
  this.name = name;
}
inheritPrototype(Dog, Animal);

✅ 只调用一次父类构造函数,性能最优
✅ 是 ES5 时代最推荐的继承方式

🌟 现代替代方案:ES6 的 class extends 本质上就是寄生组合继承的语法糖。

class Animal {
  constructor() {
    this.species = 'animal';
  }
}

class Dog extends Animal {
  constructor(name) {
    super();
    this.name = name;
  }
}

五、instanceof 在大型项目中有用吗?

你可能会问:“现在都用 class 和 TypeScript 了,还要 instanceof 吗?”

答案是:依然有用!

5.1 类型守卫(Type Guard)

在 TypeScript 或运行时类型检查中:

function handleResponse(res: AxiosError | Error) {
  if (res instanceof AxiosError) {
    console.log('Network error:', res.response);
  } else {
    console.log('General error:', res.message);
  }
}

5.2 多态处理

不同子类执行不同逻辑:

class Circle { area() { return Math.PI * this.radius ** 2; } }
class Square { area() { return this.side ** 2; } }

function calculateArea(shape) {
  if (shape instanceof Circle) {
    return shape.area();
  } else if (shape instanceof Square) {
    return shape.area();
  }
}

5.3 调试与日志

快速识别对象来源:

console.log(err instanceof CustomError ? '业务错误' : '未知错误');

5.4 注意事项

  • 不要用于基本类型判断(用 typeof
  • 跨 iframe 时慎用(用 Array.isArrayObject.prototype.toString.call 更安全)
  • 在微前端或多窗口环境中,优先使用鸭子类型(Duck Typing)或 Symbol 标识

六、结语:知其然,更要知其所以然

JavaScript 的继承不是“类”的复制,而是基于原型链的对象委托instanceof 不是魔法,它只是忠实地沿着 __proto__ 一路向上“认祖归宗”。

下次当你看到:

[] instanceof Array // true
[] instanceof Object // true

你就知道:数组既是 Array 的后代,也是 Object 的子孙 —— 因为它的原型链通向两者。

而你,已经不再是那个被原型链绕晕的新手了。👏

掌握这些底层机制,不仅能写出更健壮的代码,还能在面试中从容应对“手写 instanceof”、“实现继承”等经典问题。更重要的是,你会真正理解 JavaScript 这门语言的设计哲学:万物皆对象,一切靠委托


📚 延伸阅读建议