重新认识JS面向对象:没有Class,原型才是原生真相

0 阅读5分钟

学过 Java、Python 的同学刚接触 JS 面向对象时,基本都会被“颠覆认知”。

其他语言的面向对象,核心是 类(Class) 模板;但 JS 的原生设计里:根本没有类,只有对象。

我们现在写的  class ,只是 ES6 给开发者的语法糖。真正支撑 JS 面向对象体系的,永远是:构造函数 + 原型 + 原型链。

今天结合栈模拟队列,搞懂它和传统面向对象的区别。

一、先分清:线性结构与非线性结构

先铺垫基础数据结构概念,方便我们后续理解封装场景:

日常刷题、开发最常见的两种线性结构:

  • 栈(Stack):FILO 后进先出

  • 队列(Queue):FIFO 先进先出

  • 链表:同样属于线性结构

树属于非线性结构,和栈、队列的存储、遍历逻辑完全不同。

其中队列规定四个核心方法:

  •  push(x) :元素入队,添加到队尾

  •  pop() :删除队首元素

  •  peek() :获取队首元素

  •  empty() :判断队列是否为空

二、JS 的独特之处:函数是一等对象

在绝大多数语言里,函数就是一段可执行代码。

但在 JS 中:函数是一等对象,它有两种完全不同的形态:

  1. 普通函数直接调用

普通函数直接执行时,内部的  this  指向全局对象,只能执行逻辑,无法生成对象模板,不具备面向对象能力。

  1. 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 和所有静态语言最大的不同:语言层面没有类的概念,全程基于原型实现面向对象。

  1. 顶层根对象: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 也能完美实现面向对象,原型体系才是这门语言的灵魂。