前言
一共有三篇,这一篇写在JS中运用面向对象思想来编程。因为typescript不熟悉所以这部分在实际运用后再回来补。
内容来源于《你不知道的js》《阮一峰ES6入门》《JavaScript语言精粹》《JavaScript高级程序设计》《JavaScript设计模式》《JavaScript模式》《Typescript官网》《MDN web文档》
本博客没有什么有价值的知识,仅作总结梳理之用,初学者可以看看。
原型链
JS中有两条链,一条作用域链,闭包与this的指向与此链有关;一条是原型链,方法属性的查找和委托与此链有关。
如下面代码所示
let foo = {
a: 2
}
let bar = {}
bar.__proto__ = foo
bar.a //? 2
bar.__proto__=== foo //? true
bar.hasOwnProperty('a') //? false 没有a但是可以调用a
foo.hasOwnProperty('a') //? true
// __proto__它的含义是原型指向,但不是标准属性不推荐使用,应该用
Object.setPrototypeOf()(写操作)、
Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替
新建一个空白的bar对象,并将原型链至foo。如下图所示
在js中任何对象的原型链最终指向于Object.prototype
(它自身指向null)。在JS中,当访问对象属性或者调用函数时,如果未在当前对象找到,那么将会沿着原型链向上追溯,譬如bar.hasOwnProperty
方法,hasOwnProperty
是定义在Object.prototype
上的,但是bar却可以调用,同样的bar也可以访问foo的属性a,值为2,但是bar.hasOwnProperty('a')
可以看到false,说明其自身并不具有这个属性。
类的一些概念
因为这一篇是写类的,所以用词用语都是下方的用语,在js中是不准确的也是和实际情况有歧义的。但是还是要这么说。
- 类(Class): 定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通常通过
new
生成,是具体的事物 - 面向对象(OOP)三大特征:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口,外界调用端不需要也不可能知道细节,就能通过对外的接口来访问对象,同时也保证外界无法任意更改对象内不的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。
- 存取器(getter&&setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如
public
表示共有属性或方法 - 抽象类(Abstract Class):抽象类是供其他类继承的基类,本身不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口。
new
先看看new的时候发生了什么事情
function FunOne(a) {
this.a = a
this.b = function () {
return 3
}
}
let ObjOne = new FunOne(1)
ObjOne.a //?1
按照面向对象类的说法是,这个FunOne是构造函数,而new创建了一个新实例,也就是实例化的过程。
不过并不是这样,FunOne只是一个普通的函数和其他任何一个函数都没有什么区别,
函数可以立即执行可以当作返回值可以被函数或对象调用,这里就是new调用,FunOne是个普通的函数当它new调用时就被构造调用了。不过为了行文方便还是称之为构造函数,因为类总是要有构造函数的,那就假装它是吧。
记录下new调用的过程
new做了四件事:
- 一个全新的对象会凭空创建
- 这个新构建的对象被加入原型链
- 这个新构建对象被设置为函数调用的this绑定(即是this指向这个新对象)然后执行构造函数中的语句
- 返回这个对象
可以自己写个函数实现new的功能(毫无必要仅仅演示)
function build(name) {
//新建一个对象
let F = {}
//设置原型链
F.__proto__ = Foo.prototype
//运行构造函数内语句
F.name = name
//返回这个对象
return F
}
function Foo(name) {
this.name = name
}
let foo1 = build('Mike') //起到了和new一样的作用
foo1.name //?Mike
每一个函数都有一个prototype属性,且这个属性的值是对象,这个对象除了从构造函数复制过去的所有属性之外,还有个construcor属性且值为此构造函数,也就是这俩函数和对象互相为对方一个属性的值。如果查询一下会发现这个这个对象在控制台会输出FunOne{}也就是和构造函数同名了,不过为了不混淆,下文继续用Funone.prototype
表示这个对象,写为prototype对象。原型对象定义为这个对象的__proto__
指向的那个对象,名词指代清楚才能不搞混。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby','Court']
}
Person.prototype = {
constructor: Person, //因为Person.prototype对象被重新定义了所以要在新对象中添加上constructor属性来确认指向
sayName: function () {
console.log(this.name)
}
}
let person1 = new Person('Nicholas', 29, 'Software Engineer')
let person2 = new Person('Greg', 27, 'Doctor')
person1.friends.push('Van')
person1.friends //? [ 'Shelby', 'Court', 'Van' ]
person2.friends //? [ 'Shelby', 'Court' ]
person1.friends === person2.friends //? false
person1.sayName === person2.sayName //? true
person1.hasOwnProperty('sayName') //? false 实例person1并没有sayName方法
Person.prototype.hasOwnProperty('sayName') //? true 在这个prototype对象上
知道了它们的指向了。我们可以这样构造一个对象,虽然没什么用。这个构造方式能成立因为 bar
被Objec.prototype
创建并被链接到foo.prototype
这个对象上,然后bar
可以沿着原型链查找并调用foo.prototype
上的construcor属性,且此属性指向foo()
函数,最终bar调用了foo函数且函数内this指向为bar。
function foo(name) {
this.name = name
}
let bar = Object.create(foo.prototype)
bar.constructor('mike')
bar.name //? mike
总结: 我们现在有了构造函数,有了prototype对象可以当作类,也有看new语句当作实例化。离伪装成类又近了一步。
原型模仿继承
伪经典继承
《高程》上的例子
function SuperType(name){
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.sayColor = function () {
return 'blue'
}
SuperType.prototype.sayName = function(){
return this.name
}
function SubType(name, age){
SuperType.call(this, name)
this.age = age
}
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function(){
return this.age
}
let instance1 = new SubType("Nicholas", 29)
instance1.colors.push("black")
instance1.colors//"red,blue,green,black"
instance1.sayName()//"Nicholas";
instance1.sayAge() //29
let instance2 = new SubType("Greg", 27);
instance2.colors //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
SuperType.sayColor() //? 'blue'
instance1.sayColor() //? not a function
=========================================================
Object.getOwnPropertyDescriptors(SuperType.prototype)
//?{constructor:SuperType,sayName:fuc}
Object.getOwnPropertyDescriptors(SubType.prototype)
//?{construor:SubType,name:undefined,colors:"red,blue,green",sayAge:func}
Object.getOwnPropertyDescriptors(instance1)
//?{name:'Nicolas',colors:"red,blue,green,black",age:'29'}
Object.getOwnPropertyDescriptors(instance2)
//?{name:'Greg',colors:"red,blue,green",age:'27'}
先利用SubType.prototype = new SuperType()
得到一个对象只有name和colors的SubType.prototype
且与SuperType.prototype
链接,现在用construcor
再连回去连到SubType()
上。现在调用new SubType()
创建新对象时不仅运行了SubType()
内的语句并且用call
使SuperType
内语句在此环境运行,并且得到一个新的对象拥有name,colors和age属性。instance1
的方法分别定义在原型链上方的两个对象上,调用的时候沿链查找。sayColor()
为SuperType()
独有,其他不能调用且SubType
都不能继承。
现在有了父类,子类,实例。
extend 函数
这个方法在《高程》里被称为寄生组合方法用的函数名是interitPorototype
,这里用extend和interit。思路依然是原型链的链接,将子类与父类链接起来
借用一个空对象然后将child.prototype
和parent.child
联系起来,下面用的Object.create
方法可以少了一次new调用。child.prototype
上少了不需要的属性,同样的上文中的也可以如此替换,new F()
也是为了少创建个父类的新实例。都是为了减少调用父类的构造函数又起到链接的目的。
其余的就是将原来手工链接的语句放进函数里,基本上就是原来的省代码版,所以原型图不画了
function extend(child,parent) {
let F = function() {}
F.prototype = parent.prototype
child.prototype = new F()
child.prototype.constructor = child
}
function inherit(child, parent) {
let F= Object.create(parent.prototype)
F.constructor = child
child.prototype = F
}
复制下Typescript官网上由ts中class转译的继承代码,分为两部分,第一部分将子类构造函数与父类构造函数链接起来extendStatics
实现继承静态属性,第二部分和上面写法一个意思。
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
ES6中的Class
class是es6新增的,class本质上是语法糖,内在还是原型链。
class的写法
先看看class的写法
class Person {
constructor(name,age) {
this.name = name
this.age = age
}
static classMethod() {
return 'hello'
}
sayName() {
return this.name
}
}
Person.classProperty = 'hi'
let person1 =new Person('Mike',18)
person1.sayName() //? Mike
Person.classMethod() //? hello
Person.classProperty //? hi
语法很简单原来当作构造函数的里的语句被放在了constructor里面,函数本身的静态方法可以通过static添加,但是静态属性只能在类外添加。(提案可以将实例属性在class中的constructor外添加,且静态属性可以在类内添加。用babel转码可以使用,暂不讨论)。现在来测试一下并且画个原型链图
typeof Person //? functhion
Person === Person.prototype.constructor //? true
可以看出类也只是原型的另一种写法。
再测一下各个函数·对象拥有的属性
person1.classProperty //?undefined
person1.classMethod() //?not a function
Object.getOwnPropertyDescriptors(Person)
// {classMethod(), classproperty: 'hi',prototype: Person{}}
Object.getOwnPropertyDescriptors(Person.prototype)
// {contructor: Person(), sayName()}
Object.getOwnPropertyDescriptors(person1)
// {name: 'Mike', age:'18', sex'man'}
类中定义的原型属性都是不可枚举的,和new构造函数写法不同,那个是可枚举的。
定义在Person上的静态属性好好的保存在Person函数上并且实例person1无法调用,person1拥有自己的三个属性由类构造方法里的语句设置,实例可以通过原型链调用Person.prototype上的方法。和原型写法一样,除了枚举性。但是如果想要设置实例共享属性的话,就没办法在类上定义只能在SuperType.prototype
上定义,依然要用到原型。
class的继承
为了和《高程》上写的对比所以写相似的
class SuperType {
constructor (name){
this.name = name
this.colors = ["red", "blue", "green"]
}
sayName () {
return this.name
}
static sayColor() {
return 'blue'
}
}
class SubType extends SuperType {
constructor(name,age) {
super(name)
this.age = age
}
sayAge() {
return this.age
}
}
let instance1 = new SubType('Nicholas',29)
instance1.colors.push("black")
let instance2 = new SubType('Greg',27)
SubType.sayColor() //? 'blue'
instance1.colors //? ["red", "blue", "green",''black'']
instance1.sayAge() //? 28
instance1.sayName() //? Nicholas
很明显能看出了省了很多代码,
定义在原型上的方法现在可以直接定义在类里,直接用extends继承,也就是链接原型链时,比用new少了一次构造函数调用,且不用被constructor指向迷惑。super起到了和call类似作用。下面检测一下画个原型图看看和《高程》上的有没有区别
Object.getPrototypeOf(SubType.prototype)
//SuperType{} 即SuperType().prototype
Object.getPrototypeOf(SubType)
//SuperType()
Object.getOwnPropertyDescriptors(SuperType)
//{prototype: SuperType{} ,sayColor()}
Object.getOwnPropertyDescriptors(SuperType.prototype)
//{constructor: SuperType(),sayName()}
Object.getOwnPropertyDescriptors(SubType)
//{prototype: SubType{}}
Object.getOwnPropertyDescriptors(SubType.prototype)
//{constructor: SubType(), sayAge()}
Object.getOwnPropertyDescriptors(instance1)
//{name:'Nicolas',age:'28', colors:["red", "blue", "green","black"]}
Object.getOwnPropertyDescriptors(instance2)
//{name:'Greg',age:'27', colors:["red", "blue", "green"]}
原型链的链接和《高程》写法多了一个SubType和SuperType两个函数直接的链接,以往都是直接链到Function上。这样子类能直接“继承”父类的静态方法sayColor(),实例不可调用。
super的使用
在es6中新增了super关键字。
一种用法是用在子类的构造函数中,super()代表着父类的构造函数,必须写在子类构造函数this之前。起到的作用与SuperType.call(this, name)起到的作用是一样的。让父类的构造函数语句在子类的上下文环境下运行。
二种是super对象,在普通方法中,指向父类的prototype对象即SuperType.prototype,也就是说只有SuperType.prototype上的方法可以被调用,在子类上调用时父类原型方法中的this会指向当前子类,如果在实例上调用时,方法中的this会指向当前实例。
在静态方法中也就是static定义的方法中使用,此方法中指向父类。在子类调用通过super调用父类的静态方法时,this指向同样会变为当前子类。
用代码说明 还是用《高程》上的改造
class SuperType {
constructor (name){
this.name = name
this.colors = ["red", "blue", "green"]
this.size = 'big'
}
saySize() {
return this.size
}
sayName () {
return this.name
}
static sayColor() {
return this.colors
}
}
class SubType extends SuperType {
constructor(name,age,size) {
super(name)
this.size = size || 'medium'
this.age = age
super.saySize() //? small, medium 因为下面new了两个实例所以运行了两次都指向了Subtype.protype子类prototype对象
}
sayAge() {
return this.age
}
subSaySize() {
return super.saySize()
}
static subSayColor() {
return super.sayColor()
}
}
SubType.colors = ["red", "blue", "green",'white'] //静态属性暂时只能在外面添加
SuperType.sayColor() //? undefined 因为父类没定义静态属性
SubType.subSayColor() //?["red", "blue", "green",'white'] this指向了子类
let instance1 = new SubType('Nicholas',29, 'small')
let instance2 = new SubType('Greg',27)
instance1.subSaySize() //? small 指向子类实例本身
instance2.subSaySize() //? medium 指向子类实例本身
总结 super
总是会绑定到当前方法在_Prototype_
链中的位置的更高一层,
其实就是在哪调用就指向谁,相当于自动call(this),再就是自动识别是否是静态方法。
Typescript中的类与接口
ts是js的超集,添加了可选的静态类型和基于类的面向对象编程。
ts也是转译成js后运行的,但和es6语法糖不同,当在ts文件中写ts时,语法规则是按照ts来的,譬如加了private
属性在转译成js后失效,但是在写ts时是限制外部访问性了的。如果访问会报错。
同样的ts中增加了继承接口抽象类等新概念,虽然可以转译成js再分析在js中会如何,但是这样是没有意义的。因为在js中类只是语法糖是原型的伪装,如果不清楚具体原型链接,那就会写出不能如期运行的代码,但是ts中可以完全假装存在ts定义的类,因为写错了语法提示器会提示错误的。所以这节就不会画原型图并且分析属性的归属,这节按照完全Typescript中定义的类的想法去理解。
基本写法
和es6的class对比,基本一致,但是要先定义name
,不然会提示类型‘Person’上不存在属性name
class Person {
name: String
constructor(name: String) {
this.name = name
}
sayName() {
return this.name
}
}
let person1 = new Person('Mike')
person1.sayName() //?
测试一下发现依然和es6差不多依然是存在充当构造函数的函数,与函数的prototype属性,下面就不再测了。
继承
class SuperType {
name: string
colors: Array //此处写出array会报错,其他都能小写这个不行。不知原因是什么
constructor(name: string) {
this.name = name
this.colors = ["red", "blue", "green"]
}
sayName () {
return this.name
}
}
class SubType extends SuperType {
age: number
constructor(name: string,age: number) {
super(name)
this.age = age
}
sayAge() {
return this.age
}
}
let instance1 : SubType
instance1 = new SubType('Nicholas', 28)
instance1.sayAge() //?
instance1.sayName() //?
基本上一样,不赘述。
一些修饰符与特性
-
public
修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的 -
private
修饰的属性或方法是私有的,不能在声明它的类的外部访问 -
protected
修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的 -
readonly
修饰的属性或方法是只读的,只读属性必须在声明时或构造函数里被初始化 -
static
修饰的属性或方法是静态的,静态属性是定义在类本身上的而不是实力上,prototype对象与实例都不能读取 - 参数属性 参数属性可以方便地让我们在一个地方定义并初始化一个成员,少了一个赋值操作
class SuperType {
constructor(public name: string) {
}
sayName () {
return this.name
}
}
===========================================
function SuperType(name) {
this.name = name
}
SuperType.prototype.sayName = function () {
return this.name;
}
- 存取器:与es6一样在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
- 抽象类:抽象类是供其他类继承的基类,本身不允许被实例化。抽象类中的抽象方法必须在子类中被实现
接口
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implements)。TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对对象的形状(Shape)进行描述。作用就是为这些类型命名和为你的代码或第三方代码定义契约。
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
没有调查就没有发言权,js中本来就没有强制类型,这个接口又是用类型约束对象、函数、类,没使用过。抄官网也没意思,不写了,如果以后用了有心得了再返回来写。
总结
首先,js中原来没有类,连类的概念都没有,一开始只能用prototype模仿,在es6中新加入了class增加了类的写法,但是依然只是语法糖,可以看见和原来的写法基本没区别,还是伪装成的类。在Typescript中的类功能比较完备,并且编写时有较好的错误提示。如果非要用类与继承的思路来写代码就用Typescript。