JS的鏈式編程

前言

鏈式編程實際是將多個方法(函數)通過某種方式鏈接在一起,使多個邏輯塊能按流程逐步執行(或跳過執行),從而實現解耦,在js上最典型的鏈式代碼:

/* 鏈式 */
console.log(
    [1,2,3,4]
        .concat(5)
        .filter((item)=>(item<3))
        .concat(6)
        .join("")
);  // 輸出 126

/* 非鏈式 */
const arr = [1,2,3,4];
const arr1 = arr.concat(5);
const arr2 = arr1.filter((item)=>(item<3));
const arr3 = arr2.concat(6);
console.log(arr3.join(""));

實現鏈式反應的本質為:每次該對象(Object-A)調用其方法(Method-1)時,返回值仍為本對象(Object-A),從而后面使用鏈式的方式再調用另外一個方法(Method-2)時,得到的this仍為原對象(Object-A),然后返回值同樣(Object-A),從而仍可通過鏈式的方式再調用該對象上的別的方法(Method-3),以此類推。

在js上常見的鏈式編程有以下幾種具體應用:

  • 對象方法return this的鏈式操作
  • Promise
  • 責任鏈(Chain of responsibility)

它們有不同的目標與思路,下面就逐一介紹~

一、對象方法

對同一個對象不斷執行相同或不同的方法

jQuery不用多說了吧,jQuery里面有很多的方法的使用方式就是此類形式,如:

$("#myDiv")
    .css('color','red')
    .html('<p>123</>')
    .appendTo('<div>dddd</div>')

我們來寫一個對象里掛載多個方法:

var myObj = {
    name: '',
    setName: function(newName) {
        this.name = newName;
        // 要實現在調用setName方法后仍能鏈式調用myObj的其他方法就必須返回this,即返回myObj
        return this;
    },
    addStr: function(str) {
        this.name += str;
        return this;
    },
    consoleName: function() {
        console.log(this.name);
        return this;
    }
};

myObj
    .setName('帥哥')
    .addStr('就是我')
    .consoleName();     // 輸出'帥哥就是我'

上面的三個方法的返回值都為this,所以每次調用之后返回值均為myObj,下面我們驗證下:

var obj1 = myObj.setName('帥哥');
var obj2 = obj1.addStr('就是我');
var obj3 = obj2.consoleName();
console.log(obj1 === myObj);    // true
console.log(obj2 === myObj);    // true
console.log(obj3 === myObj);    // true

既然obj1、obj2、obj3、myObj都是同一個,那我們不就可以合并代碼了嘛,不需要每次都多聲明一個變量:

/* 第一次簡化 */
myObj.setName('帥哥');
myObj.addStr('就是我');
myObj.consoleName();

/* 第二次簡化 */
myObj.setName('帥哥').addStr('就是我').consoleName();

二、Promise

將多個異步邏輯塊解耦,并使其能按序執行,若其中一個出現錯誤則退出鏈式,直接進入catch

Promise為ES6新特性,用于避免寫出沖擊波式代碼(callback hell),那么就會有人問,什么是沖擊波代碼了,給你們瞧一瞧:

getData(x => {
    getMoreData(x, y => {
        getPerson(person => {
            getPlanet(person, (planet) => {
                getGalaxy(planet, (galaxy) => {
                    getLoca(planet, (galaxy) => {
                        console.log(galaxy);
                    });
                });
            });
        });
    });
});

如果你想的話,還可以弄得更大更長~:smirk::smirk:

下面先來個最簡單的Promise使用:

var myPromise = new Promise(function(resolve, reject) {
    // 一秒鐘后執行resolve方法
    window.setTimeout(resolve, 1000);
});
myPromise.then(function() {
    // 一秒鐘之后將會進入此callback
    console.log('!');
});

可以看到構造Promise對象需要傳入一個Function,該Function接受兩個參數,分別是resolvereject,前者作為成功回調,后者作為失敗回調

下面展示如何使用Promise來封裝異步請求的發送與處理

/* 封裝異步請求 */
function getUserInfo(userId){
    return new Promise(function(resolve,reject){
        if(!userId){
            reject('userId不能為空');
            return;
        }
        // 異步請求
        ajax({
            url:'./getUserInfo',
            method:'GET',
            params:{userId},
            success:function(res){
                resolve(res);
            },
            error:function(){
                reject('請求錯誤');
            }
        })
    });
}

/* 調用 */
getUserInfo()
    .then(function(data){
        console.log(data)
    })
    .catch(function(msg){
        console.log(msg)
    });
// 最后輸出 'userId不能為空'

那么來修改剛開始的沖擊波代碼:

function getUserInfo(obj){
    return new Promise(function(resolve,reject){
        if(!obj.id){
            reject('對象id不能為空');
            return;
        }
        // 使用定時器來模擬異步請求(或其他異步操作)
        window.setTimeout(
            ()=>resolve(obj),
            3000
        );
    })
}
function getUserLocal(obj){
    return new Promise(function(resolve,reject){
        if(!obj.lastIP){
            reject('對象的IP不能為空');
            return;
        }
        window.setTimeout(resolve,2000);
    })
}
getBaseInfo({id:null,lastIP:123})
    .then(function(obj){
        return getUserDetail(obj);
    })
    .catch(function(errMsg){
        //在最后加catch的話,如果then中某處出現了錯誤,這不再繼續執行下面的語句,直接執行catch,并且將錯誤信息傳給catch
        console.log(errMsg);
    })
// 最后會輸出(console) '對象id不能為空'

Promise中的catch會捕捉當前鏈式中的最終的錯誤(the eventual error)

三、責任鏈(Chain of responsibility)

劃分多個任務(責任)塊,按序執行,每個任務塊都有權決定是否繼續交給下一個任務塊

簡單的來講,就像是面試一樣:

  • 人事篩選簡歷,如果簡歷信息各項符合就交給技術負責人,否則就沒有然后了
  • 技術負責人面試,如果技術過關了交給主管
  • 主管面試,如果各方面都合適了交給老板
  • 老板....以此類推

其中這一個個的就是任務塊(handler)

下面來個栗子:

// 任務塊:篩選性別
const genderHandler = function(next, data) {
    if(data.gender === 'male') {
        console.log('我們不要男的');
        return;
    }
    next(data);
};
// 任務塊:篩選年齡
const ageHandler = function(next, data) {
    if(data.age > 30) {
        console.log('年齡太大了');
        return;
    }
    next(data);
};
// 任務塊:最終處理函數
const finalSuccHandler = function(next, data) {
    console.log('emmmm...不錯不錯');
};


import Chain from './chain.js';
// 使用Chain來構建鏈式,類似于“建立生產線”
const peopleChain = new Chain()
    .setNextHandler(genderHandler)
    .setNextHandler(ageHandler)
    .setNextHandler(finalSuccHandler);


/* 往責任鏈上載入不同的信息 */
peopleChain.start({
    gender: 'male',
    age: 21
});     // 輸出 '我們不要男的'

peopleChain.start({
    gender: 'female',
    age: 48
});     // 輸出 '年齡太大了'

peopleChain.start({
    gender: 'female',
    age: 18
});     // 輸出 'emmmm...不錯不錯'

構造簡單的Chain類,用以構建鏈式:

// chain.js
class Chain {
    handlers = [];      // 處理函數集合,用于存儲當前鏈式上所有的func
    cache = [];         // 緩存,用于存儲當前鏈式上還未觸發的func

    /* 設置下一個 handler */
    setNextHandler(fn) {
        if (typeof fn !== "function") {
            throw new Error("[chain] successor must be a function.");
        }
        this.handlers.push(fn);
        return this;
    }

    next() {
        if (this.cache && this.cache.length > 0) {
            let ware = this.cache.shift();    // 釋放隊頭 handler
            ware.call(
                this,
                this.next.bind(this),       // 遞歸
                arguments && arguments[0]
            );
        }
    }

    /* 開始觸發鏈式 */
    start() {

        // 將 [this.handlers] 復制一份,賦給 [this.cache]
        this.cache = this.handlers.map(function(fn) {
            return fn;
        });

        // 主動觸發第一個 handler
        this.next(arguments[0]);
    }
}
export default Chain;

在vue、react、小程序等框架中使用的話,鏈式內部可能需要使用到上下文(this),需要看下面的栗子:

// chain.js
class Chain {
    handlers = [];      // 處理函數集合,用于存儲當前鏈式上所有的func
    cache = [];         // 緩存,用于存儲當前鏈式上還未觸發的func
    context = null;     // 上下文,用于存儲外部this

    /* 設置下一個 handler */
    setNextHandler(fn) {
        if (typeof fn !== "function") {
            throw new Error("[chain] successor must be a function.");
        }
        this.handlers.push(fn);
        return this;
    }

    next() {
        if (this.cache && this.cache.length > 0) {
            let ware = this.cache.shift();    // 釋放隊頭 handler
            ware.call(
                this,
                this.context,
                this.next.bind(this),       // 遞歸
                arguments && arguments[0]
            );
        }
    }

    /* 開始觸發鏈式 */
    start() {

        // start 方法接受 [context] 及其他參數
        const { context, ...rest } = arguments[0];

        // 將 [this.handlers] 復制一份,賦給 [this.cache]
        this.cache = this.handlers.map(function(fn) {
            return fn;
        });

        // 暫存上下文
        this.context = context;

        // 主動觸發第一個 handler
        this.next(rest);

    }
}

export default Chain;
    // 任務塊:篩選性別
    const genderHandler = function(context, next, data) {
        if(data.gender === 'male') {
            context.showTips('我們不要男的');
            return;
        }
        next(data);
    };
    // 任務塊:篩選年齡
    const ageHandler = function(context, next, data) {
        if(data.age > 30) {
            context.showTips('年齡太大了');
            return;
        }
        next(data);
    };

    // 使用Chain來構建鏈式
    const peopleChain = new Chain()
        .setNextHandler(genderHandler)
        .setNextHandler(ageHandler);

    // 這里使用objA來作為上下文,如:在vue中的話context參數傳該組件的vm即可
    const objA = {
        showTips: function(str) {
            window.alert(str);
        }
    };

    peopleChain.start({
        context: objA,
        gender: 'male',
        age: 21
    });

    peopleChain.start({
        context: objA,
        gender: 'female',
        age: 48
    });
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容