深入理解 JavaScript 原型继承与 instanceof 的底层原理
在 JavaScript 这门“一切皆对象”的语言中,继承并非通过类实现,而是依赖原型(Prototype)与原型链(Prototype Chain) 。这种机制灵活却容易让人困惑。本文将从一段简单的 instanceof 判断出发,层层深入,带你彻底搞懂 JavaScript 的继承模型,并手写 instanceof,对比多种继承方式的优劣。
一、从一个看似简单的判断说起
// 1.js
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并不关心“谁创建了这个对象”,而是检查:目标构造函数的prototype是否出现在该对象的原型链上。
这正是 JavaScript “基于血缘关系”而非“基于类型声明”的 OOP 设计哲学。
二、原型链长什么样?我们来“拆解”一个数组
// 2.js
const arr = [];
console.log(arr.__proto__); // Array.prototype
console.log(arr.__proto__.constructor); // Array
console.log(arr.constructor); // Array
console.log(arr.__proto__.__proto__); // Object.prototype
console.log(arr.__proto__.__proto__.__proto__); // null
这段代码揭示了 JavaScript 对象的标准原型链结构:
arr → Array.prototype → Object.prototype → null
- 所有普通对象最终都继承自
Object.prototype。 constructor属性默认指向构造函数,但它是可被覆盖的(后面我们会看到副作用)。- 原型链的终点是
null,这也是Object.getPrototypeOf(Object.prototype)返回null的原因。
三、手写 instanceof:揭开它的神秘面纱
既然 instanceof 是查原型链,那我们完全可以自己实现:
// 3.html
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 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的本质是原型链遍历匹配。- 它能正确识别多层继承(如
Dog → Animal → Object)。 - 即使
Cat和Dog都继承自Animal,它们之间也没有血缘关系。
四、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('coke', '黑色');
console.log(cat.species); // '动物'
- ✅ 优点:每个实例拥有独立属性,避免引用共享。
- ❌ 缺点:无法继承父类原型上的方法(如
Animal.prototype.eat)。
适合仅需复用初始化逻辑的场景。
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
-
✅ 优点:同时继承属性和方法。
-
❌ 缺点:
- 父类构造函数被无参调用一次(可能浪费资源或出错);
- 所有子类实例共享父类实例属性(若属性是引用类型,会互相影响)。
这是早期最常用的继承方式,但存在明显缺陷。
3. 直接赋值 prototype —— 高效但危险
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;
-
✅ 表面看:高效,无多余实例创建。
-
❌ 实际问题:Cat.prototype 和 Animal.prototype 是同一个对象!
- 在
Cat.prototype上添加方法(如eat),会污染 Animal 的原型。 - 违背封装原则,极易引发 bug。
- 在
切勿直接赋值
Child.prototype = Parent.prototype!
五、最佳实践:寄生组合式继承(简要提及)
现代推荐的继承方式是 寄生组合式继承:
function inheritPrototype(Child, Parent) {
const prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
它结合了:
Parent.call(this)→ 继承属性(不共享)Object.create(Parent.prototype)→ 继承方法(无多余调用,无污染)
这是 ES5 时代最稳健的继承方案,也是 class extends 的底层实现思路之一。
六、为什么 instanceof 在大型项目中很重要?
在多人协作或复杂框架中,对象来源多样,类型模糊。例如:
function handleData(data) {
if (data instanceof Array) { /* 处理数组 */ }
else if (data instanceof MyCustomModel) { /* 处理模型 */ }
}
typeof无法区分数组、普通对象、自定义类实例。instanceof能精准判断对象在原型链中的位置,是类型守卫的关键工具。
结语
JavaScript 的继承不是“复制”,而是“链接”。
理解 __proto__、prototype、constructor 三者的关系,掌握 instanceof 的判断逻辑,才能真正驾驭这门语言的面向对象能力。
你提供的几段代码,看似简单,实则涵盖了从现象观察 → 原理剖析 → 方案对比 → 实践反思的完整学习路径。希望本文能帮你把零散的知识点串联成体系,在开发中游刃有余。
记住:原型链不是障碍,而是 JavaScript 赋予我们的强大武器。
如果你打算发布到掘金,这篇文章已具备清晰结构、技术深度与可读性,可直接使用。需要配图、动图演示原型链,或补充 ES6 class 对比,也可以告诉我!