41、JavaScript 中的类、继承与异步处理

JavaScript类、继承与异步详解

JavaScript 中的类、继承与异步处理

1. 类与继承

在 JavaScript 里,类和继承是重要的概念。下面我们来详细探讨。

1.1 构造函数

构造函数是一种特殊的方法,仅在对象创建时执行一次。以 Tax 类为例:

class Tax {
    constructor(income) {
        this.income = income;
    }
}
var myTax = new Tax(50000);

在这个例子中, Tax 类没有单独声明类级别的 income 变量,而是在 this 对象上动态创建它,并使用构造函数参数的值初始化 this.income this 变量指向当前对象的实例。

1.2 子类的创建

我们可以创建 Tax 类的子类 NJTax ,并为其构造函数提供收入值:

class Tax {
    constructor(income) {
        this.income = income;
    }
}
class NJTax extends Tax {
    // 新泽西州税收的特定代码放在这里
}
let njTax = new NJTax(50000);
console.log(`The income in njTax instance is ${njTax.income}`);

由于 NJTax 子类没有定义自己的构造函数,在实例化 NJTax 时会自动调用 Tax 超类的构造函数。若子类定义了自己的构造函数,情况就不同了。

1.3 super 关键字和 super 函数

super() 函数允许子类调用超类的构造函数, super 关键字用于调用超类中定义的方法。以下是示例代码:

class Tax {
    constructor(income) {
        this.income = income;
    }
    calculateFederalTax() {
        console.log(`Calculating federal tax for income ${this.income}`);
    }
    calcMinTax() {
        console.log("In Tax. Calculating min tax");
        return 123;
    }
}
class NJTax extends Tax {
    constructor(income, stateTaxPercent) {
        super(income);
        this.stateTaxPercent = stateTaxPercent;
    }
    calculateStateTax() {
        console.log(`Calculating state tax for income ${this.income}`);
    }
    calcMinTax() {
        let minTax = super.calcMinTax();
        console.log(`In NJTax. Will adjust min tax of ${minTax}`);
    }
}
const theTax = new NJTax(50000, 6);
theTax.calculateFederalTax();
theTax.calculateStateTax();
theTax.calcMinTax();

运行这段代码的输出如下:

Calculating federal tax for income 50000
Calculating state tax for income 50000
In Tax. Calculating min tax
In NJTax. Will adjust min tax of 123

NJTax 类有自己明确定义的构造函数,包含两个参数 income stateTaxPercent 。为确保 Tax 的构造函数被调用(它会在对象上设置 income 属性),我们在子类的构造函数中显式调用 super(income) 。若没有这一行,运行代码会报错。

通过调用 super.calcMinTax() ,我们确保在计算州税时考虑了联邦税的基础金额。若不调用,子类的 calcMinTax() 方法将单独应用。方法重写常用于在不改变超类代码的情况下替换其方法的功能。

1.4 静态变量

若需要一个由多个类实例共享的类属性,需在类声明外部创建它。示例如下:

class A {
    printCounter() {
        console.log("static counter=" + A.counter);
    };
}
A.counter = 25;
let a1 = new A();
a1.printCounter();
console.log("In the a1 instance counter=" + a1.counter);
let a2 = new A();
a2.printCounter();
console.log("In the a2 instance counter=" + a2.counter);

这段代码的输出为:

static counter=25
In the a1 instance counter=undefined
static counter=25
In the a2 instance counter=undefined

可以看到,静态变量 counter 可通过调用 printCounter() 方法从对象 A 的两个实例中访问,但在实例级别访问 counter 变量将为 undefined

2. Getters、Setters 和方法定义

在 ES6 中,对象的 getter 和 setter 方法的语法并非新特性,但在探讨新的方法定义语法之前,我们先回顾一下。

2.1 Getters 和 Setters

Getters 和 setters 将函数绑定到对象属性。以下是 Tax 对象字面量的声明和使用示例:

const Tax = {
    taxableIncome: 0,
    get income() { return this.taxableIncome; },
    set income(value) { this.taxableIncome = value }
};
Tax.income = 50000;
console.log("Income: " + Tax.income); // 输出 Income: 50000

注意,我们使用点符号来分配和检索 income 的值,就好像它是 Tax 对象声明的属性一样。

2.2 方法定义

在 ES5 中,我们需要使用 function 关键字,如 calculateTax = function(){…} 。而在 ES6 中,我们可以在任何方法定义中省略 function 关键字:

const Tax = {
    taxableIncome: 0,
    get income() { return this.taxableIncome; },
    set income(value) { this.taxableIncome = value },
    calculateTax() { return this.taxableIncome * 0.13 }
};
Tax.income = 50000;
console.log(`For the income ${Tax.income} your tax is ${Tax.calculateTax()}`);

这段代码的输出为:

For the income 50000 your tax is 6500

Getters 和 setters 为处理属性提供了方便的语法。例如,如果我们决定在 income getter 中添加一些验证代码,使用 Tax.income 表示法的脚本无需更改。但不好的是,ES6 不支持类中的私有变量,因此无法阻止程序员直接访问 getter 或 setter 中使用的变量(如 taxableIncome )。

3. 异步处理

在 ECMAScript 的早期实现中,我们必须使用回调函数来安排异步处理。回调函数作为参数传递给另一个函数以供调用,可同步或异步调用。

3.1 回调地狱

考虑一个从服务器获取一些订购产品数据的例子。首先,我们需要异步调用服务器以获取客户信息,然后为每个客户再进行一次调用以获取订单。对于每个订单,我们需要获取产品,最后一次调用将获取产品详细信息。
由于在异步处理中,我们不知道每个操作何时完成,因此需要编写回调函数,在前一个操作完成时调用。我们使用 setTimeout() 函数来模拟延迟,假设每个操作需要一秒钟完成。以下是代码示例:

// 模拟异步操作
setTimeout(() => {
    console.log("Getting customers");
    setTimeout(() => {
        console.log("Getting orders");
        setTimeout(() => {
            console.log("Getting products");
            setTimeout(() => {
                console.log("Getting product details");
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);

使用回调函数被认为是一种反模式,也称为“厄运金字塔”。在我们的代码示例中,有四个回调函数,这种嵌套级别使代码难以阅读。在实际应用中,金字塔可能会迅速增长,使代码非常难以阅读和调试。

运行上述代码将以一秒的延迟打印以下消息:

Getting customers
Getting orders
Getting products
Getting product details
3.2 ES6 承诺(Promises)

ES6 引入了 Promise 对象,它代表异步操作的最终完成或失败。 Promise 对象可以处于以下三种状态之一:
- Fulfilled :操作成功完成。
- Rejected :操作失败并返回错误。
- Pending :操作正在进行中,既未完成也未失败。

以下是一个使用 Promise 的示例:

function getCustomers() {
    return new Promise(
        function (resolve, reject) {
            console.log("Getting customers");
            // 模拟异步服务器调用
            setTimeout(function () {
                var success = true;
                if (success) {
                    resolve("John Smith");
                } else {
                    reject("Can't get customers");
                }
            }, 1000);
        }
    );
}
getCustomers()
   .then((cust) => console.log(cust))
   .catch((err) => console.log(err));
console.log("Invoked getCustomers. Waiting for results");

在这个例子中, getCustomers() 函数返回一个 Promise 对象。当操作成功时,调用 resolve() 函数;当操作失败时,调用 reject() 函数。我们使用 then() 方法处理成功结果,使用 catch() 方法处理错误。

运行这段代码的输出如下:

Getting customers
Invoked getCustomers. Waiting for results
John Smith

可以看到, Invoked getCustomers. Waiting for results 消息在 John Smith 之前打印,这证明 getCustomers() 函数是异步工作的。

我们还可以将多个 Promise 链起来,以保证特定的执行顺序。以下是一个将 getCustomers() getOrders() 函数链起来的示例:

function getCustomers() {
    return new Promise(
        function (resolve, reject) {
            console.log("Getting customers");
            // 模拟异步服务器调用
            setTimeout(function () {
                const success = true;
                if (success) {
                    resolve("John Smith");
                } else {
                    reject("Can't get customers");
                }
            }, 1000);
        }
    );
}
function getOrders(customer) {
    return new Promise(
        function (resolve, reject) {
            // 模拟异步服务器调用
            setTimeout(function () {
                const success = true;
                if (success) {
                    resolve(`Found the order 123 for ${customer}`);
                } else {
                    reject("Can't get orders");
                }
            }, 1000);
        }
    );
}
getCustomers()
   .then((cust) => {
        console.log(cust);
        return cust;
    })
   .then((cust) => getOrders(cust))
   .then((order) => console.log(order))
   .catch((err) => console.error(err));
console.log("Chained getCustomers and getOrders. Waiting for results");

运行这段代码的输出如下:

Getting customers
Chained getCustomers and getOrders. Waiting for results
John Smith
Found the order 123 for John Smith

我们可以使用 then() 方法链多个函数调用,并为所有链式调用使用一个错误处理脚本。如果发生错误,它将通过整个 then 链传播,直到找到错误处理程序。错误发生后,不会再调用任何 then 方法。

3.3 同时解决多个承诺

另一种情况是处理不相互依赖的异步函数。我们可能需要以任意顺序调用两个函数,但只在它们都完成后执行某些操作。 Promise 对象有一个 all() 方法,它接受一个可迭代的 Promise 集合,并执行(解决)所有这些 Promise

以下是一个示例,假设我们有一个需要进行多个异步调用以获取天气、股票市场新闻和交通信息的 Web 门户:

Promise.all([getWeather(), getStockMarketNews(), getTraffic()])
   .then((results) => {
        // 在这里渲染门户 GUI
    })
   .catch(err => console.error(err));

需要注意的是, Promise.all() 只有在所有 Promise 都解决后才会解决。如果其中一个 Promise 被拒绝,控制将转到 catch() 处理程序。

与回调函数相比, Promise 使我们的代码更线性、更易读,并且能表示应用程序的多种状态。但 Promise 无法取消,例如,一个不耐烦的用户多次点击按钮以从服务器获取数据,每次点击都会创建一个 Promise 并发起一个 HTTP 请求,无法只保留最后一个请求并取消未完成的请求。

4. async 和 await

async await 关键字在 ES8(也称为 ES2017)中引入,它们允许我们将返回 Promise 的函数视为同步函数。只有当前一行代码完成后,才会执行下一行代码。需要注意的是,等待异步代码完成是在后台进行的,不会阻塞程序其他部分的执行。

  • async :用于标记一个返回 Promise 的函数。
  • await :放在异步函数调用之前,指示 JavaScript 引擎在异步函数返回结果或抛出错误之前不要继续执行下一行代码。JavaScript 引擎会在内部将 await 关键字右侧的表达式包装成一个 Promise ,并将方法的其余部分包装成一个 then() 回调。

以下是一个使用 async await 的示例,我们复用之前的 getCustomers() getOrders() 函数:

function getCustomers() {
    return new Promise(
        function (resolve, reject) {
            console.log("Getting customers");
            // 模拟一个需要 1 秒完成的异步调用
            setTimeout(function () {
                const success = true;
                if (success) {
                    resolve("John Smith");
                } else {
                    reject("Can't get customers");
                }
            }, 1000);
        }
    );
}
function getOrders(customer) {
    return new Promise(
        function (resolve, reject) {
            // 模拟一个需要 1 秒的异步调用
            setTimeout(function () {
                const success = true;
                if (success) {
                    resolve(`Found the order 123 for ${customer}`);
                } else {
                    reject(`getOrders() has thrown an error for ${customer}`);
                }
            }, 1000);
        }
    );
}
(async function getCustomersOrders() {
    try {
        const customer = await getCustomers();
        console.log(`Got customer ${customer}`);
        const orders = await getOrders(customer);
        console.log(orders);
    } catch (err) {
        console.log(err);
    }
})();
console.log("This is the last line in the app. Chained getCustomers() and getOrders() are still running without blocking the rest of the app.");

运行这段代码的输出如下:

Getting customers
This is the last line in the app. Chained getCustomers() and getOrders() are still running without blocking the rest of the app.
Got customer John Smith
Found the order 123 for John Smith

可以看到,关于代码最后一行的消息在客户姓名和订单号之前打印,即使这些值稍后异步检索,但这个小应用的执行并未被阻塞,脚本在异步函数 getCustomers() getOrders() 完成执行之前就到达了最后一行。

5. ES6 模块

在任何编程语言中,将代码拆分为模块有助于将应用程序组织成逻辑且可能可重用的单元。模块化应用程序可以更有效地将编程任务分配给软件开发人员。开发人员可以决定模块应向外部暴露哪些 API,以及哪些 API 应在内部使用。

5.1 ES5 中的模块实现

ES5 没有用于创建模块的语言结构,因此我们必须采用以下选项之一:
- 手动实现一个立即初始化函数的模块设计模式。
- 使用第三方实现的 AMD(http://mng.bz/JKVc)或 CommonJS(http://mng.bz/7Lld)标准。

CommonJS 是为在 Web 浏览器之外运行的 JavaScript 应用程序(如使用 Node.js 编写并部署在 Google 的 V8 引擎下的应用程序)模块化而创建的。AMD 主要用于在 Web 浏览器中运行的应用程序。

5.2 ES6 中的模块

从 ES6 开始,模块成为了语言的一部分,这意味着开发人员将不再使用第三方库来实现各种标准。如果一个脚本使用了 import 和/或 export 关键字,它就成为了一个模块。

以下是一个关于 ES6 模块和全局作用域的示例:

// 非 ES6 模块
class Person {}
// 这将在全局作用域中创建 Person 类的实例

// ES6 模块
export class Person {}
// 现在 Person 类型的对象不会在全局作用域中创建,其作用域将仅限于导入 Person 的其他 ES6 模块

在多文件项目中,如果一个文件没有导出任何内容,它就不是 ES6 模块, Person 类的实例将在全局作用域中创建。如果项目中已经有另一个脚本也声明了 Person 类,TypeScript 编译器将在上述代码中给出错误,指出你试图声明一个已经存在的重复项。而添加 export 语句后,该脚本就成为了一个模块, Person 对象的作用域将仅限于导入它的其他模块。

综上所述,JavaScript 中的类、继承、异步处理和模块等特性为我们开发复杂的应用程序提供了强大的支持。通过合理运用这些特性,我们可以编写更易读、更易维护的代码。同时,我们也需要注意它们的一些局限性,如 Promise 无法取消、ES6 不支持类中的私有变量等,在实际开发中做出合适的选择。

JavaScript 中的类、继承与异步处理

6. 类与继承的总结与注意事项

在使用 JavaScript 的类与继承时,有一些要点需要注意:
- ES6 类的本质 :ES6 类只是语法糖,它提高了代码的可读性。但在底层,JavaScript 仍然使用原型继承,这允许在运行时动态替换祖先,而类只能有一个祖先。
- 避免深继承层次 :创建深继承层次会降低代码的灵活性,并且在需要重构时会使代码变得复杂。例如,当一个类有多层父类时,修改其中一个父类的方法可能会影响到多个子类,导致难以调试和维护。
- 谨慎使用 super 关键字 :虽然使用 super 关键字可以调用祖先的代码,但应尽量避免使用,以减少子类和祖先对象之间的紧密耦合。子类对祖先了解得越少越好,这样可以提高代码的独立性和可维护性。

7. 异步处理的总结与对比

下面我们对 JavaScript 中的异步处理方式进行总结和对比:
| 异步处理方式 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 回调函数 | 简单直接,在早期 JavaScript 中广泛使用 | 容易形成回调地狱,代码嵌套层次深,难以阅读和调试 | 简单的异步操作,且嵌套层次不深的情况 |
| ES6 承诺(Promises) | 避免了嵌套调用,使异步代码更易读;可以链式调用多个异步操作,并统一处理错误 | 无法取消,一旦创建就会执行 | 多个异步操作需要按顺序执行,或者需要统一处理错误的情况 |
| async await | 使异步代码看起来像同步代码,更符合编程习惯;使用 try/catch 进行错误处理,更直观 | 只能在 async 函数中使用 | 处理多个异步操作,且需要按顺序执行,同时希望代码更易读的情况 |

8. 代码示例的 mermaid 流程图

下面是一个使用 async await 处理 getCustomers() getOrders() 函数的 mermaid 流程图:

graph LR
    A[开始] --> B[调用 getCustomersOrders 函数]
    B --> C{getCustomers 成功?}
    C -- 是 --> D[打印客户信息]
    C -- 否 --> E[捕获错误并打印]
    D --> F{getOrders 成功?}
    F -- 是 --> G[打印订单信息]
    F -- 否 --> E
    G --> H[结束]
    E --> H
9. ES6 模块的使用建议

在使用 ES6 模块时,可以参考以下建议:
- 合理划分模块 :根据功能将代码拆分成多个模块,每个模块负责一个特定的功能。例如,将处理用户认证的代码放在一个模块中,将处理数据存储的代码放在另一个模块中。
- 明确导出和导入 :在模块中明确哪些内容需要导出供外部使用,哪些内容是内部使用的。使用 export 关键字导出需要暴露的类、函数或变量,使用 import 关键字在其他模块中导入所需的内容。
- 按需加载模块 :对于大型应用程序,应尽量按需加载模块,避免在应用启动时加载过多的 JavaScript 代码。可以使用动态导入( import() )来实现按需加载。

10. 综合示例:使用类、异步处理和模块

下面是一个综合示例,展示了如何在一个项目中使用类、异步处理和模块:

tax.js 模块

// 定义 Tax 类
export class Tax {
    constructor(income) {
        this.income = income;
    }
    calculateFederalTax() {
        console.log(`Calculating federal tax for income ${this.income}`);
    }
    calcMinTax() {
        console.log("In Tax. Calculating min tax");
        return 123;
    }
}

// 定义 NJTax 子类
export class NJTax extends Tax {
    constructor(income, stateTaxPercent) {
        super(income);
        this.stateTaxPercent = stateTaxPercent;
    }
    calculateStateTax() {
        console.log(`Calculating state tax for income ${this.income}`);
    }
    calcMinTax() {
        let minTax = super.calcMinTax();
        console.log(`In NJTax. Will adjust min tax of ${minTax}`);
    }
}

asyncFunctions.js 模块

// 模拟异步获取客户信息
export function getCustomers() {
    return new Promise((resolve, reject) => {
        console.log("Getting customers");
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve("John Smith");
            } else {
                reject("Can't get customers");
            }
        }, 1000);
    });
}

// 模拟异步获取订单信息
export function getOrders(customer) {
    return new Promise((resolve, reject) => {
        console.log(`Getting orders for ${customer}`);
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve(`Found the order 123 for ${customer}`);
            } else {
                reject(`Can't get orders for ${customer}`);
            }
        }, 1000);
    });
}

main.js 模块

import { Tax, NJTax } from './tax.js';
import { getCustomers, getOrders } from './asyncFunctions.js';

// 使用类
const theTax = new NJTax(50000, 6);
theTax.calculateFederalTax();
theTax.calculateStateTax();
theTax.calcMinTax();

// 使用异步处理
(async function getCustomersOrders() {
    try {
        const customer = await getCustomers();
        console.log(`Got customer ${customer}`);
        const orders = await getOrders(customer);
        console.log(orders);
    } catch (err) {
        console.log(err);
    }
})();

在这个示例中,我们将不同的功能封装在不同的模块中,使用类和继承来处理税收计算,使用 async await 来处理异步操作。通过这种方式,代码更加模块化、易读和易维护。

11. 总结

JavaScript 中的类、继承、异步处理和模块等特性为我们开发复杂的应用程序提供了强大的支持。通过合理运用这些特性,我们可以编写更易读、更易维护的代码。同时,我们也需要注意它们的一些局限性,如 Promise 无法取消、ES6 不支持类中的私有变量等,在实际开发中做出合适的选择。在未来的 JavaScript 开发中,我们可以继续关注这些特性的发展和改进,以更好地应对不断变化的需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值