JavaScript 类与继承

目录

  1. 创建对象的方式
  2. 继承
  3. TypeScript 类与继承
  4. 实际应用示例
  5. 最佳实践
  6. 总结

创建对象的方式

工厂模式

1
2
3
4
5
6
7
8
9
function createPerson(name) {
const o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};
return o;
}
const person1 = createPerson('kevin');

工厂模式是一种创建对象的设计模式,它通过一个函数来封装创建对象的过程。工厂模式的核心思想是将对象的创建与使用分离,使得代码更加模块化和可维护。

工厂模式的优点

  • 封装了对象创建的细节,使代码更加清晰
  • 可以根据不同的参数创建不同类型的对象
  • 避免了重复的对象创建代码

工厂模式的缺点

  • 无法确定对象的类型,因为所有实例都是通过 Object() 构造函数创建的
  • 每创建一个对象实例,都会为每个对象实例创建一遍相同的方法,造成内存浪费
  • 缺乏对象之间的关联关系,无法利用原型链的优势

现代实践
在现代 JavaScript 中,工厂模式仍然被广泛使用,特别是在需要创建多个具有相似结构但不同数据的对象时。例如,创建配置对象、服务实例等。

构造函数模式

1
2
3
4
5
6
7
function Person(name) {
this.name = name;
this.getName = function () {
console.log(this.name);
};
}
const person1 = new Person('kevin');

构造函数模式是 JavaScript 中创建对象的一种重要方式。构造函数是一个特殊的函数,当使用 new 关键字调用时,会创建一个新的对象,并将 this 绑定到这个新对象上。

构造函数的执行过程

  1. 创建一个新的空对象
  2. 将这个新对象的原型指向构造函数的原型对象
  3. this 绑定到这个新对象上
  4. 执行构造函数内部的代码
  5. 返回这个新对象(如果构造函数没有显式返回其他对象)

构造函数模式的优点

  • 可以识别对象的类型,通过 instanceof 操作符可以判断对象是否是某个构造函数的实例
  • 可以通过 new 关键字创建多个实例,每个实例都有自己的属性和方法

构造函数模式的缺点

  • 每次创建实例时,每个方法都要重新被创建一次,造成内存浪费
  • 将方法定义在构造函数内部,无法共享方法

解决方案
将方法移到构造函数外部,让所有实例共享同一个方法:

1
2
3
4
5
6
7
8
function Person(name) {
this.name = name;
this.getName = getName;
}
function getName() {
console.log(this.name);
}
const person1 = new Person('kevin');

现代实践
在现代 JavaScript 中,构造函数模式仍然被广泛使用,但通常与原型模式结合使用,以充分利用原型链的优势。

原型模式

1
2
3
4
5
6
7
function Person() { }
Person.prototype.name = 'kevin';
Person.prototype.getName = function () {
console.log(this.name);
};

const person1 = new Person();

原型模式是 JavaScript 中一种重要的创建对象的方式,它利用了 JavaScript 的原型继承机制。每个函数都有一个 prototype 属性,这个属性是一个对象,包含了可以由该构造函数创建的所有实例共享的属性和方法。

原型模式的优点

  • 所有实例共享原型上的属性和方法,避免了重复创建,节省内存
  • 可以在运行时动态修改原型,所有实例都会自动继承这些修改

原型模式的缺点

  • 无法在创建实例时初始化参数
  • 原型上的引用类型属性会被所有实例共享,修改一个实例的引用类型属性会影响其他实例

优化版

1
2
3
4
5
6
7
8
9
function Person() { }
Person.prototype = {
constructor: Person, // 手动设置 constructor 属性,否则会指向 Object
name: 'kevin',
getName: function () {
console.log(this.name);
}
};
const person1 = new Person();

注意:当使用对象字面量重写原型时,会丢失原来的 constructor 属性,需要手动设置回构造函数本身。

现代实践
在现代 JavaScript 中,原型模式通常与构造函数模式结合使用,形成组合模式,这是目前最广泛使用的创建对象的方式之一。

组合模式

组合模式是将构造函数模式和原型模式结合起来使用的一种创建对象的方式。它的核心思想是:在构造函数内定义实例的私有属性和方法,在构造函数原型上定义共有的属性和方法。这是目前最广泛使用的创建对象的方式之一。

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
};
const person1 = new Person('kevin');

组合模式的优点

  • 实例的私有属性和方法在构造函数内定义,每个实例都有自己的副本
  • 共有的属性和方法在原型上定义,所有实例共享,节省内存
  • 可以在创建实例时初始化参数
  • 可以通过 instanceof 操作符判断对象类型

动态原型模式

动态原型模式是组合模式的一种变体,它将原型方法的定义移到构造函数内部,通过条件判断确保原型方法只被创建一次。

1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name;
if (typeof this.getName !== 'function') {
Person.prototype.getName = function () {
console.log(this.name);
};
}
}
const person1 = new Person('kevin');

注意:使用动态原型模式时,不能用对象字面量重写原型,否则会导致实例的原型链断裂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
if (typeof this.getName !== 'function') {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
};
}
}
const person1 = new Person('kevin');
const person2 = new Person('daisy');
// 报错 并没有该方法
// person1.getName();
// 注释掉上面的代码,这句是可以执行的。
// person2.getName();

原因:当执行 new Person('kevin') 时,会先创建一个新对象,然后将该对象的原型指向 Person.prototype,接着执行构造函数内部的代码。当使用对象字面量重写 Person.prototype 时,只是改变了构造函数的 prototype 属性指向的对象,但已经创建的实例 person1 的原型仍然指向原来的原型对象,而原来的原型对象上没有 getName 方法,所以会报错。

解决方案:如果想在动态原型模式中使用对象字面量重写原型,可以在重写后重新创建实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {
this.name = name;
if (typeof this.getName !== 'function') {
Person.prototype = {
constructor: Person,
getName: function () {
console.log(this.name);
}
};
return new Person(name);
}
}
const person1 = new Person('kevin');
const person2 = new Person('daisy');
person1.getName(); // kevin
person2.getName(); // daisy

现代实践
在现代 JavaScript 中,组合模式仍然是创建对象的主流方式,但随着 ES6 的引入,越来越多的开发者开始使用 class 语法来创建对象,因为它更加简洁明了,并且底层实现也是基于组合模式的。

class 定义类

class 是 ES6 引入的一种新语法,它是组合模式的语法糖,让定义类更加简单和直观。class 语法的底层实现仍然是基于原型继承的,但提供了更加清晰和面向对象的语法。

1
2
3
4
5
6
7
8
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert(`Hello, ${this.name}!`);
}
}

class 语法的特点

  • 使用 constructor 方法定义构造函数
  • 方法直接定义在类体内,不需要使用 function 关键字
  • 方法会自动添加到类的原型上,不需要手动设置
  • 支持 extends 关键字实现继承
  • 支持 super 关键字调用父类方法

class 与传统构造函数的区别

  1. class 声明不会被提升,而函数声明会被提升
  2. class 中的方法都是不可枚举的,而传统构造函数的原型方法是可枚举的
  3. class 中的方法内部不能使用 arguments 对象,而传统函数可以
  4. class 必须使用 new 关键字调用,否则会抛出错误

现代实践
在现代 JavaScript 开发中,class 语法已经成为创建对象和实现继承的首选方式,特别是在使用 React、Vue 等现代前端框架时。它的语法更加简洁明了,易于理解和维护,同时也更加接近其他面向对象编程语言的语法。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

// 实例方法
sayHello() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
}

// 静态方法
static create(name, age) {
return new Person(name, age);
}
}

// 使用类
const person = new Person('Kevin', 30);
person.sayHello(); // Hello, my name is Kevin, I'm 30 years old.

// 使用静态方法创建实例
const person2 = Person.create('John', 25);
person2.sayHello(); // Hello, my name is John, I'm 25 years old.

其他创建对象的模式

寄生构造函数模式

寄生构造函数模式跟工厂模式的唯一区别在于,在创建对象时使用了 new。实际上两者结果是一样的。

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
const o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};
return o;
}
const person1 = new Person('kevin');
console.log(person1 instanceof Person); // false
console.log(person1 instanceof Object); // true

它只是寄生在构造函数的一种方法,创建的实例使用 instanceof 都无法指向其构造函数。这样方法可以在特殊情况下使用。比如当要创建一个具有额外方法的特殊数组,但又不想直接修改 Array 构造函数时,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SpecialArray() {
const values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function () {
return this.join('|');
};
return values;
}
const colors = new SpecialArray('red', 'blue', 'green');
const colors2 = SpecialArray('red2', 'blue2', 'green2');
console.log(colors);
console.log(colors.toPipedString()); // red|blue|green
console.log(colors2);
console.log(colors2.toPipedString()); // red2|blue2|green2

虽然本意是希望像使用普通 Array 类型一样使用 SpecialArray,但把 SpecialArray 当成函数也一样能用,即使这不优雅。在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name){
const o = new Object();
o.sayName = function(){
console.log(name);
};
return o;
}
const person1 = createPerson('kevin');
person1.sayName(); // kevin
person1.name = 'daisy';
person1.sayName(); // kevin
console.log(person1.name); // daisy

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。与寄生构造函数模式有两点不同:

  • 新创建的实例方法不引用 this
  • 不使用 new 操作符调用构造函数

稳妥对象最适合在一些安全的环境中。稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。

继承

原型链继承

原型链继承是 JavaScript 中最基本的继承方式,它利用原型链的特性实现继承。具体来说,就是将子类的原型指向父类的实例,这样子类的实例就可以访问父类的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
function Parent() {
this.name = 'kevin';
}
Parent.prototype.getName = function() {
console.log(this.name);
};
function Child() { }
Child.prototype = new Parent();

const child1 = new Child();
console.log(child1.getName()); // kevin

原型链继承的工作原理

  1. 创建一个 Parent 构造函数,并在其原型上定义方法
  2. 创建一个 Child 构造函数
  3. Child 的原型指向 Parent 的一个实例
  4. 当访问 child1.getName() 时,会先在 child1 实例上查找 getName 方法,找不到则沿着原型链向上查找,直到找到 Parent.prototype 上的 getName 方法

原型链继承的优点

  • 实现简单,代码量少
  • 子类可以继承父类的所有属性和方法

原型链继承的缺点

  • 引用类型的属性会被所有实例共享,修改一个实例的引用类型属性会影响其他实例
  • 在创建子类实例时,不能向父类构造函数传递参数
  • 无法实现多继承

示例

1
2
3
4
5
6
7
8
9
10
11
function Parent() {
this.names = ['kevin', 'daisy'];
}
function Child() { }
Child.prototype = new Parent();

const child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ['kevin', 'daisy', 'yayu']
const child2 = new Child();
console.log(child2.names); // ['kevin', 'daisy', 'yayu']

现代实践
原型链继承是 JavaScript 继承的基础,但在实际开发中,很少直接使用原型链继承,而是使用组合继承或 ES6 的 class 继承。

借用构造函数

借用构造函数(也称为经典继承)是一种通过在子类构造函数中调用父类构造函数来实现继承的方式。具体来说,就是使用 call()apply() 方法在子类构造函数中调用父类构造函数,从而将父类的属性和方法复制到子类实例中。

1
2
3
4
5
6
7
8
9
10
11
12
function Parent() {
this.names = ['kevin', 'daisy'];
}
function Child() {
Parent.call(this);
}

const child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ['kevin', 'daisy', 'yayu']
const child2 = new Child();
console.log(child2.names); // ['kevin', 'daisy']

借用构造函数的工作原理

  1. 创建一个 Parent 构造函数,定义属性
  2. 创建一个 Child 构造函数,在其中使用 Parent.call(this) 调用父类构造函数
  3. 当创建 Child 实例时,会执行 Parent.call(this),将父类的属性复制到子类实例中
  4. 每个子类实例都有自己的属性副本,修改一个实例的属性不会影响其他实例

借用构造函数的优点

  • 避免了引用类型的属性被所有实例共享
  • 可以在创建子类实例时向父类构造函数传递参数
  • 可以实现多继承(通过多次调用不同父类的构造函数)

借用构造函数的缺点

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法,造成内存浪费
  • 无法继承父类原型上的方法,只能继承父类构造函数中定义的属性和方法
  • 函数复用性差

示例

1
2
3
4
5
6
7
8
9
10
11
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}

const child1 = new Child('kevin');
console.log(child1.name); // kevin
const child2 = new Child('daisy');
console.log(child2.name); // daisy

现代实践
借用构造函数通常不单独使用,而是与原型链继承结合使用,形成组合继承。

组合继承

组合继承是将原型链继承和借用构造函数继承结合起来使用的一种继承方式。它的核心思想是:使用原型链继承来继承父类的方法和共享属性,使用借用构造函数继承来继承父类的实例属性。这是 JavaScript 中最常用的继承模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
console.log(this.name);
};

function Child(name, age) {
Parent.call(this, name); // 借用构造函数继承实例属性
this.age = age;
}
Child.prototype = new Parent(); // 原型链继承方法和共享属性
Child.prototype.constructor = Child; // 修复 constructor 属性

const child1 = new Child('kevin', 18);
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
const child2 = new Child('daisy', 20);
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ['red', 'blue', 'green']

组合继承的工作原理

  1. 创建一个 Parent 构造函数,定义实例属性和原型方法
  2. 创建一个 Child 构造函数,在其中使用 Parent.call(this, name) 调用父类构造函数,继承实例属性
  3. Child 的原型指向 Parent 的一个实例,继承父类的方法和共享属性
  4. 修复 Child.prototype.constructor 属性,使其指向 Child 构造函数

组合继承的优点

  • 结合了原型链继承和借用构造函数继承的优点
  • 既可以继承父类的实例属性,又可以继承父类的原型方法
  • 避免了引用类型的属性被所有实例共享
  • 可以在创建子类实例时向父类构造函数传递参数

组合继承的缺点

  • 会调用两次父类构造函数:一次是在创建 Child.prototype 时,一次是在创建 Child 实例时
  • 会在 Child.prototype 上创建不必要的属性

现代实践
组合继承是 ES6 之前 JavaScript 中最常用的继承方式,它解决了原型链继承和借用构造函数继承的缺点,提供了一种相对完善的继承方案。在 ES6 引入 class 语法后,组合继承的使用频率有所下降,但它仍然是理解 JavaScript 继承机制的重要基础。

class 继承

ES6 引入的 class 语法为 JavaScript 提供了更加简洁和直观的继承方式。class 继承底层仍然基于原型继承,但提供了更加清晰的语法,类似于其他面向对象编程语言的继承语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert(`Hello, ${this.name}!`);
}
}

class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用 super 调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert(`I am at grade ${this.grade}`);
}
}

class 继承的工作原理

  1. 使用 class 关键字定义父类 Student,包含构造函数和方法
  2. 使用 extends 关键字定义子类 PrimaryStudent,继承自 Student
  3. 在子类构造函数中使用 super() 调用父类构造函数,传递必要的参数
  4. 子类可以添加自己的属性和方法,也可以重写父类的方法

class 继承的优点

  • 语法简洁明了,易于理解和维护
  • 自动处理原型链的设置,不需要手动修复 constructor 属性
  • 支持 super 关键字,方便调用父类的构造函数和方法
  • 更加接近其他面向对象编程语言的语法,降低学习成本

class 继承的注意事项

  • 在子类构造函数中,必须在使用 this 之前调用 super()
  • 如果子类没有定义构造函数,会自动生成一个默认构造函数,该构造函数会调用父类的构造函数
  • class 语法是基于原型继承的语法糖,底层实现仍然是原型链继承

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 父类
class Animal {
constructor(name) {
this.name = name;
}

makeSound() {
console.log('Some generic sound');
}
}

// 子类
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}

// 重写父类方法
makeSound() {
console.log('Woof! Woof!');
}

// 子类特有方法
fetch() {
console.log(`${this.name} is fetching the ball.`);
}
}

// 使用
const dog = new Dog('Buddy', 'Golden Retriever');
dog.makeSound(); // Woof! Woof!
dog.fetch(); // Buddy is fetching the ball.

现代实践
在现代 JavaScript 开发中,class 继承已经成为实现继承的首选方式,特别是在使用 React、Vue 等现代前端框架时。它的语法更加简洁明了,易于理解和维护,同时也更加接近其他面向对象编程语言的语法。

其他继承模式

原型式继承

1
2
3
4
5
function createObj(o) {
function F() {}
F.prototype = o;
return new F();
}

ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。缺点在于包含引用类型的属性值始终都会共享相应的值,跟原型链继承一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
const person = {
name: 'kevin',
friends: ['daisy', 'kelly']
};

const person1 = createObj(person);
const person2 = createObj(person);

person1.name = 'person1';
console.log(person2.name); // kevin

person1.friends.push('taylor');
console.log(person2.friends); // ['daisy', 'kelly', 'taylor']

注意:修改 person1.name 的值,person2.name 的值并未发生改变,并不是因为 person1person2 有独立的 name 值,而是因为 person1.name = 'person1',给 person1 添加了 name 值,并非修改了原型上的 name 值。

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

1
2
3
4
5
6
7
function createObj(o) {
const clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
};
return clone;
}

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

寄生组合式继承

组合继承最大的缺点是会调用两次父构造函数:一次是设置子类型实例的原型,一次是创建子类型实例。

如果不使用 Child.prototype = new Parent() ,而是间接地让 Child.prototype 访问到 Parent.prototype,那就可以避免这一次重复调用。实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
console.log(this.name);
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

// 关键的三步
const F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();

const child1 = new Child('kevin', '18');
console.log(child1);

最后我们封装一下这个继承方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

function inheritPrototype(child, parent) {
const prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

// 当我们使用的时候:
inheritPrototype(Child, Parent);

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的属性。与此同时,原型链还能保持不变;因此能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

TypeScript 类与继承

TypeScript 对 JavaScript 的类系统进行了扩展,添加了类型注解、访问修饰符、抽象类等特性,使面向对象编程更加严谨和强大。

TypeScript 类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 基本类定义
class Person {
// 成员变量(属性)
name: string;
age: number;

// 构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}

// 成员方法
sayHello(): void {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
}
}

// 使用类
const person = new Person('Kevin', 30);
person.sayHello(); // Hello, my name is Kevin, I'm 30 years old.

TypeScript 继承

TypeScript 使用 extends 关键字实现继承,与 JavaScript ES6 的 class 继承语法类似,但增加了类型检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 父类
class Animal {
name: string;

constructor(name: string) {
this.name = name;
}

makeSound(): void {
console.log('Some generic sound');
}
}

// 子类
class Dog extends Animal {
breed: string;

constructor(name: string, breed: string) {
super(name); // 调用父类构造函数
this.breed = breed;
}

// 重写父类方法
makeSound(): void {
console.log('Woof! Woof!');
}

// 子类特有方法
fetch(): void {
console.log(`${this.name} is fetching the ball.`);
}
}

// 使用子类
const dog = new Dog('Buddy', 'Golden Retriever');
dog.makeSound(); // Woof! Woof!
dog.fetch(); // Buddy is fetching the ball.

TypeScript 特殊语法

访问修饰符

TypeScript 提供了三种访问修饰符:

  • public:默认修饰符,公共成员,可以在任何地方访问
  • private:私有成员,只能在类内部访问
  • protected:受保护成员,可以在类内部和子类中访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Person {
public name: string; // 公共成员
private age: number; // 私有成员
protected email: string; // 受保护成员

constructor(name: string, age: number, email: string) {
this.name = name;
this.age = age;
this.email = email;
}

// 公共方法
public getAge(): number {
return this.age; // 可以访问私有成员
}
}

class Employee extends Person {
constructor(name: string, age: number, email: string) {
super(name, age, email);
}

// 可以访问受保护成员
public getEmail(): string {
return this.email;
}
}

const person = new Person('Kevin', 30, 'kevin@example.com');
console.log(person.name); // 可以访问
// console.log(person.age); // 错误:私有成员
// console.log(person.email); // 错误:受保护成员

const employee = new Employee('John', 25, 'john@example.com');
console.log(employee.getEmail()); // 可以访问

只读属性

使用 readonly 关键字定义只读属性,只能在构造函数中赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
readonly id: number;
name: string;

constructor(id: number, name: string) {
this.id = id; // 只能在构造函数中赋值
this.name = name;
}
}

const person = new Person(1, 'Kevin');
console.log(person.id); // 1
// person.id = 2; // 错误:只读属性

静态成员

使用 static 关键字定义静态成员,属于类本身,而不是实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MathUtils {
// 静态属性
static PI: number = 3.14159;

// 静态方法
static calculateArea(radius: number): number {
return this.PI * radius * radius;
}
}

// 直接通过类名访问
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateArea(5)); // 78.53975

抽象类

使用 abstract 关键字定义抽象类,不能直接实例化,只能被继承。抽象类可以定义抽象方法,子类必须实现这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 抽象类
abstract class Shape {
// 抽象方法
abstract calculateArea(): number;

// 普通方法
display(): void {
console.log('This is a shape.');
}
}

// 实现抽象类
class Circle extends Shape {
radius: number;

constructor(radius: number) {
super();
this.radius = radius;
}

// 必须实现抽象方法
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}

// 使用
const circle = new Circle(5);
circle.display(); // This is a shape.
console.log(circle.calculateArea()); // 78.53981633974483

实际应用示例

JavaScript 示例:实现一个简单的用户管理系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 基类:用户
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}

getInfo() {
return `ID: ${this.id}, Name: ${this.name}, Email: ${this.email}`;
}
}

// 子类:管理员
class Admin extends User {
constructor(id, name, email, permissions) {
super(id, name, email);
this.permissions = permissions;
}

getInfo() {
return `${super.getInfo()}, Permissions: ${this.permissions.join(', ')}`;
}

addUser(user) {
console.log(`Admin ${this.name} added user ${user.name}`);
}
}

// 子类:普通用户
class RegularUser extends User {
constructor(id, name, email, preferences) {
super(id, name, email);
this.preferences = preferences;
}

getInfo() {
return `${super.getInfo()}, Preferences: ${JSON.stringify(this.preferences)}`;
}

updatePreferences(newPreferences) {
this.preferences = { ...this.preferences, ...newPreferences };
console.log(`User ${this.name} updated preferences`);
}
}

// 使用
const admin = new Admin(1, 'Admin', 'admin@example.com', ['add', 'edit', 'delete']);
const user = new RegularUser(2, 'User', 'user@example.com', { theme: 'dark', language: 'en' });

console.log(admin.getInfo());
console.log(user.getInfo());

admin.addUser(user);
user.updatePreferences({ language: 'zh' });

TypeScript 示例:实现一个简单的几何图形系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 抽象基类
abstract class Shape {
abstract calculateArea(): number;
abstract calculatePerimeter(): number;

display(): void {
console.log(`Area: ${this.calculateArea()}, Perimeter: ${this.calculatePerimeter()}`);
}
}

// 圆形
class Circle extends Shape {
constructor(private radius: number) {
super();
}

calculateArea(): number {
return Math.PI * this.radius * this.radius;
}

calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
}

// 矩形
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}

calculateArea(): number {
return this.width * this.height;
}

calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
}

// 三角形
class Triangle extends Shape {
constructor(private a: number, private b: number, private c: number) {
super();
}

calculateArea(): number {
// 使用海伦公式
const s = (this.a + this.b + this.c) / 2;
return Math.sqrt(s * (s - this.a) * (s - this.b) * (s - this.c));
}

calculatePerimeter(): number {
return this.a + this.b + this.c;
}
}

// 使用
const shapes: Shape[] = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 5)
];

shapes.forEach(shape => {
shape.display();
});

最佳实践

JavaScript 类与继承最佳实践

  1. 优先使用 ES6+ 的 class 语法:class 语法更加简洁明了,易于理解和维护
  2. 合理使用继承:不要过度使用继承,优先考虑组合
  3. 使用 super 调用父类构造函数:确保父类属性正确初始化
  4. 避免修改原型链:直接修改原型链可能导致意外的行为
  5. 使用 Object.create() 创建对象:对于简单对象,使用 Object.create() 更加灵活
  6. 注意 this 的绑定:在方法中使用箭头函数或手动绑定 this
  7. **避免使用 new Object()**:直接使用对象字面量 {} 更加简洁

TypeScript 类与继承最佳实践

  1. 使用访问修饰符:明确成员的访问级别,提高代码的可维护性
  2. 使用接口定义类型:接口可以定义对象的结构,提高代码的类型安全性
  3. 使用抽象类和方法:对于需要被继承的基类,使用抽象类定义共同行为
  4. 使用泛型:对于可重用的组件,使用泛型提高代码的灵活性
  5. 避免循环依赖:类之间的依赖关系应该是单向的
  6. 使用 readonly 关键字:对于不需要修改的属性,使用 readonly 提高代码的安全性
  7. 使用静态成员:对于类级别的属性和方法,使用静态成员

性能考虑

  1. 避免频繁创建对象:对于频繁使用的对象,考虑使用对象池
  2. 合理使用原型:将方法定义在原型上,避免每次创建实例时重复创建方法
  3. 避免深度继承:过深的继承层次会影响性能和可维护性
  4. 使用组合优于继承:组合更加灵活,避免继承带来的问题

总结

JavaScript 和 TypeScript 的类与继承系统是面向对象编程的重要组成部分。通过本文的学习,我们了解了:

  1. 创建对象的方式:从简单的工厂模式到现代的 class 语法,每种方式都有其适用场景
  2. 继承的实现:从原型链继承到组合继承,再到现代的 class 继承,JavaScript 的继承机制不断演进
  3. TypeScript 的增强:TypeScript 为类系统添加了类型注解、访问修饰符、抽象类等特性,使面向对象编程更加严谨
  4. 实际应用:通过具体示例,我们看到了类与继承在实际项目中的应用
  5. 最佳实践:掌握了类与继承的最佳实践,有助于编写更加高质量的代码

在实际开发中,我们应该根据具体的需求选择合适的创建对象和继承方式,同时遵循最佳实践,编写清晰、可维护的代码。随着 JavaScript 和 TypeScript 的不断发展,类与继承的语法和特性也在不断完善,我们需要持续学习和更新自己的知识,以适应不断变化的前端开发环境。