把大象装进冰箱-面向对象编程

背景知识

从编程懵懂期,大学老师就在讲台上强调面向对象,面向对象巴拉巴拉。从业至今写的代码并没有把这种编程思维展现的淋漓尽致,这篇文章开始梳理js中的设计模式,梳理一下多年以来的编程思路。


面向过程和面向对象

这个概念本人是从毕向东毕老师的java入门开始理解的,毕老师说了一个非常形象生动的例子,即把大象装进冰箱:

面向过程:冰箱.open() -> 冰箱.put(‘大象’) -> 冰箱.close()

面向对象:冰箱.push(‘大象’)

面向过程的思路是亲力亲为,我们开门,放进去,关门,中间的过程我们一清二楚。而面向对象的观念是冰箱你提供给我一个api,这个api可以放进去东西,具体你是开门还是锯门,i dont care,我只需要知道我调用你的api即可把我的大象放进去。


js中的面向对象

js基础系列一共写了4篇文章,分别是对象类型,原型链,this和闭包。个人感觉把js中的难点和重点都梳理了一遍,也是为js进阶做铺垫。虽说基础决定代码质量,但是设计模式和编程思路才是过关斩将的工作利器,有好体格更要有强大的武器。后面的代码就不会去过多的解释一些其中的原理,有什么晦涩难懂的地方尽量去翻看一下之前的文章,基本都有说明。

js对象

都知道js中函数和对象是互相继承的,因为之前的文章有详细介绍过彼此的关系,所以我们这里把函数对象和对象归为一类统称为对象。

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
let Car = function () {}

Car.prototype = {
dirve: function () {
console.log('开车')
return this
},
turnOnLight: function () {
console.log('开灯')
return this
},
turnOffLight: function () {
console.log('关灯')
return this
},
carAdaptations: function (name, fn) {
this[name] = fn
return this
}
}

let car = new Car()

car.dirve().turnOnLight().turnOffLight().carAdaptations('jump',function () {
console.log('车翻了')
return this // <- 注意return this
}).jump().turnOffLight() // 开车,开灯,关灯,车翻了,关灯

我们定义了一个车对象,该对象有默认4个方法,分别是开车,开灯,关灯和改装。我们在所有方法中包括通过改装的jump方法中都返回了this即Car对象本身所以我们实现了Car对象的链式调用。

看到这里。我们是不是发现这种调用方式非常的熟悉,很像下面的代码:

1
2
3
4
5
6
7
8
9
10
$.fn.extend({
hello:function(){
console.log('hello');
return this // <- 注意 return this
}
});

$('html').css('color','#ffffff').hello().css('font-size','30px').on('click', function () {
console.log(111)
}).css('background-color', '#000000')

对,就是我们jquery的调用方式。虽然jquery的fn.extend方法不像我们carAdaptations方法一样直接挂在原型上,但是实现原理是一样的。当然别人比我们考虑的要多得多!

注:在添加自定义方法的时候切记return this,否则无法加入链式调用大军。因为只有return this才能在函数执行完成拿到对象本身继续进行下一次调用,如果return this不存在那么在该函数执行结束后继续链式调用会报错。

封装

面向对象编程的第一步,即将我们的冰箱封装成为一个工具,冰箱提供的方法内部实现不需要我们关心,我们只需要知道冰箱提供给我们的api即可。下面我们来用车的例子来写两种不同的封装。

1.闭包实现对象封装

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
let CarClosure = (function () {
console.log('CarClosure 被实例化啦')
let carComputer = { // 私有属性
name: 'carCompouter'
}

function carComputerOpen() { // 私有构建方法
/*
* 省略代码
* */
}

function _carClosure(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
this.carComputer = carComputer // <- 仅用来验证是否只在内存中出现一次 私有方法和私有变量是不应该向外部提供的
this.carComputerOpen = carComputerOpen // <- 同上
}

_carClosure.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany',
}
return _carClosure
})()

let carClosure1 = new CarClosure()
let carClosure2 = new CarClosure('red')

2.通过安全模式创建的类

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
let CarSafe = function (color, brand, country) {
if (!(this instanceof CarSafe)) {
return new CarSafe(color, brand, country)
}
console.log('CarSafe 被实例化啦')
let carComputer = { // 私有属性
name: 'carCompouter'
}

function carComputerOpen() { // 私有构建方法
/*
* 省略代码
* */
}

this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
this.carComputer = carComputer // <- 仅用来验证是否只在内存中出现一次 私有方法和私有变量是不应该向外部提供的
this.carComputerOpen = carComputerOpen // <- 同上
}

CarSafe.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany'
}

let carSafe1 = new CarSafe()
let carSafe2 = new CarSafe()
let carSafe3 = CarSafe('red')

我们通过两种模式创建了两个实例化对象相同的类,第一种叫做闭包封装,顾名思义即通过闭包机制封装的对象。第二种叫安全模式封装的对象即原型封装,顾名思义无论是否通过new关键字来创建对象都是安全的。

那么通过这两种方式创建出来的对象有什么差别呢,我们统一执行一下代码:

1
2
3
4
5
6
console.log(carClosure1, carClosure2)
console.log(carClosure1.carComputerOpen === carClosure2.carComputerOpen)
console.log(carClosure1.carComputer === carClosure2.carComputer)
console.log(carSafe1, carSafe2, carSafe3)
console.log(carSafe1.carComputerOpen === carSafe2.carComputerOpen)
console.log(carSafe1.carComputer === carSafe2.carComputer)

有意思的事情来了。我们通过闭包机制实现的封装,在实例化两个对象时,carClosure只实例化了一次,而通过安全模式实现的封装每实例话一个对象carSafe即要实例话一次。于是乎我们猜想carSafe和carClosure内部私有化方法以及私有变量,carClosure中只在内存中出现一次,而carSafe则会出现多次。所以在执行验证代码中我们输出了

1
2
3
4
console.log(carClosure1.carComputerOpen === carClosure2.carComputerOpen)
console.log(carClosure1.carComputer === carClosure2.carComputer)
console.log(carSafe1.carComputerOpen === carSafe2.carComputerOpen)
console.log(carSafe1.carComputer === carSafe2.carComputer)

通过控制台输出的两个true和两个false也可以验证咱们的猜想。所以说两种封装机制在引擎内部执行的机制并不相同。各有利弊取舍看场景。

思考题来了。在写博客的时候有考虑过对闭包封装做检察长适配无new实例化,但是并没有完美解决代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let _carClosure = function (color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
return this
}

_carClosure.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany',
}

let CarClosure = (function () {
let _this = {}
_this.__proto__ = _carClosure.prototype
return _carClosure.bind(_this)
})()

console.log(CarClosure(), new CarClosure())

虽然代码中bind的_this已经做了_carClosure的原型链继承,但是在浏览器描述中并没有体现出来,不知道是不是自执行函数还是闭包对此有影响查证半天也无果。各位看官如果有自己的理解或者说上面的代码有错误的地方欢迎指正。(可以通过个人qq联系我。在微博的关于中有个人联系方式,非常欢迎拍砖。)

继承

说到继承就有很多东西可以扯一下,因为js没有明确的类定义,所以说在js中的继承种类非常的多。我们挑几个有特点的继承方式说一下。分别是:类式继承,构造函数继承,组合继承,原型式继承和寄生式继承。我们先写一个类,然后通过不通方式来实现继承。

声明类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Car(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
this.type = 'car'
this.shoe = ['Michelin', 'Michelin', 'Michelin', 'Michelin']
this.changeShoe = function (count, brand) {
this.shoe[--count] = brand
}
}

Car.prototype = {
color: 'black',
brand: 'Benz',
country: 'Germany',
drive: function () {
console.log('车开了')
},
}

类式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function MyCar() {}

MyCar.prototype = new Car() //为MyCar实例化出的对象添加上Car的内置方法

var car1 = new MyCar()
var car2 = new MyCar()

console.log(car1.color, car1.brand, car1.country) // black Benz Germany 注1

console.log(car1 instanceof MyCar) // true 注2
console.log(car1 instanceof Car) // true
console.log(MyCar instanceof Car) // false
console.log(MyCar.prototype instanceof Car) // true

car2.drive() // 车开了 注3
car2.changeShoe(1, 'BRIDGESTONE')
console.log(car1.shoe) // ["BRIDGESTONE", "Michelin", "Michelin", "Michelin"]

我们来看看类式继承的实现过程和特点,拿出我们之前的Car的例子稍加改造。然后再定义个MyCar类,然后把Car的实例赋给MyCar的原型。都知道原型对象(prototype)是为该类实例话对象添加共有方法,MyCar.prototype = new Car()的作用就是为MyCar实例化出的对象添加上Car的内置方法。于是乎MyCar的实例即继承了Car的属性和方法。我们完成了一个类式继承。

通过注1,我们可以发现car1确实拿到了Car类下的所有属性。方法当然也是有的,通过注3我们就能知道。

注释2表示的是继承关系的结构。看看就行,跟我们分析的实现过程差不多。

注3得强调一下,我们通过换轮胎的通用方法修改了car2的轮胎,但是结果car1的轮胎也跟着一起变了。这是为什么。我们来分析一下:我们来看car1和car2是MyCar的实例,MyCar.prototype是Car的实例。也就是说MyCar创建的实例,下所有的实例引用的属性都指向了同一个Car实例的属性,也就是说car1和car2的引用数据类型都指向了同一个Car实例下的引用数据类型。对引用数据类型有疑问的同学请移步JS中数据类型剖析。当我们修改了car2的轮胎类型时,修改的是被MyCar实例化的Car.shoe该内存地址下的数据。所以我们car1下的数据也同时发生了改变。当然不通过内置方法修改的结果也是一样,不信的话请自行尝试。

再说我们在实例化car1和car2的时候,其实MyCar已经完成了对Car的继承。所以我们无法在实例化car1和car2的时候去设置Car的可变属性,这也是类式继承的一个缺点!

构造函数继承

1
2
3
4
5
6
7
function MyCar(color, brand, country) {
Car.call(this, color, brand, country)
}

let myCar = new MyCar('blue')
console.log(myCar) // MyCar {color: "blue", brand: undefined, country: undefined, type: "car"}
console.log(myCar.drive()) // myCar.drive is not a function

首先我们来看构造函数式的继承是什么,即通过执行父类的构造函数获取父类的共有方法,并且给父类绑定当前类下的this来实例化当前类的对象。大白话说,MyCar实例化了一个对象,并且把这个对象丢给Car,然后Car对这个对象进行了一些修改再做返回。

this和call我们也不多说了,有疑问请移步This到底是个什么东西。我们通过call方法改变了Car的this,所以Car的this已经不是拥有其默认属性的this,而是我们通过call传递给Car的MyCar的this。所以我们无法获取到Car.prototype下的属性。只能拿到Car构造函数所修改的this。这也是为什么我们能拿到type,blur。而brand和country无法获取的原因。

因为color是通过参数传递进去的,而type是Car构造函数中添加的。所以说要通过构造函数实现继承,哪必须把所有需要继承的方法和属性放在构造函数中,而子类创建的实例中,每个实例都会单独拥有一份属性和方法并且不能公用。这样代码的复用性就大大的降低了。

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyCar(color, brand, country) {
Car.call(this, color, brand, country)
}

MyCar.prototype = new Car()

var car1 = new MyCar()
var car2 = new MyCar()

console.log(car1.color, car1.brand, car1.country) // red Benz Germany

car1.drive() // 车开了
car2.changeShoe(1, 'BRIDGESTONE')
console.log(car1.shoe) // ["Michelin", "Michelin", "Michelin", "Michelin"]

组合继承顾名思义就是类式继承和构造函数继承一起。我们通过类式继承拿到共有方法,然后每次实例实例化MyCar的时候再实例化一次Car所以car1和car2并没有指向同一个Car实例。当然他们继承的引用数据类型的属性并没有指向同一个地址。也就不会发生冲突,且继承了Car.prototype的方法。

正因为我们在类式继承时实例化了一次Car,且在每次实例化MyCar时都会实例化一次Car,所以该继承方式也是有缺点的,因为Car的构造函数会被多次调用。

原型式继承

所谓原型式继承即为:通过已有的对象创建一个新的对象,且不必创建新的对象类型。拿代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function platfrom(o) {
function PLATFROM() {
}
PLATFROM.prototype = o
return new PLATFROM()
}

var car = {
color: 'black',
brand: 'Benz',
country: 'Germany',
shoe: ['Michelin', 'Michelin', 'Michelin', 'Michelin'],
drive: function () {
console.log('车开了')
},
}

let newCar = new platfrom(car);
console.log(newCar.shoe) // ["Michelin", "Michelin", "Michelin", "Michelin"]
newCar.shoe[0] = 'BRIDGESTONE'
console.log(new platfrom(car).shoe) // ["BRIDGESTONE", "Michelin", "Michelin", "Michelin"]

和类式继承很像,只不过原型式继承更像通过流水线或者平台去copy一个产品原型。car即为产品原型,我们拿平台方法platfrom去copy产品原型可以得到无数量车来实现继承。此类模式的继承和类式继承一样在操作引用数据类型时也会发生连锁改变。具体原因跟类式继承相同。

寄生式继承

如果构造函数式继承是类式继承的升级版,那么寄生式继承则为原型式继承的升级版。上代码

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
function platfrom(o) {
function PLATFROM() {
}

PLATFROM.prototype = o
return new PLATFROM()
}

var car = {
color: 'black',
brand: 'Benz',
country: 'Germany',
drive: function () {
console.log('车开了')
},
}

function carFactory(obj) {
var o = new platfrom(obj)
o.shoe = ['Michelin', 'Michelin', 'Michelin', 'Michelin']
o.changeShoe = function (count, brand) {
this.shoe[--count] = brand
return this
}
return o
}

let newCar = carFactory(car)
let newCar2 = carFactory(car)

console.log(newCar.shoe) // ["Michelin", "Michelin", "Michelin", "Michelin"]
console.log(newCar.changeShoe(1, 'BRIDGESTONE').shoe)
console.log(newCar2.shoe) // ["BRIDGESTONE", "Michelin", "Michelin", "Michelin"]

寄生式继承可以理解为:嗯哼,我大众很厉害,有原型产品,有生产产品的平台,有MQB有PQ35,还有一汽和上汽。我把原型和平台给到工厂。工厂再添加一些自己的配件于是乎捷达和桑塔纳,朗逸和宝来,帕萨特和迈腾都问世了!

寄生组合式继承

先贴代码再BB。不以代码为基础的扯淡都是耍流氓!注意前方高能,代码又绕又复杂,请备好纸巾!

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
function platfrom(o) { // 平台
function PLATFROM() {}
PLATFROM.prototype = o
return new PLATFROM()
}

function platfromEnhance(carFactory, carModule) { // 增强平台
var p = platfrom(carModule.prototype)
p.constructor = carFactory
carFactory.prototype = p
}

function CarModule(color, brand, country) { // 产品原型
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
}

CarModule.prototype = { // 产品原型属性
color: 'black',
brand: 'Benz',
country: 'Germany',
drive: function () {
console.log('车开了')
}
}

function Volkswagen(color) { // 造车工厂
CarModule.call(this, color)
this.platform = 'PQ35'
this.color = color ? color : this.color
this.brand = 'Volkswagen'
this.shoe = ['Michelin', 'Michelin', 'Michelin', 'Michelin']
}

platfromEnhance(Volkswagen, CarModule) // 工厂借鉴

// 不要写成Volkswagen.prototype = {} 形式。该形式为赋值形式,会覆盖CarModule的方法
// 工厂添加自定义方法
Volkswagen.prototype.changeShoe = function (count, brand) {
this.shoe[--count] = brand
return this
}

Volkswagen.prototype.getShoe = function () {
return this.shoe
}

var car1 = new Volkswagen()
var car2 = new Volkswagen('red')

console.log(car1)
console.log(car2)

console.log(car1.changeShoe(1, 'BRIDGESTONE').getShoe())
console.log(car2.getShoe())
car1.drive()

先说一下每个注释的解释,这个东西是啥,要它来干啥,有它的好处。一个漂亮的代码段当然是用更少的代码实现更美个功能。
平台:定义一个空函数,该函数仅做继承使用。在造车方面,我们可以理解成框架和底盘。
增强平台:给平台附加一些功能,从模型中拿一些功能和属性添加在平台上面。
产品原型&产品默认属性:我们产品的构造函数,添加默认属性和方法。
造车工厂:实例化量产车型的工厂。用来copy产品原型,添加自定义方法或者修改默认属性。

再说各个功能之间的关系。他们是怎么连接起来的。
先看平台和工厂借鉴。其实所有产品,除了发明者以外都属于借鉴。比如汽车这个东西当然是奔驰发明的汽车,所以其他品牌无论怎么说,说到底都属于借鉴奔驰的方法。哪怕它有自己强大的流水线,在最初的时候也是借鉴发明者的想法,思路和技术。类比一下国内的有名车企。这里没有任何指责的意思,站在巨人肩上当然可以走的更快看的更远。
在这里得多谢好多人,因为本人的博客也是借鉴各路大神的书以及文章才能写的出来的。例举几本个人觉得比较好的书大家可以看看:
《JavaScript高级程序设计》作者是(美)(Nicholas C.Zakas)扎卡斯 —— 入门必,提升必看,看一遍一遍的看。
《你不知道的javascript》【作者】[美]辛普森( Kyle Simpson ) —— 好书,闭包,this等等都讲的特别好。茅塞顿开。
《JavaScript设计模式》张容铭 —— 设计模式写的很好很生动,本篇就是从这边书梳理来的。
其他就是一些概念的整理查阅,就很多了,比如MDN、方应航方方老师等各路知乎大神。虽然没有直接提问和引用,到底还是看过你们的文章才能初窥js门径的。

好了先扯着么多,扯的有点远,但是怎么说,其实任何人都离不开借鉴。

回归正题,说我们的平台和工厂借鉴。我们在执行platfromEnhance(Volkswagen, CarModule)这句代码的时候,把车模型的原型交给平台,平台按照模型构建了一个对象框架反回给增强平台。然后优化框架让框架符合造车工厂的流水线,p.constructor = carFactory不能量产的汽车都不是好对象!当然流水线生产出的汽车得有车的默认属性和方法。即carFactory.prototype = p。

从逻辑层面我们说明的其中的关系。下面我们从代码底层描述一下这几者的关系:

梳理一下还是比较好理解的。

关于多继承

多继承其实有太多太多解决方案了,其实无非是复制或者其他方式来实现多对象继承。后面单独写一篇博客吧,再揉在一起的话篇幅太长了。

多态

多态是什么意思呢,即同一个方法会根据传入参数的不同拿到不同的结果。然后很多人就在这里想到了java中的函数重载,其实不然。函数重载虽然是通过同一个函数名调用,但是在底层的逻辑中,重载的函数并不是同一个方法。

而多态调用的是同一个函数,只是拿到的结果不同。写个简单的例子

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
function buyCar() {
let carBrandList = ['Volkswagen', 'Audi', 'BMW', 'Benz', 'Porsche']

function randomCar(count) {
let countN = count ? count : 2
return carBrandList[parseInt((Math.random() * 10) / countN)]
}

function buyCarAndMoney(money) {
if (isNaN(money)) {
throw 'type of money is error'
}
let moneyCount = money / 10000
if (moneyCount < 3) {
return '这点钱就不要凑热闹了!'
} else if ((3 <= moneyCount) && (moneyCount < 20)) {
return randomCar(10)
} else if ((20 <= moneyCount) && (moneyCount < 50)) {
return randomCar(2.5)
} else {
return randomCar()
}
}

this.buyCar = function () {
switch (arguments.length) {
case 0:
return randomCar()
case 1:
return buyCarAndMoney(arguments[0])
}
}
}

let myCar = new buyCar()

console.log(myCar.buyCar()) // 5选一随机
console.log(myCar.buyCar('3000')) // 根据金额随机

冰箱我要把大象放进去

把大象放进冰箱和冰箱我要把大象放进去的区别仅仅是思维模式的不同,这也就是面向过程编程和面向对象编程的区别。

因为在大型项目构建的时候并不是说一个人能完成的,我们需要配合,而面向对象的思维更适合这种协作式的开发,并且可移植性及扩展性更强!

而js也不是那个仅仅用验证用户名和密码的脚本语言。它可以完成更多的功能,适用更多的场景。当然我们的编程思维也需要迭代更新。让自己的代码更加健壮、优雅可移植性更强,我想是每一个程序员应有的基本素质吧。