JavaScript的鏈?zhǔn)秸{(diào)用,都在這了

文章首發(fā)公眾號(hào),歡迎來(lái)撩看廣告~

鏈?zhǔn)秸{(diào)用在JavaScript語(yǔ)言中很常見(jiàn),是一種非常有用的代碼構(gòu)建技巧,如jQuery、Promise等,都是使用的鏈?zhǔn)秸{(diào)用

對(duì)象鏈?zhǔn)秸{(diào)用通常有以下幾種實(shí)現(xiàn)方式,但是本質(zhì)上都是通過(guò)返回對(duì)象之后進(jìn)行調(diào)用。

  • this的作用域鏈,jQuery的實(shí)現(xiàn)方式,通常鏈?zhǔn)秸{(diào)用都是采用這種方式。
  • 返回對(duì)象本身, 同this的區(qū)別就是顯示返回鏈?zhǔn)綄?duì)象。

函數(shù)鏈?zhǔn)秸{(diào)用通常有以下幾種實(shí)現(xiàn)方式,

  • 遍歷調(diào)用函數(shù)組、利用遍歷、按順序調(diào)用函數(shù)元素
  • 利用函數(shù)的調(diào)用棧、例如koa的洋蔥圈的鏈?zhǔn)秸{(diào)用
  • 閉包返回對(duì)象的方式實(shí)現(xiàn),這種方式與柯里化有相似之處、例如reduce的鏈?zhǔn)秸{(diào)用
image.png

我將JavaScript鏈?zhǔn)秸{(diào)用分為以上幾類,歡迎大家補(bǔ)充一起學(xué)習(xí)、

對(duì)象鏈?zhǔn)秸{(diào)用-基礎(chǔ)

要點(diǎn)就是 return this

/* 簡(jiǎn)單的鏈?zhǔn)秸{(diào)用 */
function Person (name, age) {
    this.name = name
    this.age = age
}
Person.prototype = {
    info() {
        console.log(`我的名字是${this.name},我今年${this.age}歲`);
        return this
    },
    start() {
        console.log('開(kāi)始起床!');
        return this
    },
    eat() {
        console.log('開(kāi)始吃飯');
        return this
    },
    work() {
        console.log('開(kāi)始工作!');
        return this
    },
    run() {
        console.log('下班啦!下班啦!');
        return this
    }
}
const person = new Person('rose', 18)
person.info().start().eat().work().run()

// 我的名字是rose,我今年18歲
// 開(kāi)始起床!
// 開(kāi)始吃飯
// 開(kāi)始工作!
// 下班啦!下班啦!

對(duì)象鏈?zhǔn)秸{(diào)用-高階

要點(diǎn):

  • return this
  • 任務(wù)隊(duì)列
//首先定義構(gòu)造函數(shù) Person
function Person(name) {
    this.name = name;
    //任務(wù)隊(duì)列(使用隊(duì)列的先進(jìn)先出性質(zhì)來(lái)模擬鏈?zhǔn)秸{(diào)用函數(shù)的執(zhí)行順序)
    this.queue = [];

    let fn = () => {
        console.log('init 組要做的事情')
        //next方法是 Person 原型上的方法,用于從任務(wù)隊(duì)列中取出函數(shù)執(zhí)行
        this.next();
    }
    
    //函數(shù)入隊(duì)
    this.queue.push(fn);
    
    // 一定要添加定時(shí)器、將其放入函數(shù)隊(duì)列中
    setTimeout(() => {
        this.next();
    },0);

    return this;
}

//在Person的原型上實(shí)現(xiàn)eat、sleep、sleepFirst以及輔助方法next
Person.prototype = {
    eat(food) {
        let fn = () => {
            console.log('吃' + ' ' +food)
            this.next();
        };
        this.queue.push(fn);
        return this;
    },
    sleep(time) {
        let fn = () => {
            setTimeout(() => {
                console.log('碎覺(jué)' + '' + time);
                this.next();
            },time*1000)
        };
        this.queue.push(fn);
        return this;
    },
    sleepFirst(time) {
        let fn = () => {
            setTimeout(() => {
                console.log('等待' + '' + time);
                this.next();
            },time*1000)
        };
        //sleepFirst要優(yōu)先執(zhí)行,所以放到隊(duì)列首部,
        this.queue.unshift(fn);
        return this;
    },
    next() {
        //從隊(duì)列首部取出一個(gè)函數(shù)
        let fn = this.queue.shift();
        fn && fn();//如果fn存在就執(zhí)行fn
    }
}

//測(cè)試
new Person('Hank').sleep(1).sleepFirst(5).eat('晚飯')

// 等待5
// init 組要做的事情
// 碎覺(jué)1
// 吃晚飯

如果不想要obj.fn(),這種調(diào)用方式,就將顯示的調(diào)用,再封裝一層、底層都是對(duì)象的鏈?zhǔn)秸{(diào)用

function _add(num){
    this.sum = 0
    this.sum += num
    return this
}
_add.prototype.add = function(num){
    this.sum += num
    return this
}
 function add(num){
     return new _add(num)
 }
let res = add(1).add(2).add(3)
console.log(res.sum); //6

對(duì)象鏈?zhǔn)秸{(diào)用-promise的異步調(diào)用原理

function MyPromise (fn) {
    // 回調(diào)收集
    this.callbackList = []
    // 傳遞給Promise處理函數(shù)的resolve
    const resolve = (value) => {
        // 注意promise的then函數(shù)需要異步執(zhí)行
        setTimeout(() => {
            // 保存 value
            this.data = value;
            // 把callbackList數(shù)組里的函數(shù)依次執(zhí)行一遍
            this.callbackList.forEach(cb => cb(value))
        });
    }
    /*
        - fn 為用戶傳進(jìn)來(lái)的函數(shù)
        - 執(zhí)行用戶傳入的函數(shù) 
        - 并且把resolve方法交給用戶執(zhí)行
    */ 
    fn(resolve)
}

// 往構(gòu)造函數(shù)的原型上掛載.then方法
MyPromise.prototype.then = function (onReaolved) {
    // return 一個(gè)promise 實(shí)例
    return new MyPromise((resolve) => {
        // 往回調(diào)數(shù)組中插入回調(diào)
        this.callbackList.push(()=>{

            const response = onReaolved(this.data)
            // 判斷是否是一個(gè) MyPromise
            if(response instanceof MyPromise){
                // resolve 的權(quán)力被交給了user promise
                response.then(resolve)
            }else{
                // 如果是普通值,直接resolve
                // 依次執(zhí)行callbackList里的函數(shù) 并且把值傳遞給callbackList
                resolve(response)
            }
        })
    })
}

var p1 = new MyPromise((resolve, reject) => {
        console.log('p1')
        setTimeout(() => {
            resolve(1)
        }, 1000);
    }).then(res => {
        return new MyPromise((resolve, reject) => {
            setTimeout(() => {
                resolve(res+1)
            }, 1000);
        })
    }).then(res => {
        console.log(res); // 2
        return res+1;
    })

p1.then(res => {
    console.log(res);  // 3
})


  • 每一個(gè)then都會(huì)返回一個(gè)新的promise
  • 將傳給 then 的函數(shù)和新 promise 的 resolve 一起 push 到前一個(gè) promise 的 callbacks 數(shù)組中
  • 當(dāng)前一個(gè) promise 完成后,調(diào)用其 resolve 變更狀態(tài),在這個(gè) resolve 里會(huì)依次調(diào)用 callbacks 里的回調(diào),這樣就執(zhí)行了 then 里的方法了
  • 當(dāng) then 里的方法執(zhí)行完成后,返回一個(gè)結(jié)果,如果這個(gè)結(jié)果是個(gè)簡(jiǎn)單的值,就直接調(diào)用新 promise 的 resolve,讓其狀態(tài)變更,這又會(huì)依次調(diào)用新 promise 的 callbacks 數(shù)組里的方法,循環(huán)往復(fù)
  • 如果返回的結(jié)果是個(gè) promise,則需要等它完成之后再觸發(fā)新 promise 的 resolve,所以可以在其結(jié)果的 then 里調(diào)用新 promise 的 resolve
image.png

函數(shù)的鏈?zhǔn)秸{(diào)用-遞歸調(diào)用

遍歷函數(shù)組進(jìn)行函數(shù)鏈?zhǔn)秸{(diào)用,比較簡(jiǎn)單

// 模擬一系列函數(shù)
function fn1(ctx, next) {
    console.log('函數(shù)fn1執(zhí)行...');
}
function fn2(ctx, next) {
    console.log('函數(shù)fn2執(zhí)行...');
}
function fn3(ctx, next) {
    console.log('函數(shù)fn3執(zhí)行...');
}    

let fns = [fn1, fn2, fn3];

// 定義一個(gè)觸發(fā)函數(shù)
const trigger = (fns) => {
    fns.forEach(fn => {
        fn();
    })
}
// 執(zhí)行觸發(fā),所有函數(shù)依次執(zhí)行
trigger(fns); //

函數(shù)的鏈?zhǔn)秸{(diào)用-洋蔥圈調(diào)用

koa的鏈?zhǔn)秸{(diào)用的底層原理、其實(shí)是利用函數(shù)調(diào)用棧

// 模擬一系列函數(shù)
function fn1(ctx, next) {
    console.log(ctx, '函數(shù)fn1執(zhí)行...'); // 打印順序 1
    next();
    console.log(ctx, 'fn1 ending'); // 打印順序 6
}
function fn2(ctx, next) {
    console.log(ctx,'函數(shù)fn2執(zhí)行...'); // 打印順序 2
    next();
    console.log(ctx, 'fn2 ending'); // 打印順序 5
}
function fn3(ctx, next) {
    console.log(ctx, '函數(shù)fn3執(zhí)行...'); // 打印順序 3
    next();
    console.log(ctx, 'fn3 ending'); // 打印順序 4
}

function wrap(fns) {
    // 必然會(huì)返回一個(gè)函數(shù)...
    return (ctx) => {
        // 閉包保留fns數(shù)組的長(zhǎng)度
        let l = fns.length;
        // 調(diào)用時(shí)從第一個(gè)函數(shù)開(kāi)始
        return next(0);

        function next(i) {
            // 此時(shí)已經(jīng)是最后一個(gè)函數(shù)了,因?yàn)橐呀?jīng)沒(méi)有下一個(gè)函數(shù)了,因此直接返回即可
            if (i === l) return;
            // 拿到相應(yīng)的函數(shù)
            let fn = fns[i];
            // 執(zhí)行當(dāng)下函數(shù),將參數(shù)透?jìng)鬟^(guò)來(lái),每個(gè)函數(shù)的next是一個(gè)函數(shù),因此通過(guò)bind返
            // 回,留在每個(gè)函數(shù)內(nèi)部調(diào)用,并保留參數(shù),實(shí)現(xiàn)遞歸
            return fn(ctx, next.bind(null, (i + 1)));
        }
    }
}

let arr = [fn1, fn2, fn3];
// 組合后的函數(shù)
let fn = wrap(arr);
// 執(zhí)行 并 傳入ctx
fn({ word: 'winter is comming!' });

看????圖觀察調(diào)用棧

  • 每次調(diào)用next函數(shù)的時(shí)候、都回去調(diào)用下一個(gè)函數(shù)
  • 到棧頂時(shí)再一層一層退回來(lái)執(zhí)行、看圖更清晰
image.png

函數(shù)的鏈?zhǔn)秸{(diào)用-組合(reduce)鏈?zhǔn)秸{(diào)用

典型的利用閉包實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用

// 模擬幾個(gè)函數(shù)
function fn1(arg1) {
    // ...對(duì)arg1的操作邏輯
    console.log('fn1的參數(shù):', arg1); 
    let arg = arg1 + 30;
    return arg;
}
function fn2(arg2) {
    // ...對(duì)arg2的操作邏輯
    console.log('fn2的參數(shù):', arg2);
    let arg = arg2 + 20;
    return arg;
}
function fn3(arg3) {
    // ...對(duì)arg3的操作邏輯
    console.log('fn3的參數(shù):', arg3);
    let arg = arg3 + 10;
    return arg;
}
// 省略所有容錯(cuò)判斷
function compose(fns) {
    let l = fns.length;
    if (!l) throw new Error('至少得有一個(gè)函數(shù)呀...');

    // 一個(gè),就直接返回這個(gè)函數(shù)...
    if (l === 1) return fns[0];

    // 數(shù)組迭代,返回一個(gè)函數(shù),函數(shù)的實(shí)體為后一個(gè)函數(shù)執(zhí)行的返回值作為前一個(gè)函
    // 數(shù)的參數(shù),然后前一個(gè)函數(shù)執(zhí)行,最終返回第一個(gè)函數(shù)的返回值
    return fns.reduce((a, b, i) => {
        return function c(...arg) {
            return a(b(...arg))
        }
    });
}

let fns = [fn1, fn2, fn3];

// 將函數(shù)組合,形成復(fù)雜函數(shù)
let fn = compose(fns);

// 執(zhí)行
let r = fn(10);

console.log(r)
// 執(zhí)行過(guò)程打印
// fn3的參數(shù): 10
// fn2的參數(shù): 20
// fn1的參數(shù): 40
// 70
image.png
  • 1、返回值fn是一個(gè)閉包、調(diào)用 fn(10)、此時(shí)的a = function c , b = fn3 參數(shù) arg = 10,那么fn3(10) 返回值是 20 再傳入a = function c( 10)
  • 2、此時(shí) function c 又是一個(gè)閉包、在它的閉包環(huán)境下、a=fn1 b=fn2、arg = 20、所以調(diào)用 fn2(20)、 返回值是40、再傳入 a = fn1(40)、即70
  • 3、最后因?yàn)閍 是 fn1、調(diào)用fn1后 直接return 、所以最后返回值為70

函數(shù)的鏈?zhǔn)秸{(diào)用-jQuery中的鏈?zhǔn)秸{(diào)用

jQuery中的鏈?zhǔn)秸{(diào)用非常經(jīng)典、這里以最基礎(chǔ)的jQuery框架為例探查一下jQuery如何通過(guò)this實(shí)現(xiàn)的鏈?zhǔn)秸{(diào)用。

function jQuery(selector){
    return new jQuery.fn.init(selector);
}
jQuery.fn = jQuery.prototype = {
    constructor: jQuery,
    init: function(selector){
        this[0] = document.querySelector(selector);
        this.length = 1;
        return this;
    },
    length: 3,
    size: function(){
        return this.length;
    }
}
jQuery.fn.init.prototype = jQuery.fn;
var body = jQuery("body");
  • 首先這是一個(gè)最基本的類,通過(guò)實(shí)例化之后,實(shí)例共享原型上的方法
  • jQuery 的原型對(duì)象有一個(gè)init屬性,這個(gè)屬性才是真正的構(gòu)造函數(shù)
  • 因?yàn)槊總€(gè)構(gòu)造函數(shù)都一個(gè)原型對(duì)象,構(gòu)造函數(shù)的實(shí)例對(duì)象,都可以使用原型對(duì)象中封裝的屬性和方法、所以通過(guò)init()創(chuàng)建出來(lái)的對(duì)象,都可以使用原型對(duì)象上的方法、jQuery的原型對(duì)象上有這些方法, 那么 jQuery.fn.init.prototype = jQuery.fn即可
  • 所以當(dāng)調(diào)用jQuery("body")的時(shí)候,執(zhí)行init函數(shù)、實(shí)例化一個(gè)對(duì)象,并且能夠共享原型上的方法、并且返回這個(gè)對(duì)象
  • 即經(jīng)典的 return this 鏈?zhǔn)秸{(diào)用

本文參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,285評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,548評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,328評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,878評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,098評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,554評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,725評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,971評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,361評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,613評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,339評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,695評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容