类理论
- 对象:具体的事物
- 类:对对象的抽象,可以看做对象的模板
- 多态:父类的行为可以被子类重写,相对性多态可以从重写行为之中引用基础行为
就好像人就是对象。搞对象搞对象嘛。那么你怎么知道站你对面的是人而不是棵树呢?有鼻子有眼会说话等等,总结(抽象)完了就是个类,包括人的一些特征(属性)和行为(方法)
面向对象编程强调的是数据和操作数据的行为,俩者本质上是关联的。所以好的设计就是把俩者封装起来,也被称为数据结构
- 类的设计模式
从没写过类是种设计模式,我们知道的观察者模式、单例模式等等都是面向对象设计模式。
其实类不是必须的编程基础,它是一种可选的代码抽象。Java里,类是必选的,万物皆类,C/C++里你可以选择面向类或者过程化(意大利面代码)或者混用 - Javascript中的类
JS只有一些近似类的语法元素(new、instanceof),ES6之中新增了class,不过这仅仅是语法糖
类的机制
很多面向类的语言的标准库会提供Stack类,不过你实际上并不是直接操作它,它仅仅是个抽象的描述,你需要实例化它,然后操作实例
- 建造
“类”和“实例”来源于房屋建造
建筑师会考虑好一栋建筑的特性,多高、几个窗户、窗户在哪等等,他并不关心这个房屋造在哪,也不关心造多少,也不关心房屋内的内容(家具等),他只关心用什么结构来容纳这些内容
建筑蓝图只是建筑计划,不是真正的建筑,我们还需要建筑工人根据蓝图造出来一栋真正的房子。
你可以通过蓝图来知道房子的结构,只观察房子本身并不能获得这些信息(有点像逆向工程)。你想开下窗户那么蓝图是不能满足你的,必须真正的房子才行
以上可知,类就是蓝图,我们为了得到可交互对象就必须按照类来建造(实例化)一个东西(实例)
你不大可能使用实例类直接访问操作它的类,不过你大致可以判断这个实例对象来自哪个类
类通过复制操作被实例化为对象(建筑本质上是对蓝图的复制) - 构造函数
类实例是由一个特殊的类方法够早的,这个方法和类名系统(构造函数),他的任务就是初始化实例所需要的所有信息(状态)
类构造函数属于类,大多需要new
来调用
class Car {
color = 'red'
Car(c) {
color = c
}
drive() {
console.log('drive')
}
}
myCar = new Car('blue') // 实际上是调用构造函数,通常使用new实例化,这样子引擎才知道你想构造一个类实例
myCar.drive()
类的继承
父类和子类就像父母和孩子。孩子一旦出生他就是单独的个体,虽然会从父母继承一些特性,不过他也是独一无二的。同理,子类也是独一无二的相对父类而言,可以重写继承的行为和定义新行为
- 多态
class Foo {
constructor(x) {
this.x = x
}
print() {
console.log('Foo print')
}
output() {
this.print()
console.log('Foo output')
}
}
class Bar extends Foo {
constructor(x, y) {
super(x)
this.y = y
}
print() {
console.log('Bar print')
}
// 重写继承父类的output,不过调用了super.output(),可以引用继承来的原始的方法
// 这个技术就叫多态或者虚拟多态。也叫相对多态
output() {
super.output()
console.log('Bar output')
}
}
var bar = new Bar(1, 2)
bar.output()
/**
*
Bar里的output通过多态引用了从Foo继承来的output方法,
Foo的output方法又引用了print方法(请注意,这里因为JS的原因加了this.,请忽略),
那么这里引用的print到底是Bar的还是Foo的呢?
其实是Bar的,除非你是实例化Foo调用的output,
也就是多态性取决于你在哪个类的实例中引用它
*
*/
相对只是多态的一个方面。任何方法都可以引用继承层次上高层的方法(名字一不一样无所谓),之所以说相对是因为没有指定层次,相对引用上一层
上面说到在真正的类而言,构造函数属于类,不过JS里,“类”(Foo.prototype,方法都挂在prototype上)属于构造函数(Foo)。父类和子类关系只存在于俩者构造函数对应的
.prototype
上,构造函数本身之间没什么关系,所以无法实现相对引用
下图箭头代表复制。为什么呢?因为子类Bar继承的父类Foo的方法其实只是一个副本。因为子类可以重写Foo的方法,还能使用多态引用原父类方法。那必须是只能复制一份副本才能互不影响。
类继承、实例化
所以继承、多态其实并不是子类和父类有关联,子类其实只是得到了父类的一个副本。类的继承就是复制
- 多重继承
我们以上讨论均在子类只有一个父类上,不过现实生活中后代一般都是有双亲的。所以多重继承更符合现实类比。也就是多个父类的定义会被复制到子类里。
但是问题就来了,俩父类都有output
方法,子类继承哪个?
还有如下图所示,假如A有output方法,B、C分别重写了该方法,那么D引用output时选择B还是C的
钻石问题
这个问题贼复杂,之后另起一章记录
混入
在JS中没有类,那么自然也就没有继承时的复制,那么为了模拟类的复制,人们就想了个方法--混入。其实就是对象的复制,然后混进一些特性。就像Object.assign
。
混入分为显性和隐性,显性就是很明显的把父类复制给子类,隐性就是没有那么明显,不过也做到了复制
- 显示混入
其实就是俩对象,然后浅拷贝了下,模拟了下继承。多态的话就用具体父类的具体方法call调用并且显示传递this,利用this的绑定特性来模仿多态。
值得注意的是,“继承”而来的方法复制的是引用。
function mixin(sourceObj, targetObj) {
// sourceObj是不能被破坏的
for (var key in sourceObj) {
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key]
}
}
return targetObj
}
var Foo = {
print() {
console.log('Foo print')
},
output() {
this.print()
console.log('Foo output')
}
}
var Car = mixin(Foo, {
output() {
Foo.output.call(this)
console.log('Bar output')
}
})
Foo.output()
Car.output()
- 再说多态
Foo.output.call(this)
,这个就是显示多态(都指定了具体“父类”了)。因为Foo和Bar都有output方法,不显示指定的话没辙
其实是因为俩名字一样,如果不一样就像print方法,就可以直接this.print()
调用。就是因为它被“复制”过来了
显示伪多态缺点就是在需要使用多态的地方创建函数关联(Foo.output.call(this)
),和支持相对多态的语言比起来,人家在头部轻轻松松一个extend就成了(class Bar extends Foo
).主要是“类”的规模大了就恶心了 - 混合复制
之前的mixin
有个存在性校验。换个思路,我先把父类(Foo)复制一个出来,然后对这个新对象进行子类特殊化处理。这样子就可以不用存在性检查了。不过想想相对而言也没啥效率
function mixin(sourceObj, targetObj) {
// sourceObj是不能被破坏的
for (var key in sourceObj) {
targetObj[key] = sourceObj[key]
}
return targetObj
}
var Foo = {
print() {
console.log('Foo print')
},
output() {
this.print()
console.log('Foo output')
}
}
var Car = mixin(Foo, {})
mixin({
output() {
Foo.output.call(this)
console.log('Bar output')
}
}, Car)
Foo.output()
Car.output()
- 复制的不是对象还行,是对象的话,就像这个print,父子共享的一个函数。因为复制的是引用,牵一发而动全身
- 显示混入其实并不是什么多厉害的东西。无非就是少了几条代码,还带来了问题
- 寄生继承
既是显式的也是隐式的
function Foo() {
}
Foo.prototype.print = function() {
console.log('Foo print')
}
Foo.prototype.output = function() {
this.print()
console.log('Foo output')
}
function Car() {
var car = new Foo()
// 关键在于这点保存了父类的引用
var fooOutput = car.output
// 屏蔽属性,详见下一章__proto__
car.output = function() {
fooOutput.call(this)
console.log('Bar output')
}
return car
}
new Foo().output()
// 值得注意的是,这里可以不用new,因为Car返回了Car实例,使用new的话得到的对象会被抛弃
new Car().output()
- 隐式混入
var Foo = {
set() {
this.val = 'value'
}
}
var Bar = {
set() {
// 隐式的把Foo混入Bar
// 其实就是通过this的绑定规则,在Bar的上下文上调用了Foo的set方法,所以Foo上的set方法赋值操作作用在Bar对象上
// 也就是Foo的行为混入到了Bar里
Foo.set.call(this)
}
}
Bar.set()
Foo.val // undefined
Bar.val // value
个人觉得总的来说没有什么意思,模拟类。无非就是少几行定义代码而已(当然是在ES5而言,ES6的语法糖还是不错的)
网友评论