文章首发公众号,欢迎来撩看广告~
链式调用在JavaScript语言中很常见,是一种非常有用的代码构建技巧,如jQuery、Promise等,都是使用的链式调用
对象链式调用通常有以下几种实现方式,但是本质上都是通过返回对象之后进行调用。
- this的作用域链,jQuery的实现方式,通常链式调用都是采用这种方式。
- 返回对象本身, 同this的区别就是显示返回链式对象。
函数链式调用通常有以下几种实现方式,
- 遍历调用函数组、利用遍历、按顺序调用函数元素
- 利用函数的调用栈、例如koa的洋葱圈的链式调用
- 闭包返回对象的方式实现,这种方式与柯里化有相似之处、例如reduce的链式调用
我将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
函数的链式调用-递归调用
遍历函数组进行函数链式调用,比较简单
// 模拟一系列函数
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函数的时候、都回去调用下一个函数
- 到栈顶时再一层一层退回来执行、看图更清晰
函数的链式调用-组合(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
- 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
链式调用
本文参考
- JS函数链式调用的几种方式 https://juejin.cn/post/7169138123182456862#heading-0
- 面试官必问系列之js的链式调用 https://codeleading.com/article/65804907612/
- JavaScript中的链式调用 https://www.cnblogs.com/WindrunnerMax/p/14043455.html
- 链接:https://juejin.cn/post/6844903992711987208
- https://zhuanlan.zhihu.com/p/110512501
- https://juejin.cn/post/6844904030221631495
- https://segmentfault.com/a/1190000011863232
- https://github.com/songjinzhong/JQuerySource
- https://leohxj.gitbooks.io/front-end-database/content/jQuery/jQuery-source-code/index.html
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/%E5%8F%AF%E9%80%89%E9%93%BE