MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于类与对象的JavaScript模块设计

2021-07-024.2k 阅读

理解 JavaScript 中的类与对象基础

在深入探讨基于类与对象的 JavaScript 模块设计之前,我们首先要对 JavaScript 中的类与对象概念有清晰的认识。JavaScript 是一种基于原型的面向对象编程语言,虽然从 ES6 开始引入了 class 关键字,使得代码看起来更像是传统的基于类的面向对象语言,但底层仍然是基于原型的机制。

面向对象编程基础概念

面向对象编程(OOP)主要围绕三个核心概念:封装、继承和多态。

封装:是指将数据和操作数据的方法绑定在一起,隐藏对象的内部实现细节,只对外暴露必要的接口。例如,我们创建一个 Person 对象,它有 nameage 属性以及 sayHello 方法,外部代码只需要调用 sayHello 方法,而不需要关心内部是如何实现的。

// 创建一个简单的 Person 对象
const person = {
    name: 'John',
    age: 30,
    sayHello: function() {
        return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
    }
};
console.log(person.sayHello());

继承:允许一个对象从另一个对象获取属性和方法。在传统的基于类的语言中,通过 class 继承实现。在 JavaScript 中,基于原型的继承是通过 __proto__Object.create 等方式实现。ES6 的 class 语法也提供了更直观的继承方式。

多态:指的是同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在 JavaScript 中,函数的重载和重写可以实现多态的效果。例如,不同的对象可能有相同名称的方法,但实现不同。

JavaScript 中的对象创建方式

  1. 对象字面量方式:这是最常见的创建对象的方式,直接使用花括号定义对象,并在其中定义属性和方法。
const car = {
    brand: 'Toyota',
    color: 'blue',
    start: function() {
        console.log(`The ${this.color} ${this.brand} car is starting.`);
    }
};
car.start();
  1. 构造函数方式:使用构造函数可以创建多个相似的对象。构造函数通常以大写字母开头,通过 new 关键字调用。
function Animal(name, species) {
    this.name = name;
    this.species = species;
    this.speak = function() {
        console.log(`${this.name} is a ${this.species}`);
    };
}
const dog = new Animal('Buddy', 'dog');
dog.speak();
  1. Object.create 方式Object.create 方法创建一个新对象,使用现有的对象来提供新创建对象的 __proto__
const animalPrototype = {
    speak: function() {
        console.log(`${this.name} is a ${this.species}`);
    }
};
const cat = Object.create(animalPrototype);
cat.name = 'Whiskers';
cat.species = 'cat';
cat.speak();
  1. ES6 类方式:ES6 引入的 class 语法提供了更简洁、更像传统面向对象语言的方式来定义对象。
class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    getArea() {
        return this.width * this.height;
    }
}
const rect = new Rectangle(5, 10);
console.log(rect.getArea());

原型与原型链

在 JavaScript 中,每个对象都有一个 __proto__ 属性,它指向该对象的原型对象。原型对象也是一个普通对象,它包含了一些属性和方法,当我们访问对象的某个属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的顶端(null)。

例如,所有的数组都继承自 Array.prototype,它定义了 pushpop 等方法。

const arr = [1, 2, 3];
// arr.__proto__ === Array.prototype
console.log(arr.push(4));

理解原型和原型链对于优化内存使用和实现继承至关重要。例如,通过在原型上定义方法,可以让所有基于该原型创建的对象共享这些方法,而不是每个对象都有自己的副本。

function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
const jane = new Person('Jane');
// john 和 jane 共享 Person.prototype 上的 sayHello 方法
john.sayHello();
jane.sayHello();

基于类与对象的模块设计原则

在进行 JavaScript 模块设计时,遵循一些良好的设计原则可以使代码更易于维护、扩展和理解。

单一职责原则(SRP)

一个类或模块应该只有一个引起它变化的原因,即一个类或模块应该只负责一项职责。例如,我们有一个处理用户认证的模块,它不应该同时负责用户数据的存储和检索。

假设我们有一个简单的用户认证模块,按照 SRP 设计如下:

class UserAuthenticator {
    constructor(username, password) {
        this.username = username;
        this.password = password;
    }
    authenticate() {
        // 模拟认证逻辑
        if (this.username === 'admin' && this.password === '123456') {
            return true;
        }
        return false;
    }
}

这样,这个类只专注于用户认证这一项职责,如果以后认证逻辑发生变化,只需要修改这个类即可,不会影响到其他功能。

开闭原则(OCP)

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当需求发生变化时,我们应该通过扩展现有代码来实现新功能,而不是修改现有代码。

例如,我们有一个图形绘制模块,当前只支持绘制圆形和矩形。如果我们要支持绘制三角形,按照 OCP 原则,我们不应该修改原有的绘制圆形和矩形的代码,而是添加新的三角形绘制类。

class Shape {
    draw() {
        throw new Error('draw method must be implemented in sub - classes');
    }
}
class Circle extends Shape {
    constructor(radius) {
        super();
        this.radius = radius;
    }
    draw() {
        console.log(`Drawing a circle with radius ${this.radius}`);
    }
}
class Rectangle extends Shape {
    constructor(width, height) {
        super();
        this.width = width;
        this.height = height;
    }
    draw() {
        console.log(`Drawing a rectangle with width ${this.width} and height ${this.height}`);
    }
}
// 新增三角形类
class Triangle extends Shape {
    constructor(base, height) {
        super();
        this.base = base;
        this.height = height;
    }
    draw() {
        console.log(`Drawing a triangle with base ${this.base} and height ${this.height}`);
    }
}

里氏替换原则(LSP)

所有引用基类的地方必须能透明地使用其子类的对象。这意味着子类对象可以替换掉它们的基类对象,而程序的行为不会发生变化。

例如,我们有一个计算图形面积的函数,它接受一个 Shape 类型的参数。如果我们有 CircleRectangle 作为 Shape 的子类,那么这两个子类的对象都应该可以传递给这个函数。

function calculateArea(shape) {
    if (shape instanceof Circle) {
        return Math.PI * shape.radius * shape.radius;
    } else if (shape instanceof Rectangle) {
        return shape.width * shape.height;
    }
    return 0;
}
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
console.log(calculateArea(circle));
console.log(calculateArea(rectangle));

接口隔离原则(ISP)

客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。

假设我们有一个 Printer 类,它有 printfax 方法,但是有些客户端只需要使用 print 方法。按照 ISP 原则,我们可以将接口拆分。

class Printable {
    print() {
        throw new Error('print method must be implemented in sub - classes');
    }
}
class Faxable {
    fax() {
        throw new Error('fax method must be implemented in sub - classes');
    }
}
class Printer implements Printable, Faxable {
    print() {
        console.log('Printing...');
    }
    fax() {
        console.log('Faxing...');
    }
}
class SimplePrinter implements Printable {
    print() {
        console.log('Simple printing...');
    }
}

这样,只需要打印功能的客户端可以使用 SimplePrinter,而不需要依赖 fax 方法。

依赖倒置原则(DIP)

高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。

例如,我们有一个高层模块 ReportGenerator 负责生成报告,它依赖一个低层模块 DataFetcher 来获取数据。按照 DIP 原则,我们可以通过抽象接口来解耦它们。

class DataFetcher {
    fetchData() {
        // 模拟数据获取逻辑
        return { data: 'Some data' };
    }
}
class ReportGenerator {
    constructor(dataFetcher) {
        this.dataFetcher = dataFetcher;
    }
    generateReport() {
        const data = this.dataFetcher.fetchData();
        return `Report generated with data: ${data.data}`;
    }
}
const dataFetcher = new DataFetcher();
const reportGenerator = new ReportGenerator(dataFetcher);
console.log(reportGenerator.generateReport());

通过依赖抽象接口,我们可以更灵活地替换 DataFetcher 的具体实现,而不影响 ReportGenerator 的逻辑。

实现基于类与对象的 JavaScript 模块

创建模块的方式

  1. 立即执行函数表达式(IIFE):IIFE 可以创建一个独立的作用域,避免变量污染全局作用域。我们可以在 IIFE 中定义类和对象,并通过返回值暴露需要的接口。
const myModule = (function() {
    class MyClass {
        constructor() {
            this.message = 'Hello from MyClass';
        }
        sayMessage() {
            console.log(this.message);
        }
    }
    return {
        MyClass: MyClass
    };
})();
const myObject = new myModule.MyClass();
myObject.sayMessage();
  1. ES6 模块:ES6 引入了原生的模块系统,使用 exportimport 关键字来导出和导入模块。
// math.js
export class Calculator {
    add(a, b) {
        return a + b;
    }
    subtract(a, b) {
        return a - b;
    }
}
// main.js
import { Calculator } from './math.js';
const calculator = new Calculator();
console.log(calculator.add(5, 3));
console.log(calculator.subtract(5, 3));

模块间的依赖管理

在大型项目中,模块之间通常存在依赖关系。正确管理这些依赖关系可以确保代码的稳定性和可维护性。

  1. 使用 ES6 模块的静态导入:ES6 模块的导入是静态的,即在编译时就确定了依赖关系。这使得依赖关系更加清晰,也有利于进行优化,如 Tree - shaking(摇树优化,去除未使用的代码)。
// moduleA.js
export const funcA = () => {
    console.log('Function A');
};
// moduleB.js
import { funcA } from './moduleA.js';
export const funcB = () => {
    funcA();
    console.log('Function B');
};
  1. CommonJS 模块的动态加载(Node.js 环境):在 Node.js 中,CommonJS 模块使用 require 进行动态加载。require 是在运行时加载模块,这使得依赖关系在运行时才确定。
// moduleA.js
exports.funcA = () => {
    console.log('Function A');
};
// moduleB.js
const moduleA = require('./moduleA.js');
exports.funcB = () => {
    moduleA.funcA();
    console.log('Function B');
};

模块的封装与暴露

  1. 封装内部实现:在模块设计中,我们通常需要隐藏模块的内部实现细节,只暴露必要的接口。在 ES6 模块中,未使用 export 导出的变量和函数都是模块内部的,外部无法访问。
// myModule.js
const privateVariable = 'This is a private variable';
const privateFunction = () => {
    console.log('This is a private function');
};
export const publicFunction = () => {
    privateFunction();
    console.log(privateVariable);
    return 'Public function result';
};
  1. 暴露接口:通过 export 关键字,我们可以暴露类、函数、变量等作为模块的接口,供其他模块使用。
// utility.js
export function sum(a, b) {
    return a + b;
}
export function multiply(a, b) {
    return a * b;
}
// main.js
import { sum, multiply } from './utility.js';
console.log(sum(3, 5));
console.log(multiply(3, 5));

模块的继承与组合

  1. 继承:在 JavaScript 模块中,我们可以使用 ES6 类的继承来实现代码复用。例如,我们有一个基础的 Animal 模块,然后创建 DogCat 模块继承自 Animal
// animal.js
export class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound`);
    }
}
// dog.js
import { Animal } from './animal.js';
export class Dog extends Animal {
    speak() {
        console.log(`${this.name} barks`);
    }
}
// cat.js
import { Animal } from './animal.js';
export class Cat extends Animal {
    speak() {
        console.log(`${this.name} meows`);
    }
}
  1. 组合:组合是另一种实现代码复用的方式,它通过将一个对象作为另一个对象的属性来实现功能复用。例如,我们有一个 Logger 模块,然后在其他模块中通过组合使用它。
// logger.js
export class Logger {
    log(message) {
        console.log(`[LOG] ${message}`);
    }
}
// userModule.js
import { Logger } from './logger.js';
export class User {
    constructor(name) {
        this.name = name;
        this.logger = new Logger();
    }
    login() {
        this.logger.log(`${this.name} is logging in`);
    }
}

基于类与对象的模块设计实践案例

小型项目:任务管理系统

  1. 模块设计
    • Task 模块:负责定义任务对象,包含任务的标题、描述、截止日期等属性,以及任务的完成状态管理方法。
// task.js
export class Task {
    constructor(title, description, dueDate) {
        this.title = title;
        this.description = description;
        this.dueDate = dueDate;
        this.completed = false;
    }
    markAsCompleted() {
        this.completed = true;
    }
    getDetails() {
        return `Title: ${this.title}, Description: ${this.description}, Due Date: ${this.dueDate}, Completed: ${this.completed}`;
    }
}
- **TaskList 模块**:管理任务列表,包含添加任务、删除任务、获取所有任务等方法。
// taskList.js
import { Task } from './task.js';
export class TaskList {
    constructor() {
        this.tasks = [];
    }
    addTask(task) {
        this.tasks.push(task);
    }
    removeTask(taskIndex) {
        if (taskIndex >= 0 && taskIndex < this.tasks.length) {
            this.tasks.splice(taskIndex, 1);
        }
    }
    getTasks() {
        return this.tasks;
    }
}
- **UI 模块(简化示例)**:负责与用户交互,显示任务列表,提供添加和删除任务的界面(这里仅以简单的控制台输出模拟)。
// ui.js
import { TaskList } from './taskList.js';
export class TaskUI {
    constructor(taskList) {
        this.taskList = taskList;
    }
    displayTasks() {
        const tasks = this.taskList.getTasks();
        tasks.forEach((task, index) => {
            console.log(`${index + 1}. ${task.getDetails()}`);
        });
    }
    addTaskUI(title, description, dueDate) {
        const task = new Task(title, description, dueDate);
        this.taskList.addTask(task);
        this.displayTasks();
    }
    removeTaskUI(taskIndex) {
        this.taskList.removeTask(taskIndex);
        this.displayTasks();
    }
}
  1. 使用模块
// main.js
import { TaskUI } from './ui.js';
import { TaskList } from './taskList.js';
const taskList = new TaskList();
const taskUI = new TaskUI(taskList);
taskUI.addTaskUI('Learn JavaScript', 'Study JavaScript concepts', '2024 - 12 - 31');
taskUI.addTaskUI('Complete project', 'Finish the assigned project', '2024 - 11 - 15');
taskUI.removeTaskUI(1);

大型项目:电商系统

  1. 模块划分
    • User 模块:负责用户的注册、登录、信息管理等功能。包含 User 类,管理用户的基本信息,以及 UserService 类,提供用户相关的业务逻辑。
// user.js
export class User {
    constructor(username, password, email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }
}
// userService.js
import { User } from './user.js';
export class UserService {
    constructor() {
        this.users = [];
    }
    registerUser(user) {
        this.users.push(user);
        console.log(`${user.username} has been registered.`);
    }
    loginUser(username, password) {
        const user = this.users.find(u => u.username === username && u.password === password);
        if (user) {
            console.log(`${user.username} has logged in.`);
            return user;
        }
        console.log('Login failed.');
        return null;
    }
}
- **Product 模块**:管理商品信息,包括商品的添加、查询、库存管理等。`Product` 类定义商品的属性,`ProductService` 类处理商品相关的业务逻辑。
// product.js
export class Product {
    constructor(id, name, price, stock) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.stock = stock;
    }
}
// productService.js
import { Product } from './product.js';
export class ProductService {
    constructor() {
        this.products = [];
    }
    addProduct(product) {
        this.products.push(product);
        console.log(`${product.name} has been added to the product list.`);
    }
    getProductById(id) {
        return this.products.find(product => product.id === id);
    }
    updateStock(product, quantity) {
        product.stock -= quantity;
        console.log(`${product.name}'s stock has been updated.`);
    }
}
- **Cart 模块**:处理购物车相关功能,如添加商品到购物车、从购物车移除商品、计算购物车总价等。`CartItem` 类表示购物车中的商品项,`Cart` 类管理整个购物车。
// cartItem.js
import { Product } from './product.js';
export class CartItem {
    constructor(product, quantity) {
        this.product = product;
        this.quantity = quantity;
    }
    getTotalPrice() {
        return this.product.price * this.quantity;
    }
}
// cart.js
import { CartItem } from './cartItem.js';
export class Cart {
    constructor() {
        this.items = [];
    }
    addItem(product, quantity) {
        const existingItem = this.items.find(item => item.product.id === product.id);
        if (existingItem) {
            existingItem.quantity += quantity;
        } else {
            const cartItem = new CartItem(product, quantity);
            this.items.push(cartItem);
        }
        console.log(`${product.name} has been added to the cart.`);
    }
    removeItem(productId) {
        this.items = this.items.filter(item => item.product.id!== productId);
        console.log('Item has been removed from the cart.');
    }
    getTotalPrice() {
        return this.items.reduce((total, item) => total + item.getTotalPrice(), 0);
    }
}
- **Order 模块**:负责订单的创建、处理和跟踪。`Order` 类定义订单的属性,`OrderService` 类处理订单相关的业务逻辑。
// order.js
import { User } from './user.js';
import { Cart } from './cart.js';
export class Order {
    constructor(orderId, user, cart) {
        this.orderId = orderId;
        this.user = user;
        this.cart = cart;
        this.status = 'pending';
    }
    processOrder() {
        this.status = 'processing';
        console.log(`Order ${this.orderId} is being processed.`);
    }
    completeOrder() {
        this.status = 'completed';
        console.log(`Order ${this.orderId} has been completed.`);
    }
}
// orderService.js
import { Order } from './order.js';
export class OrderService {
    constructor() {
        this.orders = [];
    }
    createOrder(user, cart) {
        const orderId = Date.now();
        const order = new Order(orderId, user, cart);
        this.orders.push(order);
        console.log(`Order ${orderId} has been created.`);
        return order;
    }
    getOrderById(orderId) {
        return this.orders.find(order => order.orderId === orderId);
    }
}
  1. 模块协作: 在实际使用中,这些模块相互协作。例如,用户注册后登录,添加商品到购物车,创建订单等操作涉及多个模块的交互。
// main.js
import { UserService } from './userService.js';
import { ProductService } from './productService.js';
import { Cart } from './cart.js';
import { OrderService } from './orderService.js';
const userService = new UserService();
const productService = new ProductService();
const cart = new Cart();
const orderService = new OrderService();
const user = new User('john_doe', 'password123', 'john@example.com');
userService.registerUser(user);
const loggedInUser = userService.loginUser('john_doe', 'password123');
if (loggedInUser) {
    const product = new Product(1, 'Sample Product', 25, 100);
    productService.addProduct(product);
    cart.addItem(product, 2);
    const order = orderService.createOrder(loggedInUser, cart);
    order.processOrder();
    order.completeOrder();
}

通过以上案例,我们可以看到基于类与对象的模块设计在不同规模项目中的应用,合理的模块划分和设计原则的遵循可以使项目更易于开发、维护和扩展。在实际开发中,还需要根据项目的具体需求和特点进行灵活调整和优化。