你想实现一个队列。在别的语言里,这样写:
java
// Java
class MyQueue {
void push(int x) { ... }
int pop() { ... }
}
MyQueue q = new MyQueue();
在 JS 里,你也可以这样写:
js
// JS --- ES6 class 语法
class MyQueue {
push(x) { ... }
pop() { ... }
}
const q = new MyQueue()
但 JS 的 class 只是语法糖 。底层运行的是一套完全不同的机制------原型(prototype)。不理解原型,你在 JS 里写的每一个 new、每一个 this、每一次方法调用,都是在"碰运气"。
这篇文章用实现 MyQueue 的过程,把原型链、new 的过程、prototype 和 __proto__ 的区别一次性拆清楚。
一、起点:不用 class,怎么实现队列?
先忘掉 class。用最原始的方式------构造函数------来实现 MyQueue:
js
function MyQueue() {
this.items = [] // 每个实例自己的数据
this.push = function(x) { this.items.push(x) }
this.pop = function() { return this.items.shift() }
this.peek = function() { return this.items[0] }
}
const q1 = new MyQueue()
const q2 = new MyQueue()
能跑。但有一个隐藏的问题:
js
console.log(q1.push === q2.push) // false
q1 和 q2 的 push 是两个不同的函数对象 。每 new 一次,三个方法就被重新创建一次------100 个实例 = 300 个函数对象,每个都占内存。
这不是"性能问题",是"设计问题"。 方法应该是共享的,数据才应该是各自的。Java、Python、C++ 的 class 天然就是这样------方法存在类上,数据存在实例上。JS 没有类,怎么办?
二、解法:把方法搬到原型上
js
function MyQueue() {
this.items = [] // 数据 --- 每个实例自己的
}
MyQueue.prototype.push = function(x) { this.items.push(x) }
MyQueue.prototype.pop = function() { return this.items.shift() }
MyQueue.prototype.peek = function() { return this.items[0] }
const q1 = new MyQueue()
const q2 = new MyQueue()
console.log(q1.push === q2.push) // true --- 同一个函数!
q1 和 q2 共享了同一组方法。但 q1.push() 调用时,this 还是正确指向 q1。这是怎么做到的?
答案在 new 里。 要理解原型,必须先理解 new 到底做了什么。
三、new 的三个步骤
当你写 const q = new MyQueue(),JS 引擎做的事可以压缩成三步:
kotlin
① 创建一个空对象,this 指向它
② 执行构造函数(在 this 上添加属性)
③ 把新对象的 __proto__ 指向构造函数的 prototype
拆开看:
js
// 模拟 new 的行为(不要在生产代码里这样写)
function myNew(Constructor) {
const obj = {} // ① 空对象
Constructor.call(obj) // ② 执行构造函数,this = obj
obj.__proto__ = Constructor.prototype // ③ 连接原型
return obj
}
第三步是关键。它建了一条链:
javascript
q1.__proto__ → MyQueue.prototype
当你调用 q1.push('a'),JS 引擎的查找顺序是:
javascript
① q1 自身有没有 push? → 没有(q1 只有 items)
② q1.__proto__ 上有没有 push?→ 有!在 MyQueue.prototype 上找到了
③ 执行,this 绑定为 q1
这就是原型最核心的机制:实例先在自身查找属性,找不到就沿着
__proto__链往上找。
四、prototype 和 __proto__:你到底该看哪个?
这是原型里最容易搞混的一对概念。一句话区分:
| 属性 | 长在谁身上 | 含义 |
|---|---|---|
prototype |
函数身上 | "用我 new 出来的实例,原型指向这里" |
__proto__ |
对象身上 | "我找不到属性时,去这里找" |
验证:
js
function MyQueue() {}
const q = new MyQueue()
// prototype 是函数才有的属性
console.log(MyQueue.prototype) // { push, pop, peek, constructor, ... }
console.log(q.prototype) // undefined --- 实例没有 prototype
// __proto__ 是对象都有的属性
console.log(q.__proto__) // 指向 MyQueue.prototype
console.log(q.__proto__ === MyQueue.prototype) // true
关系图:
ini
MyQueue(构造函数 / 函数对象)
│
│ .prototype
▼
MyQueue.prototype(原型对象)
│ .constructor ────────────→ MyQueue
│ .push / .pop / .peek (共享方法)
▲
│ .__proto__
│
q1(实例对象)
.items = [...]
注意
MyQueue.prototype.constructor指回了MyQueue本身。所以原型对象和构造函数之间是双向的------prototype从函数指向原型,constructor从原型指回函数。
五、原型链:一层一层往上找,直到 null
刚才说 q1 找不到属性时会沿着 __proto__ 往上找。那 MyQueue.prototype 自己也是一个对象------它找不到属性时怎么办?
它也往上找。
js
// MyQueue.prototype 上没有 toString,但 q1 能调用:
console.log(q1.toString()) // '[object Object]'
查找路径:
javascript
q1 自身 → 没有 toString
q1.__proto__ → MyQueue.prototype → 也没有 toString
q1.__proto__.__proto__ → Object.prototype → 找到了 toString
MyQueue.prototype 的 __proto__ 指向 Object.prototype。因为 MyQueue.prototype 本质上就是一个普通对象------相当于 new Object() 创建的。
原型链的终点:
js
console.log(Object.prototype.__proto__) // null --- 链的终点
javascript
q1 ──__proto__──→ MyQueue.prototype ──__proto__──→ Object.prototype ──__proto__──→ null
↑ ↑
共享方法 toString, valueOf, hasOwnProperty...
任何对象,要么原型直接是
Object.prototype,要么原型链的终点前一定是Object.prototype。null 是整条链的尽头。
六、回到 JS 的设计哲学
理解了原型链,JS 为什么"不走寻常路"就清楚了:
6.1 JS 没有类,只有对象
arduino
人(概念 / class)
└→ 张三(具体的人 / 对象)
└→ prototype(张三的原型------描述"人"应该有什么)
Java 先有 class Person,再用 new Person() 制造对象。JS 反过来------先有对象,再通过原型链表达"这个对象应该像谁"。
6.2 一切对象,终点相同
js
// 这些写法等价:
let obj1 = {}
let obj2 = new Object()
// 这些"类型"本质上都是函数对象:
typeof Function // 'function'
typeof Array // 'function'
typeof Date // 'function'
typeof RegExp // 'function'
Function、Array、Date 都是函数对象 。它们的 prototype 上挂着自己的方法(Array.prototype.push、Date.prototype.getFullYear 等),而它们的 prototype.__proto__ 全部指向 Object.prototype。
6.3 原型链统一了继承
javascript
null
▲
│ __proto__
Object.prototype ← 所有对象的终点
▲
│ __proto__
Array.prototype ← 数组方法:push, pop, map, forEach...
▲
│ __proto__
[1, 2, 3] ← 一个具体的数组实例
数组能调用 toString(),不是因为它自己定义了,也不是因为 Array.prototype 定义了------而是因为 Array.prototype.__proto__ 指向 Object.prototype,那里有 toString。
这就是 JS 的继承:不是 class 嵌套 class,而是对象链对象。
七、完整实现:MyQueue 的原型全貌
把学到的串起来,MyQueue 的完整结构:
js
// 构造函数 --- 定义实例自己的数据
function MyQueue() {
this.items = []
}
// 原型对象 --- 定义共享的方法
MyQueue.prototype.push = function(x) { this.items.push(x) }
MyQueue.prototype.pop = function() { return this.items.shift() }
MyQueue.prototype.peek = function() { return this.items[0] }
MyQueue.prototype.empty = function() { return this.items.length === 0 }
运行时它们之间的关系:
rust
MyQueue(函数)
│
.prototype │
▼
MyQueue.prototype
┌─────────────────────┐
│ .constructor → MyQueue│
│ .push → fn │
│ .pop → fn │
│ .peek → fn │
│ .empty → fn │
│ .__proto__ → ↓ │
└─────────────────────┘
│
.__proto__ │
▼
Object.prototype
┌─────────────────────┐
│ .toString → fn │
│ .valueOf → fn │
│ .hasOwnProperty → fn│
│ .__proto__ → null │
└─────────────────────┘
q1 (实例) q2 (实例)
┌──────────────┐ ┌──────────────┐
│ .items = [] │ │ .items = [] │
│ .__proto__ → │ │ .__proto__ → │
└──────┬───────┘ └──────┬───────┘
└──────────┬───────────┘
▼
MyQueue.prototype
调用 q1.push('a') 时的查找路径:
javascript
q1 自身有 push 吗? → 没有
q1.__proto__ 有 push 吗? → 有(MyQueue.prototype.push)
→ 执行 MyQueue.prototype.push,this = q1
→ this.items 就是 q1.items
→ push('a') 推入了 q1.items
八、一句话收住
JS 的面向对象不是"类→实例"的模具模型,而是"对象→原型→Object.prototype→null"的链式查找。
理解这条链,你就理解了 JS 里的 this 为什么指向它指向的东西、方法为什么能共享、class 语法糖到底甜在哪里、以及 Array.map 和 Object.toString 为什么能在同一个对象上共存。
下次写 const arr = [1, 2, 3],你可以想:这个数组背后站着一整条原型链。
附:核心结论速查
| 概念 | 一句话 |
|---|---|
| 构造函数 | 用 new 调用的普通函数,负责往 this 上挂实例属性 |
prototype |
函数独有的属性,指向"用我 new 出来的实例的原型对象" |
__proto__ |
对象都有的属性,指向"我找不到属性时去这里找" |
| 原型对象 | 挂在 prototype 上的对象,存共享方法,有 constructor 指回构造函数 |
| 原型链 | 实例.__proto__ → 构造函数.prototype → Object.prototype → null |
new 三步 |
创建空对象 → 执行构造函数 → 连接原型 |
| JS 设计哲学 | 没有类,只有对象;通过原型链实现继承和方法共享 |