学过 Java、Python 的同学刚接触 JS 面向对象时,基本都会被“颠覆认知”。
其他语言的面向对象,核心是 类(Class) 模板;但 JS 的原生设计里:根本没有类,只有对象。
我们现在写的 class ,只是 ES6 给开发者的语法糖。真正支撑 JS 面向对象体系的,永远是:构造函数 + 原型 + 原型链。
今天结合栈模拟队列,搞懂它和传统面向对象的区别。
一、先分清:线性结构与非线性结构
先铺垫基础数据结构概念,方便我们后续理解封装场景:
日常刷题、开发最常见的两种线性结构:
-
栈(Stack):FILO 后进先出
-
队列(Queue):FIFO 先进先出
-
链表:同样属于线性结构
树属于非线性结构,和栈、队列的存储、遍历逻辑完全不同。
其中队列规定四个核心方法:
-
push(x) :元素入队,添加到队尾
-
pop() :删除队首元素
-
peek() :获取队首元素
-
empty() :判断队列是否为空
二、JS 的独特之处:函数是一等对象
在绝大多数语言里,函数就是一段可执行代码。
但在 JS 中:函数是一等对象,它有两种完全不同的形态:
- 普通函数直接调用
普通函数直接执行时,内部的 this 指向全局对象,只能执行逻辑,无法生成对象模板,不具备面向对象能力。
- new + 构造函数 调用
当我们用 new 配合函数调用时,这个函数就变成了构造函数:
-
this 不再指向全局
-
this 指向新创建的实例对象
-
可以批量给实例挂载专属属性
这就是 JS 实现面向对象的起点:不靠 class,而是靠构造函数造实例。
通俗一点可以理解:
-
传统语言的 Class 是抽象概念模板
-
JS 的 构造函数+原型 是实实在在的「象模板
三、彻底吃透 new 的标准执行流程
很多人写代码,对 new 的过程模棱两可,当我们按照 JS 标准机制梳理:
执行 new 构造函数() 时,会自动走完 3 个核心流程:
1. 创建一个全新的空对象
2. 让构造函数内部的 this 指向这个新对象,执行构造函数代码,通过 this.xxx 为实例绑定自有属性
3. 构造函数自带 prototype 原型对象,新实例会自动关联这个原型,继承原型上所有方法和属性
简单来说:
-
实例自身的属性,来自构造函数的 this 挂载
-
实例共享的方法,来自构造函数的 prototype
这也是 JS 节省内存的关键:独有属性放实例,公共方法放原型。
四、JS 底层设计哲学:一切皆对象,无类全原型
JS 和所有静态语言最大的不同:语言层面没有类的概念,全程基于原型实现面向对象。
- 顶层根对象:Object
Object 是 JS 所有引用类型的顶层对象:
-
Object() 是函数对象
-
Object.prototype 是所有对象的顶层原型
我们日常简写:
let obj = {}
本质等价于:
let obj = new Object()
2. 所有内置构造函数都源于 Object
JS 内置的: Function、Array、Date、RegExp 全部都是函数对象,它们的原型链最终都会追溯到 Object.prototype 。
五、原型、原型链、constructor 完整严谨定义
1. 实例对象拥有 proto 隐式属性 作用:指向自己的构造函数原型对象,用于查找属性和方法
2. 原型链查找规则 访问对象属性/方法时:
-
先在实例自身查找
-
找不到则通过 proto 向上查找原型对象
-
层层回溯,最终终点为 null
3. 只有函数拥有 prototype 属性 专门用来:给所有实例提供共享方法与属性,避免重复创建、浪费内存
4. 原型对象自带 constructor 属性 作用:指回原本的构造函数,标记实例的构造来源
最终可以总结出: JS 中所有对象的原型链,最终顶层一定是 Object.prototype
六、实战:用原生原型面向对象实现「双栈模拟队列」
放弃class语法,仅依靠ES5构造函数与原型完成封装,贴合JavaScript原生面向对象的设计思路,完整实现队列所需四项接口:
// 构造函数:定义实例独有属性(两个栈)
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
}
代码结构对应面向对象思想
1. 私有实例属性: stack1、stack2 定义在构造函数 this 上,每个 new 出来的队列实例,都有自己独立的两个栈。
2. 公共原型方法: push、pop、peek、empty 挂载在 prototype 上,无论创建多少队列实例,方法在内存中仅一份,极致节省性能。
这就是 JS 最纯正的原型式面向对象:构造函数造实例属性,原型共享实例方法。
七、最后:为什么一定要学原生原型?
现在大家都在用 class ,看起来简洁优雅。
但本质:ES6 class 只是语法糖,底层依然是构造函数 + 原型链。
如果只会写 class,你永远不懂:
-
为什么实例可以共享方法
-
原型链查找为什么不会报错
-
this 在 new 之后为什么发生改变
-
JS 独特的无类面向对象设计思想
看懂这篇你就明白: JS 没有 class 也能完美实现面向对象,原型体系才是这门语言的灵魂。