Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
01-【JavaScript-Day 1】从零开始:全面了解 JavaScript 是什么、为什么学以及它与 Java 的区别
02-【JavaScript-Day 2】开启 JS 之旅:从浏览器控制台到 <script> 标签的 Hello World 实践
03-【JavaScript-Day 3】掌握JS语法规则:语句、分号、注释与大小写敏感详解
04-【JavaScript-Day 4】var 完全指南:掌握变量声明、作用域及提升
05-【JavaScript-Day 5】告别 var 陷阱:深入理解 let 和 const 的妙用
06-【JavaScript-Day 6】从零到精通:JavaScript 原始类型 String, Number, Boolean, Null, Undefined, Symbol, BigInt 详解
07-【JavaScript-Day 7】全面解析 Number 与 String:JS 数据核心操作指南
08-【JavaScript-Day 8】告别混淆:一文彻底搞懂 JavaScript 的 Boolean、null 和 undefined
09-【JavaScript-Day 9】从基础到进阶:掌握 JavaScript 核心运算符之算术与赋值篇
10-【JavaScript-Day 10】掌握代码决策核心:详解比较、逻辑与三元运算符
11-【JavaScript-Day 11】避坑指南!深入理解JavaScript隐式和显式类型转换
12-【JavaScript-Day 12】掌握程序流程:深入解析 if…else 条件语句
13-【JavaScript-Day 13】告别冗长if-else:精通switch语句,让代码清爽高效!
14-【JavaScript-Day 14】玩转 for 循环:从基础语法到遍历数组实战
15-【JavaScript-Day 15】深入解析 while 与 do…while 循环:满足条件的重复执行
16-【JavaScript-Day 16】函数探秘:代码复用的基石——声明、表达式与调用详解
17-【JavaScript-Day 17】函数的核心出口:深入解析 return 语句的奥秘
18-【JavaScript-Day 18】揭秘变量的“隐形边界”:深入理解全局与函数作用域
19-【JavaScript-Day 19】深入理解 JavaScript 作用域:块级、词法及 Hoisting 机制
20-【JavaScript-Day 20】揭秘函数的“记忆”:深入浅出理解闭包(Closure)
21-【JavaScript-Day 21】闭包实战:从模块化到内存管理,高级技巧全解析
22-【JavaScript-Day 22】告别 function 关键字?ES6 箭头函数 (=>) 深度解析
23-【JavaScript-Day 23】告别繁琐的参数处理:玩转 ES6 默认参数与剩余参数
24-【JavaScript-Day 24】从零到一,精通 JavaScript 对象:创建、访问与操作
25-【JavaScript-Day 25】深入探索:使用 for...in 循环遍历 JavaScript 对象属性
26-【JavaScript-Day 26】零基础掌握JavaScript数组:轻松理解创建、索引、长度和多维结构
27-【JavaScript-Day 27】玩转数组:push, pop, slice, splice 等方法详解与实战
28-【JavaScript-Day 28】告别繁琐循环:forEach, map, filter 数组遍历三剑客详解
29-【JavaScript-Day 29】数组迭代进阶:掌握 reduce、find、some 等高阶遍历方法
30-【JavaScript-Day 30】ES6新特性:Set与Map,让你的数据管理更高效!
31-【JavaScript-Day 31】对象的“蓝图”详解:构造函数、new 与 instanceof 完全指南
32-【JavaScript-Day 32】深入理解 prototype、__proto__ 与原型链的奥秘
33-【JavaScript-Day 33】深入浅出 ES6 Class:从入门到精通面向对象新姿势
文章目录
前言
在 JavaScript 的世界里,面向对象编程(OOP)一直是一个核心概念。在 ES6(ECMAScript 2015)出现之前,我们主要依赖构造函数和原型链来实现对象的创建和继承,这种方式虽然强大,但语法上对于习惯了类(Class)语法的开发者来说略显繁琐和不直观。ES6 class 关键字的引入,为 JavaScript 提供了一种更简洁、更清晰、更易于理解的面向对象编程方式。它本质上是基于原型继承的语法糖,但极大地提升了代码的可读性和易维护性。本文将带你深入探索 ES6 Class 的方方面面,从基础定义到高级特性,助你掌握现代 JavaScript 面向对象编程的利器。
一、ES6 Class 基础
在正式学习 ES6 Class 之前,我们先来理解一下它的基本概念和作用。
1.1 什么是 ES6 Class?
简单来说,ES6 class 提供了一种创建对象的蓝图。它封装了数据(属性)和操作数据的方法,使得我们可以更方便地创建和管理具有相似结构和行为的对象。
1.1.1 告别构造函数与原型模式的繁琐
回想一下 ES5 中创建自定义类型的方式:
// ES5 构造函数
function PersonES5(name, age) {
this.name = name;
this.age = age;
}
PersonES5.prototype.sayHello = function() {
console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
};
var person1ES5 = new PersonES5("老王", 30);
person1ES5.sayHello();
虽然功能完善,但 prototype 的写法对于初学者或者有其他面向对象语言背景的开发者来说,可能不够直观。ES6 class 的出现正是为了简化这一过程。
1.1.2 类:对象的蓝图
class 就像一个模板或者设计图,它描述了所创建的对象共同的属性和方法。通过这个模板,我们可以创建出具体的对象实例。
1.2 定义一个简单的类
使用 ES6 class 定义一个类非常直观。
1.2.1 class 关键字
我们使用 class 关键字来声明一个类。类名通常采用首字母大写的驼峰命名法(PascalCase)。
class Person {
// 类的代码写在这里
}
1.2.2 constructor 方法:对象的初始化器
constructor 方法是一个特殊的方法,用于创建和初始化由 class 创建的对象。每个类只能有一个 constructor 方法。
(1) constructor 的作用与特点
- 作用:当通过
new关键字创建类的实例时,constructor方法会自动被调用。它的主要任务是初始化实例的属性。 - 特点:
- 方法名固定为
constructor。 - 如果一个类没有显式定义
constructor方法,JavaScript 会自动为其添加一个空的constructor方法:constructor() {}。 - 在
constructor内部,this关键字指向新创建的实例对象。
- 方法名固定为
(2) new 关键字与 constructor
当我们使用 new 操作符作用于一个类时,会发生以下事情:
- 创建一个新的空对象。
- 将这个新对象的原型 (
__proto__) 指向类的prototype对象。 - 将这个新对象作为
this的上下文调用constructor方法,并传入参数。 - 如果
constructor方法没有显式返回一个对象,则返回这个新创建的this对象。
class Person {
constructor(name, age) {
// this 指向新创建的实例
this.name = name;
this.age = age;
console.log('Person constructor called!');
}
}
1.3 创建和使用类的实例
定义好类之后,我们就可以使用 new 关键字来创建它的实例(对象)。
1.3.1 实例化对象
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`你好,我叫 ${this.name},今年 ${this.age} 岁。`); // 使用模板字符串更简洁
}
}
// 创建 Person 类的实例
const person1 = new Person('张三', 28);
const person2 = new Person('李四', 32);
1.3.2 访问属性和方法
创建实例后,我们可以通过点(.)操作符来访问实例的属性和调用实例的方法。
console.log(person1.name); // 输出: 张三
person2.sayHello(); // 输出: 你好,我叫 李四,今年 32 岁。
二、深入理解 Class 的特性
除了基本的定义和实例化,ES6 Class 还提供了许多强大的特性,如实例方法、静态方法、Getter 和 Setter。
2.1 实例方法
实例方法是定义在类的原型(prototype)上的方法,只有类的实例才能调用它们。
2.1.1 定义与调用
在 class 的主体中直接定义的方法,默认就是实例方法。
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
// 实例方法
getArea() {
return this.width * this.height;
}
// 另一个实例方法
printDescription() {
console.log(`这是一个宽为 ${this.width},高为 ${this.height} 的矩形。`);
}
}
const rect = new Rectangle(10, 5);
console.log(rect.getArea()); // 输出: 50
rect.printDescription(); // 输出: 这是一个宽为 10,高为 5 的矩形。
2.1.2 实例方法中的 this
在实例方法内部,this 关键字通常指向调用该方法的实例对象。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++; // this 指向 Counter 的实例
console.log(this.count);
}
}
const myCounter = new Counter();
myCounter.increment(); // 输出: 1, this.count 是 myCounter.count
myCounter.increment(); // 输出: 2
注意:如果实例方法被用作回调函数或者赋值给其他变量后调用,this 的指向可能会改变,这与 JavaScript 中 this 的一般规则一致。
2.2 静态方法 (static)
静态方法是直接定义在类本身上的方法,而不是在类的实例上。它们通常用于实现与类相关但不依赖于特定实例的功能,例如工具函数或工厂方法。
2.2.1 什么是静态方法?
静态方法通过 static 关键字定义。它们不能被类的实例调用,只能通过类本身来调用。
2.2.2 如何定义和调用静态方法
class MathHelper {
static PI = 3.1415926535; // 静态属性 (ES2022+ 或需要Babel转译)
static add(x, y) {
return x + y;
}
static subtract(x, y) {
return x - y;
}
}
console.log(MathHelper.PI); // 输出: 3.1415926535
console.log(MathHelper.add(5, 3)); // 输出: 8
// const helperInstance = new MathHelper();
// console.log(helperInstance.add(1, 1)); // 错误!helperInstance.add is not a function
在上面的例子中,PI 是一个静态属性,add 和 subtract 是静态方法。它们都直接通过 MathHelper 类名来访问。
2.2.3 静态方法与实例方法的区别及应用场景
- 调用方式:静态方法通过类名调用,实例方法通过实例调用。
this指向:在静态方法中,this指向类本身,而不是实例。在实例方法中,this指向调用该方法的实例。- 访问权限:静态方法不能直接访问实例的属性(因为它们不依赖于任何特定实例),但可以访问类的静态属性。实例方法可以访问实例的属性和类的静态属性。
- 应用场景:
- 静态方法:创建工具函数(如
Math.random())、配置信息、实例工厂等。 - 实例方法:操作或表示特定实例状态的方法。
- 静态方法:创建工具函数(如
2.3 Getter 和 Setter
Getter 和 Setter 允许我们定义在获取或设置属性值时执行的特定操作。它们看起来像普通的属性访问,但实际上会调用相应的函数。
2.3.1 使用 get 定义 Getter
Getter 方法用于获取属性值。它使用 get 关键字定义,后面跟着一个函数名,该函数不接受参数,并应返回计算后的属性值。
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this._age = 0; // 通常用下划线表示这是一个内部属性
}
// Getter
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
get age() {
return this._age;
}
}
const user = new User('诸葛', '亮');
console.log(user.fullName); // 输出: 诸葛 亮 (像访问属性一样,但实际调用了 get fullName() 方法)
2.3.2 使用 set 定义 Setter
Setter 方法用于设置属性值。它使用 set 关键字定义,后面跟着一个函数名,该函数接受一个参数(即要设置的值)。
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this._age = 0;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// Setter for age
set age(value) {
if (typeof value === 'number' && value > 0 && value < 150) {
this._age = value;
} else {
console.error('无效的年龄值!');
}
}
get age() {
return this._age;
}
}
const user = new User('司马', '懿');
user.age = 35; // 调用 set age(value) 方法
console.log(user.age); // 输出: 35 (调用 get age() 方法)
user.age = 200; // 输出: 无效的年龄值!
console.log(user.age); // 输出: 35 (因为上次设置失败,age 仍为 35)
2.3.3 Getter 和 Setter 的应用场景(数据校验、计算属性)
- 数据校验:如上述
age的 Setter,可以在设置属性值之前进行验证。 - 计算属性:如
fullNameGetter,它的值是根据其他属性动态计算出来的。 - 副作用处理:当属性值改变时,可能需要执行一些额外的操作,例如更新 UI 或记录日志。
三、Class 的继承 (extends 与 super)
继承是面向对象编程的三大特性之一(封装、继承、多态),它允许我们创建一个类(子类),从另一个类(父类)继承属性和方法。ES6 class 通过 extends 和 super 关键字简化了继承的实现。
3.1 使用 extends 实现继承
extends 关键字用于创建一个类作为另一个类的子类。
3.1.1 基本语法
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出声音。`);
}
}
// Dog 类继承自 Animal 类
class Dog extends Animal {
constructor(name, breed) {
// ...
this.breed = breed;
}
wagTail() {
console.log(`${this.name} 摇尾巴。`);
}
}
const myDog = new Dog('旺财', '中华田园犬');
console.log(myDog.name); // 输出: 旺财 (继承自 Animal)
myDog.speak(); // 输出: 旺财 发出声音。 (继承自 Animal)
myDog.wagTail(); // 输出: 旺财 摇尾巴。 (Dog 自己的方法)
3.1.2 子类与父类
- 父类 (Superclass/Base Class):被继承的类,如
Animal。 - 子类 (Subclass/Derived Class):继承父类的类,如
Dog。子类会自动拥有父类的所有非私有属性和方法。
3.2 super 关键字
super 关键字用于访问和调用一个对象的父对象上的函数,通常用在子类的 constructor 和方法中。
3.2.1 在 constructor 中调用父类构造函数 (super())
当子类继承父类时,子类的 constructor 必须调用 super() 方法,以便执行父类的构造函数,并初始化父类中定义的属性。
(1)为何以及何时调用 super()
- 为何:子类实例也是父类实例,因此父类的初始化逻辑也需要被执行。
super()就是用来调用父类的constructor。 - 何时:在子类的
constructor中,必须在使用this关键字之前调用super()。这是因为this的初始化是由父类的构造函数完成的。如果不调用super()或者在super()之前使用this,JavaScript 会抛出错误。
(2)super() 的注意事项
super()只能在子类的constructor方法中使用。- 它必须在
this关键字被访问之前调用。 - 如果父类构造函数需要参数,
super()也需要传递相应的参数。
class Animal {
constructor(name) {
this.name = name;
console.log('Animal constructor called');
}
// ...
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类的 constructor,并传递 name 参数
// 必须在 this.breed 之前
this.breed = breed;
console.log('Dog constructor called');
}
// ...
}
const myDog = new Dog('小黑', '拉布拉多');
// 输出:
// Animal constructor called
// Dog constructor called
3.2.2 调用父类的普通方法 (super.methodName())
在子类的方法中,可以使用 super.methodName() 来调用父类中同名的方法。这在方法重写时非常有用。
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} 发出声音。`);
}
}
class Cat extends Animal {
constructor(name, color) {
super(name);
this.color = color;
}
speak() {
super.speak(); // 调用父类的 speak 方法
console.log(`${this.name} 喵喵叫,它是 ${this.color} 的。`);
}
}
const myCat = new Cat('咪咪', '橘色');
myCat.speak();
// 输出:
// 咪咪 发出声音。
// 咪咪 喵喵叫,它是 橘色 的。
3.3 方法重写 (Overriding)
子类可以定义一个与父类同名的方法,从而覆盖(重写)父类的该方法。
3.3.1 子类覆盖父类方法
如上例中 Cat 类的 speak 方法就重写了 Animal 类的 speak 方法。当调用 myCat.speak() 时,执行的是 Cat 类中定义的版本。
3.3.2 通过 super 调用被覆盖的父类方法
即使子类重写了父类的方法,我们仍然可以通过 super.methodName() 在子类方法内部调用父类被覆盖的方法。这允许我们在扩展父类功能的同时,保留其原有行为。
四、ES6 Class 与传统构造函数的区别与联系
虽然 ES6 class 的语法看起来很像传统的面向对象语言中的类,但理解其与 JavaScript 原有原型继承机制的关系非常重要。
4.1 语法糖的本质
ES6 class 本质上是 JavaScript 现有原型继承机制的语法糖 (Syntactic Sugar)。这意味着它并没有引入新的对象继承模型,而是提供了一种更简洁、更符合传统面向对象思维的语法来操作原型。
4.1.1 底层仍然是原型继承
我们定义的类、类的方法,最终都会转换成基于原型的实现。
class MyClass {
constructor() {}
myMethod() {}
}
// 上述代码在概念上(非完全等价)类似于:
function MyClassES5() {}
MyClassES5.prototype.myMethod = function() {};
console.log(typeof MyClass); // 输出 "function"
console.log(MyClass === MyClass.prototype.constructor); // true
这表明 MyClass 本身是一个函数,它的 prototype 属性的 constructor 指回 MyClass 自身。
4.1.2 typeof MyClass 的结果
如上所示,对一个类使用 typeof 操作符,结果是 "function"。这进一步证明了类在 JavaScript 底层仍然是基于函数的。
4.2 Class 的一些不同之处
尽管是语法糖,但 class 语法与传统的构造函数写法还是有一些关键区别:
4.2.1 类声明不会被提升
与函数声明不同,类声明不会被提升 (Hoisting)。这意味着你必须先声明类,然后才能使用它,否则会引发 ReferenceError。
// new MyGadget(); // ReferenceError: Cannot access 'MyGadget' before initialization
class MyGadget {
constructor(name) {
this.name = name;
}
}
const gadget = new MyGadget('Phone'); // 正确
4.2.2 类内部默认启用严格模式 ('use strict')
类声明和类表达式的主体都默认在严格模式下执行。这意味着你不需要在类定义的顶部显式写 'use strict';。
4.2.3 类的所有方法都是不可枚举的(除了 constructor)
在 ES6 class 中定义的方法(包括实例方法、静态方法、getter 和 setter),默认情况下其 enumerable 属性为 false。这与 ES5 中通过 prototype 添加的方法(默认为可枚举)不同。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() { /* ... */ }
}
console.log(Object.keys(Point.prototype)); // 输出: []
console.log(Object.getOwnPropertyNames(Point.prototype)); // 输出: ["constructor", "toString"]
function PointES5(x, y) {
this.x = x;
this.y = y;
}
PointES5.prototype.toString = function() { /* ... */ };
console.log(Object.keys(PointES5.prototype)); // 输出: ["toString"]
可以看到,Point.prototype.toString 方法没有出现在 Object.keys 的结果中,因为它不可枚举。
五、实际应用场景与最佳实践
理解了 ES6 Class 的核心概念后,我们来看看它在实际开发中的应用和一些需要注意的地方。
5.1 什么时候使用 Class?
5.1.1 构建具有明确结构和行为的对象
当你需要创建一组具有相似属性(状态)和方法(行为)的对象时,使用 class 是一个非常好的选择。例如:
- UI 组件(按钮、表单、对话框等)
- 数据模型(用户、产品、订单等)
- 游戏中的角色、物品等
- 各种工具类或服务类
5.1.2 在框架和库中的应用 (如 React 组件)
许多现代 JavaScript 框架和库广泛使用 class 语法。例如,在 React 中,类组件就是通过继承 React.Component 来创建的:
// 这是一个非常简化的 React 类组件示例
// import React from 'react'; // 实际使用时需要引入
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => { // 使用箭头函数绑定 this
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
5.2 常见问题与排查建议
5.2.1 this 指向问题 (尤其在事件处理或回调中)
在类的方法中,this 的指向是一个常见的问题来源,特别是在将方法作为事件处理器或回调函数传递时。
class Button {
constructor(label) {
this.label = label;
// this.onClick = this.onClick.bind(this); // 解决方案1: 在构造函数中绑定 this
}
onClick() {
console.log(`${this.label} button clicked! this is:`, this);
}
// 解决方案2: 使用箭头函数定义方法 (推荐)
// onClick = () => {
// console.log(`${this.label} button clicked! this is:`, this);
// }
simulateClick() {
// 模拟将 onClick 方法传递给事件监听器
const triggerEvent = this.onClick;
triggerEvent(); // 此时 triggerEvent 内部的 this 将是 undefined (严格模式) 或全局对象 (非严格模式)
}
}
const myButton = new Button('Submit');
myButton.simulateClick(); // 输出: Submit button clicked! this is: undefined (在严格模式下)
// 如果使用解决方案1或2,则 this 会正确指向 Button 实例
解决方案:
- 在构造函数中使用
bind:this.methodName = this.methodName.bind(this); - 使用箭头函数定义方法:箭头函数会捕获其定义时所在上下文的
this值。这是目前比较推荐的做法。 - 在调用时使用箭头函数包裹:例如在事件监听中
element.addEventListener('click', () => this.methodName());
5.2.2 super() 必须在 this 之前调用
在子类的 constructor 中,如果你需要访问 this 或者从构造函数返回任何东西,都必须先调用 super()。
class Parent {
constructor() {
this.parentProp = "Hello from Parent";
}
}
class Child extends Parent {
constructor() {
// this.childProp = "Hello from Child"; // 错误!Cannot access 'this' before 'super()' call
super();
this.childProp = "Hello from Child"; // 正确
console.log(this.parentProp);
}
}
new Child();
5.2.3 忘记使用 new 关键字
类必须通过 new 关键字来实例化。如果直接像调用普通函数那样调用类构造函数,会抛出 TypeError。
class MyData {
constructor() {
this.value = 42;
}
}
// const data = MyData(); // TypeError: Class constructor MyData cannot be invoked without 'new'
const data = new MyData(); // 正确
console.log(data.value);
六、总结
ES6 class 为 JavaScript 的面向对象编程带来了革命性的便利。通过本文的学习,我们掌握了:
- 基本概念:
class关键字用于定义类,constructor方法用于初始化实例。 - 核心特性:
- 实例方法:定义在类原型上,由实例调用,
this指向实例。 - 静态方法 (
static):定义在类本身上,由类名调用,this指向类。常用于工具函数。 - Getter (
get) 和 Setter (set):允许在读写属性时执行自定义逻辑,实现计算属性或数据校验。
- 实例方法:定义在类原型上,由实例调用,
- 继承机制:
extends:用于实现类之间的继承关系。super():在子类构造函数中调用父类构造函数,必须在this之前。super.methodName():在子类中调用父类的同名方法。
- 与传统方式的联系与区别:
- ES6
class是原型继承的语法糖,底层机制不变。 - 类声明不提升,内部默认严格模式,类方法不可枚举。
- ES6
- 实际应用与常见问题:
- 适用于构建结构化对象和在现代框架中使用。
- 需注意
this指向问题,并正确使用super()和new。
掌握 ES6 class 是现代 JavaScript 开发者的必备技能。它不仅让代码更易读、更易维护,也让我们能够更好地运用面向对象的思想来构建复杂应用。希望本文能为你打下坚实的基础,在未来的 JavaScript 编程之路上更加得心应手!
428



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



