在这篇博文中,我将带您了解一个使用 Javascript 中的类概念的真实示例。

我认为使用实际用例会很有帮助,因为当你能够将概念与现实生活联系起来时,理解概念会简单得多。

因此在本指南中,您将了解 JavaScript 中的类、继承、抽象函数、如何使用和等关键字 super extend 静态关键字以及类的私有成员。

让我们开始吧。

目录

  • 先决条件
  • JavaScript 中的类是什么?
  • 用例描述
  • 椅子管理系统中的抽象函数和继承
  • JavaScript 中的静态关键字
  • JavaScript 中的私有成员

先决条件

在开始阅读这篇博文之前,您应该对以下主题有基本的了解:

  • 类图:我们将使用它们来展示我们的例子
  • 上下文图和容器图
  • 了解 OOP
  • 原型继承和原型链简介
  • JS 中的构造函数介绍

JavaScript 中的类是什么?

引入了类, EcmaScript 2015 以提供一种更清晰的方式来遵循面向对象的编程模式。

JavaScript 仍然遵循基于原型的继承模型。JavaScript 中的类是基于原型的继承模型的语法糖,我们用它来实现 OOP 概念。

因此,在 JS 中引入类使得开发人员能够更轻松地围绕 OOP 概念构建软件。它还带来了与 C++ 和 Java 等不同基于 OOP 的编程语言的相似性。

在上课之前,我们使用构造函数在 JavaScript 中执行 OOP。请看下面的例子:

function Pen(name, color, price) {
    this.name = name;
    this.color = color;
    this.price = price;
}

const pen1 = new Pen("Marker", "Blue", "$3");
console.log(pen1);
Pen Constructor function

上面的代码显示了一个 Pen 具有 name、color 和 price 属性的构造函数。我们使用关键字 new Pen 构造函数来创建一个对象 pen1 .

现在假设我们想向 Pen 构造函数添加一个新函数。为此,我们需要将该函数添加到的prototype属性中 Pen 。看看 showPrice 下面的函数:

function Pen(name, color, price) {
    this.name = name;
    this.color = color;
    this.price = price;
}

const pen1 = new Pen("Marker", "Blue", "$3");

Pen.prototype.showPrice = function(){
    console.log(`Price of ${this.name} is ${this.price}`);
}

pen1.showPrice();
Adding function in a constructor

如果你不明白这些概念,我建议你通过先决条件部分中提到的文章来复习一下你的 JS/背景知识。特别是,查看有关原型和构造函数的文章。

看看上面的代码,我们可以说我们已经完成了我们想做的事情——即向 showPrice 构造函数添加一个函数 Pen 。但你可以看到,与我们在 C++ 或 Java 中实现的 OOP 概念相比,它的可读性并不高。

我们可以借助关键字重新创建上述示例 class 。看看下面的代码:

class Pen {
    constructor(name, color, price){
        this.name = name;
        this.color = color; 
        this.price = price;
    }
    
    showPrice(){
        console.log(`Price of ${this.name} is ${this.price}`);
    }
}

const pen1 = new Pen("Marker", "Blue", "$3");
pen1.showPrice();
Using Class keyword in JS

注意到了差异!我们实现了相同的结果,但语法更简洁。 showPrice 与直接在构造函数的原型中添加函数相比,添加新的成员函数要容易得多。

让我们使用一个示例用例更深入地了解 JS 中的类。通过这个用例,我们将了解这些概念如何有助于解决一些实际问题。

用例描述

简单说明一下 并不完全遵循上述图表的惯例。我已对这些图表进行了近似处理,以帮助您理解一般概念。

在开始之前,如果您需要复习一下,我建议您阅读 c4models、容器图和上下文图。您可以在先决条件部分找到它们。

我们将解决以下问题:帮助店主对库存中的椅子进行分类并将其显示在屏幕上。

用例很简单,而且非常容易理解。请看下面的图表,它展示了整个提议的系统:

js_classes_tut_context.drawio--1-
Context Diagram for the Chair Management System

从上图可以看出,它有 3 个主要组成部分:

  1. 人: 店主将与我们的系统进行互动。
  2. 软件系统:库存界面门户 - 这是一个允许店主查看或修改库存中的椅子信息的界面。
  3. 软件系统:椅子管理系统 - 该系统将允许界面获取或修改店主请求的所需详细信息。

现在我们了解了用例,让我们从本篇博文中要关注的目标系统开始。它是 椅子管理系统。

我们首先在椅子管理系统中创建一些主要组件。我们在此系统中的组件只是不同的类,它们将有助于满足店主的不同需求。

chairModel.drawio--2--1
Chair component of Chair Management System

让我们添加一个名为 Chair 。由于它是一个类,它将具有自己的属性 (properties) 和行为 (methods)。

看一下上面的图表。我们可以看到:

  • 第二行包含椅子类的属性,例如颜色、座椅高度、倾斜角度等等。
  • 第三行对应的是告诉我们椅子可以执行哪些功能的方法,例如adjustSeatHeight、adjustAngle、moveChair等等。

对于本文中创建的所有组件,我们都会遵循上述表示。

Chair 组件将成为我们的基础组件。这意味着所有其他类型的椅子(如办公椅、餐椅等)都将归属于此类/组件。

让我们首先在 JS 中创建基础椅子类。请看以下代码:

class Chair {
    constructor(color, seatHeight, recliningAngle, backSupport, headSupport, padding, armRests, seatSize, isHeightAdjustable, isMovable){
        this.color = color;
        this.seatHeight = seatHeight;
        this.recliningAngle = recliningAngle;
        this.backSupport = backSupport;
        this.headSupport = headSupport;
        this.padding = padding;
        this.armRests = armRests;
        this.seatSize = seatSize;
        this.isHeightAdjustable = isHeightAdjustable;
        this.isMovable = isMovable;
    }
    
    adjustableHeight() {};
    adjustAngle(){};
    moveChair(){};    
}

const newChair = new Chair("Blue","25 inch","20 deg",true,false,"3 inch",true,"16 inch",false,false);

console.dir("Chair Prototype", Chair);
console.log("Chair Object", newChair);
Base Class Chair

chair 类有以下成员:

  • 属性 :这些将定义椅子的属性,例如颜色、座椅高度、背部支撑等。
  • 函数 :这些函数定义了椅子的行为。例如,如果椅子设置 isHeightAdjustable 为 true,那么它就可以使用函数 adjustableHeight 。您可以看到所有函数都在 Chair 类中声明。这些是抽象函数。我们将在本文后面详细讨论这些函数。

在代码的底部,我们有两个控制台日志语句。第一个将打印出类的定义 Chair 。第二个对象将打印 newChair 实例。

Screenshot-from-2021-12-11-11-58-14
First console.dir output

如果你看一下第一个输出,它会打印出该类 Chair 。让我们看看它的内容:

  • 它由一个属性组成 prototype 。这是 Chair 类的所有实例都具有的原型。
  • 属性 name 是对象的名称。
  • 最后,我们有 __proto__ or [[Prototype]] 属性。这是该类的实际原型 Chair .
{
    "color": "Blue",
    "seatHeight": "25 inch",
    "recliningAngle": "20 deg",
    "backSupport": true,
    "headSupport": false,
    "padding": "3 inch",
    "armRests": true,
    "seatSize": "16 inch",
    "isHeightAdjustable": false,
    "isMovable": false,
    [[Prototype]]: {
        adjustAngle: ƒ adjustAngle()
        adjustableHeight: ƒ adjustableHeight()
        constructor: class Chair
        moveChair: ƒ moveChair()
        [[Prototype]]: Object
    }
}
Second console log output

第二条日志语句打印出 chair 对象实例的信息。它将由 Chair 类的所有属性组成。如果仔细观察,您会发现此实例的原型与 chair prototype 类的属性的原型相似。这是由于原型继承而发生的。

椅子管理系统 中添加新组件/类来使用这个概念

椅子管理系统中的抽象函数和继承

抽象函数只是类中的一个函数签名,没有任何实现。它帮助我们概括代码,以便子类可以使用它们并向其中添加自己的实现。

椅子管理系统 中添加一个组件

我修改了椅子类,现在它由默认值组成。所有实例都将使用这些默认值。稍后子类可以修改它。我们很快就会看到如何实现这一点。看看 Chair 下面的新类:

class Chair {
    constructor(color, seatHeight, recliningAngle, backSupport, headSupport, padding, armRests, seatSize, isHeightAdjustable, isMovable){
        //Defaults which can be changed by the subclass class.
        this.color = color;
        this.seatHeight = seatHeight;
        this.recliningAngle = recliningAngle;
        this.backSupport = true;
        this.headSupport = false;
        this.padding = "3 inch";
        this.armRests = true;
        this.seatSize = "16 inch";
        this.isHeightAdjustable = false;
        this.isMovable = false;
        this.type = "Chair";
    }
    
    adjustableHeight() {};
    adjustAngle(){};
    moveChair(){};    
}

const newChair = new Chair();

newChair;
Chair class with defaults

现在让我们添加一个名为 OfficeChair 。这将继承该类的属性和方法 Chair 。新修改的类图将如下所示:

chairModel.drawio--1---1-
Class diagram

请注意,新类 OfficeChair 仅包含方法而不包含属性。我们在此假设所有属性都将从该类继承 Chair

对于 OfficeChair 类,我们已经实现了类中存在的抽象方法 Chair

看一下下面的类的代码 OfficeChair

class OfficeChair extends Chair{
    constructor(color, isHeightAdjustable, seatHeight, recliningAngle){
        super();
        this.type = "Office Chair";
        this.color = color;
        this.isHeightAdjustable = isHeightAdjustable;
        this.seatHeight = seatHeight;
        this.recliningAngle = recliningAngle;
        this.isMovable = true;
    }
    
    adjustableHeight(height){
        if(height > this.seatHeight){
            console.log(`Chair height changed to ${height}`);        
        } else {
            console.log(`Height cannot be decreased more than the seat height ${this.seatHeight}`);
        }
    }
    
    adjustAngle(angle){
        if(angle >= this.recliningAngle){
            console.log(`Chair angle changed to ${angle}`);        
        } else {
            console.log(`Angle cannot be decreased more than the min reclining angle ${this.recliningAngle}`);
        }
    }
    
    moveChair(x,y){
        console.log(`Chair moved to co-ordinates = (${x}, ${y})`);
    }
}

const newOfficeChair = new OfficeChair("Red", true, 30, 30);

console.log(newOfficeChair.adjustableHeight(31));
console.log(newOfficeChair.adjustAngle(40));
console.log(newOfficeChair.moveChair(10,20));
OfficeChair class implementation

这是一个从超类继承功能和属性的类 chair 。它使用 extends 关键字允许 OfficeChair 类执行继承。

extends 关键字的语法如下:

class ChildClass extends ParentClass{...}

接下来,我们有一个构造函数和一些超类函数的实现。请注意,我们 super 在构造函数中使用了关键字。

我们使用 super 关键字来调用父类的构造函数。我们也可以使用它来调用父类的函数和属性。

使用 super 关键字时请注意:

  • 确保 super 在构造函数开始时调用该函数。如果不这样做,并且在子类构造函数中使用之前尝试访问父类的属性 super ,则会引发错误。
  • 一旦 super 调用该函数,就可以访问父类的所有属性和功能。
  • Super 不仅与类相关——您还可以使用它来调用对象父级上的函数。

在 MDN super 文档 docs .

最后,如果你注意到,我们添加了抽象函数的实现。这些函数如下:

  • adjustableHeight :此函数将检查输入的高度是否大于椅子的最小高度。如果是,我们可以更改高度,否则显示错误消息。人们还可以增加或减少椅子的高度。请注意,这 this.seatHeight 是椅子距离地面的最小高度,人不能将高度降低到低于此高度。
  • adjustAngle :该函数会检查输入的角度是否大于默认值 this.recliningAngle ,若输入的角度大于默认值,则角度会改变,否则会显示错误信息。
  • moveChair :任何椅子,只要其 isMovable 属性为真,则相应的类将具有该函数的实现 moveChair 。它只是根据输入的 x 和 y 坐标帮助移动椅子。

请注意,我们还重新初始化了类的某些属性, Chair 例如 type 。我们将明确定义 type 每个子类的属性。这将帮助我们通过将这些类分配给每个椅子来对库存中的椅子进行分类。

现在您应该了解抽象函数是什么以及它们有多有用。抽象函数的一些优点如下:

  • 减少代码库中的冗余。
  • 提供一种适当的概括类别的方法。
  • 允许子类灵活地实现其所需的任何抽象函数。

JavaScript 中的静态关键字

关键字 static 可帮助您定义类中的函数和属性,这些函数和属性不能由对象的实例调用。它们只能由由这些静态函数和属性组成的类本身调用。

通常,我们使用 static 类中的方法来实现实用目的,例如打印出类的所有属性、创建新对象、清除类的其他对象等等。

使用函数或属性的优点 static 在于:

  • 它们可用于创建不需要在实例中存在的函数/属性。这有助于在代码库中保持一定的隔离性。
  • 在某些情况下它们可以减少代码冗余。

现在让我们看看如何在我们的 Chair 类中实现这个概念。我们还将看看可以使用关键字的一些用例 static

可以使用该 static 关键字的场景如下:

  • 课堂使用
  • 静态中的静态
  • 从构造函数调用静态
  • 类静态初始化块

有关上述场景的更多信息,请访问 MDN docs .

通过以下场景 Chair 了解该类的所有变体

如何 static 在课程中使用关键字

和任何其他编程语言一样,这是使用 static 关键字最适合初学者的方法之一。让我们定义一些类的方法和属性,并 static 观察其行为。

看一下下面的代码:

class Chair {
//Defaults that will be common for all the instances:
    static backSupport = true;
    static armRests = true;
    
    constructor(color, seatHeight, recliningAngle, headSupport, padding, seatSize, isHeightAdjustable, isMovable){
        //Defaults which can be changed by the subclass class.
        this.color = color;
        this.seatHeight = seatHeight;
        this.recliningAngle = recliningAngle;
        this.headSupport = false;
        this.padding = "3 inch";
        this.seatSize = "16 inch";
        this.isHeightAdjustable = false;
        this.isMovable = false;
        this.type = "Chair";
    } 
        
    static logObjectProps(){
        console.dir(this);
    }
    
    adjustableHeight() {};
    adjustAngle(){};
    moveChair(){};    
}

以下是上述代码的输出:

Screenshot-from-2021-12-01-11-05-15
Static variables
Screenshot-from-2021-12-01-11-06-35
The output of the static function

如上所示,静态方法只能通过类本身访问。 Chair 类的实例无法访问它。类的实例不具有静态属性:

Screenshot-from-2021-12-01-11-09-20
No static members in instances

该类的 x 实例 Chair 在其定义中没有静态方法或属性。

如果您尝试使用类实例访问静态方法或属性,那么它将抛出引用错误或只是返回未定义。

如何 static 在另一个静态函数中使用关键字

在某些情况下,您可能需要在另一个静态函数中使用静态属性或函数。您可以通过在静态函数中使用 this 关键字引用其他属性/函数来实现这一点。

让我们修改我们的 Chair 类来展示它是如何工作的:

class Chair {
//Defaults that will be common for all the instances:
    static backSupport = true;
    static armRests = true;
    
    constructor(color, seatHeight, recliningAngle, headSupport, padding, seatSize, isHeightAdjustable, isMovable){
        //Defaults which can be changed by the subclass class.
        this.color = color;
        this.seatHeight = seatHeight;
        this.recliningAngle = recliningAngle;
        this.headSupport = false;
        this.padding = "3 inch";
        this.seatSize = "16 inch";
        this.isHeightAdjustable = false;
        this.isMovable = false;
        this.type = "Chair";
    } 
        
    static logObjectProps(){
        console.dir(this);
    }

		//Static within static usage
		static printDefaultProps(){
				console.log(`Chair Back Support = ${this.backSupport}`);
				console.log(`Arm rests support = ${this.armRests}`);
		}
    
    adjustableHeight() {};
    adjustAngle(){};
    moveChair(){};    
}
Static within static implementation
Screenshot-from-2021-12-05-16-49-12
Output of the above code

如您所见,该 printDefaultProps 函数可以访问静态属性 backSupport armRests .

如何从构造函数调用静态属性/函数

与上面类似,您也可以在构造函数中访问这些静态属性/函数。要做到这一点,这里的情况有些不同。

在构造函数中调用静态属性/函数时,需要使用 <classname>.property <classname>.functionName() 。发生这种情况是因为 this 关键字无法直接访问静态成员。这不仅适用于构造函数,也适用于任何非静态函数。

让我们尝试通过修改 Chair 类来理解这一点。

class Chair {
//Defaults that will be common for all the instances:
    static backSupport = true;
    static armRests = true;
    
    constructor(color, seatHeight, recliningAngle, headSupport, padding, seatSize, isHeightAdjustable, isMovable){
        //Defaults which can be changed by the subclass class.
        this.color = color;
        this.seatHeight = seatHeight;
        this.recliningAngle = recliningAngle;
        this.headSupport = false;
        this.padding = "3 inch";
        this.seatSize = "16 inch";
        this.isHeightAdjustable = false;
        this.isMovable = false;
        this.type = "Chair";
		console.log(Chair.printDefaultProps()); //Usage of static method inside constructor
    } 
        
    static logObjectProps(){
        console.dir(this);
    }

		//Static within static usage
		static printDefaultProps(){
				console.log(`Chair Back Support = ${this.backSupport}`);
				console.log(`Arm rests support = ${this.armRests}`);
		}
    
    adjustableHeight() {};
    adjustAngle(){};
    moveChair(){};    
}

在上面的代码中,最后一行 console.log(Chair.printDefaultProps()); 展示了如何在构造函数中使用静态方法。

JavaScript 中类的私有成员

私有成员是类的成员,只能由类本身内部使用。它们不能在类外部访问。即使是类的实例也不能访问这些私有成员。

所有私有成员都使用 #<propertName> 语法声明。它们通常称为 哈希名称 .

让我们看一个基于我们的用例的示例。

我们将在类中定义一些新属性 OfficeChair 。假设我们想为所有办公椅添加默认计费信息。我们还希望只有类可以访问这些信息, OfficeChair 以便其他实用函数可以使用这些变量。

我们不希望其他类干扰其他类的帐单信息。为了解决这个问题,我们可以使用私有字段。

考虑添加以下字段:

  • 价格
  • 最大折扣
  • 卖家地址
chairModel2.drawio--1-
Updated Class Diagram

请注意,我们可以使用破折号在类图中表示私有字段,如下所示: - .

看一下下面的代码,它演示了我们如何将这些字段添加到类中 OfficeChair

class OfficeChair extends Chair {
	//Newly Added Properties
	#basePrice;
	#maxDiscount;
	#sellerAddress;

	constructor(type, color, isHeightAdjustable, seatHeight, recliningAngle) {
		super();
		this.type = type;
		this.color = color;
		this.isHeightAdjustable = isHeightAdjustable;
		this.seatHeight = seatHeight;
		this.recliningAngle = recliningAngle;
		this.isMovable = true;
		this.#basePrice = 1000;
		this.#maxDiscount = 5; //In percentage
		this.#sellerAddress = "XYZ, street";
	}

	adjustableHeight(height) {
		if (height > this.seatHeight) {
			console.log(`Chair height changed to ${height}`);
		} else {
			console.log(`Height cannot be decreased more than the seat height ${this.seatHeight}`);
		}
	}

	adjustAngle(angle) {
		if (angle >= this.recliningAngle) {
			console.log(`Chair angle changed to ${angle}`);
		} else {
			console.log(`Angle cannot be decreased more than the min reclining angle ${this.recliningAngle}`);
		}
	}

	moveChair(x, y) {
		console.log(`Chair moved to co-ordinates = (${x}, ${y})`);
	}

	//Newly Added function
	#getChairAmount(taxCharge) {
		return this.#basePrice + (this.#basePrice - this.#basePrice * this.#maxDiscount / 100) + taxCharge;
	}

	//Newly Added function
	generateBill() {
		console.log("**** BILLING INFORMATION ****");
		console.log(`Chair Price = ${this.#getChairAmount(20)}`);
		console.log(`Seller Address = ${this.#sellerAddress}`);
	}
}
Private members usage

当您在控制台中运行上述代码时,您应该看到以下输出:

Screenshot-from-2021-12-05-17-03-53
Output of private members

从上面的输出可以看出,我们已经执行了该 generateBill 函数。此函数访问类内的私有字段和函数来生成账单信息。

这些私有变量只能在类本身内访问。如果你尝试引用该类的任何私有成员,则会引发如下所示的语法错误:

Uncaught SyntaxError: Private field '#basePrice' must be declared in an enclosing class

让我演示一下如果子类尝试访问基类的私有变量会是什么样子:

class DinningChair extends OfficeChair{}

let dineChair = new DinningChair();
dineChair.#basePrice(); //Throws syntax error

由于您尝试访问另一个类的私有属性,因此上述代码将引发语法错误。

静态私有变量超出了本博文的讨论范围,因此我们不会进一步讨论它们。但你可以 在这里 .

概括

这些是我们可以利用 JavaScript 中的类在真实示例中实现面向对象编程概念的一些方法。

您可以阅读下面有关高级面向对象概念的更多信息:

  • 多态性
  • 继承类型

感谢您的阅读!

和 Twitter , GitHub 上关注我 LinkedIn .

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信小程序

微信扫一扫体验

立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部