作为前端也要懂的面向对象编程

随着ES6语法的全面普及,面向对象的思想在前端领域也变得更加的重要,作为后端转过来的前端,更加明白面向对象在编程中的优势。
而写这篇文章的缘由是因为在一个群里看到的一条面试题,以及若干个群友的回答。

题目

我需要一只碗,今天可以用来吃饭,明天可以用来喝粥,一般我会吃白米饭或者糙米饭,也会喝瘦肉粥或者白粥,请编写一些Javascript类来实现我的需求。

先来看看群友们的回答

来自某群友的答案:

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

}

Bowl.prototype.eatRice = function (rice) {
console.log(rice)
}

Bowl.prototype.drinkZhou = function (zhou) {
console.log(zhou)
}

var bowl = new Bowl()

bowl.eatRice('whiteRice')

// bowl.drinkZhou('whiteZhou')

这里只放这一个回答吧,因为其他人的答案都跟这个大同小异。如果由我来评审的话,这个答案无限接近0分。原因如下:

  1. 没有深刻了解面向对象编程。
  2. 扩展性差,如果后天要吃面的话,就只能在原型上增加方法,如此下来,代码会越来越臃肿。并且每次有加的食物,都需要改动到类代码。

解析

要解答这条题,先要审题,究竟题目要考什么。编写一些Javascript类,从这几个关键字不难看出,需要我们为题目中出现的对象抽象出相应的类。

我们来看看题目中出现的几个对象:

  1. 米饭
  2. 糙米饭
  3. 瘦肉粥
  4. 白粥
  5. 我 (人)

其中第6个对象是最容易被忽视的,群友们的回答基本都是把的行为赋给了碗,但是碗能够自己吃喝吗?

因此,我们还需要定义一个Person类。

Bowl类

首先来定义一下碗的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Bowl {
constructor () {
// 定义content用来表示碗里面有什么
this.content = null
}

// 把食物放进碗中
put (food) {
if (this.content) {
throw new Error('碗不为空')
}
this.content = food
}
}

这个例子中,暂时不考虑食物本身的体积和碗的容积,可以看到,碗自身的属性在这道题目里面其实就一个content,用来记录碗里面装了什么。

然后看到put方法,这里面我们需要知道装进来的东西是什么,只需要给定一个food接口实现类。

Food类

接下来我们看看米饭,糙米饭,瘦肉粥,白粥,其实他们都可以抽象为叫食物的类,因此我们可以定义一个食物类:

1
2
3
4
5
6
7
8
9
10
11
class Food {
constructor ({
name = '',
materials = []
}) {
// 食物的名称
this.name = name
// 食物的材料
this.materials = materials
}
}

到这里,那我们要表示四种食物的时候可以怎么做呢?我们可以再继承出一个饭类和粥类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 饭类
class Rice extends Food {
constructor (props) {
super(props)
// 用米
this.materials.push(props.rice)
}
}

// 粥类
class Porridge extends Food {
constructor (props) {
super(props)
// 用米
this.materials.push(props.rice)
}
}

这里我把饭类和粥类分开定义是为了提高贴近题目清晰度,而实际上,粥类完全可以继承饭类,只要增加一个含水量的属性,当含水量高的时候,饭自然会成为粥。另外看到,其实rice也是Food中材料的一种,因此我们并不需要额外的属性去保存米饭,只要把它也丢进去材料就行。

OK,现在有了这两个类,表示四种食物就没难度了:

1
2
3
4
5
6
7
8
// 米饭
const rice = new Rice({ name: '米饭', rice: '白米'})
// 糙米饭
const brownRice = new Rice({ name: '糙米饭', rice: '糙米'})
// 瘦肉粥
const meatPorridge = new Porridge({ name: '瘦肉粥', materials: ['瘦肉'], rice: '白米'})
// 白粥
const purePorridge = new Porridge({ name: '白粥', rice: '白米'})

接下来就是验证四种食物的时候,我们要怎么能证明meatPorridge就是粥呢?怎么证明brownRice是米饭呢?很简单,看下面:

1
2
3
4
5
6
7
8
meatPorridge instanceof Porridge // true
meatPorridge instanceof Rice // false

brownRice instanceof Porridge // false
brownRice instanceof Rice // true

brownRice instanceof Food // true
meatPorridge instanceof Food // true

可以看到,它们都分别对应上了应该对应的父类,以及它们都是Food的子类。

Person类

到了这里,我们就结束了吗?当然不是,还有最后的一个类Person。定义Person的原因是,吃和喝是属于人类的行为,而不是碗的行为,因此我们来简单定义一个可以吃和喝的人:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
constructor (props) {
// 简单的给他一个名字
this.name = props.name
}

eat (bowl) {
if (!bowl.content) {
throw new Error('给一个空碗我干嘛?')
}
bowl.content = null
console.log(`${this.name}吃完啦`)
}

drink (bowl) {
if (!bowl.content) {
throw new Error('给一个空碗我干嘛?')
}
bowl.content = null
console.log(`${this.name}喝完啦`)
}
}

最后我们来表达一下题目:

我需要一只碗,今天可以用来吃饭,明天可以用来喝粥,一般我会吃白米饭或者糙米饭,也会喝瘦肉粥或者白粥,请编写一些Javascript类来实现我的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 首先定义一个我
const me = new Person({ name: '靓仔' })
// 给一个碗
const bowl = new Bowl()
// 今天吃饭
const rice = new Rice({ name: '米饭', rice: '白米'})
// 饭装进碗中
bowl.put(rice)
me.eat(bowl)
// 明天喝粥
const meatPorridge = new Porridge({ name: '瘦肉粥', materials: ['瘦肉'], rice: '白米'})
// 饭装进碗中
bowl.put(rice)
me.drink(bowl)

至此这道题就可以结束了。

稍等,那扩展性如何呢?

这是我觉得其中一个很重要的考点,回顾一下群友的回答,他的代码如果要增加新的食物的话就需要改动到碗的代码,并且原型会原来越臃肿。反观我们的答案,完全不需要动到刚才我们写得任何一个类,只需要增加一个Noodle类:

1
2
3
4
5
6
7
class Noodle extends Food {
constructor (props) {
super(props)
// 面条种类
this.materials.push(props.noodle)
}
}

这样就可以了。

放飞思维

其实这道题除了考面向对象编程思维以外,更多是再考对代码的把控能力。