深入理解JavaScript和TypeScript中的class
本文深入解析JavaScript和TypeScript中的class机制。从ES6语法糖到底层原型继承,对比分析静态成员、实例成员及存取器的实现差异。重点探讨TypeScript中抽象类、抽象方法及私有成员的规范应用,并通过Person及其子类的完整案例,展示面向对象编程在前端开发中的实际应用与多态特性。
前言与背景
前言
对于前端开发者而言,日常开发中较少直接使用 class,因为 JavaScript 更倾向于函数式编程风格,代码中常见 function 定义,而 class 或 new 关键字的使用频率相对较低。这也导致许多前端开发者在设计模式方面存在知识短板。
近期在学习 Angular 框架时,发现其大量使用了 class 结构,这让人对 Angular 这一优秀且值得深入研究的框架刮目相看。
本文将简要介绍 JavaScript 和 TypeScript 中 class 的基本概念与应用。
类的基本概念
基本概念
在正式介绍 class 之前,需要先厘几个核心概念。
1、静态成员
静态成员属于类本身,可以被继承,但实例无法直接访问。这类成员常见于工具类,例如 jQuery 时代的 $.ajax,其中 ajax 是 $ 对象的静态方法,使用时无需通过 new 或函数调用创建新实例,使用便捷。
2、私有成员
私有成员位于类内部,通常不可继承,仅限类内部使用,实例无法访问。其特性类似于闭包中的变量,但与闭包仍有区别。目前 JavaScript 标准中无法直接定义私有成员,需借助其他技术手段辅助实现。
3、getter/setter
存取器属性允许在访问或修改实例属性时进行拦截,从而执行额外逻辑。Vue 框架正是利用此 API 来实现对数据变化的追踪。
4、实例成员
实例成员指通过 new 关键字创建的实例所拥有的成员,支持继承,借此特性可实现代码复用。
5、抽象类,抽象方法
抽象类是指不能被实例化的类,若使用 new 关键字调用会报错,通常被设计为父类。
抽象方法仅定义方法名、参数和返回值,不提供具体实现,具体逻辑由子类完成。若子类继承抽象类,则必须实现父类所有的抽象方法,否则将引发错误。
这两个概念在 JavaScript 中无法直接实现,但在 TypeScript 或其他面向对象语言中可以轻松实现。此外,这也是实现多态的重要手段。
案例设计与实现
案例介绍
为了更直观地展示 class,本文将使用三个类作为示例:Person、Chinese 和 American。从名称即可判断:Person 是父类(基类),Chinese 和 American 是子类(派生类)。
Person 类包含 name、age、gender 三个属性,以及 sayHello 方法和 fullName 存取器属性。同时,Person 还包含一些静态成员和私有成员。由于示例限制,静态和私有成员暂用 foo、bar、x、y、z 等占位符表示。
作为子类的 Chinese 和 American 继承了 Person 的实例成员和静态成员,同时它们也拥有各自特有的方法和属性。
Chinese 类包含 kungfu 属性,并具备习武 martial 的能力。
American 类包含 twitter 属性,并具备 sendTwitter 方法。
接下来,我们将分别使用 JavaScript 和 TypeScript 来实现上述案例。
JavaScript 中的 class
JavaScript 中的 class 需分阶段讨论。ES6 引入了 class 和 extends 两个关键字,虽然它们本质上是语法糖,底层仍依赖 prototype 实现继承,但这种写法确实使代码更加清晰易读。
ES6 中的 class
JavaScript 中的 class 实现
class Person {
// #x = '私有属性x';
// static x = '静态属性x';
// name;
// age;
// gender;
// 上面的写法还在提案中,并没有成为正式标准,不过变化的可能性已经不大了。
// 顺便吐槽一下,用 # 表示私有成员,真的是很无语.
/**
* Person的静态方法,可以被子类继承
* 可以通过 this 访问静态成员
*/
static foo() {
console.log(`类 ${this.name} 有一个 ${this.x}`);
}
constructor(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
/**
* 数据存储器,可以访问实例成员,子类的实例可以继承
* 以通过 this 访问实例成员
*/
get fullName() {
const suffix = this.gender === '男' ? '先生' : '女士';
return this.name + suffix;
}
set fullName(value) {
console.log(`你已改名为 ${value} `);
}
/**
* Person的实例方法,可以被子类的实例继承
* 可以通过 this 访问实例成员
*/
sayHello() {
console.log(`你好我是 ${this.fullName} ,我 ${this.age} 岁了`);
}
}
Person.x = '静态属性x';
class Chinese extends Person {
static bar() {
console.log(`类 ${this.name} 的父类是 ${super.name}`);
super.foo();
}
constructor(name, age, gender, kungfu) {
super(name, age, gender);
this.kungfu = kungfu;
}
martial() {
console.log(`${this.name} 正在修炼 ${this.kungfu} `);
}
}
class American extends Person {
// static y = '静态属性y';
static bar() {
console.log(`类 ${this.name} 有自己的 ${this.y} ,还继承了父类 ${super.name} 的 ${super.x}`);
}
constructor(name, age, gender, twitter) {
super(name, age, gender);
this.twitter = twitter;
}
sendTwitter(msg) {
console.log(`${this.name} : `);
console.log(` ${msg}`);
}
}
American.y = '静态属性y';
Person.x; // 静态属性x
Person.foo(); // 类 Person 有一个 静态属性x
Chinese.x; // 静态属性x
Chinese.foo(); // 类 Chinese 有一个 静态属性x
Chinese.bar(); // 类 Chinese 的父类是 Person
American.x; // 静态属性x
American.y; // '静态属性y
American.foo(); // 类 American 有一个 静态属性x
American.bar(); // 类 American 有自己的 静态属性y ,还继承了父类 Person 的 静态属性x
const p = new Person('Lucy', 20, '女');
const c = new Chinese('韩梅梅', 18, '女', '咏春拳');
const a = new American('特朗普', 72, '男', 'Donald J. Trump');
c.sayHello(); // 你好我是 韩梅梅女士 ,我 18 岁了
c.martial(); // 韩梅梅 正在修炼 咏春拳
a.sayHello(); // 你好我是 特朗普先生 ,我 72 岁了
a.sendTwitter('推特治国'); // 特朗普 : 推特治国
ES6 之前的 class
ES5 的继承机制实质是先创建子类的实例对象 this,
然后再将父类的方法添加到 this 上(Parent.apply(this))。
ES6 的继承机制完全不同,其实质是先创建父类的实例对象 this,因此必须先调用 super 方法,
然后再使用子类的构造函数修改 this。
为了实现继承,我们需要先实现一个 extendsClass 函数,其作用是让子类继承父类的静态成员和实例成员。
ES5 与 ES6 继承机制对比
function extendsClass(parent, child) {
// 防止子类和父类相同名称的成员被父类覆盖
var flag = false;
// 继承静态成员
for (var k in parent) {
flag = k in child;
if (!flag) {
child[k] = parent[k];
}
}
// 继承父类prototype上的成员
// 用一个新的构造函数切断父类和子类之间的数据共享
var F = function () { }
F.prototype = parent.prototype;
var o = new F();
for (var k in o) {
flag = k in child.prototype;
if (!flag) {
child.prototype[k] = o[k];
}
}
}
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = this.gender;
// 如果将 getter/setter 写在 prototype 会获取不到
Object.defineProperty(this, 'fullName', {
get: function () {
var suffix = this.gender === '男' ? '先生' : '女士';
return this.name + suffix;
},
set: function () {
console.log('你已改名为 ' + value + ' ');
},
});
}
Person.x = '静态属性x';
Person.foo = function () {
console.log('类 ' + this.name + ' 有一个 ' + this.x);
}
Person.prototype = {
constructor: Person,
// get fullName() { },
// set fullName(value) { },
sayHello: function () {
console.log('你好我是 ' + this.fullName + ' ,我 ' + this.age + ' 了');
},
};
function Chinese(name, age, gender, kungfu) {
// 用call改变this指向,实现继承父类的实例属性
Person.call(this, name, age, gender);
this.kungfu = kungfu;
}
Chinese.bar = function () {
console.log('类 ' + this.name + ' 的父类是 ' + Person.name);
Person.foo();
}
Chinese.prototype = {
constructor: Chinese,
martial: function () {
console.log(this.name + ' 正在修炼 ' + this.kungfu + ' ');
}
};
extendsClass(Person, Chinese);
function American(name, age, gender, twitter) {
Person.call(this, name, age, gender);
this.twitter = twitter;
}
American.y = '静态属性y';
American.bar = function () {
console.log('类 ' + this.name + ' 有自己的 ' + this.y + ' ,还继承了父类 ' + Person.name + ' 的 ' + Person.x);
}
American.prototype = {
constructor: American,
sendTwitter: function (msg) {
console.log(this.name + ' : ');
console.log(' ' + msg);
}
};
extendsClass(Person, American);
TypeScript 中的 class
讲完 JavaScript 中的类,我们尚未涉及抽象类、抽象方法和私有方法这三个概念。由于 JavaScript 语言的局限性,实现这三种概念较为困难,但在 TypeScript 中可以轻松实现这些特性。
首先对示例描述稍作修改:Person 被定义为抽象类,因为正常的人必然有国籍;Person 的 sayHello 方法被定义为抽象方法,因为不同国家的打招呼方式不同。此外,人的性别只能读取且不可修改,确定值为男性或女性,因此还需借助枚举类型。
TypeScript 中的 class 特性
enum Gender {
female = 0,
male = 1
};
abstract class Person {
private x: string = '私有属性x,子类和实例都无法访问';
protected y: string = '私有属性y,子类可以访问,实例无法访问';
name: string;
public age: number;
public readonly gender: Gender; // 用关键字 readonly 表明这是一个只读属性
public static x: string = '静态属性x';
public static foo() {
console.log(`类 ${this.name} 有一个 ${this.x}`);
}
constructor(name: string, age: number, gender: Gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
get fullName(): string {
const suffix = this.gender === 1 ? '先生' : '女士';
return this.name + suffix;
}
set FullName(value: string) {
console.log(`你已改名为 ${value} `);
}
// 抽象方法,具体实现交由子类完成
abstract sayHello(): void;
}
class Chinese extends Person {
public kungfu: string;
public static bar() {
console.log(`类 ${this.name} 的父类是 ${super.name}`);
super.foo();
}
public constructor(name: string, age: number, gender: Gender, kungfu: string) {
super(name, age, gender);
this.kungfu = kungfu;
}
public sayHello(): void {
console.log(`你好我是 ${this.fullName} ,我 ${this.age} 岁了`);
}
public martial() {
console.log(`${this.name} 正在修炼 ${this.kungfu} `);
}
}
class American extends Person {
static y = '静态属性y';
public static bar() {
console.log(`类 ${this.name} 有自己的 ${this.y} ,还继承了父类 ${super.name} 的 ${super.x}`);
}
public twitter: string;
public constructor(name: string, age: number, gender: Gender, twitter: string) {
super(name, age, gender);
this.twitter = twitter;
}
public sayHello(): void {
console.log(`Hello, I am ${this.fullName} , I'm ${this.age} years old`);
}
public sendTwitter(msg: string): void {
console.log(`${this.name} : `);
console.log(` ${msg}`);
}
}
Person.x; // 静态属性x
Person.foo(); // 类 Person 有一个 静态属性x
Chinese.x; // 静态属性x
Chinese.foo(); // 类 Chinese 有一个 静态属性x
Chinese.bar(); // 类 Chinese 的父类是 Person
American.x; // 静态属性x
American.y; // '静态属性y
American.foo(); // 类 American 有一个 静态属性x
American.bar(); // 类 American 有自己的 静态属性y ,还继承了父类 Person 的 静态属性x
const c: Chinese = new Chinese('韩梅梅', 18, Gender.female, '咏春拳');
const a: American = new American('特朗普', 72, Gender.male, 'Donald J. Trump');
c.sayHello(); // 你好我是 韩梅梅女士 ,我 18 岁了
c.martial(); // 韩梅梅 正在修炼 咏春拳
a.sayHello(); // Hello, I am 特朗普先生 , I'm 72 years old
a.sendTwitter('推特治国'); // 特朗普 : 推特治国
总结
以上就是本文的全部内容,希望这些内容对大家的学习或工作具有一定的参考价值。如有任何疑问,欢迎留言交流,感谢大家对本站的支持。
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38
相关标签:








