一、題目介紹
以下是我copy自網上的面試題原文:
實現一個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.方法鏈式呼叫
2.類的使用和麵向物件程式設計的思路
3.設計模式的應用
4.程式碼的解耦
5.最少知識原則,也即 迪米特法則(Law of Demeter)
6.程式碼的書寫結構和命名
三、題目思路解析
1.看題目輸出示例,可以確定這是擬人化的輸出,也就是說:應該編寫一個類來定義一類人,叫做LazyMan。可以輸出名字、吃飯、睡覺等行為。
2.從輸出的句子可以看出,sleepFrist的優先順序是最高的,其他行為的優先順序一致。
3.從三個例子來看,都得先呼叫LazyMan來初始化一個人,才能繼續後續行為,所以LazyMan是一個介面。
4.句子是按呼叫方法的次序進行順序執行的,是一個佇列。
四、採用觀察者模式實現程式碼
4.1 採用模組模式來編寫程式碼
1 2 3 |
(function(window, undefined){ })(window); |
4.2 宣告一個變數taskList,用來儲存需要佇列資訊
1 2 3 |
(function(window, undefined){ var taskList = []; })(window); |
佇列中,單個項的儲存設計為一個json,儲存需要觸發的訊息,以及方法執行時需要的引數列表。比如LazyMan(‘Hank’),需要的儲存資訊如下。
1 2 3 4 |
{ 'msg':'LazyMan', 'args':'Hank' } |
當執行LazyMan方法的時候,呼叫訂閱方法,將需要執行的資訊存入taskList中,快取起來。
儲存的資訊,會先保留著,等釋出方法進行提取,執行和輸出。
4.3 訂閱方法
訂閱方法的呼叫方式設計:subscribe("lazyMan", "Hank")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
(function(window, undefined){ var taskList = []; // 訂閱 function subscribe(){ var param = {}, args = Array.prototype.slice.call(arguments); if(args.length < 1){ throw new Error("subscribe 引數不能為空!"); } param.msg = args[0]; // 訊息名 param.args = args.slice(1); // 引數列表 if(param.msg == "sleepFirst"){ taskList.unshift(param); }else{ taskList.push(param); } } })(window); |
用一個param變數來組織好需要儲存的資訊,然後push進taskList中,快取起來。
特別的,如果是sleepFirst,則放置在佇列頭部。
4.4 釋出方法
1 2 3 4 5 6 7 8 9 10 11 12 |
(function(window, undefined){ var taskList = []; // 訂閱方法 程式碼... // 釋出 function publish(){ if(taskList.length > 0){ run(taskList.shift()); } } })(window); |
將佇列中的儲存資訊讀取出來,交給run方法(暫定,後續實現)
去執行。這裡限定每次釋出只執行一個,以維持佇列裡面的方法可以挨個執行。
另外,這裡使用shift()方法的原因是,取出一個,就在佇列中刪除這一個,避免重複執行。
4.5 實現LazyMan類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 類 function LazyMan(){}; LazyMan.prototype.eat = function(str){ subscribe("eat", str); return this; }; LazyMan.prototype.sleep = function(num){ subscribe("sleep", num); return this; }; LazyMan.prototype.sleepFirst = function(num){ subscribe("sleepFirst", num); return this; }; |
將LazyMan類實現,具有eat、sleep、sleepFrist等行為。
觸發一次行為,就在taskList中記錄一次,並返回當前物件,以支援鏈式呼叫。
4.6 實現輸出console.log的包裝方法
1 2 3 4 |
// 輸出文字 function lazyManLog(str){ console.log(str); } |
為什麼還要為console.log包裝一層,是因為在實戰專案中,產經經常會修改輸出提示的UI。如果每一處都用console.log直接呼叫,那改起來就麻煩很多。
另外,如果要相容IE等低階版本瀏覽器,也可以很方便的修改。
也就是DRY原則(Don’t Repeat Youself)。
4.7 實現具體執行的方法
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 |
// 具體方法 function lazyMan(str){ lazyManLog("Hi!This is "+ str +"!"); publish(); } function eat(str){ lazyManLog("Eat "+ str +"~"); publish(); } function sleep(num){ setTimeout(function(){ lazyManLog("Wake up after "+ num); publish(); }, num*1000); } function sleepFirst(num){ setTimeout(function(){ lazyManLog("Wake up after "+ num); publish(); }, num*1000); } |
這裡的重點是解決setTimeout執行時會延遲呼叫,也即執行緒非同步執行的問題。只有該方法執行成功後,再發布一次訊息publish()
,提示可以執行下一個佇列資訊。否則,就會一直等待。
4.8 實現run方法,用於識別要呼叫哪個具體方法,是一個總的控制檯
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 鴨子叫 function run(option){ var msg = option.msg, args = option.args; switch(msg){ case "lazyMan": lazyMan.apply(null, args);break; case "eat": eat.apply(null, args);break; case "sleep": sleep.apply(null,args);break; case "sleepFirst": sleepFirst.apply(null,args);break; default:; } } |
這個方法有點像鴨式辨型介面,所以註釋叫鴨子叫
。
run方法接收佇列中的單個訊息,然後讀取出來,看訊息是什麼型別的,然後執行對應的方法。
4.9 暴露介面LazyMan,讓外部可以呼叫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(function(window, undefined){ // 很多程式碼... // 暴露介面 window.LazyMan = function(str){ subscribe("lazyMan", str); setTimeout(function(){ publish(); }, 0); return new LazyMan(); }; })(window); |
介面LazyMan裡面的publish方法必須使用setTimeout進行呼叫。這樣能讓publish()
執行的執行緒延後,掛起。等鏈式方法都執行完畢後,執行緒空閒下來,再執行該publish()
。
另外,這是一個對外介面,所以呼叫的時候,同時也會new 一個新的LazyMan,並返回,以供呼叫。
五、總結
1. 好處
使用觀察者模式,讓程式碼可以解耦到合理的程度,使後期維護更加方便。
比如我想修改eat
方法,我只需要關注eat()
和LazyMan.prototype.eat
的實現。其他地方,我都可以不用關注。這就符合了最少知識原則。
2. 不足
LazyMan.prototype.eat
這種方法的引數,其實可以用arguments代替,我沒寫出來,怕弄得太複雜,就留個優化點吧。
使用了unshift和shift方法,沒有考慮到低版本IE瀏覽器的相容。
六、完整原始碼和線上demo
完整原始碼已經放在我的gitHub上
原始碼入口:https://github.com/wall-wxk/blogDemo/blob/master/2017/01/22/lazyMan.html
demo訪問地址:https://wall-wxk.github.io/blogDemo/2017/01/22/lazyMan.html
demo需要開啟控制檯,在控制檯中除錯程式碼。
七、番外
網上有人也實現了lazyMan,但是實現的方式我不是很喜歡和認同,但是也是一種思路,這裡順便貼出來給大夥看看。
如何實現一個LazyMan:http://web.jobbole.com/89626/