用原生 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 有更亲切的理解。代码虽小,五脏俱全,值得细细品味 ✨

相关推荐
毕设源码-邱学长1 小时前
【开题答辩全过程】以 基于JavaScript的图书销售网站为例,包含答辩的问题和答案
开发语言·javascript·ecmascript
timeweaver1 小时前
React Server Components 的致命漏洞CVE-2025-55182
前端·安全
重铸码农荣光1 小时前
深入理解 JavaScript 中的 this:一场关于作用域、调用方式与设计哲学的思辨
前端·javascript
新晨4371 小时前
跨域是服务器拒绝请求还是浏览器去拒绝的请求?
前端·浏览器
珑墨1 小时前
【包管理器】pnpm、npm、cnpm、yarn 深度对比
前端·javascript·npm·node.js
草字1 小时前
uniapp 滚动到表单的某个位置,表单验证失败时。
前端·javascript·uni-app
学到头秃的suhian1 小时前
Spring使用三级缓存解决循环依赖问题
前端·spring·缓存
CXH7281 小时前
架构师的登山之路|第十二站:服务网格 Istio——未来的标配,还是复杂过头?
前端·javascript·istio
脾气有点小暴2 小时前
详解 HTML Image 的 mode 属性:图像显示模式的灵活控制
前端·html·uniapp