前端圈经常有这样一个共识:
理解 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模式:显示 spanedit模式:显示 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" 阶段,而不是停留在语法学习。