如何写出优秀的代码?

131 阅读10分钟

1.可读性

1.命名语义清晰

变量,函数,组件的命名是否表达清晰的意图

2.函数职责单一

函数是否只做一件事,是否容易理解

3.层次清晰

逻辑是否有良好的缩进,空行,段落感

4.注释合理

1.用重构取代注释

  • 提炼函数:将一段需要注释解释的代码提炼成一个独立的函数,并用清晰的函数名代替注释
  • 改变函数声明:函数的名称和参数都可以取一个更合适的名字,使其意图一目了然
  • 引入解释性变量:if(['apple','orange'].includes(val)) => if(isFruit)

2.避免冗余注释

比如 a++ 注释为a自增
注释只是重复了代码在做什么,而没有说明为什么这么做。这种注释毫无价值,反而会增加阅读和维护的负担

3.误导性注释

注释应该及时更新,比如注释说该方法返回“用户列表”,但代码实际返回的是“用户ID列表”,要及时同步

5.重复代码

是否存在复制粘贴的代码块,可否抽象。
抽象其实就是提炼函数

  • 创建一个新函数:将重复的代码块放到一个全新的、命名良好的函数里。
  • 替换重复代码:找到所有复制粘贴了这段代码的地方,将它们替换为对这个新函数的调用。

2.可维护性

1.模块划分合理

组件/函数划分是否清晰,是否存在“巨型文件”

2.层级深度控制

嵌套是否控制在2-3层

  • 1.代码块嵌套if/else promise.then async/await
  • 2.组件/模块嵌套(Vue中的多层标签)

优化方案

  • 1.使用卫语句提前返回减少嵌套
function goodFunction(data, config, user) {
  // 卫语句提前返回,减少一层嵌套
  if (!data) return;
  if (!config.isEnabled) return;

  // 主体逻辑(现在只有1层嵌套起点)
  data.items
    .filter(item => item.isValid) // 用 filter 替代内层 if
    .forEach(validItem => { // 第2层 (回调)
      // 处理 validItem...
    });
}
  • 2.分解函数(提炼嵌套函数)ui嵌套同理提取子组件
function goodFunction(data, config, user) {
  if (!data || !config.isEnabled) return;

  // 将内层复杂的循环和判断提炼成一个函数
  processValidItems(data.items);
}

// 新函数,自己的嵌套层级独立计算
function processValidItems(items) {
  items
    .filter(item => item.isValid)
    .forEach(validItem => { // 第2层
      // ... 逻辑
    });
}

3.是否有硬编码

字符串,配置项是否写死

  1. 硬编码如何理解 “硬编码”  指的是将本应动态获取或可配置的值(如字符串、数字、配置项)直接以字面量的形式“写死”在代码逻辑中,将代码和配置耦合在一起
  • 魔法数字/字符串(Magic Numbers/Strings)
// 坏味道:这里的 86400 和 'active' 就是魔法数字和字符串
if (user.status === 'active') { // 这个状态码代表什么?还有其他状态吗?
  sessionTimeout = 86400; // 86400 秒是多少?一天?为什么是一天?
}
  • API URL / 环境配置
// 坏味道:环境相关的配置被写死
axios.get('http://123.456.78.90:8080/api/v1/production/users');
const apiKey = 'sk_live_abcd1234'; // 无法环境隔离,换环境需要更改代码
  1. 如何避免
  • 定义常量 将魔法值提取为有清晰命名、全大写的常量。
// 好的做法
const SESSION_TIMEOUT_ONE_DAY = 24 * 60 * 60; // 或者直接 86400,但有了名字
const USER_STATUS_ACTIVE = 'active';
const USER_STATUS_INACTIVE = 'inactive';

if (user.status === USER_STATUS_ACTIVE) {
  sessionTimeout = SESSION_TIMEOUT_ONE_DAY;
}
  • 使用配置文件/环境变量
// config.js
export const API_BASE_URL = process.env.VUE_APP_API_BASE_URL; //通过不同的env文件管理
export const API_KEY = process.env.VUE_APP_API_KEY; // 通过构建工具注入

// api.js
import { API_BASE_URL } from '@/config';
axios.get(`${API_BASE_URL}/users`);

4.是否有技术债(TODO HACK)

是否存在明显的技术妥协或待修复点

1.技术债如何理解 “技术债”  是一个比喻,指为了快速实现短期目标(如赶工期、临时绕坑)而采用的一种并非最优(甚至粗糙)的解决方案,这会在未来带来额外的“利息”,即更高的维护成本。

代码注释中的 // TODO:// HACK:// FIXME: 就是技术债最明显的标记欠条

常见标记的含义:

  • // TODO:计划要做的功能或改进。通常表示功能不完整,但当前不影响主流程。
  • // HACK:一种临时的、取巧的、不优雅的解决方案。通常是为了绕过某个棘手的问题(如第三方库的Bug、奇怪的业务逻辑)而写的代码,作者自己都知道这很糟糕。
  • // FIXME:已知的Bug或问题,需要修复。比 TODO 更紧急,通常表示现有代码有问题,但可能暂时没时间修复或修复影响较大。 2.如何解决
    创建工单(Issue) 定期偿还,并且在代码审查中查看hack有没有更好的方案能否提高优先级

3.代码结构/模式使用

1.状态/副作用合理

React中useEffect/useState使用得当

2.组件拆分合理

UI逻辑是否解耦,是否复用组件

3.模块组织规范

utils/services/constants 分层是否清晰

4.使用现代语法

是否使用Es6+ Ts 解构 箭头函数等

5.类型严谨

Ts项目是否写明所有类型

4.代码异味

Code Smell 需要避免代码坏味道

1.Macic Numbers(魔法数字)

写死的数字字符串,用常量代替,避免硬编码(#3.1)

2.Long Function (长函数)

函数超过50行且职责混乱

3.Nested Hell (嵌套地狱)

多层嵌套控制结构 / Promise回调地狱或滥用then

4.Copy-paste code(复制粘贴的代码)

同一逻辑出现多处

5.全局状态污染

直接使用全局变量或污染全局作用域

5.开发者思维成熟度

1.使用抽象/封装思想

逻辑提炼到utils、hooks、service层
核心思想:从“怎么做”到“做什么”

初级开发者的思维:专注于“怎么做”(How)。他们会将实现一个功能的所有步骤(逻辑)都直接写在UI组件或主函数里。
成熟开发者的思维:专注于“做什么”(What)。他们会将具体的“怎么做”隐藏起来(封装),只给调用者提供一个清晰的接口(函数名),告诉调用者这个函数“能做什么”。

“使用抽象/封装思想”  就是指这种将变化莫测的具体实现细节隐藏起来,暴露出稳定清晰的接口的能力。

1. utils (工具函数层)

  • 是什么utils 是  “Utilities”  的缩写,意为工具。这里存放的是与业务无关的纯函数

  • 封装什么:通用的、可复用的算法、数据格式转换、辅助计算等。

  • 目的消除重复代码(DRY原则) ,让任何地方需要相同功能时都能调用同一个函数。

  • 例子

    • 格式化时间formatDate(timestamp, format)
    • 防抖/节流debounce(func, delay)
    • 深拷贝deepClone(obj)
    • 生成随机IDgenerateRandomId()

2. 提炼到 hooks (React) 或 composables (Vue)

  • 是什么与UI和状态相关的可复用逻辑。这是对utils的升级,它通常包含了组件生命周期、状态响应等。

  • 封装什么:从组件中抽离出的状态逻辑(如监听窗口大小、管理表单状态、获取数据等),而不仅仅是纯计算。

  • 目的实现状态逻辑的复用,避免在多个组件中重复编写相同的逻辑代码(如 useStateuseEffect)。

  • 例子

    • 获取数据useFetch(url)
    • 监听窗口大小useWindowSize()
    • 管理本地存储useLocalStorage(key, initialValue)

3. 提炼到 service (服务层)

  • 是什么与后端API交互的集中管理层。它是对网络请求的抽象封装。

  • 封装什么:API的URL、请求方法(GET/POST)、请求数据格式、响应数据格式转换、错误处理等。

  • 目的

    1. 隔离变化:当后端API地址或接口协议变化时,只需修改service层,而不用到处去找哪个组件调用了这个API。
    2. 简化调用:为组件提供语义化的API,让组件不再关心网络请求的细节。
    3. 统一处理:可以在这里统一添加认证token、统一错误处理逻辑

2.模块边界清晰(避免跨层调用、耦合)

1.前端常见分层

  • UI组件层:渲染视图、处理用户交互事件(React components Vue files)
  • 业务逻辑/状态层:管理应用状态,处理复杂业务规则(Hooks Composables pinia vuex)
  • 服务/数据层 封装所有外部数据获取和变更(service.js api.js)
  • 工具层:提供通用的与业务无关的辅助函数(utils/ helpers/)
    跨层调用指的是一个模块跳过了它本应直接依赖的中间层,去直接调用更底层的模块。这破坏了依赖关系的单向性。

2. 耦合是个啥

指的是模块之间相互依赖的程度。 “避免耦合”  特指要避免不必要的紧密依赖
例如父子组件,父组件不再需要处理子组件的逻辑,只是传递数据

3. 如何做到 模块边界清晰

  • 单向数据流:依赖关系应该是单向的,通常是 UI层 -> 业务层 -> 服务层 -> 工具层
  • 依赖倒置:上层模块定义接口(需要什么数据),下层模块实现接口(提供数据),而不是上层直接依赖下层的具体实现。
  • 明确职责:每个文件/模块只做一件事,并把它做好。

3.可访问性考虑(语义化标签、aria属性等 )

可访问性规范的核心是要求我们在开发网页或Web应用时,有意识地确保所有用户,包括残障人士,都能感知、理解、导航并与之交互。  它主要围绕两大技术体系:

1. 语义化标签

<button onclick="doSomething()">点击我</button> <!-- 这才是真正的按钮 -->
<header></header>
<nav></nav>
<main></main>
<section></section>
<article></article>
<aside></aside>
<footer></footer>
<ul><li></li></ul> <!-- 列表 -->

2. ARIA属性

是一组特殊的HTML属性,用于弥补HTML语义的不足,特别是在复杂的动态Web应用中

  • 角色:  role="..." 属性,用于明确告诉辅助技术某个元素是什么。

    • role="button"(把这个div声明为一个按钮)
    • role="navigation"(声明一个导航区域,类似于<nav>
    • role="alert"(声明一个紧急提示信息)
  • 状态和属性:  aria-* 系列属性,用于描述元素的当前状态和额外信息。

    • aria-label="关闭弹窗"(为元素提供一个看不见的标签)
    • aria-hidden="true"(告诉屏幕阅读器忽略此元素)
    • aria-expanded="false"(告诉屏幕阅读器一个可折叠的菜单当前是收起状态)
    • aria-describedby="tip-id"(指示另一个元素包含了对此元素的描述)

核心关系:优先使用语义化标签,当HTML标签无法充分表达含义时,再用ARIA进行补充和增强

4.使用设计模式/最佳实践

例如策略模式、职责链

5.体现领域建模思维

变量/函数反应业务模型 DDD 参考 juejin.cn/post/751791… 的设计思维。我个人认为小型的crud没必要用这种设计,不复杂的业务用简单的pinia和组件化设计就够了。 什么时候有DDD?

  • 有明确的业务身份。一般有对应的id来取分是不是同一个,比如订单 购物车 商品
  • 有明确的业务规则。是否有校验,计算,状态流转,比如购物车的商品增减比如订单的状态变更
  • 有行为或者方法,不仅仅是存数据。 比如购物车可以做添加删除动作
  • 生命周期比较长。 一般都有增删改查全流程业务动作。