面向对象介绍
面向对象是程序中一个非常重要的思想,它被很多同学理解成了一个比较难,比较深奥的问题,其实不然。面向对象很简单,简而言之就是程序之中所有的操作都需要通过对象来完成。
举例来说:
- 操作浏览器要使用window对象
- 操作网页要使用document对象
- 操作控制台要使用console对象
一切操作都要通过对象,也就是所谓的面向对象,那么对象到底是什么呢?这就要先说到程序是什么,计算机程序的本质就是对现实事物的抽象,抽象的反义词是具体
,比如:照片是对一个具体的人的抽象,汽车模型是对具体汽车的抽象
等等。程序也是对事物的抽象,在程序中我们可以表示一个人、一条狗、一把枪、一颗子弹等等所有的事物。一个事物到了程序中就变成了一个对象。
在程序中所有的对象都被分成了两个部分数据和功能
,以人为例,人的姓名、性别、年龄、身高、体重等属于数据,人可以说话、走路、吃饭、睡觉这些属于人的功能。数据在对象中被成为属性
,而功能就被称为方法
。所以简而言之,在程序中一切皆是对象。
类(class)
要想面向对象,操作对象,首先便要拥有对象,那么下一个问题就是如何创建对象。要创建对象,必须要先定义类,所谓的类可以理解为对象的模型,程序中可以根据类创建指定类型的对象,举例来说:可以通过Person类来创建人的对象,通过Dog类创建狗的对象,通过Car类来创建汽车的对象,不同的类可以用来创建不同的对象。
- 定义类 :
class 类名 { //直接定义的属性是实例属性,需要通过对象的实例去访问 属性名: 类型; constructor(参数: 类型){ this.属性名 = 参数; } 方法名(){ .... } }
- 示例代码如下:
class Person{ //声明属性 name: string; age: number; //构造方法 constructor(name: string, age: number){ this.name = name; this.age = age; } //一般方法 sayHello(){ console.log(`大家好,我是${this.name}`); } }
- 使用类
//创建类的实例 const p = new Person('孙悟空', 18); //调用实例的方法 p.sayHello();
构造器(构造函数)及this指向
- constructor 被称为构造函数
- 构造函数会在对象创建时调用
- 使用构造器
class Dog { name : string; age : number; constructor(name : string,age : number) { //在实例方法中,this就是表示当前的实例 //在构造函数中当前对象就是当前创建的那个对象 //在构造函数中可以通过this向新建的对象中添加属性 this.name = name this.age = age console.log(this) } bark() { // alert('叫!') //在方法中可以通过this来表示当前调用方法的对象 console.log(this.name) } } const dog = new Dog('小黑',4) const dog1 = new Dog('小白',2) console.log(dog) console.log(dog1) dog.bark() //小黑
类的继承
- 类的继承:在 TypeScript 中要想实现继承使用 extends 关键字,只要一旦实现了继承关系,那么子类中便拥有了父类的属性和方法,而在执行方法过程中,首先从子类开始找,如果有,就使用,如果没有,就去父类中找。类的继承只能单向继承。
- 通过继承可以将其他类中的属性和方法引入到当前类中
- 例子:
class Animal{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; } } class Dog extends Animal{ bark(){ console.log(`${this.name}在汪汪叫!`); } } const dog = new Dog('旺财', 4); dog.bark();
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog 是一个 派生类,它派生自 Animal 基类,通过 extends 关键字。 派生类通常被称作子类,基类通常被称作超类(父类)。
- 继承的特点:
使用继承后,子类将会拥有父类所有的方法和属性
通过继承可以将多个类中共同的代码写在一个父类中,这样只需要写一次即可让所有的子类都同时拥有父类的属性和方法
如果希望在子类中添加一些父类中没有的属性或方法直接加就行
通过继承可以在不修改类的情况下完成对类的扩展
重写
如果在子类中添加了和父类相同的方法,则子类方法会覆盖掉父类的方法,这种子类覆盖掉父类方法的形式,我们成为方法的重写
- 示例:
class Animal{ name: string; age: number; constructor(name: string, age: number){ this.name = name; this.age = age; } run(){ console.log(`父类中的run方法!`); } } class Dog extends Animal{ bark(){ console.log(`${this.name}在汪汪叫!`); } run(){ console.log(`子类中的run方法,会重写父类中的run方法!`); } } const dog = new Dog('旺财', 4); dog.bark();
super关键字
- 如果在子类中写了构造函数,在子类的构造函数中必须对父类的构造函数进行调用
- 在类的方法中 super 就表示当前类的父类
- 示例代码:
class Animal { name : string; constructor(name : string) { this.name = name } sayHello() { console.log('动物在叫') } } class Dog extends Animal { age : number constructor(name : string , age : number) { //如果在子类中写了构造函数,在子类的构造函数中必须对父类的构造函数进行调用 super(name); //调用父类的构造函数 this.age = age } sayHello() { //在类的方法中 super 就表示当前类的父类 super.sayHello(); } } const dog = new Dog('旺财',3); dog.sayHello()
公共、私有、受保护 修饰符
TS中属性具有三种修饰符
- public(默认值),public修饰的属性可以在任意位置访问(修改)
- 示例:
class Person{ public name: string; // 写或什么都不写都是public public age: number; constructor(name: string, age: number){ this.name = name; // 可以在类中修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } } class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中可以修改 } } const p = new Person('james', 37); p.name = 'davis';// 可以通过对象修改
- protected ,受包含的属性,只能在当前类和当前类的子类中使用(修改)
- 示例:
class Person{ protected name: string; protected age: number; constructor(name: string, age: number){ this.name = name; // 可以修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } } class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中可以修改 } } const p = new Person('james', 37); p.name = 'davis';// 不能修改
- private , 当成员被标记成 private 时,它就不能在声明它的类的外部访问
- 示例:
class Person{ private name: string; private age: number; constructor(name: string, age: number){ this.name = name; // 可以修改 this.age = age; } sayHello(){ console.log(`大家好,我是${this.name}`); } } class Employee extends Person{ constructor(name: string, age: number){ super(name, age); this.name = name; //子类中不能修改 } } const p = new Person('james', 37); p.name = 'davis';// 不能修改
readonly 修饰符
如果在声明属性时添加一个readonly,则属性便成了只读属性无法修改
class Person { readonly name: string = 'james' constructor(name: string) { this.name = name } } let davis = new Person('Davis') // davis.name = 'curry' // ❌❌报错
报错如下:
参数属性
在上面的例子中,我们必须在 Person
类里定义一个只读成员 n
ame
和一个参数为 name
的构造函数,并且立刻将 name
的值赋给 this.name
,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person 类的修改版,使用了参数属性:
class Person2 { constructor(readonly name: string) {} } const p = new Person2('harden') console.log(p.name) //harden
静态属性
使用static开头的属性是静态属性(类属性),可以直接通过类访问
使用静态属性无需创建实例,通过类即可直接使用
示例如下:
class Tools{ static PI = 3.1415926; } console.log(Tools.PI); //3.1415926
静态方法
被静态修饰符修饰的方法就是静态方法,静态方法可以通过类名直接调用,但是在静态方法内部,不能直接调用当前类的非静态属性、非静态方法。
示例如下:
class Person { name : string = "孙悟空" static sex: string = "男";//被静态修饰符static修饰的属性 constructor(name : string) { this.name = name } run():void { console.log(this.name + "在跑步"); } static print(): void {//被静态修饰符static修饰的方法 // console.log('姓名:' + this.name);//错误 console.log(`性别:${Person.sex}`);//正确 // this.run();//错误 } }
属性存取器(getter和setter)
大家可能对这个知识点感到困惑,为什么我们要使用属性存取器,在我们实例化对象之后,当我们通过实例化的对象去修改某个属性的时候,会出现不安全行为例如:
我要修改定义过的age属性,我可以直接把age修改为 new Person().age = -25,众所周知,年龄是不可能为负数的,所以就需要属性存取器,在存取的过程中限制对属性的一些行为,比如我们可以在setter中做一个判断来限制age不能小于0。
TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
- 对于一些不希望被任意修改的属性,可以将其设置为private
- 直接将其设置为private将导致无法再通过对象修改其中的属性
- 我们可以在类中定义一组读取、设置属性的方法,这种对属性读取或设置的属性被称为属性的存取器
- 读取属性的方法叫做setter方法,设置属性的方法叫做getter方法
class Person{ private _name: string; constructor(name: string){ this._name = name; } get name(){ return this._name; } set name(name: string){ this._name = name; } } const p1 = new Person('孙悟空'); console.log(p1.name); // 通过getter读取name属性 p1.name = '猪八戒'; // 通过setter修改name属性
抽象类
抽象类是专门用来被其他类所继承的类,它只能被其他类所继承不能用来创建实例, 抽象类中可以有抽象方法。抽象方法使用 abstract开头,没有方法体!抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写
abstract class Animal { name : string; constructor(name : string) { this.name = name } //定义一个抽象方法 //抽象方法使用 abstract开头,没有方法体 //抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写 abstract sayHello():void; } class Dog extends Animal { sayHello() { console.log('www') //在类的方法中 super 就表示当前类的父类 // super.sayHello(); } } const dog = new Dog('旺财'); dog.sayHello() //www
函数
在文章Typescript基本类型—下篇中,虽然我们讲述了一下函数,但是未免有点粗糙,这里的话将详细的展开对函数的描述!
函数是 JavaScript 应用程序的基础,它帮助你实现抽象层,模拟类,信息隐藏和模块。在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。TypeScript 为 JavaScript 函数添加了额外的功能,让我们可以更容易地使用。
和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数。
通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:
// 命名函数 function add(x, y) { return x + y } // 匿名函数 let myAdd = function(x, y) { return x + y }
函数类型
为函数定义类型
让我们为上面那个函数添加类型:
function add(x: number, y: number): number { return x + y } let myAdd = function(x: number, y: number): number { return x + y }
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型。
书写完整函数类型
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型:
let myAdd2: (x: number, y: number) => number = function(x: number, y: number): number { return x + y }
可选参数和默认参数
TypeScript 里的每个函数参数都是必须的。 这不是指不能传递 null或 undefined 作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是 undefined。 在 TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能。 比如,我们想让 lastName 是可选的:
在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时。 它们叫做有默认初始化值的参数。 让我们修改上例,把firstName 的默认值设置为 “A”。
function buildName(firstName: string = 'A', lastName?: string): string { if (lastName) { return firstName + '-' + lastName } else { return firstName } } console.log(buildName('C', 'D')) //C-D console.log(buildName('C')) //C console.log(buildName()) //A
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用 arguments 来访问所有传入的参数。
在 TypeScript 里,你可以把所有参数收集到一个变量里: 剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( …)后面给定的名字,你可以在函数体内使用这个数组。
function info(x: string, ...args: string[]) { console.log(x, args) //['c', 'b', 'a'] } info('abc', 'c', 'b', 'a')
函数重载
函数重载: 函数名相同, 而形参不同的多个函数 在 JS 中, 由于弱类型的特点和形参与实参可以不匹配, 是没有函数重载这一说的 但在 TS 中, 与其它面向对象的语言(如 Java)就存在此语法.
我们现在要做一个需求: 我们有一个add函数,它可以接收2个string类型的参数进行拼接,也可以接收2个number类型的参数进行相加
// 重载函数声明 function add(x: string, y: string): string function add(x: number, y: number): number // 定义函数实现 function add(x: string | number, y: string | number): string | number { // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 x + y if (typeof x === 'string' && typeof y === 'string') { return x + y } else if (typeof x === 'number' && typeof y === 'number') { return x + y } } console.log(add(1, 2)) console.log(add('a', 'b')) // console.log(add(1, 'a')) // error
小结
在本篇文章中带领着大家对面向对象有一个自我的认识,虽然在我们的js开发中我们几乎不会涉及到面向对象开发,但是在ts开发项目的时候面向对象显得是多么的重要,面向对象开发不仅提升了开发效率,更是对维护有着很大的遍历,在现如今主流的框架vue中,vue3+typescript的开发站主导地位,所以面向对象必须要有深刻的见解,并且会实践!