JavaScript 为什么选择原型链?从第一性原理聊聊这个设计

JavaScript 为什么选择原型链?从第一性原理聊聊这个设计

学 JavaScript 的时候,原型链是个绑不过去的坎。很多人(包括我)的第一反应是:这玩意儿怎么这么别扭?为什么不像 Java、C++ 那样用类继承?

后来了解了 JavaScript 的诞生背景,才发现原型链不是"别扭的设计",而是在特定约束下的合理选择。今天从第一性原理的角度,聊聊 JavaScript 为什么选择了原型链。


先回到 1995 年

1995 年 5 月,Brendan Eich 在 Netscape 公司用 10 天时间写出了 JavaScript 的第一个版本(当时叫 Mocha)。

10 天,这个时间约束很关键。

当时 Netscape 急着在浏览器里加一门脚本语言,用来做简单的表单验证、页面交互。管理层给 Eich 的要求是:

  1. 语法要像 Java(因为 Netscape 和 Sun 有合作,Java 正火)
  2. 要简单(目标用户是业余开发者,不是专业程序员)
  3. 要快(10 天内搞定原型)

Eich 本来想把 Scheme(一门函数式语言)塞进浏览器,但管理层否了------语法太怪,不像 Java。于是他搞了个混合体:

  • 语法:像 Java
  • 函数:像 Scheme(一等公民、闭包)
  • 对象系统:像 Self(原型继承)

为什么对象系统选了 Self 而不是 Java?这就要从第一性原理说起。


第一性原理:对象系统的本质需求是什么?

不管用什么方式实现,对象系统要解决的核心问题就两个:

  1. 代码复用:多个对象共享相同的行为
  2. 创建对象:能方便地造出新对象

类继承和原型继承都能解决这两个问题,但方式不同。

类继承的思路

类继承把世界分成两层:实例

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)

原型继承有时候也叫委托继承,这个词更能说明它的工作方式。

当你访问一个对象的属性时:

  1. 先在对象自身找
  2. 找不到,委托给原型对象
  3. 还找不到,继续委托给原型的原型
  4. 直到 null
flowchart LR A["obj.foo"]:::start --> B{"obj 有 foo?"} B -->|有| C["返回 obj.foo"]:::success B -->|没有| D{"obj.__proto__ 有 foo?"} D -->|有| E["返回原型的 foo"]:::success D -->|没有| F["继续往上找..."] F --> G["直到 null,返回 undefined"]:::error classDef start fill:#cce5ff,stroke:#0d6efd,color:#004085 classDef success fill:#d4edda,stroke:#28a745,color:#155724 classDef error fill:#f8d7da,stroke:#dc3545,color:#721c24

这跟类继承的"复制"模型不同。类继承是在创建实例时把行为"复制"到实例上(或者通过虚函数表间接访问)。原型继承是运行时动态查找,真正的"按需委托"。

委托的好处

内存效率高:方法只在原型上存一份,所有实例共享。

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 选对了。


参考资料


如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:

Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):

全栈项目(适合学习现代技术栈):

  • 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
相关推荐
new code Boy1 小时前
vscode左侧栏图标及目录恢复
前端·javascript
唐诗1 小时前
Git提交信息太乱?AI一键美化!一行命令拯救你的项目历史🚀
前端·ai编程
BrianGriffin2 小时前
JS異步:setTimeout包裝為sleep
开发语言·javascript·ecmascript
涔溪2 小时前
有哪些常见的Vite插件及其作用?
前端·vue.js·vite
糖墨夕2 小时前
从一行代码看TypeScript的精准与陷阱:空值合并vs逻辑或
前端·typescript
Junsen2 小时前
使用 Supabase 实现轻量埋点监控
前端·javascript
Java&Develop2 小时前
html写一个象棋游戏
javascript·游戏·html
CnLiang2 小时前
React Compiler Plugin
前端·react.js
willxiao2 小时前
js 单例模式 6 种实现方式
javascript·设计模式