在javascript異步編程枕稀、函數(shù)式編程中询刹,有兩個至關(guān)重要的技術(shù)callback與this變量,又稱之為回調(diào)與當(dāng)前對象上下文萎坷。
一凹联、星際迷航
javascript中的回調(diào)函數(shù),我借用科幻小說的比喻哆档,有點類似不同的宇宙空間蔽挠。而且宇宙空間有兩類:
- 一類就像從地球到火星,在代碼上的表現(xiàn)是瓜浸,在同一個時刻(幀)代碼執(zhí)行有嚴(yán)格的先后順序澳淑。
- 另一類回調(diào)函數(shù),像從當(dāng)下去了天堂或冥界插佛,跟現(xiàn)在下不屬于同一個宇宙空間杠巡,代碼在未來某一時刻才會進入。
而且這些宇宙空間還相可以互嵌套雇寇,簡單理解可以用同步忽孽、異步函數(shù)來區(qū)別。
舉個列子谢床,先看看從地球到火星的旅行:
onLoad() {
let array = ['1','2','3','4','5'];
//過慮出數(shù)組中的奇數(shù)元素
this._num = 2;
array = array.map(function(i) {
return parseInt(i);
}).filter(function(i) {
return i % this._num;
}, this); //注意這里的this參數(shù)
}
上面代碼中array對象上的map與filter中的匿名函數(shù)兄一,就像兩個小行星。onLoad外層就是地球识腿,他們是在同一個時空之中出革,array中的元素像是做了一次星際旅行,斷點會從上到下一句一句地執(zhí)行渡讼。
Shawn對es6太過依賴骂束,忍不住寫了一行es6的等價代碼:
//再看看es6的寫法
array = array.map(i => parseInt(i)).filter(i => i % this._num);
這里解釋一下,注意兩點:
- 箭頭函數(shù)中參數(shù)只有一個時成箫,可以省略參數(shù)上的圓括號(arg)直接寫成arg展箱。
- 箭頭函數(shù)中函數(shù)體只有一行代碼,可以省略大擴號{}直接寫表達示蹬昌,同時將表達式的值默認(rèn)為函數(shù)返回值混驰,所以不需要寫return。
再來看看Creator中常見的回調(diào)用法,在不同的宇宙空間的穿梭:
onLoad() {
this._button.active = false;
this.scheduleOnce(() => {
this._button.active = true;
}, 5);
}
使用scheduleOnce延時5秒顯示_button節(jié)點栖榨,他與上面的map昆汹、filter函數(shù)不同的是異步執(zhí)行。在調(diào)試中會發(fā)現(xiàn)斷點在代碼前后跳躍婴栽,斷點前后跳躍不是關(guān)鍵满粗,關(guān)鍵的是scheduleOnce函數(shù)他不會阻塞,不論scheduleOnce函數(shù)中的回調(diào)函數(shù)如何復(fù)雜都不會影響當(dāng)前這一幀的運行效率愚争。
在Creator中cc.loader.loadRes映皆、cc.loader.load就是異步回調(diào)的,如果資源已經(jīng)被加載過了轰枝,可以使用cc.loader.getRes通過函數(shù)返回值同步獲取劫扒。理解同步與異步是編寫javascript函數(shù)的重要心法,善于駕馭異步流程你就能在javascript中自由遨游狸膏,使用async.js來控制異步流程是一個高效的作法沟饥。
二、搞清楚this是誰
在紛繁復(fù)雜的星際旅行中湾戳,不論是同步還是異步贤旷,最為重要的是不要忘記“我是誰”。No不好意思砾脑,搞清楚我不重要幼驶,在你人生旅途中,要時間清醒韧衣,此刻的你到底是誰更重要盅藻。
對于javascript中的回調(diào)函數(shù)來說,函數(shù)中的this變量到底是誰畅铭,搞不清這個你很可能就會在旅行中回不來了氏淑,回到之前代碼中的filter中的回調(diào)函數(shù):
onLoad() {
let array = ['1','2','3','4','5'];
let array = [];
let this._num = 2;
array.filter(function(i) {
return i % this._num;
}, this); //<-----注意這里的this參數(shù)
}
filter的第二個參數(shù)this是用來改變回調(diào)函數(shù)中的this變量,如果不傳這個this參數(shù)硕噩,里面的this._num訪問就會有問題假残。
例如在Creator中有不少需要注冊回調(diào)的API,后面都會緊跟一個target參數(shù)炉擅,target將來回調(diào)后的this變量辉懒。
this.node.on(cc.Node.EventType.TOUCH_START, this.memberFunction, this);
如果你不傳入第三個參數(shù)this你的代碼很可能會掛掉,函數(shù)的this上下文默認(rèn)受調(diào)用者所控制谍失。
//模擬一個組件中的點擊事件
_onButtonTouchEnd() {
//定義一個回調(diào)函數(shù)
let callback = function() {
cc.log(this); //<----這個this是全局window
}
//執(zhí)行回調(diào)函數(shù)眶俩,函數(shù)中的this是全局window
callback();
}
上面代碼callback中的this是全局window,這里我使用慣用方式總結(jié)了幾個大招可以用來改變callback中的this變量快鱼。
三颠印、星際巡航
javascript與c/c++纲岭、java等語言有個最大區(qū)別就是,函數(shù)中的this變量是可變的嗽仪。幾乎每個人都會在這一點栽跟頭,這個特性既成就了javascript的高度靈活性柒莉,但也讓不少初學(xué)者產(chǎn)生迷惑闻坚。改變js函數(shù)中this變量的技法我將其稱之為:星際巡航術(shù),為的是在迷航中認(rèn)清自己兢孝。
第一式:凝神訣
Function.bind
javascript中所有的函數(shù)對象上都有bind方法窿凤,執(zhí)行它將返回一個新的函數(shù)變量,這個返回的函數(shù)執(zhí)行時的this上下文由bind的第一個參數(shù)所決定跨蟹■ㄊ猓看看在節(jié)點事件中的運用:
//去掉了第三個target參數(shù)
this.node.on(cc.Node.EventType.TOUCH_START, this.memberFunction.bind(this));
使用bind搞定,是不是很簡單窗轩,我看好多人是這樣做的夯秃。但請你思考一下那為什么Array.map、Array.filter痢艺、CreatorAPI要設(shè)計target參數(shù)呢仓洼?使用bind注冊回調(diào),容易踩到一個坑堤舒,稍后說明一下我的理解色建。我們再稍微深入一點,看看bind更多的用法:
//模擬一個組件中的點擊事件
_onButtonTouchEnd() {
//定義一個回調(diào)函數(shù)
let callback = function(name, event) {
cc.log(this); //打印當(dāng)前this
cc.log(name, event); //打印參數(shù)
}
//施展綁定訣舌缤,將callback中的this綁定為當(dāng)前函數(shù)上下文中的this
let callback1 = callback.bind(this);
//執(zhí)行回調(diào)函數(shù)箕戳,函數(shù)中的this是曾經(jīng)bind傳入?yún)?shù),這里就是當(dāng)前組件對象
callback1('button', 'touchEnd');
//將callback中的this綁定為當(dāng)前this上的_button節(jié)點對象
let callback2 = callback.bind(this._button);
//執(zhí)行回調(diào)函數(shù)国撵,函數(shù)中的this是bind傳入的_button節(jié)點
callback2('button', 'touchEnd');
凝神訣要義在于bind時的參數(shù)設(shè)定陵吸,就像是搓出一股波動拳,蓄而未發(fā)介牙,“啊啰啰啰啰......”就是不“哽”出去走越。
而且bind函數(shù)還可以給函數(shù)傳遞參數(shù),請仔細閱讀下面代碼:
//定義一個回調(diào)函數(shù)
let callback = function(arg1, arg2) {
cc.log(this); //打印當(dāng)前this
cc.log(arguments) //打印隱藏參數(shù)對象
cc.log(arg1, arg2); //打印參數(shù)
}
//綁定決還可以傳入?yún)?shù)耻瑟,傳入的參與會排在原函數(shù)定義的參數(shù)之前
let callback1 = callback.bind(this._button, 'button', 'touchEnd');
//參數(shù)已經(jīng)在bind時傳入了旨指,此時可以不用傳入?yún)?shù)了
callback1();
//如果傳入?yún)?shù),調(diào)用時的參數(shù)會排在綁定時的參數(shù)后面
callback1(1, 2); //參數(shù)順序:['button', 'touchEnd', 1, 2]
將這股凝聚的能量任意流動(一系列的參數(shù)傳遞喳整、變量賦值)谆构,在適合的地方釋放出來,其中this變量與參數(shù)是由你之前精心設(shè)計的框都,這時會產(chǎn)生情人的效果搬素,是一般靜態(tài)語言難以做到的。
還需要特別的注意,每一股搓出的一股波動拳都是不同的函數(shù)對象熬尺。
let func1 = callback.bind(xxx);
let func2 = callback.bind(xxx);
//f1與f2是兩個不同的函數(shù)對象
f1 === f2; //返回false
這就是為什么在節(jié)點事件注冊時使用bind容易掉入進的坑摸屠,當(dāng)你想使用node.off你不能將之前事件回調(diào)給刪除掉,這就是為什么要給你一個target參數(shù)的原因了粱哼。
不過Shawn還有更簡單的辦法注冊事件季二,而且也不需要傳入target,因為bind是es5時代的產(chǎn)物揭措,es6有更好用的招數(shù)胯舷。
第二式:召喚訣
Function.call
你可能在想,Creator的API是如何利用target參數(shù)修改的回調(diào)中的this的呢绊含?其實與Function.bind一樣桑嘶,javascript中所有的函數(shù)對象上都有一個call方法:
//模擬一個組件中的點擊事件
_onButtonTouchEnd() {
//定義一個回調(diào)函數(shù)
let callback = function(name, event) {
cc.log(name, event);
}
//call的第一個參數(shù)是想變換的this上下文,后面為該函數(shù)的實際參數(shù)
callback.call(this, 'button', 'touchEnd');
}
召喚訣的特點是:隨喊隨到躬充,立即執(zhí)行逃顶,其中最為重要的是call傳入的第一個參數(shù),就是你想變換的this變量充甚,后面緊跟此函數(shù)的參數(shù)口蝠。
一個更有趣的實踐hack一下Creator的cc.Button組件,做個神奇的勾子:
//先保存button狀態(tài)切換函數(shù)
let updateState = cc.Button.prototype._updateState;
//自己寫個函數(shù)來將他覆蓋了
cc.Button.prototype._updateState = function () {
//執(zhí)行時的第一句津坑,執(zhí)行原來保存的_updateState妙蔗,相當(dāng)于執(zhí)行基類函數(shù)
//這里不能直接調(diào)用updateState,需要用call將內(nèi)部this修正為當(dāng)前button
updateState.call(this);
if (this.node.interactable === this.interactable) {
return;
}
//下面是根據(jù)是否禁用疆瑰,設(shè)置button節(jié)點下的子節(jié)點變灰
//做了條件判斷只在不設(shè)置disabledSprite時生效
this.node.interactable = this.interactable;
if (this.enableAutoGrayEffect && this.transition !== cc.Button.Transition.COLOR) {
if (!(this.transition === cc.Button.Transition.SPRITE && this.disabledSprite)) {
this.node.children.forEach((node) => {
let sprite = node.getComponent(cc.Sprite);
if (sprite && sprite._sgNode) {
sprite._sgNode.setState(this.interactable ? 0 : 1);
}
//原生平臺退出
if(cc.sys.isNative) {
return;
}
//Label的置灰實現(xiàn)目前只能在web端使用
let label = node.getComponent(cc.Label);
if (label && label._sgNode) {
let shaderProgram = this.interactable ?
cc.shaderCache.programForKey(cc.macro.SHADER_SPRITE_POSITION_TEXTURECOLOR) :
cc.Scale9Sprite.WebGLRenderCmd._getGrayShaderProgram();
label._sgNode._renderCmd.setShaderProgram(shaderProgram);
}
});
}
}
};
來看看演示效果:
Shawn還嘗試了眉反,將bind過的函數(shù)對象,再調(diào)用call穆役,this任然是之前bind時的this不受call的第一個參數(shù)控制寸五。
let func = callback.bind(xxx);
//執(zhí)行時func函數(shù)的this任然是xxx,函數(shù)參數(shù)有效
func.call(yyy, arg1, arg2);
es5的時候call出現(xiàn)的頻率是非常高的耿币,但現(xiàn)在使用了es6除了做一些hack行為與面向?qū)ο蟮哪M外梳杏,大多數(shù)回調(diào)都可以用更加簡單的一陽指可以搞定。
第三式:降龍訣
Function.apply
javascript中函數(shù)的參數(shù)變化無窮淹接,參數(shù)個數(shù)可長可短(參數(shù)個數(shù)0~n)十性,神鬼莫測,猶如一條游龍塑悼!降龍訣就是用來馴服這條善變的怪獸的劲适!
_onButtonTouchEnd() {
//定義一個回調(diào)函數(shù),根據(jù)不同的參數(shù)個數(shù)有不同的處理
let callback = function() {
switch(arguments.lenght) {
case 1:
...
break;
case 2:
...
break;
}
}
//call的第一個參數(shù)是想變換的this上下文,后面接一個數(shù)組參數(shù)
callback.apply(this, ['button', 'touchEnd']);
同樣的厢蒜,所有函數(shù)上都有一個apply方法霞势,降龍訣的精髓有兩點:
- 控制this上下文的變化烹植,
- 可以將參數(shù)用一個數(shù)組打包進行傳遞,
函數(shù)執(zhí)行任然是像普通調(diào)用一樣愕贡,在平時用的地方不多草雕,但在類的繼承、執(zhí)行基類函數(shù)固以、模擬面向?qū)ο蟮燃夹g(shù)上是離不開它的墩虹。
第四式:一陽指
箭頭函數(shù) () => { ... }
一陽指又稱箭頭函數(shù),所指之處的函數(shù)this上下文嘴纺,皆為當(dāng)時調(diào)用時的this败晴,看似平淡無其浓冒,實則威力巨大栽渴。
//模擬一個組件中的點擊事件
_onButtonTouchEnd() {
//定義一個箭頭函數(shù),當(dāng)前this為組件對象
let callback = (arg1, arg2) => {
//此刻的this為定義函數(shù)時的this上下文對象
cc.log(this);
}
callback(xxx, yyy);
}
凝神訣和召喚訣的運用大多數(shù)是為了修正匿名函數(shù)中的this為當(dāng)前調(diào)用時的this稳懒,可顯的有點啰哩叭嗦闲擦,一記一陽指輕松搞定!
在一陽指還沒有被創(chuàng)造之前场梆,使用的是閉包變量來做的:
var self = this;
function callback() {
//使用self變量墅冷,指向調(diào)用時的this上下文
cc.log(self);
...
}
callback(xxx, yyy);
此方法也正是Bable編譯器將es6轉(zhuǎn)es5時生成的套路。
對于this的控制是凌波微步的內(nèi)功基本詳見《
英雄之舞—凌波微步》或油,如果運用的不好寞忿,就會如文中所講的,強行走將起來顶岸,會造成經(jīng)脈堵塞的危境腔彰!
四、結(jié)束
最后總結(jié)一下我們介紹的招數(shù)
凝神訣—Function.bind
召喚訣—Function.call
降龍訣—Function.apply
一陽指—箭頭函數(shù)
這些招數(shù)都是為了在回調(diào)函數(shù)中不要迷失this辖佣,或都說在回調(diào)中可以任意控制this霹抛。在javascript中函數(shù)是第一位的,函數(shù)可以動態(tài)生成卷谈,可以當(dāng)參數(shù)傳遞杯拐,可以說javascript是披著c/c++的狼,骨子里其實是函數(shù)式編程語言世蔗。