文章首發(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)用
本文參考
- JS函數(shù)鏈?zhǔn)秸{(diào)用的幾種方式 https://juejin.cn/post/7169138123182456862#heading-0
- 面試官必問(wèn)系列之js的鏈?zhǔn)秸{(diào)用 https://codeleading.com/article/65804907612/
- JavaScript中的鏈?zhǔn)秸{(diào)用 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