在 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 Object 为 true ------ 因为 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); // 动物
底层发生了什么?
new Animal()创建一个新对象tempObj,其__proto__指向Animal.prototype,并执行Animal函数体,以及this绑定。Cat.prototype被赋值为tempObj,即Cat.prototype = { species: '动物', __proto__: Animal.prototype }。- 原本
Cat.prototype自带的constructor: Cat被覆盖,此时Cat.prototype本身不再有constructor属性,会沿着原型链找到Animal.prototype.constructor即Animal。所以需要手动修复:Cat.prototype.constructor = Cat。
优点 :能继承父类原型上的方法。
缺点:父类构造函数会被无意义地调用一次(只为设置原型);所有子类实例共享父类实例的引用属性(可能引发副作用)。
3. 直接共享原型(错误示范)
ini
Cat.prototype = Animal.prototype;//这行代码是引用式拷贝
Cat.prototype.constructor = Cat;
这种写法看似简洁,实则严重污染父类原型:
Cat.prototype和Animal.prototype指向同一内存地址。- 我们后续将
Cat.prototype的constructor属性指回时,也会错误地将Animal.prototype.constructor指向Cat - 在
Cat.prototype上添加方法(如sayHello),也会出现在Animal.prototype上。 - 多个子类(如
Dog、Cat)若都这样继承,会共享同一个原型对象,互相干扰。
结论:绝对不要直接赋值 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。
为什么这是最佳实践?
- ✅ 完全隔离子类与父类的原型,避免污染;
- ✅ 支持多子类继承同一个父类,互不影响;
- ✅ 可同时通过
call继承构造函数属性,通过原型链继承方法; - ✅ 符合"组合优于继承"的现代编程思想。
五、总结
| 继承方式 | 是否继承构造函数属性 | 是否继承原型方法 | 是否污染父类 | 是否推荐 |
|---|---|---|---|---|
| 构造函数绑定(call) | ✅ | ❌ | ❌ | 部分场景 |
| 原型链继承 | ✅(但有副作用) | ✅ | ❌ | 不推荐 |
| 直接共享原型 | ❌ | ✅ | ✅(严重) | 禁止 |
| Object.create 中介 | ✅(配合 call) | ✅ | ❌ | ✅ 推荐 |
而 instanceof 的本质,始终是在原型链上查找 B.prototype 是否出现。理解这一点,就能轻松应对各种继承场景下的类型判断。
在大型项目中,清晰的继承结构和正确的 instanceof 使用,能极大提升代码的可维护性和协作效率。希望本文能帮你夯实基础,在掘金社区写出更高质量的前端文章!