JavaScript 为什么选择原型链?从第一性原理聊聊这个设计
学 JavaScript 的时候,原型链是个绑不过去的坎。很多人(包括我)的第一反应是:这玩意儿怎么这么别扭?为什么不像 Java、C++ 那样用类继承?
后来了解了 JavaScript 的诞生背景,才发现原型链不是"别扭的设计",而是在特定约束下的合理选择。今天从第一性原理的角度,聊聊 JavaScript 为什么选择了原型链。
先回到 1995 年
1995 年 5 月,Brendan Eich 在 Netscape 公司用 10 天时间写出了 JavaScript 的第一个版本(当时叫 Mocha)。
10 天,这个时间约束很关键。
当时 Netscape 急着在浏览器里加一门脚本语言,用来做简单的表单验证、页面交互。管理层给 Eich 的要求是:
- 语法要像 Java(因为 Netscape 和 Sun 有合作,Java 正火)
- 要简单(目标用户是业余开发者,不是专业程序员)
- 要快(10 天内搞定原型)
Eich 本来想把 Scheme(一门函数式语言)塞进浏览器,但管理层否了------语法太怪,不像 Java。于是他搞了个混合体:
- 语法:像 Java
- 函数:像 Scheme(一等公民、闭包)
- 对象系统:像 Self(原型继承)
为什么对象系统选了 Self 而不是 Java?这就要从第一性原理说起。
第一性原理:对象系统的本质需求是什么?
不管用什么方式实现,对象系统要解决的核心问题就两个:
- 代码复用:多个对象共享相同的行为
- 创建对象:能方便地造出新对象
类继承和原型继承都能解决这两个问题,但方式不同。
类继承的思路
类继承把世界分成两层:类 和实例。
markdown
类(Class)= 模板、蓝图
↓ 实例化
实例(Instance)= 具体对象
你先定义一个类,描述"这类对象长什么样、有什么方法",然后用 new 从类创建实例。
这套东西在静态语言里运作良好,但有个问题:概念多。
类继承需要你理解:类、实例、构造函数、接口、抽象类、虚函数、多重继承、菱形继承问题......一套学下来,不轻松。
原型继承的思路
原型继承只有一层:对象。
csharp
对象 → 对象 → 对象 → ... → null
没有"类"这个概念,只有对象。要创建新对象?从现有对象复制一份,改改就行。要共享行为?让多个对象指向同一个原型对象。
Self 语言的设计者 David Ungar 和 Randall Smith 在 1987 年的论文里说:
"Prototypes are more concrete than classes because they are examples of objects rather than descriptions of format and initialization."
(原型比类更具体,因为原型是对象的实例,而类只是格式和初始化的描述。)
说白了:原型是活的对象,类是抽象的描述。
为什么 JavaScript 选择了原型?
回到 1995 年的约束条件,原型继承的优势就很明显了:
1. 实现更简单
类继承需要一套复杂的类型系统:类的定义、继承关系的解析、方法查找表的构建......
原型继承只需要:
- 每个对象有个
__proto__指针,指向它的原型 - 访问属性时,顺着指针往上找
10 天时间,选哪个?
Eich 后来回忆说:"选择原型继承意味着解释器可以非常简单,同时保留面向对象的特性。"
2. 动态性更强
JavaScript 是动态语言,对象可以随时增删属性。原型继承天然支持这种动态性:
javascript
// 随时给原型加方法,所有实例立刻能用
Array.prototype.first = function() {
return this[0];
};
[1, 2, 3].first(); // 1
类继承在静态语言里很自然,但在动态语言里反而别扭------类定义完了还能改吗?方法能动态添加吗?处理起来麻烦。
3. 概念更少
类继承需要区分"类"和"实例",原型继承只有"对象"。
对于 1995 年的目标用户(网页设计师、业余开发者)来说,概念越少越好。
原型继承的核心:委托(Delegation)
原型继承有时候也叫委托继承,这个词更能说明它的工作方式。
当你访问一个对象的属性时:
- 先在对象自身找
- 找不到,委托给原型对象
- 还找不到,继续委托给原型的原型
- 直到
null
这跟类继承的"复制"模型不同。类继承是在创建实例时把行为"复制"到实例上(或者通过虚函数表间接访问)。原型继承是运行时动态查找,真正的"按需委托"。
委托的好处
内存效率高:方法只在原型上存一份,所有实例共享。
javascript
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function() {
console.log('Woof!');
};
const dog1 = new Dog('A');
const dog2 = new Dog('B');
dog1.bark === dog2.bark; // true,同一个函数
运行时可修改:原型改了,所有实例立刻生效。
javascript
Dog.prototype.bark = function() {
console.log('汪汪!');
};
dog1.bark(); // 汪汪!(立刻变了)
这种动态性在静态类继承里很难实现。
原型链的设计权衡
原型继承不是完美的,它做了一些权衡。
放弃了什么
静态类型检查:没有类,就没法在编译时检查类型。JavaScript 是动态类型语言,这是设计选择的一部分。
封装性较弱 :原型上的东西都是公开的,没有 private/protected 的原生支持(ES2022 才加了私有字段 #)。
继承关系不明显 :类继承的 extends 一眼就能看出继承关系,原型链要顺着 __proto__ 找。
得到了什么
极致的灵活性:对象可以随时改,原型可以动态换。
简单的心智模型:只有对象,没有类/实例的二元论。
运行时效率:对于 1995 年的浏览器来说,原型链的实现比类系统轻量得多。
后来的故事:class 语法糖
ES6(2015 年)加了 class 关键字,看起来像类继承:
javascript
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof!');
}
}
但这只是语法糖,底层还是原型链:
javascript
console.log(typeof Animal); // "function"
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
class 让代码更清晰,但没有改变 JavaScript 的对象模型。理解原型链,才能理解 class 背后发生了什么。
总结
JavaScript 选择原型链,不是随意的决定,而是在特定约束下的合理选择:
| 约束 | 原型继承的优势 |
|---|---|
| 10 天开发时间 | 实现简单,解释器轻量 |
| 目标用户是业余开发者 | 概念少,只有"对象" |
| 动态语言特性 | 天然支持运行时修改 |
| 浏览器性能有限 | 内存效率高,方法共享 |
从第一性原理看,对象系统的本质是"代码复用 + 对象创建"。原型继承用最简单的方式解决了这两个问题:
- 代码复用:对象委托给原型,原型上的方法共享
- 对象创建:复制现有对象,改改就行
类继承更严谨、更适合大型静态系统。原型继承更灵活、更适合动态脚本语言。JavaScript 选对了。
参考资料
- Brendan Eich on Creating JavaScript in 10 Days
- Self: The Power of Simplicity (1987)
- MDN - Inheritance and the prototype chain
- Wikipedia - Prototype-based programming
- Master the JavaScript Interview: Class vs Prototypal Inheritance
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB