JavaScript 设计模式-面向对象
当前字数: 0 字 阅读时长: 0 分钟
概念
类
类(Class):对象的模板
类就像是一个模板,它定义了对象的属性和方法。通过类可以创建多个具有相同属性和方法的对象。
让我们用 Person
类作为例子来进一步解释面向对象编程中的“类”概念:
比如你正在开发一个管理系统,需要处理人员信息:
这些人员都有名字、年龄(属性)。
这些人员有一些共同的行为,比如 吃饭、讲话(方法)。
这时定义一个
Person
类,它就像是一个“具有共同特征和行为的人员模具”。
class Person {
// 构造函数,用于初始化对象的属性
constructor(name, age) {
this.name = name; // 名字属性
this.age = age; // 年龄属性
}
// 吃的方法
eat() {
console.log(`${this.name} 正在吃饭。`);
}
// 讲话的方法
speak() {
console.log(`大家好,我叫${this.name},今年${this.age}岁。`);
}
}
对象
对象(Object):类的实例
对象是类的实例(类是对象的模板-在这里具象了哦)。它是包含数据和功能的集合。可以通过类来实例化很多个对象。
现在继续,用 Person
类,创建不同的人员对象,并调用它们的方法来模拟这些行为:
// 创建一个叫“小明”的对象
let xiaoMing = new Person("小明", 25);
// 调用吃的方法
xiaoMing.eat();
// 输出: 小明 正在吃饭。
// 调用讲话的方法
xiaoMing.speak();
// 输出: 大家好,我叫小明,今年25岁。
// 创建另一个叫“小红”的对象
let xiaoHong = new Person("小红", 20);
// 调用吃的方法
xiaoHong.eat();
// 输出: 小红 正在吃饭。
// 调用讲话的方法
xiaoHong.speak();
// 输出: 大家好,我叫小红,今年20岁。
在上面例子中:
Person
类定义了两个方法(eat、speak)
,分别用于模拟人吃饭、讲话的行为。- 当使用
new
关键字和Person
类创建一个新对象时,这个对象就可以使用这些方法。 - 然后,我们可以通过调用这些方法来实现特定的行为。
这种方式的好处是,我们只需要编写一份 Person
类的代码,就可以轻松地创建和管理大量的人员对象,并且每个对象都可以独立地执行这些行为。它允许我们定义通用的模板,并根据需要创建具有独特特征和行为的对象。
三要素
继承
继承(Inheritance),子类继承父类。
继承是面向对象编程中一种基本的代码复用机制。它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。这样,子类就可以重用父类中已经定义好的代码,同时还可以添加或覆盖一些特定的功能。
父类:我们继续使用 Person
类作为 基类(父类):
- 这里作为 “具有共同特征和行为的人员模具” 的
Person
类,包含了: - 属性:
name
、age
- 方法:
eat
,speak
class Person {
// 构造函数,用于初始化对象的属性
constructor(name, age) {
this.name = name; // 名字属性
this.age = age; // 年龄属性
}
// 吃的方法
eat() {
console.log(`${this.name} 正在吃饭。`);
}
// 讲话的方法
speak() {
console.log(`大家好,我叫${this.name},今年${this.age}岁。`);
}
}
子类继承父类: 创建 Student
类作为派生类(子类),继承自 Person
:
- 这里除了继承
Person
父类的属性和方法, 我们要添加: Student
子类 特有的属性,比如: 学号studentId
。Student
子类 特有的行为(方法),比如:学习study
。
// 定义派生类(子类)Student,继承自Person
class Student extends Person {
// 子类的构造函数需要调用 super() 来初始化从父类继承的属性
constructor(name, age, studentId) {
super(name, age); // 调用父类的构造函数
this.studentId = studentId; // 子类特有的属性
}
// 子类可以添加新的方法
study() {
console.log(`${this.name} 正在学习。`);
}
}
// 创建一个Student对象并测试
let zhangSan = new Student("张三", 14, "S12345");
zhangSan.eat(); // 输出: 张三 正在吃饭。
zhangSan.study(); // 输出: 张三 正在学习。
const liSi = new Student("李四", 16, "S54321");
liSi.eat(); // 输出: 李四 正在吃饭。
liSi.study(); // 输出: 李四 正在学习。
在这个例子中:
Student
类通过extends
关键字继承了Person
类。Person
是父类,是公共的,它不仅仅服务于Student
Student
类的构造函数中,我们使用super(name, age)
来调用父类的构造函数,以确保从父类继承的属性(name、age)
被正确初始化。然后,我们添加了子类特有的属性
studentId
。还添加了一个新的方法study
,这是父类中没有的。
通过使用继承,我们可以轻松地创建具有共享属性和方法的类,同时还可以在子类中添加特定的功能。在这里 People
不仅仅服务于 Student
。继承可已将公共方法抽离出来,提高复用,减少冗余。
封装
封装(Encapsulation),数据的权限和保密。
封装是面向对象编程中一种保护数据的方式。它将类的属性和方法结合在一起,形成一个独立的单元,并对外部隐藏类的内部实现细节,只暴露必要的接口给外部使用。这样,类的内部状态只能通过类提供的方法进行访问和修改,从而保证了数据的安全性和完整性。
三个关键字 !!!:
封装呢,首先要提到这三个关键字:
- public: 完全开放、private: 对自己开放、 protected: 对子类开放(自己也可以访问)
纯面向对象语言就是通过这个三个关键字来做封装。
但是 JS 中并没有这个三个关键字,体现不出封装的特性!
ES2019 引入的私有字段语法(#前缀),这是 JavaScript 中实现私有属性的官方方式。在之前的 JavaScript 版本中,通常使用约定(如下划线前缀_)或闭包来模拟私有属性。不是简单和固用的方式来做封装。
有关可以阅读 MDN-类-私有属性
下面使用 TypeScript
实现! 下面使用 TypeScript
实现! 下面使用 TypeScript
实现!
这里我们继续使用 Person
类作为 基类(父类),用 TS
来改造一下:
- 给
Person
类增加一个protected
属性personId
。 - 它可以在
Person
类及其子类(如Student
)中被访问和修改。
class Person {
// 公共属性
name: string;
age: number;
// 受保护的属性,只能在Person类及其子类中访问
protected personId: string;
// 构造函数,用于初始化对象的属性
constructor(name: string, age: number, personId: string) {
this.name = name; // 名字属性
this.age = age; // 年龄属性
this.personId = personId; // 受保护的属性,通常用于标识人
}
// 吃的方法
eat(): void {
console.log(`${this.name} 正在吃饭。`);
}
// 讲话的方法
speak(): void {
console.log(`大家好,我叫${this.name},今年${this.age}岁。`);
}
// 获取personId的方法
// 虽然是受保护的,但为了演示,这里提供一个公共方法来访问它
getPersonId(): string {
return this.personId;
}
}
定义派生类(子类)Student
,继承自 Person
:
- 把
Student
子类特有的属性,studentId
改为私有属性private
。 studentId
只能在Student
类内部被访问和修改。
class Student extends Person {
// 私有的属性,只能在Student类内部访问
private studentId: string;
// 子类的构造函数需要调用 super() 来初始化从父类继承的属性
constructor(name: string, age: number, personId: string, studentId: string) {
super(name, age, personId); // 调用父类的构造函数
this.studentId = studentId; // 子类特有的私有属性
}
// 子类可以添加新的方法
study(): void {
console.log(`${this.name} 正在学习。`);
}
// 获取学生编号的方法
// 虽然是私有的,这里提供一个受保护或公共方法来访问它,但在实际封装中应保持私有
// 注意:通常我们不会从外部直接访问私有属性,这里仅为了演示目的
getStudentIdForDemo(): string {
// 在实际项目中,应该避免这种直接暴露私有属性的做法
// 这里仅作为演示,展示如何定义和使用私有属性
return this.studentId;
}
}
const wangWu = new Person("王五", 30, "P12345");
console.log(wangWu.getPersonId()); // 输出: P12345
console.log(wangWu.personId); // 错误:属性 “personId” 受保护,只能在类“Person”及其子类中访问。
const xiaoLi = new Student("小丽", 20, "S54321", "20230001");
console.log(xiaoLi.getPersonId()); // 输出: S54321
console.log(xiaoLi.studentId); // 错误:属性“studentId”为私有属性,只能在类“Student”中访问。
console.log(xiaoLi.getStudentIdForDemo()); // 输出: 20230001(仅为了演示,实际项目中应避免这样做)
xiaoLi.study(); // 输出: 小丽 正在学习。
在这个例子中:
Person
类有一个protected
属性personId
,它在Person
类及子类Student
中被访问和修改。- 而
Student
类有一个private
属性studentId
,它只能在Student
类内部被访问和修改。 - 隐藏对象的内部状态和行为,仅通过公共方法暴露必要的接口。减少耦合,不该外漏的不外漏。
多态
多态(Polymorphism),同一接口不同表现。
多态是面向对象编程中一种实现接口重用的机制。它允许一个接口在不同的实例中以不同的方式实现。多态通常通过继承和方法重写来实现。在运行时,系统可以根据对象的实际类型来决定调用哪个方法,从而实现动态绑定。
TIP
多态需要结合 Java
等语言的接口、重写、重载等功能。这些 JS
中都没有。
面向接口编程中,有时不关心子类中是怎么实现的,只需要关心父类有多少接口,是什么样的接口。但这些在 JS
中体现并不明显。
JS
多态的体现并不像在一些静态类型语言(如 Java
或 C++
)中那样显式或强制,但多态的概念和效果在 JS
中仍然是存在的,并且非常重要。
由于 JS
的灵活性,有时候开发者可能会不自觉地绕过多态性(例如,通过直接检查对象的类型或属性来决定行为)。
实现一个多态的列子:
- 有一个基类
Animal
,以及两个继承自Animal
的子类Dog
和Cat
。 - 每个类都有一个
Speak
方法,但它们的实现是不同的。
// 定义基类 Animal
class Animal {
constructor(name) {
this.name = name;
}
// 提供一个默认实现,但通常基类中的方法会被子类重写
speak() {}
}
// 定义 Dog 类,继承自 Animal
class Dog extends Animal {
constructor(name) {
super(name);
}
// 重写 speak 方法
speak() {
console.log(`${this.name} says: Woof!`);
}
}
// 定义 Cat 类,继承自 Animal
class Cat extends Animal {
constructor(name) {
super(name);
}
// 重写 speak 方法
speak() {
console.log(`${this.name} says: Meow!`);
}
}
创建实例并展示多态行为:
// 创建一个 Dog 实例和一个 Cat 实例
const dog = new Dog("Buddy");
const cat = new Cat("Whiskers");
// 调用 speak 方法,展示多态行为
function makeAnimalSpeak(animal) {
// 这里不需要知道 animal 是 Dog 还是 Cat,它会自动调用正确的 speak 方法
animal.speak();
}
makeAnimalSpeak(dog); // 输出: Buddy says: Woof!
makeAnimalSpeak(cat); // 输出: Whiskers says: Meow!
在这个例子中
我们使用了
class
和extends
关键字来定义类和继承关系。每个子类都重写了speak
方法,以提供自己的实现。makeAnimalSpeak
函数接受一个Animal
类型的参数(实际上是Animal
的实例或其子类的实例),并调用其speak
方法。由于
JavaScript
的动态类型特性,它会在运行时根据对象的实际类型调用正确的方法,这就是多态的体现。以后如果添加更多的动物类型,只需定义新的子类并重写
speak
方法即可,无需修改现有的makeAnimalSpeak
函数或其他相关代码。
应用
使用 ES6
的 class
语法来模拟 jQuery
的一个小片段:
- 定义一个基础类,用于封装
DOM
操作:
class DOMManipulator {
constructor(selector) {
this.elements = document.querySelectorAll(selector);
}
// 私有方法,用于遍历元素并应用操作
_applyToElements(callback) {
this.elements.forEach((element) => callback(element));
}
// 公共方法,添加CSS类
addClass(className) {
this._applyToElements((element) => {
if (element.classList) {
element.classList.add(className);
} else {
element.className += " " + className;
}
});
return this; // 支持链式调用
}
// ... 其他DOM操作方法(如css, removeClass等)可以类似地实现
}
请注意
虽然 JavaScript 没有正式的私有方法或属性(直到 ES2019 的私有类字段提案),但我们可以通过约定(如下划线前缀)和闭包来模拟私有成员。在上面的例子中,_applyToElements
方法被视为私有方法,因为它不应该被类的外部直接调用。
- 定义一个继承自
DOMManipulator
的类,用于更具体的操作:
class DemoJQuery extends DOMManipulator {
// 可以添加一些特定的方法或重写父类的方法
// 例如,重写addClass方法以添加日志记录功能
addClass(className) {
console.log(
`Adding class '${className}' to ${this.elements.length} elements.`
);
super.addClass(className); // 调用父类的方法
return this; // 支持链式调用
}
// 添加一个特定于DemoJQuery的方法,比如绑定事件
on(event, handler) {
this._applyToElements((element) => {
element.addEventListener(event, handler);
});
return this; // 支持链式调用
}
// ... 可以添加更多特定于DemoJQuery的方法
}
- 最后挂载一个
$
到window
:
window.$ = function (selector) {
// 工厂模式
return new DemoJQuery(selector);
};
- 使用
demo
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>继承继承继承继承继承继承继承继承继承</p>
<p>封装封装封装封装封装封装封装封装封装</p>
<p>多态多态多态多态多态多态多态多态多态</p>
</body>
<script src="./demo-jquery.js"></script>
<script>
$('p').addClass('active') // 这将调用重写后的addClass方法,并打印日志
.on('click', () => {
alert('Element clicked!');
})
</script>
</html>
// region DOMManipulator
class DOMManipulator {
constructor(selector) {
this.elements = document.querySelectorAll(selector);
}
// 私有方法,用于遍历元素并应用操作
_applyToElements(callback) {
this.elements.forEach((element) => callback(element));
}
// 公共方法,添加CSS类
addClass(className) {
this._applyToElements((element) => {
if (element.classList) {
element.classList.add(className);
} else {
element.className += " " + className;
}
});
return this; // 支持链式调用
}
// ... 其他DOM操作方法(如css, removeClass等)可以类似地实现
}
// endregion DOMManipulator
// region DemoJQuery
class DemoJQuery extends DOMManipulator {
// 可以添加一些特定的方法或重写父类的方法
// 例如,重写addClass方法以添加日志记录功能
addClass(className) {
console.log(
`Adding class '${className}' to ${this.elements.length} elements.`
);
super.addClass(className); // 调用父类的方法
return this; // 支持链式调用
}
// 添加一个特定于DemoJQuery的方法,比如绑定事件
on(event, handler) {
this._applyToElements((element) => {
element.addEventListener(event, handler);
});
return this; // 支持链式调用
}
// ... 可以添加更多特定于DemoJQuery的方法
}
// endregion DemoJQuery
// region window
window.$ = function (selector) {
// 工厂模式
return new DemoJQuery(selector);
};
// endregion window
在这个例子中:
封装:
DOMManipulator
类封装了DOM
操作的基本逻辑,而DemoJQuery
类则进一步封装了特定的功能。_applyToElements
方法是一个私有方法(通过约定使用下划线前缀来表示),它封装了遍历元素并应用操作的逻辑。
继承:
DemoJQuery
类继承DOMManipulator
类,从而能够重用其DOM
操作的基本逻辑。- 同时,
DemoJQuery
类还可以添加或重写方法以提供额外的功能。
多态:
- 在这个例子中,多态性可能不那么明显,因为我们没有创建多个子类来展示不同的行为。
- 然而,
DemoJQuery
类本身可以视为一个多态的实体,它提供了与DOMManipulator
类相似的接口(方法集),但具有额外的功能(如事件绑定和日志记录)。
体现面向对象思想的关键在于封装、继承和多态这三个基本原则。
总结
我们所有的教程文档都在说面向对象可以表达万物,比如上面我们演示的 Person
、Animal
、xiaoMing
、xiaoHong
这些,都是和我们日常生活对应的。这只是为了让我们好学习而已。面向对象的意义,根本不在于此。(所以不要纠结那些 吃、睡、猫叫、狗叫!!!)
我们日常使用的编程语言,所有程序的执行都是通过: 顺序、判断、循环 这三种方式来搞定。我们限定了一种规范,这三种可以满足所有的需求。对程序来说这就叫结构化了。
面向对象的意义是将零散的数据进行结构化。结构化的东西才是最简单的。编程也应该是简单和抽象的。
所以面向对象就是:数据结构化。