用原生 JS 手写一个“就地编辑”组件:EditInPlace 的 OOP 实践

你有没有遇到过这样的场景?页面上有一段文字,比如用户昵称、个人简介或者一句 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 有更亲切的理解。代码虽小,五脏俱全,值得细细品味 ✨

相关推荐
前端大卫12 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘12 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare12 小时前
浅浅看一下设计模式
前端
Lee川12 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix12 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人12 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl12 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人13 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼13 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端