深入理解 JavaScript 中的继承与 instanceof 原理

在 JavaScript 的面向对象编程(OOP)中,继承instanceof 是两个非常核心的概念。很多初学者容易将 instanceof 简单理解为"判断是否是某个类的实例",但其实它的本质远不止于此。本文将结合你提供的多个代码示例,系统地梳理 JavaScript 中继承的几种常见方式,并深入剖析 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 的实例。这说明 instanceof 并不是判断"直接创建关系",而是判断"原型链上是否存在指定构造函数的原型"

换句话说:

A instanceof B 的含义是:A 的原型链上是否存在 B.prototype

因此,更准确的说法是:
instanceof 是一个"原型关系判断运算符" ,而非简单的"实例判断"。


二、手写 instanceof:理解原型链查找过程

我们可以手动实现 instanceof 的逻辑:

javascript 复制代码
function myInstanceOf(left, right) {
  let proto = Object.getPrototypeOf(left); // 等价于 left.__proto__

  while (proto !== null) {
    if (proto === right.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }

  return false;
}

这个函数的核心思路就是:沿着 left 的原型链一路向上查找,看是否能找到 right.prototype

举个例子:

javascript 复制代码
function Animal() {}
function Dog() {}
Dog.prototype = new Animal();

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

这也解释了为什么数组 [] instanceof Objecttrue ------ 因为 Array.prototype.__proto__ === Object.prototype

⚠️ 注意:instanceof 对基本数据类型(如 'hello' instanceof String)返回 false,因为字面量不是对象实例。只有 new String('hello') instanceof String 才为 true


三、JavaScript 中的继承方式

JavaScript 没有传统 OOP 语言中的"类继承"语法(ES6 的 class 本质仍是基于原型),但我们可以通过多种方式模拟继承。下面逐一介绍。

1. 构造函数绑定(借用构造函数)

ini 复制代码
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. 原型链继承(Child.prototype = new Parent()

ini 复制代码
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('小黑', '黑色');
console.log(cat.species); // 动物

底层发生了什么?

  1. new Animal() 创建一个新对象 tempObj,其 __proto__ 指向 Animal.prototype,并执行 Animal 函数体,以及this绑定。
  2. Cat.prototype 被赋值为 tempObj,即 Cat.prototype = { species: '动物', __proto__: Animal.prototype }
  3. 原本 Cat.prototype 自带的 constructor: Cat 被覆盖,此时Cat.prototype本身不再有constructor属性,会沿着原型链找到Animal.prototype.constructorAnimal。所以需要手动修复:Cat.prototype.constructor = Cat

优点 :能继承父类原型上的方法。
缺点:父类构造函数会被无意义地调用一次(只为设置原型);所有子类实例共享父类实例的引用属性(可能引发副作用)。


3. 直接共享原型(错误示范)

ini 复制代码
Cat.prototype = Animal.prototype;//这行代码是引用式拷贝
Cat.prototype.constructor = Cat;

这种写法看似简洁,实则严重污染父类原型

  • Cat.prototypeAnimal.prototype 指向同一内存地址。
  • 我们后续将Cat.prototypeconstructor属性指回时,也会错误地将Animal.prototype.constructor指向Cat
  • Cat.prototype 上添加方法(如 sayHello),也会出现在 Animal.prototype 上。
  • 多个子类(如 DogCat)若都这样继承,会共享同一个原型对象,互相干扰。

结论:绝对不要直接赋值 Child.prototype = Parent.prototype


四、推荐方案:利用空对象作为中介(Object.create

为了解决上述问题,我们可以引入一个空的中介对象,让它作为桥梁连接子类和父类的原型。

核心思想:

创建一个新对象,其原型指向父类的原型,但自身不包含任何属性。再把这个对象赋给子类的 prototype

实现方式(使用 Object.create):

javascript 复制代码
function Animal() {
  this.species = '动物';
}

Animal.prototype.say = function() {
  console.log('I am an animal');
};

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

// 关键:使用 Object.create 创建中介对象
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修复 constructor

// 子类可安全添加自己的方法
Cat.prototype.meow = function() {
  console.log('Meow!');
};

const cat = new Cat('小黑', '黑色');
cat.say();  // I am an animal
cat.meow(); // Meow!

console.log(cat instanceof Cat);    // true
console.log(cat instanceof Animal); // true

底层原理:

Object.create(proto) 的作用是:

  • 创建一个新对象;
  • 将该对象的 __proto__ 指向 proto
  • 不执行任何构造函数。

因此,Object.create(Animal.prototype) 返回的对象结构为:

css 复制代码
{
  __proto__: Animal.prototype
}

没有 species 属性 (避免了无意义调用 Animal 构造函数),也不会污染 Animal.prototype

为什么这是最佳实践?

  1. ✅ 完全隔离子类与父类的原型,避免污染;
  2. ✅ 支持多子类继承同一个父类,互不影响;
  3. ✅ 可同时通过 call 继承构造函数属性,通过原型链继承方法;
  4. ✅ 符合"组合优于继承"的现代编程思想。

五、总结

继承方式 是否继承构造函数属性 是否继承原型方法 是否污染父类 是否推荐
构造函数绑定(call) 部分场景
原型链继承 ✅(但有副作用) 不推荐
直接共享原型 ✅(严重) 禁止
Object.create 中介 ✅(配合 call) ✅ 推荐

instanceof 的本质,始终是在原型链上查找 B.prototype 是否出现。理解这一点,就能轻松应对各种继承场景下的类型判断。

在大型项目中,清晰的继承结构和正确的 instanceof 使用,能极大提升代码的可维护性和协作效率。希望本文能帮你夯实基础,在掘金社区写出更高质量的前端文章!


相关推荐
杨啸_新房客1 小时前
如何优雅的设置公司的NPM源
前端·npm
linhuai1 小时前
flutter如何实现有登陆权限管理
前端
crary,记忆1 小时前
React 之 useEffect
前端·javascript·学习·react.js
BD_Marathon1 小时前
【JavaWeb】HTML常见标签——标题段落和换行
前端·html
小飞侠在吗1 小时前
vue OptionsAPI与CompositionAPI
前端·javascript·vue.js
春卷同学1 小时前
基于Electron开发的跨平台鸿蒙PC找不同游戏应用
javascript·游戏·electron
天涯路s1 小时前
qt怎么将模块注册成插件
java·服务器·前端·qt
只与明月听1 小时前
FastAPI入门实战
前端·后端·python
春卷同学1 小时前
钓鱼大师 - Electron for 鸿蒙PC项目实战案例
javascript·electron·harmonyos