文章出處

一、題目介紹

 以下是我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 采用模塊模式來編寫代碼

(function(window, undefined){

})(window);

4.2 聲明一個變量taskList,用來存儲需要隊列信息

(function(window, undefined){
    var taskList = [];
})(window);

 隊列中,單個項的存儲設計為一個json,存儲需要觸發的消息,以及方法執行時需要的參數列表。比如LazyMan('Hank'),需要的存儲信息如下。

{
    'msg':'LazyMan',
    'args':'Hank'
}

 當執行LazyMan方法的時候,調用訂閱方法,將需要執行的信息存入taskList中,緩存起來。
 存儲的信息,會先保留著,等發布方法進行提取,執行和輸出。

4.3 訂閱方法

 訂閱方法的調用方式設計:subscribe("lazyMan", "Hank")

(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 發布方法

(function(window, undefined){
    var taskList = [];

        // 訂閱方法 代碼...

    // 發布
    function publish(){
        if(taskList.length > 0){
            run(taskList.shift());
        }
    }
})(window);

 將隊列中的存儲信息讀取出來,交給run方法(暫定,后續實現)去執行。這里限定每次發布只執行一個,以維持隊列里面的方法可以挨個執行。
 另外,這里使用shift()方法的原因是,取出一個,就在隊列中刪除這一個,避免重復執行。

4.5 實現LazyMan類

// 類
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的包裝方法

// 輸出文字
function lazyManLog(str){
    console.log(str);
}

 為什么還要為console.log包裝一層,是因為在實戰項目中,產經經常會修改輸出提示的UI。如果每一處都用console.log直接調用,那改起來就麻煩很多。
 另外,如果要兼容IE等低級版本瀏覽器,也可以很方便的修改。
 也就是DRY原則(Don't Repeat Youself)。

4.7 實現具體執行的方法

// 具體方法
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方法,用于識別要調用哪個具體方法,是一個總的控制臺

// 鴨子叫
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,讓外部可以調用

(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,但是實現的方式我不是很喜歡和認同,但是也是一種思路,這里順便貼出來給大伙看看。
如何實現一個LazyManhttp://web.jobbole.com/89626/


閱讀原文:www.jianshu.com/p/f1b7cb456d37


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()