你有没有遇到过这样的场景?页面上有一段文字,比如用户昵称、个人简介或者一句 slogan,你想改它,却要跳转到另一个编辑页,或者弹出一个模态框......是不是有点打断节奏?
其实,有一种更丝滑的交互方式------就地编辑(Edit In Place) :点一下文字,它就变成输入框;改完点"保存",立马变回文本。整个过程不跳页、不弹窗,用户体验直接拉满!
今天我们就来一起看看,如何用原生 JavaScript 写一个可复用的 EditInPlace 组件,并聊聊它背后的面向对象设计思路。
一、先看效果:三行代码搞定编辑功能
假设你有这样一个 HTML 结构:
xml
<div id="app"></div>
<script src="./edit_in_place.js"></script>
<script>
const ep = new EditInPlace('slogan', '有了KFC 生活好滋润', document.getElementById('app'))
</script>
运行后,页面上就会出现一段可点击编辑的文字:"有了KFC 生活好滋润"。点它,变成输入框;改完点"保存",内容更新;点"取消",恢复原样。
是不是超简单?而这一切,都藏在 edit_in_place.js 这个文件里。
二、核心代码长啥样?
我们来看看这个组件是怎么写的:
kotlin
/**
* @func EditInPlace 就地编辑
* @params {string} value 初始值
* @params {element} parentElement 挂载点
* @params {string} id 自身ID
*/
function EditInPlace(id, value, parentElement){
//new的时候会给一个空对象 {} this指向这个空对象
this.id = id
this.value = value || '这个家伙很懒,什么都没有留下'
this.parentElement = parentElement //父DOM元素 用于挂载整个编辑组件
this.containerElement = null //可读性好 空对象
this.staticElement = null // span 文本
this.fieldElement = null //input 输入框
this.saveButton = null // 保存按钮
this.cancelButton = null // 取消按钮
//新建并挂载节点 DOM对象创建
this.createElement()
//监听事件
this.attachEvent()
//代码比较多的时候 按功能分模块 拆分为函数
}
//将所有实例方法定义在原型上 节省内存 (多个实例共享方法)
EditInPlace.prototype = {
//封装了DOM操作
createElement: function(){
// DOM操作 动态生成一个div节点
this.containerElement = document.createElement('div')
// console.log(this.containerElement,
// //this绑定
// Object.prototype.toString.apply(this.containerElement))//在内存中 通过Object.prototype.toString.apply() 也可以.call()
this.containerElement.id = this.id
//值
this.staticElement = document.createElement('span')
this.staticElement.innerHTML = this.value // 这里使用innerHTML会有风险 XSS 在底部具体讲解 推荐使用innerContent
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.parentElement.appendChild(this.containerElement)
this.convertToText() //切换到文本显示状态 (默认状态)
},
//文本显示 输入框隐藏
convertToText: function(){
this.fieldElement.style.display = 'none',//隐藏
this.staticElement.style.display = 'inline'//可见
this.saveButton.style.display = 'none'
this.cancelButton.style.display = 'none'
},
//输入框隐藏 文本可见
convertToField: function () {
this.fieldElement.style.display = 'inline',//可见
this.staticElement.style.display = 'none'//隐藏
this.fieldElement.value = this.value
this.saveButton.style.display = 'inline'
this.cancelButton.style.display = 'inline'
},
//事件监听
attachEvent: function(){
this.staticElement.addEventListener('click', () => {
this.convertToField()//切换到输入框显示状态
})
this.saveButton.addEventListener('click', () => {
this.save()//单独封装save方法用于保存到本地
})
this.cancelButton.addEventListener('click', () => {
this.cancel()//也是单独封装
})
//关键 这里使用箭头函数 是为了让this正确指向EditInPlace实例对象
//如果用普通函数 this会变成触发事件的DOM元素
//知识点在底部讲解 箭头函数和普通函数的this指向
},
save: function(){
var value = this.fieldElement.value
//中间省略 fetch 后端存储等操作
this.value = value
this.staticElement.innerHTML = value//把从输入框输入的值 给文本框显示
this.convertToText()//切换为文本框
},
cancel: function(){
//点击取消的时候 直接转换成文本框
this.convertToText()
}
}
是不是结构清晰、逻辑分明?接下来我们拆解一下它的设计巧思。
三、OOP 的魅力:封装 + 复用
这个组件最大的亮点,就是用面向对象的方式把一堆 DOM 操作、事件绑定、状态切换打包成了一个"黑盒子" 。
你作为使用者,只需要知道三件事:
- 给它一个 ID(用于 DOM 唯一标识)
- 给它初始内容
- 告诉它挂到哪个父元素上
剩下的,它自己搞定。这就是 封装 的力量。
而且,所有方法都挂在 prototype 上,多个实例共享同一套方法,省内存、效率高,这也是经典的 JS OOP 写法。
更重要的是:一个类一个文件,想用就引入,完全解耦。今天你在用户资料页用它,明天在后台管理系统里也能复用,毫无压力。
四、那些值得细品的小细节
1. innerHTML 的 XSS 风险(安全不能忘!)
代码里这行注释特别重要:
// 这里使用innerHTML会有风险 XSS 在底部具体讲解 推荐使用innerContent
虽然现在只是展示静态文案,但如果未来数据来自用户输入或接口,恶意内容(比如 <img src=x onerror=alert(1)>)就会被当作 HTML 执行,造成 XSS 攻击。
所以,生产环境建议用 textContent 替代 innerHTML,除非你明确需要渲染 HTML 并做了充分过滤。
2. 为什么事件回调用箭头函数?
看这段:
kotlin
this.staticElement.addEventListener('click', () => {
this.convertToField()
})
注释说得很清楚:
"关键 这里使用箭头函数 是为了让this正确指向EditInPlace实例对象"
因为普通函数在事件回调中,this 会指向触发事件的 DOM 元素(比如那个 span),而箭头函数没有自己的 this,它会继承外层作用域的 this ------ 也就是 EditInPlace 实例。
这样,this.convertToField() 才能正常调用。一个小技巧,避免大 bug!
3. Object.prototype.toString.call() 是干啥的?
被注释掉的这行:
arduino
// Object.prototype.toString.apply(this.containerElement)
其实是用来精准判断类型的。比如:
Object.prototype.toString.call([])→"[object Array]"Object.prototype.toString.call(null)→"[object Null]"
比 typeof 强大多了。虽然这里只是调试用,但这个知识点值得记住!
五、为什么不直接写"过程式"代码?
想象一下,如果你不用类,而是每次都在页面里手写:
arduino
// 创建 span
// 绑定点击
// 创建 input
// 创建按钮
// 写 save 逻辑
// 写 cancel 逻辑
// 处理显示隐藏......
不仅重复劳动多,而且一旦需求变化(比如加个 loading、加验证),你得改 N 个地方。
而用 EditInPlace,改一次,处处生效。这就是 OOP 带来的可维护性和扩展性。
六、小结:从流程代码到模块化组件
回顾一下我们的进化路径:
流程代码(逻辑能力和语法) → 封装成类(OOP 好习惯) → 模块化(独立文件)
EditInPlace 正是这一路径的完美体现。它不炫技,不复杂,但足够实用、清晰、安全(只要注意 innerHTML)、可复用。
下次当你需要实现"点文字就编辑"的功能时,不妨试试自己封装一个类似的组件。你会发现,OOP 不仅是一种写法,更是一种思维方式------把复杂留给自己,把简单留给别人。
希望这篇轻松一点的解读,能让你对 EditInPlace 和 OOP 有更亲切的理解。代码虽小,五脏俱全,值得细细品味 ✨