用原型实现一个队列:JS 面向对象的"不走寻常路"

你想实现一个队列。在别的语言里,这样写:

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

q1q2push两个不同的函数对象 。每 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 --- 同一个函数!

q1q2 共享了同一组方法。但 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'

FunctionArrayDate 都是函数对象 。它们的 prototype 上挂着自己的方法(Array.prototype.pushDate.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.mapObject.toString 为什么能在同一个对象上共存。

下次写 const arr = [1, 2, 3],你可以想:这个数组背后站着一整条原型链。


附:核心结论速查

概念 一句话
构造函数 new 调用的普通函数,负责往 this 上挂实例属性
prototype 函数独有的属性,指向"用我 new 出来的实例的原型对象"
__proto__ 对象都有的属性,指向"我找不到属性时去这里找"
原型对象 挂在 prototype 上的对象,存共享方法,有 constructor 指回构造函数
原型链 实例.__proto__构造函数.prototypeObject.prototypenull
new 三步 创建空对象 → 执行构造函数 → 连接原型
JS 设计哲学 没有类,只有对象;通过原型链实现继承和方法共享
相关推荐
向日的葵0061 小时前
vue路由(二)
前端·javascript·vue.js·vue
ejinxian1 小时前
Angular v22 正式发布:Signal Forms、Angular Aria 和 AI 开发工具全面生产化
前端·javascript·angular.js
sugar__salt1 小时前
基于Prompt的NLP项目实战:ES6模块化落地开发指南
javascript·自然语言处理·prompt·es6
小雨下雨的雨1 小时前
通过鸿蒙PC Electron框架技术完成-井字棋游戏 - 实现详解
前端·javascript·游戏·华为·electron·鸿蒙
冰暮流星2 小时前
javascript建立对象之构造函数
开发语言·javascript·ecmascript
小雨下雨的雨2 小时前
基于鸿蒙PC Electron框架技术完成的五子棋游戏 - 技术实现详解
前端·javascript·游戏·华为·electron·鸿蒙
老毛肚2 小时前
jeecgboot vue API 拆分02
前端·javascript·vue.js
ZC跨境爬虫2 小时前
跟着 MDN 学JavaScript day_4:如何存储你需要的信息——变量
开发语言·前端·javascript·ui·ecmascript
VcB之殇2 小时前
[Three.js] 实现两个3D模型之间的粒子化切换
前端·javascript·three.js