引言
在前端开发中,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.prototypeObject.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 Animal 是 true?
因为:
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.isArray、Object.prototype.toString.call更安全) - 在微前端或多窗口环境中,优先使用鸭子类型(Duck Typing)或 Symbol 标识
六、结语:知其然,更要知其所以然
JavaScript 的继承不是“类”的复制,而是基于原型链的对象委托。instanceof 不是魔法,它只是忠实地沿着 __proto__ 一路向上“认祖归宗”。
下次当你看到:
[] instanceof Array // true
[] instanceof Object // true
你就知道:数组既是 Array 的后代,也是 Object 的子孙 —— 因为它的原型链通向两者。
而你,已经不再是那个被原型链绕晕的新手了。👏
掌握这些底层机制,不仅能写出更健壮的代码,还能在面试中从容应对“手写 instanceof”、“实现继承”等经典问题。更重要的是,你会真正理解 JavaScript 这门语言的设计哲学:万物皆对象,一切靠委托。
📚 延伸阅读建议
- 《你不知道的 JavaScript(上卷)》—— 原型章节
- MDN: Inheritance and the prototype chain
- ECMAScript 规范:OrdinaryHasInstance