JavaScript 编码原则

各司其职

在现代 Web 开发中,HTML、CSS 和 JavaScript 扮演着各自独特的角色。遵循各司其职的原则能够使得代码结构更加清晰,维护起来更加方便,并且能够更好地适应团队合作和项目扩展。

  1. HTML(结构层):负责定义网页的结构和内容。它使用标签来组织文本、图片、链接等元素,构建出网页的基本框架。
  2. CSS(表现层):负责控制网页的外观和布局。通过CSS,开发者可以设置颜色、字体、间距等样式属性,使得网页看起来更加美观和一致。
  3. JavaScript(行为层):负责实现网页的交互功能。JavaScript可以响应用户的操作,如点击按钮、提交表单等,动态地改变网页的内容和行为。

遵循职责分离的好处

  • 可维护性:每一层(HTML、CSS、JavaScript)都有明确的职责,开发者可以轻松地定位问题和修改代码。例如,如果需要修改页面的样式,只需要修改 CSS,而不需要担心影响到页面的交互逻辑。
  • 代码复用性:分离职责的代码更容易复用。比如,CSS 样式可以在多个页面中复用,JavaScript 的功能逻辑也可以在不同的项目中复用。
  • 可读性和可理解性:代码结构清晰,职责分离后,其他开发者能够更快速理解各部分代码的作用,减少了团队协作中的理解障碍。

错误样例:直接操作 DOM 样式

在这个例子中,JavaScript 直接操作了 DOM 元素的样式,这违背了“各司其职”的原则。样式应该由 CSS 负责,JavaScript 应该只处理交互和行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
const btn = document.querySelector('#modeButton');
btn.addEventListener('click', e => {
  const body = document.body;
  if (e.target.textContent === 'Dark Mode') {
    body.style.backgroundColor = 'black';
    body.style.color = 'white';
    e.target.textContent = 'Light Mode';
  } else {
    body.style.backgroundColor = 'white';
    body.style.color = 'black';
    e.target.textContent = 'Dark Mode';
  }
});

这个例子中,JavaScript 直接操作了 DOM 元素的样式(body.style.backgroundColorbody.style.color),从而导致样式和行为的耦合。这样会让代码变得难以维护,尤其是当项目变得复杂时。

正确样例:利用 CSS 和 JavaScript 实现分离

在正确的做法中,我们通过 CSS 类的切换来改变页面样式,JavaScript 只负责管理交互逻辑(如按钮点击),而具体的样式控制交给 CSS 处理。这种做法符合职责分离的原则,也更容易维护和扩展。

1
2
3
4
const btn = document.querySelector('#modeButton');
btn.addEventListener('click', e => {
  document.body.classList.toggle('dark-mode');
});
1
2
3
4
body.dark-mode {
  background-color: black;
  color: white;
}

在这个示例中,当用户点击按钮时,JavaScript 会切换 body 元素的 dark-mode 类。CSS 中定义了 .dark-mode 类的样式,这样我们就能把样式逻辑从 JavaScript 中分离出来,使代码更加清晰和易于维护。

组件封装

在现代 Web 开发中,组件化已经成为一种常见的开发模式。组件不仅能够提高代码的可复用性、可维护性和可扩展性,还能够使得项目结构更加清晰,便于团队协作。

组件封装的基本概念

在 Web 开发中,组件指的是一个具备独立功能、样式和结构的封装单元。每个组件都应该是自包含的,能够独立执行其预定的功能,而不依赖于外部的复杂逻辑。一个典型的组件包含以下三部分:

  • 模板(HTML):定义组件的结构和内容。
  • 样式(CSS):定义组件的外观和布局。
  • 功能(JavaScript):定义组件的行为和交互逻辑。

组件的四个关键特性

  1. 封装性:组件应独立于外部环境,内部实现和外部使用之间应保持松耦合。组件的内部实现不应暴露给外部,外部只能通过暴露的 API 来与组件交互。
  2. 正确性:组件应该完成预期的功能,并且在不同的使用场景下都能够保持一致的行为。
  3. 扩展性:组件应具有良好的扩展性,能够根据需求进行功能扩展或样式修改,而不影响其他功能部分。
  4. 复用性:组件设计时应考虑到不同场景的复用性。一个好的组件应该能够在不同的项目或页面中被重复使用,减少重复开发的工作量。

行为与控制流:如何设计良好的组件 API

1. 行为:API 设计

在 JavaScript 中,组件的行为通常通过 API 来定义。一个组件的 API 设计应遵循以下原则:

  • 原子操作:API 应该尽量保持原子性,意味着每个 API 函数应只做一件事情,避免过度复杂的操作。
  • 指责单一:每个函数应该有明确的职责,避免一个函数同时承担多个责任,确保代码的清晰和易于维护。
  • 灵活性:API 设计应该尽量灵活,能够适应多种不同的使用场景,同时也应避免过度设计,导致 API 过于复杂。

例如,假设你设计一个切换主题的按钮组件,简单的 API 设计可以是:

1
2
3
4
5
6
7
8
9
10
class ThemeSwitcher {
  constructor(buttonElement) {
    this.button = buttonElement;
    this.button.addEventListener('click', this.toggleTheme.bind(this));
  }

  toggleTheme() {
    document.body.classList.toggle('dark-mode');
  }
}

这个 API 很简洁,只有一个 toggleTheme 方法,负责切换主题,它的责任非常单一,符合“指责单一”和“原子操作”的原则。

2. 行为:控制流

在组件中,控制流的设计也至关重要。为了保持组件间的低耦合,自定义事件是一种非常有效的手段。通过自定义事件,组件之间可以通过发布-订阅的方式解耦,减少直接的依赖关系。

例如,在一个表单组件中,我们可能希望当表单提交时,触发一个事件,通知外部进行相应的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FormComponent {
  constructor(formElement) {
    this.form = formElement;
    this.form.addEventListener('submit', this.handleSubmit.bind(this));
  }

  handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(this.form);
    const submitEvent = new CustomEvent('formSubmitted', { detail: formData });
    this.form.dispatchEvent(submitEvent);
  }
}

通过 CustomEvent,我们不仅封装了表单提交的逻辑,还通过事件传递了表单数据,使得外部可以通过订阅 formSubmitted 事件来处理表单提交后的操作。

组件设计的基本步骤

为了设计一个高质量的组件,可以按照以下几个步骤进行:

  1. 结构设计:定义组件的 HTML 模板,确保组件的结构清晰且语义化。
  2. 展现效果:通过 CSS 定义组件的样式,确保组件的视觉效果符合设计要求,支持响应式布局。
  3. 行为设计
    • 设计 API:确保组件的功能可通过 API 调用实现。
    • 设计控制流:通过事件机制来解耦组件间的交互,减少直接依赖。

重构:从简单组件到复杂框架

1. 插件化

在大型项目中,单一的组件功能可能会越来越复杂。为了保持代码的可维护性和灵活性,可以考虑将复杂的功能拆分成多个插件。每个插件都可以独立工作,且可以通过依赖注入的方式与其他插件或组件进行交互。这样,组件就能根据需求进行动态扩展。

例如,假设你有一个日期选择组件,默认情况下它只显示当前日期,但你可以通过插件机制扩展它,添加时间选择、日期范围选择等功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class DatePicker {
  // ...

  registerPlugin(...plugin) {
    plugin.forEach(plugin => {
      plugin.init(this);
    });
  }
}

function plugin(datePicker) {
  // ...
}

const datePicker = new DatePicker();
datePicker.registerPlugin(randomDatePlugin);

2. 模板化

为了提升组件的扩展性,我们可以通过 模板化 组件的 HTML 结构。通过模板化,组件的结构可以更加灵活,支持动态内容的渲染。这对于需要展示动态数据的组件尤其重要。

例如,使用 JavaScript 模板引擎(如 Handlebars 或者简单的模板字符串)来生成组件的 HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ListComponent {
  constructor(data) {
    this.data = data;
    this.template = `
      <ul>
        {{#each data}}
          <li>{{this}}</li>
        {{/each}}
      </ul>
    `;
  }

  render() {
    const html = Handlebars.compile(this.template)(this);
    document.getElementById('listContainer').innerHTML = html;
  }
}

3. 组件框架化

当项目变得足够复杂时,可以考虑将多个组件统一抽象成一个 组件框架。框架化的组件不仅可以提供一致的 API,还可以提供更高层次的功能,如状态管理、生命周期管理等。

例如,React 和 Vue 就是典型的组件框架,它们不仅提供了组件化的开发模式,还为组件之间的状态管理、事件传递等提供了更强大的支持。

总结

组件封装是现代 Web 开发中的一个核心原则。良好的组件应该具备封装性正确性扩展性复用性,并且能够在复杂的应用场景中灵活应对。通过合理设计组件的结构展现效果行为,以及通过插件化、模板化和框架化的重构方式,能够逐步提升组件的质量和扩展性。

组件设计的原则总结

  • 封装性:组件内部实现与外部使用解耦。
  • 正确性:组件的功能应按预期工作。
  • 扩展性:组件应支持功能和样式的扩展。
  • 复用性:组件可以在不同项目中复用,减少重复开发。

实现组件的步骤

  • 结构设计:定义组件的 HTML 结构。
  • 展现效果:定义组件的样式。
  • 行为设计:设计组件的功能 API 和控制流。

三次重构

  • 插件化:将功能拆分为插件,降低耦合。
  • 模板化:将 HTML 模板化,提升扩展性。
  • 抽象化(组件框架):将通用的组件模型抽象出来,提升复用性和可维护性。

过程抽象

函数式编程(Functional Programming,简称 FP)是一种编程范式,它将计算视为数学函数的求值过程,强调没有副作用、不可变性和高度的抽象能力。

在函数式编程中,过程抽象(Process Abstraction) 是一个核心概念,它将复杂的操作和过程封装为可重用的函数,使得程序更具模块化、可组合性和可维护性。

什么是过程抽象?

过程抽象 是将一系列操作和步骤抽象为一个函数或模块的过程,允许你通过该函数名来表示一组操作,而无需关心这些操作的具体实现细节。通过过程抽象,我们可以将复杂的行为封装起来,简化程序的结构,提高代码的可读性和可维护性。

在传统的命令式编程中,我们通常会在代码中写出多个具体的步骤来描述某个过程。但在函数式编程中,我们将这些步骤提炼成一个个纯粹的函数,它们只关心输入和输出,而不依赖于外部的状态或副作用。函数式编程强调“函数即过程”,通过函数来实现对过程的抽象。

高阶函数

高阶函数(Higher-Order Function,HOF)是指 接受一个或多个函数作为参数,或者 返回一个函数 的函数。简而言之,高阶函数就是 操作其他函数的函数。它是函数式编程的核心概念之一,因为它提供了更高的抽象层次和更多的灵活性。

高阶函数的定义

  • 接受函数作为参数:一个高阶函数可以将一个或多个函数作为参数传递给它。
  • 返回函数:一个高阶函数可以返回一个新的函数。

高阶函数的基本特点

  • 参数是函数:你可以将函数作为参数传递给另一个函数,允许你动态地改变函数的行为。
  • 返回值是函数:一个函数可以返回另一个函数,这样就能够创建动态的函数或者生成特定的行为。

高阶函数的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 高阶函数 HOF0
 *
 * 这个函数接受一个函数作为参数,并返回一个新的函数。
 * 新的函数会调用传入的函数,并将所有参数传递给它。
 *
 * @param {Function} fn - 要被调用的函数
 * @returns {Function} 返回一个新的函数,该函数会调用传入的函数并传递所有参数
 */
function HOF0(fn) {
  return function (...args) {
    return fn.apply(this, args);
  };
}

这是一个等价高阶函数,HOF0 的参数和返回的函数是一样的。

常用的高阶函数

once - 只执行一次

once 函数确保传入的函数只执行一次,后续的调用将不会触发执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 创建一个只能调用一次的函数。
 *
 * @param {Function} fn - 需要包装的函数。
 * @returns {Function} 包装后的只能调用一次的函数。
 */
function once(fn) {
  return function (...args) {
    if (fn) {
      const res = fn.apply(this, args);
      fn = null;
      return res;
    }
  };
}

throttle - 限制函数执行频率

throttle 确保一个函数在一定时间内只会执行一次,通常用于控制高频率事件(例如滚动、窗口调整等)的触发频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 创建一个节流函数,在指定的延迟时间内最多执行一次传入的函数。
 *
 * @param {Function} fn - 需要节流的函数。
 * @param {number} [delay=100] - 延迟时间,单位为毫秒,默认为100毫秒。
 * @returns {Function} 返回一个新的节流函数。
 */
function throttle(fn, delay = 100) {
  let timer;
  return function (...args) {
    if (timer == null) {
      fn.apply(this, args);
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    }
  };
}

debounce - 延迟执行

debounce 会在事件触发后的延迟时间内,确保函数只会执行一次。它通常用于用户输入、窗口调整等事件,防止在短时间内触发多次事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 创建一个防抖函数,在指定的延迟时间内,如果再次调用该函数,则重新计时。
 *
 * @param {Function} fn - 需要防抖处理的函数。
 * @param {number} [delay=100] - 延迟时间,单位为毫秒,默认为100毫秒。
 * @returns {Function} 返回一个防抖处理后的函数。
 */
function debounce(fn, delay = 100) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

iterative - 迭代执行

iterative 是一个高阶函数,用于在一定的条件下迭代地执行一个函数。每次调用都会执行并传递上一次的结果,直到满足停止条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 迭代执行给定函数,直到满足条件为止。
 *
 * @param {Function} fn - 要执行的函数。
 * @param {Function} condition - 判断条件的函数,接收上一次执行的结果作为参数,返回布尔值。
 * @returns {Function} 返回一个新的函数,该函数接收任意数量的参数,并迭代执行给定函数直到条件为假。
 */
function iterative(fn, condition) {
  return function (...args) {
    let result = fn.apply(this, args);
    while (condition(result)) {
      result = fn.apply(this, [result]);
    }
    return result;
  };
}

const fn = iterative(
  n => n * n,
  n => n < 100
);

console.log(fn(2)); // 256
console.log(fn(3)); // 6561

为什么要使用高阶函数

使用高阶函数的优势主要体现在以下几个方面:

  • 灵活性:通过将函数作为参数或返回值,可以构建更加灵活的逻辑。
  • 复用性:避免重复代码,提高代码的复用性。
  • 组合性:将多个简单的函数组合成更复杂的功能,增强代码的可组合性。
  • 解耦:使得代码更加模块化、易于维护。
  • 抽象能力:高阶函数能够抽象出复杂的逻辑,提升代码的可读性和可理解性。
  • 纯粹性:避免副作用,使代码更加可预测。
  • 可测试性:使得代码逻辑更加清晰,便于单元测试。
  • 简化异步操作:通过封装常见的异步操作,简化代码实现。

总之,高阶函数使得我们的代码更加简洁、灵活、模块化,并且符合函数式编程的原则,提升了代码的可维护性、可扩展性和可复用性。


JavaScript 编码原则
http://xiaowhang.github.io/archives/3198399177/
作者
Xiaowhang
发布于
2025年2月18日
许可协议