JavaScript的链式调用,都在这了

文章首发公众号,欢迎来撩看广告~

链式调用在JavaScript语言中很常见,是一种非常有用的代码构建技巧,如jQuery、Promise等,都是使用的链式调用

对象链式调用通常有以下几种实现方式,但是本质上都是通过返回对象之后进行调用。

  • this的作用域链,jQuery的实现方式,通常链式调用都是采用这种方式。
  • 返回对象本身, 同this的区别就是显示返回链式对象。

函数链式调用通常有以下几种实现方式,

  • 遍历调用函数组、利用遍历、按顺序调用函数元素
  • 利用函数的调用栈、例如koa的洋葱圈的链式调用
  • 闭包返回对象的方式实现,这种方式与柯里化有相似之处、例如reduce的链式调用
image.png

我将JavaScript链式调用分为以上几类,欢迎大家补充一起学习、

对象链式调用-基础

要点就是 return this

/* 简单的链式调用 */
function Person (name, age) {
    this.name = name
    this.age = age
}
Person.prototype = {
    info() {
        console.log(`我的名字是${this.name},我今年${this.age}岁`);
        return this
    },
    start() {
        console.log('开始起床!');
        return this
    },
    eat() {
        console.log('开始吃饭');
        return this
    },
    work() {
        console.log('开始工作!');
        return this
    },
    run() {
        console.log('下班啦!下班啦!');
        return this
    }
}
const person = new Person('rose', 18)
person.info().start().eat().work().run()

// 我的名字是rose,我今年18岁
// 开始起床!
// 开始吃饭
// 开始工作!
// 下班啦!下班啦!

对象链式调用-高阶

要点:

  • return this
  • 任务队列
//首先定义构造函数 Person
function Person(name) {
    this.name = name;
    //任务队列(使用队列的先进先出性质来模拟链式调用函数的执行顺序)
    this.queue = [];

    let fn = () => {
        console.log('init 组要做的事情')
        //next方法是 Person 原型上的方法,用于从任务队列中取出函数执行
        this.next();
    }
    
    //函数入队
    this.queue.push(fn);
    
    // 一定要添加定时器、将其放入函数队列中
    setTimeout(() => {
        this.next();
    },0);

    return this;
}

//在Person的原型上实现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('碎觉' + '' + time);
                this.next();
            },time*1000)
        };
        this.queue.push(fn);
        return this;
    },
    sleepFirst(time) {
        let fn = () => {
            setTimeout(() => {
                console.log('等待' + '' + time);
                this.next();
            },time*1000)
        };
        //sleepFirst要优先执行,所以放到队列首部,
        this.queue.unshift(fn);
        return this;
    },
    next() {
        //从队列首部取出一个函数
        let fn = this.queue.shift();
        fn && fn();//如果fn存在就执行fn
    }
}

//测试
new Person('Hank').sleep(1).sleepFirst(5).eat('晚饭')

// 等待5
// init 组要做的事情
// 碎觉1
// 吃晚饭

如果不想要obj.fn(),这种调用方式,就将显示的调用,再封装一层、底层都是对象的链式调用

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

对象链式调用-promise的异步调用原理

function MyPromise (fn) {
    // 回调收集
    this.callbackList = []
    // 传递给Promise处理函数的resolve
    const resolve = (value) => {
        // 注意promise的then函数需要异步执行
        setTimeout(() => {
            // 保存 value
            this.data = value;
            // 把callbackList数组里的函数依次执行一遍
            this.callbackList.forEach(cb => cb(value))
        });
    }
    /*
        - fn 为用户传进来的函数
        - 执行用户传入的函数 
        - 并且把resolve方法交给用户执行
    */ 
    fn(resolve)
}

// 往构造函数的原型上挂载.then方法
MyPromise.prototype.then = function (onReaolved) {
    // return 一个promise 实例
    return new MyPromise((resolve) => {
        // 往回调数组中插入回调
        this.callbackList.push(()=>{

            const response = onReaolved(this.data)
            // 判断是否是一个 MyPromise
            if(response instanceof MyPromise){
                // resolve 的权力被交给了user promise
                response.then(resolve)
            }else{
                // 如果是普通值,直接resolve
                // 依次执行callbackList里的函数 并且把值传递给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
})


  • 每一个then都会返回一个新的promise
  • 将传给 then 的函数和新 promise 的 resolve 一起 push 到前一个 promise 的 callbacks 数组中
  • 当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了
  • 当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promise 的 resolve,让其状态变更,这又会依次调用新 promise 的 callbacks 数组里的方法,循环往复
  • 如果返回的结果是个 promise,则需要等它完成之后再触发新 promise 的 resolve,所以可以在其结果的 then 里调用新 promise 的 resolve
image.png

函数的链式调用-递归调用

遍历函数组进行函数链式调用,比较简单

// 模拟一系列函数
function fn1(ctx, next) {
    console.log('函数fn1执行...');
}
function fn2(ctx, next) {
    console.log('函数fn2执行...');
}
function fn3(ctx, next) {
    console.log('函数fn3执行...');
}    

let fns = [fn1, fn2, fn3];

// 定义一个触发函数
const trigger = (fns) => {
    fns.forEach(fn => {
        fn();
    })
}
// 执行触发,所有函数依次执行
trigger(fns); //

函数的链式调用-洋葱圈调用

koa的链式调用的底层原理、其实是利用函数调用栈

// 模拟一系列函数
function fn1(ctx, next) {
    console.log(ctx, '函数fn1执行...'); // 打印顺序 1
    next();
    console.log(ctx, 'fn1 ending'); // 打印顺序 6
}
function fn2(ctx, next) {
    console.log(ctx,'函数fn2执行...'); // 打印顺序 2
    next();
    console.log(ctx, 'fn2 ending'); // 打印顺序 5
}
function fn3(ctx, next) {
    console.log(ctx, '函数fn3执行...'); // 打印顺序 3
    next();
    console.log(ctx, 'fn3 ending'); // 打印顺序 4
}

function wrap(fns) {
    // 必然会返回一个函数...
    return (ctx) => {
        // 闭包保留fns数组的长度
        let l = fns.length;
        // 调用时从第一个函数开始
        return next(0);

        function next(i) {
            // 此时已经是最后一个函数了,因为已经没有下一个函数了,因此直接返回即可
            if (i === l) return;
            // 拿到相应的函数
            let fn = fns[i];
            // 执行当下函数,将参数透传过来,每个函数的next是一个函数,因此通过bind返
            // 回,留在每个函数内部调用,并保留参数,实现递归
            return fn(ctx, next.bind(null, (i + 1)));
        }
    }
}

let arr = [fn1, fn2, fn3];
// 组合后的函数
let fn = wrap(arr);
// 执行 并 传入ctx
fn({ word: 'winter is comming!' });

看👇🏻图观察调用栈

  • 每次调用next函数的时候、都回去调用下一个函数
  • 到栈顶时再一层一层退回来执行、看图更清晰
image.png

函数的链式调用-组合(reduce)链式调用

典型的利用闭包实现链式调用

// 模拟几个函数
function fn1(arg1) {
    // ...对arg1的操作逻辑
    console.log('fn1的参数:', arg1); 
    let arg = arg1 + 30;
    return arg;
}
function fn2(arg2) {
    // ...对arg2的操作逻辑
    console.log('fn2的参数:', arg2);
    let arg = arg2 + 20;
    return arg;
}
function fn3(arg3) {
    // ...对arg3的操作逻辑
    console.log('fn3的参数:', arg3);
    let arg = arg3 + 10;
    return arg;
}
// 省略所有容错判断
function compose(fns) {
    let l = fns.length;
    if (!l) throw new Error('至少得有一个函数呀...');

    // 一个,就直接返回这个函数...
    if (l === 1) return fns[0];

    // 数组迭代,返回一个函数,函数的实体为后一个函数执行的返回值作为前一个函
    // 数的参数,然后前一个函数执行,最终返回第一个函数的返回值
    return fns.reduce((a, b, i) => {
        return function c(...arg) {
            return a(b(...arg))
        }
    });
}

let fns = [fn1, fn2, fn3];

// 将函数组合,形成复杂函数
let fn = compose(fns);

// 执行
let r = fn(10);

console.log(r)
// 执行过程打印
// fn3的参数: 10
// fn2的参数: 20
// fn1的参数: 40
// 70
image.png
  • 1、返回值fn是一个闭包、调用 fn(10)、此时的a = function c , b = fn3 参数 arg = 10,那么fn3(10) 返回值是 20 再传入a = function c( 10)
  • 2、此时 function c 又是一个闭包、在它的闭包环境下、a=fn1 b=fn2、arg = 20、所以调用 fn2(20)、 返回值是40、再传入 a = fn1(40)、即70
  • 3、最后因为a 是 fn1、调用fn1后 直接return 、所以最后返回值为70

函数的链式调用-jQuery中的链式调用

jQuery中的链式调用非常经典、这里以最基础的jQuery框架为例探查一下jQuery如何通过this实现的链式调用。

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");
  • 首先这是一个最基本的类,通过实例化之后,实例共享原型上的方法
  • jQuery 的原型对象有一个init属性,这个属性才是真正的构造函数
  • 因为每个构造函数都一个原型对象,构造函数的实例对象,都可以使用原型对象中封装的属性和方法、所以通过init()创建出来的对象,都可以使用原型对象上的方法、jQuery的原型对象上有这些方法, 那么 jQuery.fn.init.prototype = jQuery.fn即可
  • 所以当调用jQuery("body")的时候,执行init函数、实例化一个对象,并且能够共享原型上的方法、并且返回这个对象
  • 即经典的 return this 链式调用

本文参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容