随着ES6语法的全面普及,面向对象的思想在前端领域也变得更加的重要,作为后端转过来的前端,更加明白面向对象在编程中的优势。
而写这篇文章的缘由是因为在一个群里看到的一条面试题,以及若干个群友的回答。
题目
我需要一只碗,今天可以用来吃饭,明天可以用来喝粥,一般我会吃白米饭或者糙米饭,也会喝瘦肉粥或者白粥,请编写一些Javascript类来实现我的需求。
先来看看群友们的回答
来自某群友的答案:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function 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分。原因如下:
- 没有深刻了解面向对象编程。
- 扩展性差,如果后天要吃面的话,就只能在原型上增加方法,如此下来,代码会越来越臃肿。并且每次有加的食物,都需要改动到类代码。
解析
要解答这条题,先要审题,究竟题目要考什么。编写一些Javascript类,从这几个关键字不难看出,需要我们为题目中出现的对象抽象出相应的类。
我们来看看题目中出现的几个对象:
- 碗
- 米饭
- 糙米饭
- 瘦肉粥
- 白粥
- 我 (人)
其中第6个对象是最容易被忽视的,群友们的回答基本都是把吃和喝的行为赋给了碗,但是碗能够自己吃喝吗?
因此,我们还需要定义一个Person
类。
Bowl类
首先来定义一下碗的类:1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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
11class 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
8meatPorridge 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 | class Person { |
最后我们来表达一下题目:
我需要一只碗,今天可以用来吃饭,明天可以用来喝粥,一般我会吃白米饭或者糙米饭,也会喝瘦肉粥或者白粥,请编写一些Javascript类来实现我的需求。
1 | // 首先定义一个我 |
至此这道题就可以结束了。
稍等,那扩展性如何呢?
这是我觉得其中一个很重要的考点,回顾一下群友的回答,他的代码如果要增加新的食物的话就需要改动到碗的代码,并且原型会原来越臃肿。反观我们的答案,完全不需要动到刚才我们写得任何一个类,只需要增加一个Noodle
类:1
2
3
4
5
6
7class Noodle extends Food {
constructor (props) {
super(props)
// 面条种类
this.materials.push(props.noodle)
}
}
这样就可以了。
放飞思维
其实这道题除了考面向对象编程思维以外,更多是再考对代码的把控能力。