深入理解 JavaScript OOP:从一个「就地编辑组件」看清封装、状态与原型链

前端圈经常有这样一个共识:

理解 JavaScript 的 OOP,不是看 class,而是看你如何组织数据与行为。

很多人学 OOP 时,总觉得自己懂了、但又感觉没完全吃透。

这时候,用一个足够简单、又足够贴近业务的小组件,会比看十篇 OOP 理论文章更有效。

比如一个很常见的场景:

点击文本 → 变为输入框 → 保存 / 取消 → 回到文本显示。

也就是经典的 Edit-In-Place 组件。

本文基于我整理过的一个小案例(你可以把它理解成 mini 版本的 Vue/React component),但麻雀虽小,它几乎串起了 JS 中与 OOP 相关的核心概念------封装、状态管理、事件绑定、this 指向、原型链复用等等。

整篇文章你能学到:

  • 👉 为什么这种小功能值得写成 OOP?
  • 👉 原型对象到底发挥了什么作用?
  • 👉 UI 组件和"状态机"之间是什么关系?
  • 👉 this 在事件中为什么容易飘?
  • 👉 如何把这套代码升级成现代语法?

让我们从最终的效果开始拆起。

🎯 最终使用方式:一行代码创建一个组件

index.html 中,组件的使用方式非常清爽:

js 复制代码
new EditInPlace('slogan', '', document.getElementById('app'));

没有复杂的 API,没有乱七八糟的配置。

你只需要给:

  • id
  • 初始值
  • 挂载点

这就是"组件"。
组件的使用者不需要了解内部实现,只要会使用公共接口即可。

这就是 OOP 封装能给我们带来的第一个好处。

🧱 构造函数:对象的"骨架"与"状态容器"

来看构造函数:

js 复制代码
function EditInPlace(id, value, parentElement) {
    this.id = id;
    this.value = value || '这个家伙很懒,什么都没有留下';
    this.parentElement = parentElement;

    this.containerElement = null;
    this.staticElement = null;
    this.fieldElement = null;
    this.saveButton = null;
    this.cancelButton = null;

    this.createElement();
    this.attachEvent();
}

这里有 3 个关键点:

1. 把状态挂在 this 上:面向对象不是语法,是数据管理方式

组件的"状态"需要被存下来,这时候 this 就是天然载体。

2. 初始化阶段完成 DOM 和事件绑定

构造函数里不写逻辑,只负责把对象"准备好"。

3. createElement + attachEvent:关注点分离

任何合理的组件代码,都应该做到:

  • 一个函数只干一件事
  • DOM 创建是一块
  • 事件绑定是一块
  • UI 状态切换是一块

这个小例子已经体现了不错的代码组织方式。

🎨 createElement:UI 组件的"工厂"

js 复制代码
createElement: function(){
    this.containerElement = document.createElement('div');
    this.containerElement.id = this.id;
    this.parentElement.appendChild(this.containerElement);

    this.staticElement = document.createElement('span');
    this.staticElement.innerHTML = this.value;
    this.containerElement.appendChild(this.staticElement);

    this.fieldElement = document.createElement('input');
    this.fieldElement.type = 'text';
    this.fieldElement.value = this.value;
    this.containerElement.appendChild(this.fieldElement);

    this.saveButton = document.createElement('input');
    this.saveButton.type = 'button';
    this.saveButton.value = '保存';
    this.containerElement.appendChild(this.saveButton);

    this.cancelButton = document.createElement('input');
    this.cancelButton.type = 'button';
    this.cancelButton.value = '取消';
    this.containerElement.appendChild(this.cancelButton);

    this.convertToText();
}

这里可以看到一个值得点赞的设计:

UI 是根据"状态"渲染出来的,而不是写死在 HTML 中。

你可以把这个技巧理解为:

  • UI = DOM + 数据
  • UI 状态 = 显示什么 + 隐藏什么

这其实就是简单版的 "响应式思想"。

🔁 状态切换:UI 本质上是一台"状态机"

组件有两个状态:

  • text 模式:显示 span
  • edit 模式:显示 input + 按钮

代码:

js 复制代码
convertToText() {
    this.fieldElement.style.display = 'none';
    this.saveButton.style.display = 'none';
    this.cancelButton.style.display = 'none';
    this.staticElement.style.display = 'inline';
},

convertToField() {
    this.fieldElement.value = this.value;
    this.fieldElement.style.display = 'inline';
    this.staticElement.style.display = 'none';
    this.saveButton.style.display = 'inline';
    this.cancelButton.style.display = 'inline';
}

如果你是 Vue / React 用户,这段是不是让你很熟悉?

  • 显示/隐藏对应不同状态
  • 互动操作驱动状态变化

没错,这就是组件思想的核心:
UI = 状态驱动视图的结果。

当你写多了这样的组件后,你会自然理解:

🧠 React 不是框架,是"状态机思想"的具象化

🧠 Vue 的响应式并不是魔法,它只是自动触发 UI 更新

🧠 原来手写组件也在做同样的事情

⚡ attachEvent:事件绑定和 this 指向的隐形战场

这一段非常关键:

js 复制代码
attachEvent: function(){
    this.staticElement.addEventListener('click', () => {
        this.convertToField();
    });
    this.saveButton.addEventListener('click', () => {
        this.save();
    });
    this.cancelButton.addEventListener('click', () => {
        this.cancel();
    });
}

为什么全部使用箭头函数?

因为:

  • 普通函数的 this 会指向触发事件的 DOM 元素
  • 箭头函数的 this 保持来自外层作用域(也就是组件实例)

如果换成 function,就会踩坑:

js 复制代码
this.staticElement.addEventListener('click', function(){
    this.convertToField(); // ❌ 报错:this 指向 span
});

所以这里用箭头函数非常合理。

💾 save & cancel:业务逻辑抽象的"落脚点"

js 复制代码
save: function(){
    const value = this.fieldElement.value;
    this.value = value;
    this.staticElement.innerHTML = value;
    this.convertToText();
},

cancel: function(){
    this.convertToText();
}

真实项目里这里通常会变成:

  • 表单校验
  • API 提交
  • UI loading 状态
  • 回调通知父组件

这个类已经把扩展能力留好了。

也就是说,
一个写得好的组件,不只是能用,还得能扩展。

🧬 原型链:为什么方法写在 prototype 上?

你会看到全部方法都写在:

js 复制代码
EditInPlace.prototype = { ... }

而不是构造函数里。

原因很简单:

👉 100 个实例共享同一个方法

👉 函数不会重复创建

👉 内存更友好

👉 行为统一、易维护

这是理解 JS OOP 的关键点:
JS 的 OOP 基于原型,而不是类。class 只是语法糖。

🚀 如果换成现代语法(class)是什么效果?

js 复制代码
class EditInPlace {
    constructor(id, value, parent) { ... }
    createElement() { ... }
    attachEvent() { ... }
    convertToText() { ... }
    convertToField() { ... }
    save() { ... }
    cancel() { ... }
}

你会发现:

  • 逻辑没变
  • 方法组织方式没变
  • 本质仍然是原型链

这就是所谓:

class 改善阅读体验,不改变 JS 的底层模型。

📌 总结:一个小组件=一份 JavaScript OOP 思维的浓缩包

从头到尾,这个 EditInPlace 小组件展示了 JS OOP 的多个关键知识点:

1. 面向对象不是 class,是"数据 + 行为"

2. 构造函数负责初始化,prototype 负责复用

3. UI 本质是状态机

4. this 是 JS 中最"调皮"的东西

5. 封装让组件成为黑盒、可复用

6. 小组件也能体现架构思维

如果你能看懂并写出这样的组件,可以说:

你已经真正进入 "工程级 JavaScript" 阶段,而不是停留在语法学习。

相关推荐
郑州光合科技余经理1 小时前
基于PHP:海外版同城O2O系统多语言源码解决方案
java·开发语言·git·spring cloud·uni-app·php·uniapp
zmzb01031 小时前
C++课后习题训练记录Day43
开发语言·c++
AAA阿giao1 小时前
JavaScript 原型与原型链:从零到精通的深度解析
前端·javascript·原型·原型模式·prototype·原型链
wadesir1 小时前
C语言模块化设计入门指南(从零开始构建清晰可维护的C程序)
c语言·开发语言·算法
t198751281 小时前
MATLAB水声信道仿真程序
开发语言·算法·matlab
0***86331 小时前
SQL Server2019安装步骤+使用+解决部分报错+卸载(超详细 附下载链接)
javascript·数据库·ui
烛阴1 小时前
C#异常概念与try-catch入门
前端·c#
钮钴禄·爱因斯晨1 小时前
# 企业级前端智能化架构:DevUI与MateChat融合实践深度剖析
前端·架构
摆烂工程师2 小时前
2025年12月最新的 Google AI One Pro 1年会员教育认证通关指南
前端·后端·ai编程