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

JavaScript基于类对象和闭包模块的场景应用

2021-12-274.9k 阅读

JavaScript 基于类对象的场景应用

类对象基础概念

在 JavaScript 中,类是一种基于原型继承的语法糖,用于创建对象。ECMAScript 2015(ES6)引入了 class 关键字,使得创建对象的过程更加直观和易于理解,类似于传统面向对象编程语言(如 Java、C++)中的类概念。

一个简单的类定义如下:

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} makes a sound.`);
    }
}

const dog = new Animal('Buddy');
dog.speak(); 

在上述代码中,Animal 是一个类,constructor 方法是类的构造函数,用于初始化新创建的对象。this 关键字指向新创建的对象实例,通过 this 可以为对象添加属性和方法。speak 方法是类的实例方法,可以在类的实例上调用。

类对象在数据封装场景中的应用

数据封装是面向对象编程的重要特性之一,它将数据和操作数据的方法封装在一起,隐藏对象的内部实现细节,只暴露必要的接口给外部使用。

例如,我们创建一个简单的银行账户类:

class BankAccount {
    constructor(accountNumber, initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }
    deposit(amount) {
        if (amount > 0) {
            this.balance += amount;
            console.log(`Deposited ${amount}. New balance: ${this.balance}`);
        } else {
            console.log('Invalid deposit amount.');
        }
    }
    withdraw(amount) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            console.log(`Withdrawn ${amount}. New balance: ${this.balance}`);
        } else {
            console.log('Insufficient funds or invalid withdrawal amount.');
        }
    }
    getBalance() {
        return this.balance;
    }
}

const account = new BankAccount('1234567890', 1000);
account.deposit(500); 
account.withdraw(300); 
console.log(`Current balance: ${account.getBalance()}`); 

在这个例子中,BankAccount 类封装了账户号码和余额信息,通过 depositwithdrawgetBalance 方法提供了对账户余额操作和查询的接口。外部代码无法直接访问和修改 balance 属性,只能通过这些方法来间接操作,从而保证了数据的完整性和安全性。

类对象在继承场景中的应用

继承是面向对象编程的另一个核心特性,它允许一个类从另一个类继承属性和方法,实现代码的复用和扩展。

假设我们有一个 Vehicle 类,然后有 CarBicycle 类继承自 Vehicle

class Vehicle {
    constructor(make, model) {
        this.make = make;
        this.model = model;
    }
    drive() {
        console.log(`Driving a ${this.make} ${this.model}`);
    }
}

class Car extends Vehicle {
    constructor(make, model, numDoors) {
        super(make, model);
        this.numDoors = numDoors;
    }
    honk() {
        console.log('Beep beep!');
    }
}

class Bicycle extends Vehicle {
    constructor(make, model, hasGears) {
        super(make, model);
        this.hasGears = hasGears;
    }
    pedal() {
        console.log('Pedaling the bicycle.');
    }
}

const myCar = new Car('Toyota', 'Corolla', 4);
myCar.drive(); 
myCar.honk(); 

const myBicycle = new Bicycle('Giant', 'Escape', true);
myBicycle.drive(); 
myBicycle.pedal(); 

在上述代码中,CarBicycle 类继承自 Vehicle 类。通过 super 关键字调用父类的构造函数,以便正确初始化继承的属性。子类可以添加自己特有的属性和方法,如 Car 类的 honk 方法和 Bicycle 类的 pedal 方法。这样,我们可以在不同层次上复用和扩展代码,提高开发效率。

类对象在多态场景中的应用

多态是指同一个方法在不同的对象上有不同的行为表现。在 JavaScript 中,通过继承和方法重写可以实现多态。

继续以上面的 VehicleCarBicycle 类为例,假设我们在 Vehicle 类中定义一个 describe 方法,然后在子类中重写这个方法:

class Vehicle {
    constructor(make, model) {
        this.make = make;
        this.model = model;
    }
    describe() {
        return `This is a ${this.make} ${this.model}`;
    }
}

class Car extends Vehicle {
    constructor(make, model, numDoors) {
        super(make, model);
        this.numDoors = numDoors;
    }
    describe() {
        return `This is a ${this.make} ${this.model} with ${this.numDoors} doors`;
    }
}

class Bicycle extends Vehicle {
    constructor(make, model, hasGears) {
        super(make, model);
        this.hasGears = hasGears;
    }
    describe() {
        return `This is a ${this.make} ${this.model} ${this.hasGears? 'with gears' : 'without gears'}`;
    }
}

const vehicles = [new Car('Ford', 'Mustang', 2), new Bicycle('Trek', 'Domane', false)];
vehicles.forEach(vehicle => {
    console.log(vehicle.describe()); 
});

在这个例子中,CarBicycle 类重写了 Vehicle 类的 describe 方法,以提供特定于自己的描述。当遍历 vehicles 数组并调用每个对象的 describe 方法时,会根据对象的实际类型调用相应的重写方法,从而体现出多态性。

JavaScript 基于闭包模块的场景应用

闭包基础概念

闭包是 JavaScript 中一个强大而重要的概念。简单来说,闭包是指一个函数能够访问并记住其词法作用域中的变量,即使该函数在其原始作用域之外被调用。

例如:

function outer() {
    const outerVariable = 'I am from outer';
    function inner() {
        console.log(outerVariable);
    }
    return inner;
}

const closureFunction = outer();
closureFunction(); 

在上述代码中,outer 函数返回 inner 函数。inner 函数可以访问 outer 函数作用域中的 outerVariable 变量,即使 outer 函数已经执行完毕并返回。closureFunction 就是一个闭包,它记住了 outerVariable 变量。

闭包在数据隐私场景中的应用

闭包可以用于实现数据隐私,通过将数据和操作数据的函数封装在一个闭包中,使得外部代码无法直接访问内部数据。

以下是一个简单的计数器模块示例:

const counterModule = (function () {
    let count = 0;
    return {
        increment: function () {
            count++;
            console.log(`Count is now ${count}`);
        },
        getCount: function () {
            return count;
        }
    };
})();

counterModule.increment(); 
console.log(`Current count: ${counterModule.getCount()}`); 

在这个例子中,count 变量被封装在闭包内部,外部代码无法直接访问和修改 count。只能通过 incrementgetCount 这两个公开的函数来操作和获取 count 的值,从而实现了数据的隐私保护。

闭包在模块模式中的应用

模块模式是 JavaScript 中一种使用闭包来组织代码的设计模式。它允许将相关的代码和数据封装在一个单独的模块中,避免全局变量的污染,同时提供了一种控制访问权限的方式。

例如,我们创建一个简单的数学运算模块:

const mathModule = (function () {
    function add(a, b) {
        return a + b;
    }
    function subtract(a, b) {
        return a - b;
    }
    function multiply(a, b) {
        return a * b;
    }
    function divide(a, b) {
        if (b === 0) {
            throw new Error('Cannot divide by zero');
        }
        return a / b;
    }
    return {
        add: add,
        subtract: subtract,
        multiply: multiply,
        divide: divide
    };
})();

console.log(`5 + 3 = ${mathModule.add(5, 3)}`); 
console.log(`5 - 3 = ${mathModule.subtract(5, 3)}`); 
console.log(`5 * 3 = ${mathModule.multiply(5, 3)}`); 
console.log(`5 / 3 = ${mathModule.divide(5, 3)}`); 

在这个 mathModule 中,所有的数学运算函数都被封装在闭包内部,通过返回一个包含公开函数的对象,外部代码可以使用这些函数进行数学运算,而不会干扰到闭包内部的其他代码,也避免了全局命名冲突。

闭包在事件处理中的应用

闭包在事件处理中也有广泛应用。例如,当我们为多个 DOM 元素添加事件监听器时,闭包可以确保每个监听器都能正确访问到其对应的上下文数据。

以下是一个简单的 HTML 示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Closure in Event Handling</title>
</head>

<body>
    <button id="btn1">Button 1</button>
    <button id="btn2">Button 2</button>
    <script>
        function createButtonClickListener(message) {
            return function () {
                console.log(message);
            };
        }
        const btn1 = document.getElementById('btn1');
        const btn2 = document.getElementById('btn2');
        btn1.addEventListener('click', createButtonClickListener('You clicked Button 1'));
        btn2.addEventListener('click', createButtonClickListener('You clicked Button 2'));
    </script>
</body>

</html>

在上述代码中,createButtonClickListener 函数返回一个闭包函数。每次调用 createButtonClickListener 并传入不同的 message 参数时,都会创建一个新的闭包,使得每个按钮的点击事件监听器都能输出正确的消息,而不会因为共享相同的作用域而导致混淆。

闭包在函数柯里化中的应用

函数柯里化是一种将多参数函数转换为一系列单参数函数的技术,闭包在函数柯里化中起到了关键作用。

例如,我们有一个简单的加法函数:

function add(a, b) {
    return a + b;
}

function curryAdd(a) {
    return function (b) {
        return a + b;
    };
}

const add5 = curryAdd(5);
console.log(add5(3)); 

在上述代码中,curryAdd 函数返回一个闭包函数。通过调用 curryAdd(5),我们得到了一个新的函数 add5,这个函数记住了 a 的值为 5。当我们调用 add5(3) 时,实际上是在闭包的作用域中执行 5 + 3 的操作。函数柯里化可以提高函数的灵活性和复用性,闭包则保证了柯里化函数能够正确记住和使用其初始参数。

类对象和闭包模块场景应用的对比与结合

对比

  1. 数据封装方式
    • 类对象:通过 class 关键字和构造函数来封装数据和方法,数据和方法直接挂载在对象实例上,使用 this 关键字来引用。例如在 BankAccount 类中,balance 属性和 depositwithdraw 等方法都与对象实例紧密相关。
    • 闭包模块:利用闭包的特性将数据和函数封装在一个独立的作用域中,通过返回包含公开函数的对象来提供访问接口。如 counterModule 中,count 变量隐藏在闭包内部,通过 incrementgetCount 函数来操作和获取。
  2. 继承和复用
    • 类对象:通过 extends 关键字实现继承,子类可以复用父类的属性和方法,并进行扩展。例如 Car 类继承自 Vehicle 类,复用了 Vehicle 类的 makemodel 属性和 drive 方法,并添加了 numDoors 属性和 honk 方法。
    • 闭包模块:闭包模块本身不直接支持继承,但可以通过函数组合等方式来实现代码复用。例如在 mathModule 中,虽然没有继承关系,但不同的数学运算函数可以被组合使用,并且可以通过闭包的封装来避免代码的重复编写。
  3. 内存管理
    • 类对象:当创建类的实例时,每个实例都会拥有自己独立的属性副本。如果创建大量实例,可能会占用较多内存。例如创建多个 BankAccount 实例,每个实例都有自己的 accountNumberbalance 属性。
    • 闭包模块:闭包模块通常作为单例存在,内部数据在模块加载时初始化一次,多个调用共享这些数据。如 counterModule 只有一个 count 变量,不同的调用都是对这个变量进行操作,相对来说在内存使用上更为节省,特别是对于不需要多个独立副本的数据。

结合应用

在实际开发中,类对象和闭包模块可以结合使用,以充分发挥两者的优势。

例如,我们可以使用闭包模块来创建一个类的工厂函数,通过闭包来管理类的一些全局状态或共享资源,同时利用类对象的封装和继承特性来创建具体的对象实例。

以下是一个简单的示例,假设我们有一个游戏角色类,并且通过闭包模块来管理游戏中的一些全局配置:

const gameModule = (function () {
    const defaultHealth = 100;
    const defaultPower = 50;
    class Character {
        constructor(name) {
            this.name = name;
            this.health = defaultHealth;
            this.power = defaultPower;
        }
        attack(target) {
            target.health -= this.power;
            console.log(`${this.name} attacks ${target.name}. ${target.name}'s health is now ${target.health}`);
        }
    }
    return {
        createCharacter: function (name) {
            return new Character(name);
        }
    };
})();

const player1 = gameModule.createCharacter('Alice');
const player2 = gameModule.createCharacter('Bob');
player1.attack(player2); 

在这个例子中,gameModule 是一个闭包模块,它定义了 defaultHealthdefaultPower 等全局配置,并通过返回的 createCharacter 函数来创建 Character 类的实例。Character 类则利用了类对象的特性来封装角色的属性和行为。这样的结合方式既保证了全局配置的封装和管理,又利用了类对象的面向对象特性来创建和管理具体的游戏角色。

再比如,我们可以在类的方法中使用闭包来实现一些特定的功能。例如,在一个 FileManager 类中,我们可以使用闭包来处理文件读取操作,以确保文件读取的上下文和状态得到正确管理。

class FileManager {
    constructor() {
        this.files = [];
    }
    readFile(filePath) {
        return (function () {
            // 模拟异步文件读取操作
            setTimeout(() => {
                console.log(`Reading file: ${filePath}`);
                this.files.push(filePath);
                console.log(`File ${filePath} added to list. Current files: ${this.files.join(', ')}`);
            }.bind(this), 1000);
        })();
    }
}

const fileManager = new FileManager();
fileManager.readFile('document.txt'); 

在上述代码中,readFile 方法返回一个闭包函数。这个闭包函数内部使用了 setTimeout 模拟异步文件读取操作,并通过 bind(this) 确保在闭包内部 this 关键字指向 FileManager 类的实例。这样,我们既利用了类对象的封装特性来管理文件相关的操作,又通过闭包来处理异步操作中的上下文问题。

总之,类对象和闭包模块在 JavaScript 开发中都有各自独特的应用场景,合理地结合使用它们可以开发出更加健壮、高效和可维护的代码。无论是在大型应用程序的架构设计,还是在小型模块的实现中,理解和运用这两种编程范式都是非常重要的。在面向对象编程风格较为突出的场景中,类对象可以很好地组织和管理代码;而在需要数据隐私保护、单例模式或特定的函数式编程特性时,闭包模块则能发挥其优势。通过不断实践和探索,开发者可以根据具体的需求选择最合适的方式,或者将两者巧妙结合,打造出高质量的 JavaScript 应用。