面试高频题:Todo 应用的 3 种写法对比(流程式 vs 函数式 vs 职责分离)

48 阅读5分钟

一、项目目标与核心能力

我们要实现的功能非常经典:

  • 用户输入任务项,点击“+ Add Item”添加到列表; image.png

  • 每个任务项可被勾选/取消,表示完成状态; image.png

  • 所有数据持久化存储在浏览器的 localStorage 中,刷新页面不丢失; image.png

  • 页面加载时自动恢复历史数据。 而更重要的是——我们拒绝“流程式面条代码” ,转而采用职责分离 + 函数封装 + 函数式风格来组织逻辑。

二、关键知识点回顾

1. CSS 继承与布局细节

在前端开发中,CSS 的**继承(inheritance)**机制是一个基础但容易被误解的概念。很多初学者常常疑惑:“为什么父元素设置了 font-size,子元素却没变?”或者“为什么背景色没有传给子元素?”本文将结合一段实际代码,带你彻底搞懂 CSS 继承的规则。

一个典型例子

来看下面这段 HTML 和 CSS 代码:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS继承</title>
    <style>
        body {
            background-color: green;
        }
    </style>
</head>
<body>
    <p>Hello world</p>
    <div style="overflow:hidden; font-size: 28px; height: 300px; background-color: yellow; color: pink;">
        你好
        <p style="background-color: red; height: inherit;">大唐诡事录</p>
    </div>
    <div>1111</div>
</body>
</html>

image.png

观察现象:

  • <div> 设置了 font-size: 28px 和 color: pink,其内部的 <p> 元素自动继承了这些样式,文字变大且颜色为粉色。
  • 但 <div> 的 background-color: yellow 并没有传递给内部的 <p> —— 它显示的是自己设置的红色背景。
  • <p> 使用了 height: inherit,所以它的高度继承了父元素的 300px

什么是 CSS 继承?

CSS 继承是指:某些 CSS 属性会自动从父元素传递给子元素,而无需显式声明。这种机制减少了重复代码,让样式更易维护。

✅ 会继承的常见属性(通常与“文本”相关):

  • color
  • font-family
  • font-size
  • font-weight
  • line-height
  • text-align
  • visibility
  • letter-spacing
  • word-spacing

这些属性天然具有“上下文连续性”,比如一段文字的字体大小和颜色,通常希望在整个段落或区块内保持一致。

❌ 不会继承的常见属性(通常与“布局/盒模型”相关):

  • background-color
  • width / height
  • margin / padding / border
  • display
  • position
  • float
  • overflow

这些属性控制的是元素自身的几何结构或视觉边界,如果自动继承反而会造成混乱。

💡 小技巧:你可以通过浏览器开发者工具查看某个属性是否继承——如果子元素未设置该属性,但样式面板中显示为“inherited from...”,那就是继承来的。

强制继承:inherit 关键字

即使某个属性默认不继承,你也可以手动强制继承父元素的值,使用 inherit

.child {
  background-color: inherit; /* 强制继承父元素背景色 */
  height: inherit;           /* 如示例中的 p 标签 */
}

在上面的例子中,<p style="height: inherit;"> 正是利用这一点,让自己的高度等于父 div300px


2. localStorage:前端的“本地数据库”

  • 浏览器提供的永久性存储空间(除非用户手动清除);
  • 以 key-value 形式存储字符串,因此存对象前需用 JSON.stringify(),取回后用 JSON.parse()
  • 容量通常为 5~10MB,足够应对大多数轻量级应用。

⚠️ 注意:不要存储敏感信息(如密码),因为 localStorage 对 XSS 攻击无防护。

三、代码实现:从“能跑”到“高级”

1. HTML 结构(简洁语义化)


<div class="wrapper">
  <h2>LOCAL TAPAS</h2>
  <ul class="plates">
    <li>Loading Tapas...</li>
  </ul>
  <form class="add-items">
    <input type="text" name="item" placeholder="Item Name" required />
    <input type="submit" value="+ Add Item" />
  </form>
</div>

image.png

使用 <ul> 表示列表,<form> 处理提交,语义清晰,利于无障碍访问和 SEO。

2. JavaScript 核心逻辑(函数式封装)

✅ 第一步:获取 DOM 与初始化数据
const addItems = document.querySelector('.add-items');
const itemsList = document.querySelector('.plates');
const items = JSON.parse(localStorage.getItem('todos')) || [];

这里直接从 localStorage 读取历史数据,若无则默认空数组。一行代码完成初始化,干净利落。


✅ 第二步:封装渲染函数 —— populateList

工作十年的老程序员强调:“超过10行的流程代码,一定要封装成函数!

function populateList(plates = [], platesList) {
  platesList.innerHTML = plates.map((plate, i) => `
    <li>
      <input 
        type="checkbox" 
        data-index="${i}" 
        id="item${i}"
        ${plate.done ? 'checked' : ''}
      />
      <label for="item${i}">${plate.text}</label>
    </li>
  `).join('');
}
  • 使用 map + 模板字符串生成 HTML 片段,是典型的函数式风格
  • 函数接收数据和容器,职责单一,不关心数据来源;
  • 默认参数 plates = [] 防止传入 undefined 导致报错;
  • 调用者只需 populateList(items, itemsList),无需知道内部如何拼接。

这就是“封装”的魅力:调用的人只关心“做什么”,不关心“怎么做”

✅ 第三步:添加任务 —— addItem
function addItem(event) {
  event.preventDefault();
  const input = this.querySelector('[name=item]');
  const text = input.value.trim();

  if (text) {
    items.push({ text, done: false });
    localStorage.setItem('todos', JSON.stringify(items));
    populateList(items, itemsList);
    this.reset(); // 清空表单
  }
}
  • 利用 this 指向绑定的 <form> 元素,精准获取输入值;
  • trim() 防止用户输入纯空格;
  • 数据变更后立即持久化,并重新渲染。

✅ 第四步:切换完成状态 —— toggleDone
function toggleDone(event) {
  if (event.target.matches('input[type="checkbox"]')) {
    const index = event.target.dataset.index;
    items[index].done = !items[index].done;
    localStorage.setItem('todos', JSON.stringify(items));
    populateList(items, itemsList);
  }
}
  • 使用事件委托(监听 <ul> 而非每个 <input>),性能更优;
  • matches() 比 tagName === 'INPUT' 更健壮,避免误判其他 input 类型;
  • 同样遵循“修改数据 → 存储 → 重渲染”流程。

✅ 第五步:绑定事件 & 初始化渲染
addItems.addEventListener('submit', addItem);
itemsList.addEventListener('click', toggleDone);
populateList(items, itemsList); // 首次加载

整个脚本没有一行冗余逻辑,每个函数都短小精悍、意图明确。

四、为什么这样做更“高级”?

对比维度流程式写法封装 + 函数式写法
可读性逻辑混杂,难以快速理解每个函数职责清晰
可维护性修改一处可能影响全局局部修改,风险可控
可复用性几乎无法复用populateList 可用于任何列表
调试难度需逐行排查可单独测试每个函数
“逼格”指数🌱💎💎💎

提升代码逼格,不是炫技,而是对工程负责。


五、总结与延伸

通过这个小小的 Todo 应用,我们实践了:

  • 如何用 localStorage 实现数据持久化;
  • 如何用函数式思维替代冗长的流程代码;
  • 如何通过封装提升代码的抽象层级;
  • 如何关注 CSS 和 JS 中的“魔鬼细节”。