很多同学背过这样一句话:"JavaScript 是通过原型链(Prototype Chain)实现继承的" 。但真被问到"原型链是怎么串起来的"时,往往只能答出"__proto__ 指来指去"。
这篇文章的目标只有一个:让你在脑中建立起一张永不忘记的结构图 ,之后所有关于原型、继承、instanceof 的问题,都从这张图出发推理,而不是死记结论。
1. 为什么需要原型链:JS 的对象模型
Java / C++ 的继承靠"类(Class)",是先有类,再实例化对象。补充一点: ES6 正式发布了 JS 的 class,但他不是"类模型",而是基于已有的(函数 + prototype + 原型链)写出来的语法糖,本质上还是原型链的那一套内容。
JS 不一样------JS 从设计之初(1995 年,Brendan Eich 用 10 天完成)就只有对象 。那对象之间怎么共享方法?答案:让一个对象"借用"另一个对象的能力。
这个"借用"关系就是原型链:
- 每个对象内部都藏着一个隐式引用
[[Prototype]](浏览器里可通过__proto__访问); - 访问属性时,如果自己身上没有,就沿着
[[Prototype]]一直向上找; - 这条"对象 → 原型 → 原型的原型 → ..."的链条,就叫原型链。
一句话总结:原型链 = 对象间共享能力的机制 + 属性查找的规则。
2. 三个核心概念:prototype / proto / constructor
搞清楚这三个东西,原型链就懂了一半。请先背下这张对照表:
| 概念 | 谁拥有 | 指向哪里 | 作用 |
|---|---|---|---|
prototype |
只有函数有(箭头函数除外) | 指向一个普通对象 | 用 new 创建实例时,实例的原型指向它 |
__proto__ |
每个对象都有(函数也是对象) | 指向"创建我的那个构造函数的 prototype" |
组成原型链的"链环" |
constructor |
每个函数的 prototype 对象上有 |
指回这个函数本身 | 让原型知道"我是谁构造的" |
最容易混淆的点(先记住,后面还会展开):
js
person.__proto__ // 实例 → 原型 (对象到对象)
Person.prototype // 函数 → 原型 (函数到对象)
Person.prototype.constructor === Person // 原型 → 函数(回指)
3. 原型链长什么样:结构总览
我们定义一个构造函数,然后 new 出实例,看看这三者的关系网:
js
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log(`Hi, I'm ${this.name}`);
};
const person1 = new Person('Tom', 18);
它背后的结构是下面这张图(请把这张图刻进脑子里):

读图要点:
- 实线箭头是
__proto__,它把"实例 → 原型 → 内置原型 → null"串成一条链; - 虚线箭头是
prototype与constructor的互相指认,它们构成"三角关系",但不参与属性查找; - 链条最终指向
null------原型链是有终点的,不是无限循环。
验证一下(这几个等式能答对,你就真的懂了):
js
person1.__proto__ === Person.prototype; // true 实例的原型是构造函数的 prototype
Person.prototype.constructor === Person; // true 原型的 constructor 回指函数
Person.prototype.__proto__ === Object.prototype; // true 原型对象本身也是对象,它的原型是内置 Object.prototype
Object.prototype.__proto__ === null; // true 链的终点
4. new 到底做了什么:逐行拆解
new Person('Tom', 18) 不是魔法,它本质上做了 4 步。手写一个等价实现:
js
function myNew(Ctor, ...args) {
// 第 1 步:创建一个全新的空对象
const obj = {};
// 第 2 步:把空对象的原型指向构造函数的 prototype ------ 关键!这一步决定了 obj 能"继承"什么
obj.__proto__ = Ctor.prototype;
// 第 3 步:以 obj 为 this 执行构造函数,把实例属性挂上去
const result = Ctor.apply(obj, args);
// 第 4 步:构造函数若显式返回了对象,则用那个对象;否则返回 obj
return (result !== null && typeof result === 'object') ? result : obj;
}
const person2 = myNew(Person, 'Jerry', 20);
console.log(person2.sayHi()); // Hi, I'm Jerry ------ 方法来自原型链,属性来自构造函数
逐行解释:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 1 | const obj = {} |
造一个裸对象,它此刻还"一无所有" |
| 2 | obj.__proto__ = Ctor.prototype |
建立原型链的瞬间 :让 obj 能沿链找到 sayHi 等方法 |
| 3 | Ctor.apply(obj, args) |
让 this 指向 obj,执行 this.name = ...,把属性装进 obj |
| 4 | 判断返回值 | 模拟 new 的特殊规则:构造函数返回对象时,new 会丢弃自己造的对象 |
第 2 步就是"继承"的真相:所谓继承,本质是实例的原型指向了构造函数的 prototype。
5. 属性查找:沿链向上,命中即停
这是原型链最核心的行为。规则只有一句话:访问 obj.prop 时,先从自身找,找不到就沿着 __proto__ 一层层向上找,直到 null。

5.1 遮蔽(Shadowing):自身属性优先
js
Person.prototype.name = '原型上的名字';
const p = new Person('实例名', 20);
console.log(p.name); // '实例名' ------ 自身属性优先,遮蔽了原型上的同名属性
delete p.name; // 删掉自身的 name
console.log(p.name); // '原型上的名字' ------ 自身没了,才沿链找到原型
注意陷阱 :p.name = 'x' 只会给 p 自身新增 一个属性(遮蔽原型),永远不会去改原型上的 name 。要改原型,得显式 Person.prototype.name = 'y'。
5.2 一个经典查找演示
js
console.log(person1.sayHi()); // 自身没有 → 在 Person.prototype 找到 ✅
console.log(person1.toString()); // 自身、Person.prototype 都没有 → 在 Object.prototype 找到 ✅
console.log(person1.valueOf()); // 同上,Object.prototype 提供 ✅
console.log(person1.x); // 整条链都没有 → undefined ✅
6. 链的尽头:Object.prototype 与 null
- 所有普通对象的原型链终点都是
Object.prototype,它提供了toString/valueOf/hasOwnProperty等"人人可用"的方法; Object.prototype的__proto__是null,链到此为止,再往上没有了。
js
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true ------ 所有字面量对象的原型都是它
console.log(Object.prototype.__proto__); // null ------ 终点
console.log({}.toString()); // '[object Object]' ------ 从 Object.prototype 借来的
7. 常用工具方法:别只会用,要懂原理
7.1 hasOwnProperty ------ 只看自身
js
const p = new Person('Tom', 18);
console.log(p.hasOwnProperty('name')); // true ------ name 是自身属性
console.log(p.hasOwnProperty('sayHi')); // false ------ sayHi 在原型上,不是自身属性
7.2 in 操作符 ------ 含原型链
js
console.log('name' in p); // true 自身有
console.log('sayHi' in p); // true 原型链上有也算
console.log('toString' in p); // true 一路查到了 Object.prototype
区分口诀 :
hasOwnProperty只看"自己兜里有没有",in看"整条链上有没有"。
7.3 instanceof ------ 沿原型链做"成员检查"
js
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true ------ Object.prototype 也在链上
console.log(person1 instanceof Array); // false
原理 :obj instanceof Ctor 等价于------判断 Ctor.prototype 是否出现在 obj 的原型链上。手写:
js
function myInstanceof(obj, Ctor) {
let proto = obj.__proto__;
while (proto !== null) {
if (proto === Ctor.prototype) return true;
proto = proto.__proto__; // 沿链向上走
}
return false;
}
console.log(myInstanceof(person1, Person)); // true
console.log(myInstanceof(person1, Object)); // true
7.4 Object.create ------ 手动指定原型
不想用构造函数?Object.create(proto) 直接造一个"原型是你指定的对象"的新对象:
js
const base = { greet() { return 'hello'; } };
const obj = Object.create(base);
console.log(obj.greet()); // 'hello' ------ 继承自 base
console.log(obj.__proto__ === base); // true ------ 原型被直接指定为 base
console.log(obj.hasOwnProperty('greet')); // false ------ greet 在原型上
注意 :Object.create(null) 可以造出"没有任何原型"的裸对象(常用于字典/缓存,不会被原型方法干扰)。
8. class 与 extends:语法糖下的原型链
class 是 ES6 引入的语法糖,底层依然是原型链。别被它"像 Java 的类"的外表骗了。
js
class Person {
constructor(name) { this.name = name; } // 对应原来的构造函数
sayHi() { return `Hi, ${this.name}`; } // 自动挂到 Person.prototype 上
}
class Student extends Person { // extends = 设置原型链继承
constructor(name, grade) {
super(name); // super() = 调用父类构造函数
this.grade = grade;
}
study() { return `${this.name} 在写作业`; }
}
const s = new Student('小明', '高一');
console.log(s.sayHi()); // Hi, 小明 ------ 沿链找到 Person.prototype.sayHi
console.log(s.study()); // 小明 在写作业 ------ 找到 Student.prototype.study
它的原型链结构如下(注意有两条链):

两张"链"要分清:
| 链条 | 用途 | 关键等式 |
|---|---|---|
实例链 (s → Student.prototype → Person.prototype → Object.prototype → null) |
实例找方法 | s.__proto__ === Student.prototype |
类链 (Student.__proto__ === Person) |
类找静态方法 | Student.extend === Person.extend(若父类有静态方法) |
所以 class 让 JS 同时拥有了"实例继承"和"静态成员继承",而底层都是原型引用。
9. 常见误区避坑:一张表说清
| # | 误区说法 | 真相 |
|---|---|---|
| 1 | "__proto__ 和 prototype 是一回事" |
__proto__ 是每个对象 的隐式原型(链环);prototype 是函数独有的属性,用来给实例当原型 |
| 2 | "普通对象也有 prototype" |
只有函数有 prototype;普通对象(包括实例)只有 __proto__ |
| 3 | "箭头函数可以用作构造函数" | 箭头函数没有 prototype ,也不能被 new |
| 4 | "原型链是无限循环的" | 终点是 null,Object.prototype.__proto__ === null |
| 5 | "obj.name = 'x' 会修改原型" |
只会给 obj 自身 新增属性(遮蔽),不会动原型;改原型要显式操作 Ctor.prototype |
| 6 | "class 是全新的继承机制" |
纯语法糖,底层仍是原型链;extends 就是在设置 Student.prototype.__proto__ |
| 7 | "instanceof 检查实例的构造函数" |
它检查的是 Ctor.prototype 是否在实例的原型链上,跟实例是谁 new 出来的没有直接关系 |
10. 面试高频题:会做才算真懂
Q1:手写 new
js
function myNew(Ctor, ...args) {
const obj = Object.create(Ctor.prototype); // 一步到位:空对象 + 连原型
const res = Ctor.apply(obj, args);
return (res !== null && typeof res === 'object') ? res : obj;
}
Q2:instanceof 的原理是什么?
判断构造函数的
prototype是否出现在实例的原型链上(从obj.__proto__一路查到null)。
Q3:p.hasOwnProperty('x') 与 'x' in p 的区别?
前者只看自身属性;后者看整条原型链(含继承来的属性)。
Q4:下面的输出是什么?
js
function A() {}
A.prototype.x = 1;
const a = new A();
a.x = 2;
console.log(a.x); // 2 ------ 自身遮蔽原型
delete a.x;
console.log(a.x); // 1 ------ 自身删除后沿链找到原型
console.log(a.hasOwnProperty('x')); // false ------ 现在 x 在原型上
Q5:为什么 Object 和 Function 互为"祖先"?
js
Function.prototype.__proto__ === Object.prototype; // true ------ 函数最终也是对象
Object.__proto__ === Function.prototype; // true ------ Object 本身也是个函数
// 妙处:Function.prototype 是函数,但它没有 prototype 指向自己(Function.prototype 也是对象)
记忆法:"万物皆对象,函数也是对象" 。所以
Function.prototype的__proto__指向Object.prototype;而Object作为函数,它的__proto__指向Function.prototype。二者互相"认亲",但不构成循环(终点仍是 null)。
11. 一页速记表(面试前 30 秒扫一遍)
| 问题 | 答案 |
|---|---|
| 原型链是什么? | 对象通过 __proto__ 串联起来、用于属性查找的链 |
谁有 prototype? |
只有函数(箭头函数除外) |
谁有 __proto__? |
所有对象(含函数) |
| 实例怎么连上原型? | new 时执行 obj.__proto__ = Ctor.prototype |
| 属性查找顺序? | 自身 → __proto__ → ... → Object.prototype → null → undefined |
| 链的终点? | Object.prototype.__proto__ === null |
instanceof 原理? |
Ctor.prototype 是否在 obj 原型链上 |
class extends 本质? |
设置 Student.prototype.__proto__ = Person.prototype |
如何避开 __proto__? |
用 Object.getPrototypeOf(obj) / Object.create(proto) |
结语
原型链不是玄学,它只是一条**"属性找不到就往上级借"的查找链**,外加"函数通过 prototype 把方法共享给所有实例"这个设计。
下次再遇到原型题,别背结论,先在脑中画出那条链: 实例 → 构造函数的 prototype → 再往上 → ... → Object.prototype → null,然后在链上"走一遍属性查找",答案自然浮出水面。
附赠一句口诀:"函数有 prototype 来发方法,实例用 proto 来找方法,原型的 constructor 认回函数。"