提起类,不得不说一下,强类型编程语言,如php,java,c++等都有类的概念。而js作为一门弱类型语言,是没有类这个概念的,虽然也能模拟类的实现,但总归不是类。so,ts也只是模拟类而已,使得更贴切那些强类型编程语言。
面向对象编程(oop)
首先,我们得树立起这么一个思维,那就是面向对象编程。那什么是面向对象编程呢?面向对象程序设计(Object Oriented Programming)是软件开发方法。面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。
而面向对象的三大基本特性即 封装,继承和多态。
封装
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。什么意思呢?我们代码演示下
// 定义一个父类,有name属性和prientName方法 class Parent { constructor (name) { this.name = name; } printName () { console.log(`My name is ${ this.name }`) } }
上面的代码就是实现了对parent类的封装,我们不必要关心里面的实现逻辑,只需要使用它对我们暴露的接口,比如传入name值,调用printName方法,即可完成我们想要的结果。
继承
继承,指可以让某个类型的对象获得另一个类型的对象的属性的方法。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。接下来我们代码演示下
// 定义一个父类,有name属性和prientName方法 class Parent { constructor (name) { this.name = name; } printName () { console.log(`My name is ${ this.name }`) } } // 定义一个子类并且继承父类的属性和方法 class Child extends Parent { constructor (name, age) { super(name); this.age = age; } printAge () { console.log(`My age is ${ this.age}`) } } let p1 = new Child('小胖纸', 22); p1.printName(); // My name is 小胖纸 p1.printAge(); // My age is 22
上面的代码中,我们定义了父类parent,子类child,以及实例对象person,通过继承,我们的子类具备了父类的属性和方法。以及子类自己的属性和方法。这样我们把公共的,重复的属性和方法定义为父类,子类则定义自己的属性和方法,这样就实现了对父类进行扩展。
多肽
多肽,是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。大致总结下就是,父类定义一个方法不去实现,让继承它的子类去实现而且每一个子类都有不同的体现,另外多肽也是继承的一种。我们来看代码
// 定义一个父类,有name属性和prientName方法 class Parent { constructor (name) { this.name = name; } // 这里我们只做定义,声明父类有这个方法 printName () {} } // 定义一个子类并且继承父类的属性和方法 class ChildOne extends Parent { constructor (name) { super(name); } printName () { console.log(`我是个${ this.name }`) } } // 在定义一个子类同样继承父类的属性和方法 class ChildTwo extends Parent { constructor (name, age) { super(name); this.age = age; } printName () { console.log(`我是个${ this.age }岁的${ this.name }`) } } let p1 = new ChildOne ('大胖纸'); let p2 = new ChildTwo ('小胖纸', 22); p1.printName // 我是个大胖纸 p2.printName // 我是个22岁的小胖纸
上面我们在父类中定义了printName方法,但是没做具体实现,都是让继承它的子类去实现,ChildOne 子类中只打印了大胖纸,而ChildTwo 方法不止有名字还有年龄,不同的实现方式,这就是多肽。实际中我们也用到过,比如
function print () { console.log(this.name) } var a = { name: '我是a', print: print } var b = { name: '我是b', print: print } a.print() // 我是a b.print() // 我是b
这个例子中,我们就复用了print函数,让一个函数在不同的情况下有不同的实现方式。
函数重载
函数重载不属于面向对象的三个特点,但是我们也经常用到,顺便写在这里。所谓函数重载,就是函数名称一样,但是输入输出不一样。或者说,允许某个函数有各种不同输入,根据不同的输入,调用不同的函数,然后返回不同的结果。什么意思呢?翻译过来就是函数名可以一样,根据参数的不同来调用不同的方法。嗯?js有这个嘛?js定义函数不是后一个会覆盖前一个同名函数嘛?怎么有重载呢?对的,js没有重载,但是我们可以模拟。上代码
// 强类型语言的重载 function a (number) { console.log(number) } function a (number, string) { console.log(number, string) } a(1); // 1 a(1, '小胖纸') // 1, 小胖纸 // js模拟的重载 function b (number, string) { if (string) { console.log(number, string) } else { console.log(number) } } a(1); // 1 a(1, '小胖纸') // 1, 小胖纸
这就是属于js的函数重载,简单的来讲就是通过判断arguments,通过arguments我们来进一步判断,来决定我们接下来的逻辑。
es5定义类
工厂模式 // 创建一个模版函数 function person (name, age) { // 通过Object来实例化一个对象 var obj = new Object(); // 给实例化对象添加属性 obj.name = name; obj.age = age; // 给实例化对象添加方法 obj.print = function () { console.log('hello ts'); }; // 返回实例化对象 return obj; } // 调用函数来获取对象 var p1 = person('小胖纸', 22);
但是这种方法也有一个问题,那就是我们无法辨别对象,因为他们统一为Object
构造函数
// 创建一个构造函数 function Person (name, age) { // 给实例对象添加属性 this.name = name; this.age = age; // 给实例化对象添加方法 this.print = function () { console.log('hello ts'); }; } // 通过new 关键字来实例化对象 var p1 = new Person('小胖纸', 22);
通过new构造函数创建对象的过程:
(1) 首先创建一个空对象(此过程会自动在堆内存中分配空间)。
(2) 在空对象中创建一个this,将构造函数的作用域赋值给了this,等于在构造函数内部调用了Person.call(p1),这样构造函数内部的上下文就指向了实例化对象。简单来说就是谁通过new构造函数创建了对象,那么构造函数内部的this就指向该实例化对象。
(3) 设置实例化对象的_proto_属性指向构造函数的prototype,即
p1.__ proto __ === Person.prototype,然后顺序执行构造函数,为实例化对象挂载属性和方法。
(4) 判断构造函数返回的是不是对象,若是则返回该对象,不是则返回new出来的实例化对象。这句话什么意思呢?首先需要明白构造函数和普通函数的区别,构造函数一般首字母大写,并且内部通过this来创建属性和方法,并且通过关键字new来创建对象,而普通函数则不是。一般而言构造函数是你没有返回值的,但万一出现返回值呢?上代码
function Person (name, age) { // 给实例对象添加属性 this.name = name; this.age = age; // 给实例化对象添加方法 this.print = function () { console.log('hello ts'); }; return 1; } var p1 = new Person('小胖纸', 22); p1.name // 小胖纸
因为构造函数返回的是number类型,或者boolean、string类型,所以构造函数会正常返回通过new创建的实例化对象。
function Person (name, age) { // 给实例对象添加属性 this.name = name; this.age = age; // 给实例化对象添加方法 this.print = function () { console.log('hello ts'); }; return { name: '大胖纸' }; } var p1 = new Person('小胖纸', 22); p1.name // 大胖纸
这个时候因为构造函数返回的是引用类型,所以会改写构造函数的返回值。我们通过new创建的就不是实例化对象,而是{ name: ‘大胖纸’ }。
构造函数的方式虽然确定了对象的归属问题,能够确定对象的类型,但构造函数中的方法需要在每个对象中都要重新创建一遍。
原型模式
// 创建构造函数 function Person (name, age) { // 在构造函数的原型上对其挂载属性和方法 Person.prototype.name = name; Person.prototype.age = age; Person.prototype.print = function() { console.log('hello ts'); }; } // 通过new 关键字来实例化对象 var p1 = new Person('小胖纸', 22);
虽然原型模式每一个属性只会被创建一次,但是一旦更改其中一个对象的属性,就会影响到所有通过new创建的实例化对象。通过new关键字创建,和上述创建过程一样。同时创建的过程中,js会默默的给你加上这么个东东Person.prototype.constructor === Person,即构造函数的prototype属性是一个对象,其constructor属性等于该构造函数,有点绕,看图
这张图也解释了new Person的__proto__ === Person的prototype。
我们上面也说了工厂模式,构造函数模式和原型模式都有缺点,而构造函数和原型模式的优缺点却恰恰相反,那把他们组合一下不就完美了。bingo,那就是接下来的
组合模式
通过组合模式,我们即解决了重复创建属性和方法的性能问题,又能识别对象,还不用污染通样通过new该对象所创建出来的实例化对象。上代码
function Person (name) { // 给实例对象添加属性 this.name = name; // 给实例化对象添加方法 this.print = function () { console.log('hello ts'); }; } Person.prototype.age = 22; Person.prototype.noPrint = function () { console.log('hello js'); }
公共的属性和方法放到原型上,而私有的则放在构造函数内部。
在多说一嘴,es5类的静态属性和方法。静态属性和方法是属性构造函数的,调用方式是Person.属性和Persoan.方法,静态属性和方法实例化对象是访问不到的。简单的例子来解释下
function Person (name) { // 给实例对象添加属性 this.name = name; } // 构造函数的静态属性 Person.age = 18; // 构造函数的静态方法 Person.print = function () { console.log('hello js') } var p1 = new person('小胖纸'); p1.name // 小胖纸 p1.age // undefined p1.print() // undefined Person.age // 18
es6定义类
其实,es6的类是模仿那些高级语言的。其本质还是调用es5的方法。你可以理解为es5的语法糖,嗯,真甜。
es6,通过class关键字来定义类名,class 类名 {}这样就完成了es5类的创建,而constructor则对应着es5的构造函数。上代码
class Person { // 定义属性 constructor (name) { this.name = name; } // 定义方法 切记中间没有逗号 print () { console.log('hello ts'); } } let p1 = new Person('小胖纸'); p1.name // 小胖纸 p1.print() // hello ts
这样就完成了es6类的定义,当然es6同样也存在静态属性和静态方法。上代码
class Person { // 定义属性 constructor (name) { this.name = name; } static age = 18 // 定义方法 切记中间没有逗号 print () { console.log('hello ts'); } static printClassName () { console.log(Person.name) } } let p1 = new Person('小胖纸'); p1.name // 小胖纸 p1.print() // hello ts p1.age // undefined Person.age // 18
通过static关键字我们来为类定义静态属性和方法
ts定义类
基本上ts的定义类和es6是一样的,只不过加上了类型而已,看代码
class Person { // 必须定义name的属性,public 共有,private 私有,protected 受保护 默认为public name: string; // 定义属性 constructor (name: string) { this.name = name; } // 定义方法 切记中间没有逗号,没有返回值,指定方法类型为void print (): void { console.log('hello ts'); } } let p1: Person = new Person('小胖纸'); p1.name // 小胖纸 p1.print() // hello ts
public
pbulic指明这个类中的属性或者方法为公有,即可以在类里面,子类以及类外面调用
// 定义一个父类,有name属性和prientName方法 class Parent { public name: string; constructor (name: string) { this.name = name; } printName () { console.log(`My name is ${ this.name }`) } } // 定义一个子类并且继承父类的属性和方法 class Child extends Parent { constructor (name: string, age: number) { super(name); this.age = age; } printAge () { console.log(`My name is ${ this.name }, ${ this.age} years old`) } } let p1: Child = new Child('小胖纸', 22); // 父类中可以使用name属性 p1.printName(); // My name is 小胖纸 // 子类中可以使用name属性 p1.printAge(); // My name is 小胖纸 , 22岁啦 // 类外部可以访问 p1.name // 小胖纸
private
private指明这个类中的属性或者方法为私有,即可以在类里面调用,子类以及类外面无法调用
// 定义一个父类,有name属性和prientName方法 class Parent { private name: string; constructor (name: string) { this.name = name; } printName () { console.log(`My name is ${ this.name }`) } } // 定义一个子类并且继承父类的属性和方法 class Child extends Parent { constructor (name: string, age: number) { super(name); this.age = age; } printAge () { console.log(`My name is ${ this.name }, ${ this.age} years old`) } } let p1: Child = new Child('小胖纸', 22); // 父类中可以使用name属性 p1.printName(); // My name is 小胖纸 // 子类中可以使用name属性 p1.printAge(); // My name is undefined , 22岁啦 // 类外部可以访问 p1.name // undefined
protected
protected指明这个类中的属性或者方法为受保护,即可以在类里面,子类调用,类外面无法调用
// 定义一个父类,有name属性和prientName方法 class Parent { protected name: string; constructor (name: string) { this.name = name; } printName () { console.log(`My name is ${ this.name }`) } } // 定义一个子类并且继承父类的属性和方法 class Child extends Parent { constructor (name: string, age: number) { super(name); this.age = age; } printAge () { console.log(`My name is ${ this.name }, ${ this.age} years old`) } } let p1: Child = new Child('小胖纸', 22); // 父类中可以使用name属性 p1.printName(); // My name is 小胖纸 // 子类中可以使用name属性 p1.printAge(); // My name is 小胖纸 , 22岁啦 // 类外部可以访问 p1.name // undefined
至此,定义类已经全部结束,如果有错误的地方,敬请指明。