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 开发中,我们可以继续关注这些特性的发展和改进,以更好地应对不断变化的需求。
JavaScript类、继承与异步详解
超级会员免费看
3066

被折叠的 条评论
为什么被折叠?



