彻底搞懂JavaScript原型与原型链:从底层原理到生产最佳实践
前言
原型与原型链是JavaScript的灵魂,也是前端面试100%必问的考点。但绝大多数开发者对它的理解都停留在"背概念"的层面:
- 知道"每个函数都有prototype",但不知道为什么要有
- 知道"实例的__proto__指向构造函数的prototype",但不知道MDN为什么说__proto__已弃用
- 能写出ES5继承的代码,但不知道每一行到底在底层做了什么
这篇文章我会从JS引擎的底层设计出发,用最直观的图表和可运行的代码,带你彻底搞懂原型与原型链的本质。读完这篇,你不仅能轻松应对所有原型相关的面试题,还能写出更优雅、更安全、性能更好的JS代码。
一、先搞懂3个核心概念(90%的人都搞混了)
在看任何代码之前,先把这3个概念刻在脑子里,后面所有内容都围绕它们展开。
1.1 最容易混淆的两个属性
这是原型体系中最核心的区分,也是所有误解的根源:
| 属性 | 拥有者 | 本质 | 状态 | 作用 |
|---|---|---|---|---|
prototype |
只有函数才有 | 函数的一个普通对象属性 | ✅ 标准特性 | 存放所有实例共享的属性和方法 |
__proto__ |
所有对象都有 | 浏览器实现的非标准访问器 | ❌ 已弃用(遗留兼容) | 访问对象内部的[[Prototype]]槽 |
1.2 真正的核心:[[Prototype]]内部槽
[[Prototype]]是JavaScript语言最底层的核心机制 ,永远不会被弃用。它是每个对象内部的一个隐藏属性,指向该对象的原型,原型链就是由无数个[[Prototype]]指针串联而成的。
__proto__只是早期浏览器厂商为了让开发者能操作这个隐藏属性,私自实现的一个"后门"。ES6虽然将其勉强纳入标准,但明确标记为"遗留特性",不推荐在生产代码中使用。
1.3 标准替代API
现在操作原型的标准方式是使用以下API,完全替代__proto__的所有功能:
- 获取原型:
Object.getPrototypeOf(obj)/Reflect.getPrototypeOf(obj) - 设置原型:
Object.setPrototypeOf(obj, proto)(尽量避免) - 创建指定原型的对象:
Object.create(proto)(强烈推荐)
二、一张图看懂原型的基础关系
这是JavaScript原型最经典、最权威的基础结构图,所有面向对象的底层逻辑都建立在这个结构之上。

逐关系解释
-
构造函数 ↔ 原型对象(黑色双向箭头)
- JS在创建任何函数时,都会自动生成一个对应的原型对象
Person.prototype.constructor === Person永远成立- 这组关系对所有构造函数都成立,包括内置的
Object、Array、Function
-
实例 → 原型对象(蓝色箭头)
- 当执行
new Person()时,JS引擎会自动将新对象的[[Prototype]]指向Person.prototype - 这是实例能访问原型方法的根本原因
- 验证:
Object.getPrototypeOf(person) === Person.prototype
- 当执行
-
原型链的终点
- 所有原型对象最终都会指向
Object.prototype Object.prototype是JS中最顶层的原型,它的[[Prototype]]指向nullnull表示"没有对象",是属性查找的终止符
- 所有原型对象最终都会指向
三、原型链的本质:属性查找机制
原型链不是什么神秘的东西,它就是JS引擎查找对象属性的一套规则。当你访问obj.xxx时,JS引擎会严格按照以下步骤执行:
lua
开始
│
▼
┌─────────────────────────────┐
│ 检查 obj 自身是否有 xxx 属性 │
└─────────────────────────────┘
│ │
│ 有 │ 没有
▼ ▼
┌─────────┐ ┌─────────────────────────────┐
│ 返回值 │ │ 检查 obj 的 [[Prototype]] │
└─────────┘ │ 是否有 xxx 属性 │
└─────────────────────────────┘
│ │
│ 有 │ 没有
▼ ▼
┌─────────┐ ┌─────────────────────────────┐
│ 返回值 │ │ [[Prototype]] 是 null 吗? │
└─────────┘ └─────────────────────────────┘
│ │
│ 是 │ 否
▼ ▼
┌─────────┐ ┌─────────────────────────────┐
│ undefined│ │ 将 obj 指向它的 [[Prototype]]│
└─────────┘ └─────────────────────────────┘
│
└──────────────────┘
回到开始
举个例子:
javascript
const person = new Person('张三', 18);
console.log(person.toString()); // 输出 "[object Object]"
查找过程:
person自身没有toString方法- 找
Object.getPrototypeOf(person)(即Person.prototype),也没有 - 找
Object.getPrototypeOf(Person.prototype)(即Object.prototype),找到了 - 执行
toString方法,this自动绑定到person对象
这就是为什么所有对象都能调用toString()、hasOwnProperty()等方法 ------它们都定义在Object.prototype上。
四、ES5实现继承的完整原理
ES5没有原生的class和extends关键字,继承完全是通过构造函数借用 + 原型链来实现的。这是面试最高频的考点,我们逐行拆解。
4.1 完整代码
javascript
// 1. 定义父类
function Person(name, age) {
this.name = name; // 实例自有属性
this.age = age;
}
// 父类实例方法(所有实例共享)
Person.prototype.sayHello = function() {
console.log(`我是${this.name},今年${this.age}岁`);
};
// 2. 定义子类
function Student(name, age, grade) {
// 第一步:借用父类构造函数,继承自有属性
Person.call(this, name, age);
this.grade = grade; // 子类自有属性
}
// 第二步:原型链继承,继承父类方法
Student.prototype = Object.create(Person.prototype);
// 第三步:修正constructor指向(非常重要)
Student.prototype.constructor = Student;
// 第四步:添加子类自己的方法
Student.prototype.study = function() {
console.log(`${this.name}正在${this.grade}学习`);
};
// 测试
const student = new Student('张三', 18, '高三');
student.sayHello(); // "我是张三,今年18岁"
student.study(); // "张三正在高三学习"
4.2 继承后的原型链图
执行完上面的代码后,完整的原型链结构如下:
构造函数] -- prototype --> B[Student.prototype] B -- constructor --> A C[student
实例] -- "[[Prototype]]" --> B B -- "[[Prototype]]" --> D[Person.prototype] D -- constructor --> E[Person
构造函数] E -- prototype --> D D -- "[[Prototype]]" --> F[Object.prototype] F -- "[[Prototype]]" --> G[null] style A fill:#e1f5fe,stroke:#0288d1 style B fill:#f3e5f5,stroke:#7b1fa2 style C fill:#e8f5e9,stroke:#2e7d32 style D fill:#fff3e0,stroke:#f57c00 style F fill:#ffecb3,stroke:#ff9800 style G fill:#ffcdd2,stroke:#c62828
4.3 每一步的底层作用
-
Person.call(this, name, age)- 调用父类构造函数,但将
this绑定到子类实例 - 把父类的
name和age属性复制到子类实例上 - ❌ 错误写法:直接写
Person(name, age)会导致this指向全局对象
- 调用父类构造函数,但将
-
Student.prototype = Object.create(Person.prototype)- 创建一个新的空对象,它的
[[Prototype]]指向Person.prototype - 这是原型链继承的核心,让子类实例能访问父类的原型方法
- ❌ 绝对不能写
Student.prototype = Person.prototype,会导致父子类原型污染
- 创建一个新的空对象,它的
-
Student.prototype.constructor = StudentObject.create()创建的新对象,constructor继承自Person.prototype- 如果不修正,
student.constructor会指向Person而不是Student - 这是一个非常容易被忽略但极其重要的细节
-
添加子类方法
- 必须在修正原型之后添加,否则会被
Object.create()创建的新对象覆盖
- 必须在修正原型之后添加,否则会被
五、ES6 class的底层真相
很多人以为ES6的class是JS引入了全新的面向对象系统,其实它只是原型链的语法糖,底层做的事情和我们上面手写的ES5继承完全一样。
javascript
// ES6 写法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 等价于 Person.prototype.sayHello
sayHello() {
console.log(`我是${this.name},今年${this.age}岁`);
}
// 等价于 Person.create = function() {}
static create(name, age) {
return new Person(name, age);
}
}
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 等价于 Person.call(this, name, age)
this.grade = grade;
}
// 等价于 Student.prototype.study
study() {
console.log(`${this.name}正在${this.grade}学习`);
}
}
extends关键字自动帮我们完成了:
- 原型链的建立
constructor指向的修正super关键字的正确绑定
这就是为什么现在生产环境推荐使用class语法------它更清晰、更易读,也避免了手写ES5继承时容易犯的错误。
六、90%开发者都会踩的5个坑
坑1:把实例方法写在构造函数里
javascript
// ❌ 错误:每创建一个实例都会生成一个新的sayHello函数
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log(`我是${this.name}`);
};
}
// ✅ 正确:所有实例共享同一个sayHello函数
Person.prototype.sayHello = function() {
console.log(`我是${this.name}`);
};
坑2:继承时直接赋值原型
javascript
// ❌ 错误:Student.prototype和Person.prototype指向同一个对象
// 给Student.prototype加方法会污染Person的实例
Student.prototype = Person.prototype;
// ✅ 正确:创建一个新的空对象,原型指向Person.prototype
Student.prototype = Object.create(Person.prototype);
坑3:忘记修正constructor指向
javascript
Student.prototype = Object.create(Person.prototype);
// 忘记写这行
// Student.prototype.constructor = Student;
const student = new Student();
console.log(student.constructor); // 输出 Person,而不是 Student!
坑4:滥用Object.setPrototypeOf
虽然Object.setPrototypeOf是标准API,但它和__proto__一样,会严重破坏JS引擎的性能优化。现代JS引擎通过"隐藏类"优化属性访问,修改原型会导致所有相关对象的隐藏类被销毁,性能下降10-100倍。
✅ 最佳实践:永远在创建对象时就指定原型(使用Object.create()),而不是事后修改。
坑5:原型污染攻击
这是Web开发中最常见的安全漏洞之一,根源就是__proto__的可写性。
javascript
// 攻击者提交的JSON数据
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
// 所有对象都被污染了!
console.log({}.isAdmin); // true
console.log([] .isAdmin); // true
✅ 防御方案:
- 使用
JSON.parse的reviver函数过滤__proto__键 - 使用
Object.create(null)创建无原型对象 - 使用成熟的库(如
lodash.clonedeep)进行深拷贝
七、生产环境最佳实践总结
- 优先使用ES6
class语法,它是现在的标准写法,隐藏了原型操作的底层细节 - 永远不要使用
__proto__,生产代码中使用Object.getPrototypeOf()获取原型 - 尽量不要修改任何对象的原型 ,如果必须创建指定原型的对象,使用
Object.create() - 绝对不要直接赋值原型,避免原型污染
- 处理用户输入时必须过滤
__proto__键,防止原型污染攻击 - 实例方法挂载到
prototype上,静态方法挂载到构造函数本身
八、面试考点速记
- 原型链的本质是JS引擎的属性查找机制
prototype是函数的属性,[[Prototype]]是对象的内部槽__proto__已弃用,标准替代是Object.getPrototypeOf()- ES5继承的4个步骤:借用构造函数、原型链继承、修正constructor、添加子类方法
- ES6
class是原型链的语法糖,底层实现和ES5完全一致 - 原型污染的原理和防御方法
最后
原型与原型链是JavaScript最独特、最核心的设计,也是区分前端入门和进阶的重要标志。很多人觉得它难,是因为没有从底层理解它的设计初衷。
当你真正搞懂了原型链,你会发现JS的面向对象其实非常优雅和灵活。希望这篇文章能帮你彻底打通原型这一关,写出更优秀的JavaScript代码。
如果觉得文章对你有帮助,欢迎点赞、收藏、评论交流,我会持续更新更多JavaScript核心原理的内容。
需要我把文中的Mermaid图表导出为高清PNG图片 ,或者补充一个原型污染攻击的完整防御代码示例吗?