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

相关推荐
DEMO派4 分钟前
首页图片懒加载实现方案解析
前端
用户952081611799 分钟前
百度地图MapVThree Editor:地图编辑
前端
程序员龙语27 分钟前
CSS 文本样式与阴影属性
前端·css
LYFlied36 分钟前
【每日算法】LeetCode 152. 乘积最大子数组(动态规划)
前端·算法·leetcode·动态规划
狼与自由40 分钟前
excel 导入 科学计数法问题处理
java·前端·excel
TAEHENGV41 分钟前
导入导出模块 Cordova 与 OpenHarmony 混合开发实战
android·javascript·数据库
小徐_233342 分钟前
不如摸鱼去的 2025 年终总结,今年的关键词是直面天命
前端·年终总结
GISer_Jing1 小时前
交互式圣诞树粒子效果:手势控制+图片上传
前端·javascript
3824278271 小时前
CSS 选择器(CSS Selectors) 的完整规则汇总
前端·css
放逐者-保持本心,方可放逐1 小时前
PDFObject 在 Vue 项目中的应用实例详解
前端·javascript·vue.js