前言:
在 JavaScript 的江湖里,"对象"这个词听起来很美好(毕竟我们都缺对象),但一旦涉及到 原型链 和 继承,无数前端er就像陷入了一场大型"家庭伦理剧":
- "我是谁?我从哪里来?"
- "为什么改了儿子的属性,爹的属性也变了?"
- "Object 和 Function 到底谁是谁的爸爸?"
今天,我们就把这些"家务事"断个明白。我们将先从手写 instanceof 入手,彻底搞懂血缘关系,再看看 JS 继承是如何从"刀耕火种"进化到"现代文明"的。
一、寻根问祖 ------ 手写 instanceof 🕵️♀️
在判断数据类型时,typeof 对于对象类型(Array, Object, null)往往力不从心,这时候就需要 instanceof 出场了。
1.1 核心原理
A instanceof B 的本质并不是判断 A 是否由 B 创建的,而是:
👉 B 的原型对象(prototype),是否出现在 A 的原型链上。
这就好比查家谱:只要在你的祖宗十八代里发现了 B 的名字,那你们就是一家人。
1.2 灵魂三问(基础复习)
在手写之前,必须理清这三个属性:
- prototype(显式原型) :函数的专属属性,相当于产品说明书。
- proto(隐式原型) :对象(实例)的属性,相当于产地贴纸,指向构造函数的 prototype。
- constructor:原型对象上的属性,指回构造函数本身(说明书上写着"某某制造")。

1.3 手撕代码:myInstanceof
原理:顺着实例的 proto 链条一直往上找,直到找到目标,或者找到宇宙尽头(null)。
JavaScript
/**
* 模拟 instanceof 原理
* @param {Object} left - 实例对象 (p)
* @param {Function} right - 构造函数 (Person)
*/
function myInstanceof(left, right) {
// 1. 获取右侧构造函数的显式原型(目标说明书)
let prototype = right.prototype;
// 2. 获取左侧对象的隐式原型(起始查寻点)
// 推荐使用 Object.getPrototypeOf(left),兼容性更好,这里用 __proto__ 演示原理
let proto = left.__proto__;
// 3. 开启寻根之旅
while (true) {
// 走到尽头了还没找到 -> 不是这一家的
if (proto === null) return false;
// 找到了!-> 确实是一家人
if (proto === prototype) return true;
// 没找到,继续去爷爷辈找
proto = proto.__proto__;
}
}
// --- 测试一下 ---
function Person() {}
const p = new Person();
console.log(myInstanceof(p, Person)); // true
console.log(myInstanceof(p, Object)); // true (所有人都是 Object 的后代)
console.log(myInstanceof(p, Array)); // false
二、 终极难点:Function 与 Object 的"鸡生蛋"问题 🥚
这是面试中的死亡考题,能把这段逻辑理顺,你的原型链功底就超越了 90% 的人。

请看这几行诡异的代码:
JavaScript
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true
解析:
-
万物皆对象:所有的对象,原型链的顶端都是 Object.prototype。
- Object.prototype.__proto __ === null (宇宙尽头)。
-
函数也是对象:所有的函数(包括 Object 构造函数、Function 构造函数),都是 Function 的实例。
- Object.__proto __ === Function.prototype (Object 是被 Function 生产出来的)。
- Function.__proto __ === Function.prototype (Function 自己生了自己?)。
原型链中的关系(关键!)
JavaScript 的对象系统基于原型链(prototype chain) ,来看几个关键点:
1. 所有函数都有 prototype 属性
JavaScript
function Foo() {}
console.log(Foo.prototype); // { constructor: Foo }
- 这个
prototype对象会被用作通过new Foo()创建的实例的原型。
2. 所有对象都有 __proto__(或通过 Object.getPrototypeOf 访问)
JavaScript
const obj = new Foo();
console.log(obj.__proto__ === Foo.prototype); // true
3. 函数的原型链
JavaScript
function Foo() {}
// Foo 是一个函数
console.log(Foo.__proto__ === Function.prototype); // true
// Function.prototype 本身是一个函数对象
console.log(Function.prototype.__proto__ === Object.prototype); // true
// Object.prototype 是原型链的顶端(再往上是 null)
console.log(Object.prototype.__proto__); // null
所以整个链条是:
JavaScript
Foo (函数)
→ __proto__ → Function.prototype
→ __proto__ → Object.prototype
→ __proto__ → null
而 Foo.prototype(注意不是 Foo.__proto__)是将来实例的原型:
JavaScript
new Foo()
→ __proto__ → Foo.prototype
→ __proto__ → Object.prototype
→ __proto__ → null
总结:三者关系图
| 实体 | 类型 | 是对象? | 是函数? | 由谁构造? |
|---|---|---|---|---|
普通对象 {} |
object | ✅ | ❌ | Object |
普通函数 function f(){} |
function | ✅ | ✅ | Function |
Object |
function | ✅ | ✅ | Function |
Function |
function | ✅ | ✅ | Function(自身) |
🎯 关键结论:
- 一切皆对象(除了原始类型:string, number, boolean, symbol, bigint, null, undefined)。
- 函数是可调用的对象。
- 构造函数只是函数的一种使用方式。
Object和Function互相依赖,构成 JS 对象系统的基石。
第二章:JS 中的6种继承方式 ------ 从青铜到王者
搞懂了原型链,我们就可以开始谈"继承"了。在 ES6 class 出现之前,JS 继承就是一部"踩坑史"。
目标:创建一个子类 Cat,继承父类 Animal 的属性(name, color)和方法(eat)。
准备工作:父类
JavaScript
function Animal() {
this.species = "动物";
this.hobbies = ["睡觉", "干饭"]; // 引用类型,这也是个坑
}
Animal.prototype.eat = function() {
console.log("阿姆阿姆~");
}
1.构造函数继承(只借身,不借魂)
这是最直接的思路:"把爸爸的代码拿来在我身体里跑一遍"。
JavaScript
function Cat(name, color) {
// 核心:修改 this 指向,执行父类代码
Animal.call(this);
this.name = name;
this.color = color;
}
const cat = new Cat("小黑", "黑");
console.log(cat.hobbies); // ["睡觉", "干饭"] -> 属性继承成功
// cat.eat(); // 报错!Uncaught TypeError -> 原型上的方法没继承到
- 优点:简单,解决了引用属性共享问题(每个猫有自己的 hobbies),可传参。
- 缺点 :只能继承属性,继承不了方法。Animal.prototype 上的东西它统统看不见。这叫"有其父之形,无其父之神"。
2.原型链继承(简单粗暴改族谱)
为了拿到方法,我们尝试直接修改子类的原型。
JavaScript
function Cat(name) {
this.name = name;
}
// 核心:把 Cat 的原型指向一个 Animal 的实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修正身份证
const cat1 = new Cat("大黄");
const cat2 = new Cat("小白");
cat1.eat(); // "阿姆阿姆" -> 方法继承成功!
// 💥 灾难现场
cat1.hobbies.push("抓老鼠");
console.log(cat2.hobbies); // ["睡觉", "干饭", "抓老鼠"] -> 小白也被迫喜欢抓老鼠了
-
缺点:
- 引用篡改:所有子类实例共用同一个原型对象,修改一个实例的引用类型属性,其他实例全受影响。
- 无法传参:实例化子类时,没法给父类传参。
3.直接继承 Prototype(这也是个坑)
既然 new Animal() 浪费内存,那我直接指向 Animal.prototype 行不行?
JavaScript
function Cat(name, color) {
Animal.call(this); // 借用属性
this.name = name;
}
// 核心代码:直接引喻
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
Cat.prototype.sayHi = function () {
console.log("喵喵");
};
// 灾难现场
console.log(Animal.prototype.constructor); // Cat !!!
- 缺点 :这是毁灭性的。因为 Cat.prototype 和 Animal.prototype 指向了内存中同一个对象。你改了儿子的原型(加了 sayHi,改了 constructor),爹的原型也被改了。这叫"子债父偿"。
4.圣杯模式(The Holy Grail)------ 经典的手动优化
这是在 ES5 Object.create 普及之前,业界公认的最佳实践,也是道格拉斯·克罗克福德(Douglas Crockford)提出的经典方案。尤雨溪(Vue创始人)早年也在论坛推崇过这种写法。
它的核心思想是:既然 new Animal() 浪费内存且有副作用,那我就造一个空的构造函数 F 来当中间人!
JavaScript
// 圣杯模式封装函数
function inherit(Child, Parent) {
// 1. 创建一个干净的中间商(空函数)
const F = function() {};
// 2. 把中间商的原型指向父类原型
F.prototype = Parent.prototype;
// 3. 子类继承中间商的实例
// 关键点:new F() 没有实例属性,非常轻量,完全避免了 new Animal() 的副作用
Child.prototype = new F();
// 4. 修正身份证
Child.prototype.constructor = Child;
// 5. (可选) 记录"生父",方便以后查找或调用父类方法
// uber 是德语"super"的意思,这是当时的命名习惯
Child.prototype.uber = Parent.prototype;
}
function Cat(name) {
Animal.call(this); // 借属性
this.name = name;
}
// 使用圣杯
inherit(Cat, Animal);
const cat = new Cat("贝塔");
cat.eat(); // 成功继承方法
🌟 为什么它叫"圣杯"?
因为它完美解决了"既要继承原型方法,又不要父类实例属性干扰"的难题,同时没有引入多余的内存开销。
5.寄生组合式继承(ES5 终极方案)🏆
这是 Vue/React 源码中(ES5转译版)真正使用的继承方式。它结合了上面两种方式的优点,并去掉了副作用。
核心思路:属性用 call 借用,原型用 Object.create 连接。
JavaScript
function Cat(name, color) {
// 1. 构造函数借用:继承属性,解决引用共享,可传参
Animal.call(this);
this.name = name;
this.color = color;
}
// 2. 原型继承:继承方法
// ❌ 别写 Cat.prototype = new Animal() -> 会多执行一次构造函数
// ✅ 用 Object.create 创建一个干净的空对象,其 __proto__ 指向 Animal.prototype
Cat.prototype = Object.create(Animal.prototype);
// 3. 修正 constructor 指向
Cat.prototype.constructor = Cat;
// 4. 添加子类独有方法
Cat.prototype.say = function() {
console.log("喵喵喵~");
};
// 测试
const cat1 = new Cat("大橘", "橘色");
cat1.hobbies.push("跑酷");
const cat2 = new Cat("英短", "灰");
console.log(cat1.hobbies); // [..., "跑酷"]
console.log(cat2.hobbies); // [..., "干饭"] -> 互不干扰
cat1.eat(); // 方法可用
🌟 为什么这是最完美的 ES5 方案?
它避免了 new Animal() 的调用,因此不会在 Cat.prototype 上产生多余的、无用的父类属性,只保留了纯净的原型链关系。
6.ES6 Class extends(现代标准)
虽然写起来像 Java,但切记:JS 中没有类,class 只是原型的语法糖。底层逻辑依然是上面的"寄生组合式继承"。
JavaScript
class Animal {
constructor() {
this.species = "动物";
}
eat() {
console.log("eating...");
}
}
class Cat extends Animal {
constructor(name, color) {
// 🚨 重点:子类 constructor 里必须先调用 super()
// 否则无法使用 this,因为 ES6 的继承机制是先创造父类的实例对象
super();
this.name = name;
this.color = color;
}
say() {
console.log("miao~");
}
}
🕵️♂️ 面试深度追问:ES6 和 ES5 继承的区别?
这是区分初级和中级开发者的关键点:
-
this 的创建机制不同(最重要):
- ES5:先创建子类的实例对象 this,然后再将父类的方法添加到 this 上(Animal.call(this))。
- ES6:子类自己没有 this,必须先调用 super()(即父类构造函数),由父类构造出 this,再传给子类加工。
- 这就是为什么不写 super() 会报错的原因。
-
静态方法的继承:
- ES6 的 class 会把父类的静态方法(static)也继承下来,而普通的 ES5 原型链写法默认是不会继承构造函数本身的静态方法的。
📝 总结:面试突击小抄
-
instanceof 原理:顺着 __proto __ 链往上找,看能不能找到构造函数的 prototype。
-
原型链口诀:实例的隐式原型等于构造函数的显式原型。
-
继承推荐:
-
手写 ES5:寄生组合式继承(call 借属性 + Object.create 借原型)。
-
项目开发:ES6 class extends(语法糖,语义清晰)。
-
-
在 JavaScript 中,所有函数对象(包括
Function、Object、Array等内置构造函数)的内部原型([[Prototype]],可通过__proto__访问)都指向Function.prototype
