投身烈火
Keep Walking

微信的一道面试题 -- LazyMan

看到标题就应该知道,没错,我又来水了~怎么样?来打我呀打我呀打我呀~噗噗噗噗~

话说前两天在知乎上看到篇文章【周二放送】LazyMan 面试题详解

具体题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")输出:
Hi! This is Hank!
LazyMan("Hank").sleep(10).eat("dinner")输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
LazyMan("Hank").eat("dinner").eat("supper")输出
Hi This is Hank!
Eat dinner~
Eat supper~
LazyMan("Hank").sleepFirst(5).eat("supper")输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
以此类推。

其实里面是有视频详细解读的,但是我没看……

所以我就直接说我看到题目时的思考过程吧:

  1. LazyMan的调用不需要实例化,如果想只用一个函数定义LazyMan的话,其内部应该有个够生成实例类,LazyMan返回的就是这个类的实例,以便支持后续方法的调用。
  2. 后续的方法是链式调用的,所以每个方法都得现在原型链上,并且返回this。
  3. 需求里要求支持sleep操作,因为js里面没有类似的原生api,所以得使setTimeout来实现,也就是异步操作了。
  4. 说到异步队列,自然而然就想到了promise,但是由于sleepFirst需要在整个链之前加入暂停操作,用promise不太好实现,逻辑会有些绕。
  5. 比较稳妥的方案是自己实现个任务队列,和任务队列的自动执行机制。
  6. 还需要两个函数,负责将任务插入到队列的最前面和最后面。

好的,分析完了基本上实现的框架就有了,下面是我写的答案:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
function LazyMan(name) {
// 这个是用来实现LazyMan功能的类
// 直接放在这里完全是为了只用一个函数就实现LazyMan的功能
// 要想省资源,还是应该放在外面独立定义的
class Lazy {
constructor(name) {
// 由于LazyMan执行后肯定会有一个输出
// 所以初始化任务队列时就加入一个固定动作
this._actionlist = [(next) => {
console.log(`Hi! This is ${name}!`);
next();
}];
// 这里用Promise.resolve().then(fn)也可以
// 但是兼容性没setTimeout好
setTimeout(this._run.bind(this), 0);
}
sleepFirst(time) {
return this._first(this._sleep(time));
}
eat(food) {
return this._next((next) => {
console.log(`Eat ${food}~`);
next();
});
}
sleep(time) {
return this._next(this._sleep(time));
}
// 生成暂停功能的函数工厂
// 主要是为了降低代码重复率
_sleep(time){
return (next) => {
setTimeout(() => {
console.log(`Wake up after ${time}`);
next();
}, time * 1000);
};
}
// _next和_first用来向队列中加入任务
// 也可以考虑用concat替换unshift和push
// 因为ie6和ie7里unshift有bug……
_next(fn) {
if (fn && typeof fn === 'function') {
this._actionlist.push(fn);
return this;
}
}
_first(fn) {
if (fn && typeof fn === 'function') {
this._actionlist.unshift(fn);
return this;
}
}
// 执行队列的方法,里面递归调用,让队列能自动执行
// 最早的时候是把_next和_run的功能写在一起的
// 但是考虑到以后可能要扩展执行队列时传入上一个任务的结果的功能
// 拆成独立函数更好实现
_run() {
const { _actionlist: actionlist, _run } = this;
if (actionlist.length) {
actionlist.shift()(_run.bind(this));
}
}
}
return new Lazy(name);
}
console.log('start:');
LazyMan("Hank").eat('cake').sleepFirst(3).sleep(5).eat("dinner").sleep(3).eat('super');

可以在jsbin上开到执行的效果,jsbin: lonunab

刚才说到用 Promise 也能实现,但是逻辑有些别扭,主要的坑有:

  1. 使用 Promise 的话,要在LazyMan实例上留个属性,保存一个 promise 实例。但是在其他方法中想继续给这个 promise 实例添加后续的方法,不能直接调用then方法,而是要调用then方法之后,对内部保存实例的属性进行重新赋值。
  2. 要在向队列前面插入异步操作,单独为sleepFirst写个还行,要想要扩展更多的类似功能的函数就费劲了……

以下是使用 Promise 实现的相同功能,可以对比参考一下:

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
function LazyMan(name) {
class Lazy {
constructor(name) {
this.sleepFirstTime = 0;
this.promise = Promise.resolve().then(
() => this.sleepFirstTime && this._sleep(this.sleepFirstTime)
).then(() => {
console.log(`Hi! This is ${name}!`);
});
}
sleepFirst(time) {
this.sleepFirstTime = time;
return this;
}
eat(food) {
this.promise = this.promise.then(() => {
console.log(`Eat ${food}~`);
});
return this;
}
sleep(time) {
this.promise = this.promise.then(()=>this._sleep(time));
return this;
}
_sleep(time){
return new Promise((next) => {
setTimeout(() => {
console.log(`Wake up after ${time}`);
next();
}, time * 1000);
});
}
}
return new Lazy(name);
}
console.log('start:');
LazyMan("Hank").eat('cake').sleepFirst(3).sleep(5).eat("dinner").sleep(3).eat('supper');

js bin: tuyacoc

其实整个程序还是有优化空间的,比如替换 promise 属性的逻辑可以再抽一个函数;还有在队列前插入任务的逻辑,用另一个新的 promise 实例来替代等等,因为晚上去团建了而且还喝了点儿酒,实在是懒得写了,有兴趣的同学可以试试,让用 Promise 实现的逻辑也简单优雅起来~

好的那么由于时间不足,本期的博客就写到这里了,为啥这次更新的内容这么少?因为光一个updateListeners我就看了一上午……(T_T)……如果不出意外的话,maybe可能也许大概下周五会更新吧,能不能准时更新,就全看米娜桑点赞转发安利留言的力度了~!

最后附上一张今天团建的照片,其实我跟人是很英俊的,拍成这样完全是因为他们丫黑我……(T_T)

memememememememe

祝各位晚安&周末愉快了~白了个白~!