揭秘JavaScript面向对象:从栈模拟队列到原型链的深度剖析

你可能已经习惯了 class 语法,但早期的 JS 没有类,却一样玩转面向对象 ------ 这一切都藏在 prototype 和原型链里。今天我们从"用栈模拟队列"这个经典问题出发,彻底搞懂 JS 的原型式面向对象。

一、从栈模拟队列说起

先来看一个经典问题:如何用两个栈实现一个队列?

队列是 FIFO(先进先出),栈是 FILO(先进后出)。两个栈倒腾一下,就能实现队列的效果:

  • push(x) → 元素放进队列尾部
  • pop() → 从队列头部移除元素
  • peek() → 返回队列头部元素
  • empty() → 判断队列是否为空

如果用传统面向对象语言(Java/C++),你会先写一个 class MyQueue。但在 JS 里,我们不需要 class 也能完成面向对象

用函数 + 原型实现 MyQueue

kotlin 复制代码
// 构造函数:负责创建实例,添加实例私有属性
const MyQueue = function () {
    this.stack1 = [];  // 用于入队
    this.stack2 = [];  // 用于出队
}

// 在原型上添加共享方法
MyQueue.prototype.push = function (x) {
    this.stack1.push(x);
}

MyQueue.prototype.pop = function () {
    if (this.stack2.length === 0) {
        while (this.stack1.length) {
            this.stack2.push(this.stack1.pop());
        }
    }
    return this.stack2.pop();
}

MyQueue.prototype.peek = function () {
    if (this.stack2.length === 0) {
        while (this.stack1.length) {
            this.stack2.push(this.stack1.pop());
        }
    }
    return this.stack2[this.stack2.length - 1];
}

MyQueue.prototype.empty = function () {
    return this.stack1.length === 0 && this.stack2.length === 0;
}

// 使用 new 创建实例
const queue = new MyQueue();
queue.push(1);
queue.push(2);
console.log(queue.peek());  // 1
console.log(queue.pop());   // 1
console.log(queue.empty()); // false

运行这段代码,你会发现完全没有 class 关键字,却实现了完整的队列类。这就是 JS 基于原型的面向对象

二、JS 的面向对象:没有类,只有对象

JavaScript 的设计哲学是:一切皆对象,没有类。如果你来自 Java 或 C++,可能会觉得奇怪 ------ 没有类怎么做面向对象?

答案是:使用构造函数 + 原型对象

2.1 构造函数是什么?

任何函数都可以作为构造函数,只要用 new 来调用它。

javascript 复制代码
function Greeting(name) {
    console.log(this);  // 指向新创建的空对象
    this.name = name;
}

const zzh = new Greeting('zzh');
console.log(zzh.name);  // 'zzh'

new 的过程经历了四个步骤:

  1. 创建一个空的普通对象 {}
  2. 将这个空对象的 __proto__ 指向构造函数的 prototype
  3. 将构造函数的 this 绑定到这个空对象上,并执行构造函数
  4. 如果构造函数返回了一个对象,则返回该对象;否则返回步骤 1 创建的对象

这就是为什么 this.name = name 能给实例添加属性。

2.2 函数是一等对象

在 JS 里,函数不仅是可执行的代码块,它本身也是一个对象 ------ 可以有自己的属性。

javascript 复制代码
function greeting() {
    console.log('hello world');
}

greeting.a = '1';      // 函数也可以添加属性
console.log(greeting.a);  // '1'
greeting();            // 'hello world'

这说明了 函数在 JS 中是一等公民:它可以被赋值、作为参数传递、作为返回值,甚至像普通对象一样拥有属性。

三、prototype 与原型链

这是 JS 面向对象的核心。先看一张图(概念图):

javascript 复制代码
构造函数 (如 MyQueue)
    |
    |-- prototype 指向 -----> 原型对象 (MyQueue.prototype)
                                  |
                                  |-- constructor 指向 -----> 构造函数
                                  |
                                  |-- __proto__ 指向 -----> Object.prototype

实例 (如 queue)
    |
    |-- __proto__ 指向 -----> 原型对象 (MyQueue.prototype)

3.1 prototype 属性

每个函数都有一个 prototype 属性,它指向一个原型对象。这个原型对象上的所有属性和方法,都会被通过该函数创建的实例共享。

javascript 复制代码
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 在原型上定义共享方法
Person.prototype.say = function () {
    console.log(`我叫${this.name},很高兴认识你`);
}

Person.prototype.work = function () {
    console.log(`我叫${this.name},我正在工作`);
}

const zwy = new Person('zwy', 18);
zwy.say();   // 我叫zwy,很高兴认识你
zwy.work();  // 我叫zwy,我正在工作

如果每个实例都有一份 say 方法,会浪费大量内存。放在 prototype 上,所有实例共用同一个方法,这就是共享方法的精髓。

3.2 constructor 属性

原型对象上有一个 constructor 属性,它指回构造函数本身。这形成了一个闭环:

ini 复制代码
console.log(Person.prototype.constructor === Person);  // true
console.log(zwy.constructor === Person);               // true(通过原型链找到)

你可以通过 constructor 判断一个实例是由哪个构造函数创建的,虽然实际开发中不太常用,但在某些框架或库中(比如做深拷贝或类型判断)会派上用场。

3.3 __proto__ 与原型链

每个实例都有一个内置属性 __proto__(现在规范中是 Object.getPrototypeOf()),它指向构造函数的 prototype

javascript 复制代码
console.log(zwy.__proto__ === Person.prototype);  // true
console.log(zwy.__proto__.__proto__ === Object.prototype);  // true
console.log(zwy.__proto__.__proto__.__proto__);  // null

当访问实例的一个属性或方法时,JS 引擎会沿着这条链一直往上找,直到找到或者到达 null。这就是原型链。

  • 先在实例自身找(比如 zwy.name
  • 找不到就去 zwy.__proto__(即 Person.prototype)找
  • 还找不到就去 Person.prototype.__proto__(即 Object.prototype)找
  • 最终到 null,停止,返回 undefined

3.4 顶层对象 Object

Object.prototype 是原型链的倒数第二环(再往上就是 null)。它上面定义了所有对象都有的方法,比如 toString()hasOwnProperty() 等。

arduino 复制代码
console.log(zwy.toString());  
// 明明 Person 里没有定义 toString,却能调用!
// 因为 zwy -> Person.prototype -> Object.prototype
// 最终在 Object.prototype 上找到了 toString

所有对象最终都继承自 Object.prototype。这也是为什么说"JS 中一切皆对象" ------ 哪怕是普通对象字面量 {},它的原型也是 Object.prototype

四、this 的指向问题

在上面的例子中,我们多次看到 this.namethis 在构造函数中指向新创建的实例,在原型方法中也指向调用该方法的实例。

javascript 复制代码
MyQueue.prototype.push = function (x) {
    // 这里的 this 指向调用 push 的实例(比如 queue)
    this.stack1.push(x);
}

关键规则this 在函数执行时动态绑定,谁调用它,它就指向谁。

  • 构造函数中:new 绑定了新对象
  • 原型方法中:实例调用,所以 this 指向实例
  • 全局函数中:this 指向 window(浏览器)或 global(Node.js)

五、共享方法的实际价值

假设你有 1000 个 Person 实例。如果每个实例都包含 say 方法的副本,内存占用会很大。但如果把 say 放在 Person.prototype 上,1000 个实例共用同一个函数,内存节省立竿见影。

javascript 复制代码
// 不推荐:每个实例都重复创建方法
function BadPerson(name) {
    this.name = name;
    this.say = function () {
        console.log(this.name);
    };
}

// 推荐:方法放在原型上
function GoodPerson(name) {
    this.name = name;
}
GoodPerson.prototype.say = function () {
    console.log(this.name);
};

六、从 ES6 class 看本质

现在你可能会说:"ES6 不是有 class 吗?" 是的,但 ES6 的 class 只是语法糖,底层仍然是原型链。

kotlin 复制代码
class MyQueueES6 {
    constructor() {
        this.stack1 = [];
        this.stack2 = [];
    }
    push(x) {
        this.stack1.push(x);
    }
    // ...
}

上面的代码等价于我们最开始写的 MyQueue 构造函数 + 原型方法。class 让代码更清晰,但背后的原理没有变。

七、总结

概念 说明
构造函数 new 调用的函数,负责创建实例和添加私有属性
prototype 函数上的属性,指向原型对象,存放共享方法
__proto__ 实例上的属性,指向构造函数的 prototype
原型链 通过 __proto__ 串联起来的查找链,终点是 null
constructor 原型对象上的属性,指向构造函数本身
共享方法 定义在 prototype 上的方法,所有实例共用,节省内存
this 动态绑定,谁调用指向谁

记住这几点:

  1. JS 没有类,只有对象,用构造函数 + 原型实现面向对象
  2. 实例的 __proto__ 指向构造函数的 prototype
  3. 属性/方法查找会沿着原型链向上,直到 null
  4. 共享方法定义在 prototype 上,私有属性定义在构造函数中
  5. 即使你用 ES6 class,背后的原型链机制从未改变

希望这篇文章能帮助你彻底理解 JS 的面向对象。下次再看到原型链相关的面试题,你一定能游刃有余了!

相关推荐
老毛肚1 小时前
jeecgboot TS + Vue 模板化 03
前端·javascript·vue.js
FlyWIHTSKY1 小时前
React 19 + Next.js 16(App Router)项目中集成 MSW
开发语言·javascript·vue.js
冰暮流星1 小时前
javascript之对象的建立-使用Object
开发语言·javascript·ecmascript
AI_零食1 小时前
呼吸灯 - 通过鸿蒙PC Electron框架技术完成-在焦虑时代守护每一次呼吸的数字禅修
前端·javascript·华为·electron·前端框架·鸿蒙
ZC跨境爬虫2 小时前
跟着 MDN 学JavaScript day_5:技能测试——变量实战
java·开发语言·前端·javascript
HjhIron2 小时前
深入理解 JavaScript 执行机制:从编译到运行的完整揭秘
javascript
云水一下2 小时前
TypeScript 从零基础到精通(四):面向对象编程(类与继承)
javascript·typescript
shmily麻瓜小菜鸡2 小时前
Bootstrap 4 常用工具类速查表
前端·javascript·bootstrap
CDN3602 小时前
【架构进阶】告别配置漂移!用 NodeNext + Workspace 打造优雅的 TypeScript Monorepo
前端·javascript·typescript