apply,call和bind的基本介紹
語法
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
返回值
all/apply: 返回fun執(zhí)行的結(jié)果
bind: 返回fun的拷貝姜骡,并擁有指定的this值和初始參數(shù)
參數(shù)
thisArg(可選)
- fun的this指向thisArg對象。
- 在非嚴(yán)格模式下屿良,thisArg指定null圈澈,undefined,fun中this指向window對象尘惧。
- 在嚴(yán)格模式下康栈,fun的this指向undefined
- 值是原始值(比如:數(shù)字,字符串喷橙,布爾值)的this會指向該原始值的自動包裝對象啥么,如:String,Number贰逾,Boolean悬荣。
param1,param2(可選): 傳給fun的參數(shù)。
如果param不傳或?yàn)?null/undefined疙剑,則表示不需要傳入任何參數(shù).
apply第二個參數(shù)為數(shù)組氯迂,數(shù)組內(nèi)的值為傳給fun的參數(shù)践叠。
call與apply
在JavaScript中,call和apply都是為了改變函數(shù)執(zhí)行的上下文而存在的嚼蚀,也就是
為了改變函數(shù)體內(nèi)部的this的指向禁灼。
JavaScript的一大特點(diǎn)是函數(shù)存在「定義是上下文」和「運(yùn)行是上下文」以及上下文是可以被改變的。
為何要改變執(zhí)行上下文轿曙?舉一個生活中的小例子:平時(shí)沒時(shí)間做飯的我弄捕,周末想給孩子燉個腌篤鮮嘗嘗。但是沒有適合的鍋导帝,而我又不想出去買守谓。所以就問鄰居借了一個鍋來用,這樣既達(dá)到了目的舟扎,又節(jié)省了開支分飞,一舉兩得悴务。
改變執(zhí)行上下文也是一樣的睹限,A 對象有一個方法,而 B 對象因?yàn)槟撤N原因讯檐,也需要用到同樣的方法羡疗,那么這時(shí)候我們是單獨(dú)為 B 對象擴(kuò)展一個方法呢,還是借用一下 A 對象的方法呢别洪?當(dāng)然是借用 A 對象的啦叨恨,既完成了需求,又減少了內(nèi)存的占用挖垛。
另外痒钝,它們的寫法也很類似,調(diào)用call和apply的對象必須包含一個函數(shù)Function痢毒。
function fruit() {}
fruit.prototype = {
color: 'red',
say: function() {
console.log("my color is " + this.color);
}
}
var apple = new fruit();
apple.say(); //my color is red
這個時(shí)候如果我們又想重新定義一個banana={color: "yellow"};
,我們不想重新定義一個say方法送矩,那么這個時(shí)候我們就可以使用call和apply方法:
banner = {
color: "yellow"
};
apple.say.call(banana); //my color is yellow
apple.say.apply(banana); //my color is yellow
所以從上面可以看出,call和apply是為了動態(tài)改變this而出現(xiàn)的哪替,當(dāng)一個對象沒有某個方法的時(shí)候(本例子中banner對象沒有say方法)栋荸,但是其他對象有某個方法(本例子中apple中有say方法),我們就可以借助call和apply用其他對象的方法來實(shí)現(xiàn)凭舶。
apply和call的區(qū)別
apply和call的作用是完全一樣的晌块,只是傳遞的參數(shù)不一樣而已。例如有一個函數(shù):
var func = function(arg1,arg2){
};
就可以通過下面的方式調(diào)帅霜。
func.call(this,arg1,arg2);
func.apply(this,[arg1,arg2]);
其中this是你想指定的上下文匆背,它可以是任何JavaScript對象,call把參數(shù)按照順序傳遞進(jìn)去身冀,而apply是把參數(shù)放在數(shù)組里再傳進(jìn)去钝尸。
apply和call該用哪個呢蜂大?
- 參數(shù)數(shù)據(jù)、順序確定就用call蝶怔,參數(shù)數(shù)量/殊勛不確定的話就用apply
- 考慮可讀性:參數(shù)數(shù)量不多就用call奶浦,參數(shù)數(shù)量比較多的話,把參數(shù)整合成數(shù)組踢星,使用apply澳叉。
- 參數(shù)集合已經(jīng)是一個數(shù)組的情況,用apply沐悦,比如上下文的獲取數(shù)組最大值/最小值成洗。
參數(shù)數(shù)量/順序不確定的話就用apply,比如以下示例:
const obj = {
age: 24,
name: 'linKGe'
}
const obj2 = {
age: 27
}
callObj(obj, handle);
callObj(obj2, handle);
//根據(jù)某些條件決定要傳遞參數(shù)的數(shù)量藏否,以及順序
function callObj(thisAge,fn) {
let params = [];
if(thisAge.name) {
params.push(thisAge.name);
}
if(thisAge.age) {
params.push(thisAge.age);
}
fn.apply(thisAge,params)// 數(shù)量和順序不確定瓶殃,不能使用call
}
function handle(...params) {
console.log('params',params);
}
結(jié)果:
params [ 'linKGe', 24 ]
params [ 27 ]
call和apply的應(yīng)用場景
下面會分別列舉 call 和 apply 的一些使用場景。聲明:例子中沒有哪個場景是必須用 call 或者必須用 apply 的副签,只是個人習(xí)慣這么用而已遥椿。
1.call的使用場景
1.1 對象的繼承
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this);
this.print();
}
subClass(); //1
subClass 通過 call 方法,繼承了 superClass 的 print 方法和 a 變量淆储。此外冠场,subClass 還可以擴(kuò)展自己的其他方法。
1.2 借用方法
如果一個類數(shù)組想使用Array原型上的方法本砰,可以使用:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
這樣碴裙,domNodes 就可以使用 Array 下的所有方法了。
2.apply應(yīng)用場景
apply獲取數(shù)組最大值和最小值
apply直接傳遞數(shù)組做要調(diào)用方法的參數(shù)点额,也省一步展開數(shù)組舔株,比如使用Math.max、Math.min
來獲取數(shù)組的最大值和最小值还棱。
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
面試題
定義一個 log 方法载慈,讓它可以代理 console.log 方法,常見的解決方法是:
function log(msg) {
console.log(msg);
}
log(1); //1
log(1,2); //1
上面方法可以解決最基本的需求诱贿,但是當(dāng)傳入?yún)?shù)的個數(shù)是不確定的時(shí)候娃肿,上面的方法就失效了,這個時(shí)候就可以考慮使用 apply 或者 call珠十,注意這里傳入多少個參數(shù)是不確定的料扰,所以使用apply是最好的,方法如下:
function log(){
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
接下來的要求是給每一個 log 消息添加一個"(app)"的前輟焙蹭,比如:
log("hello world"); //(app)hello world
這個時(shí)候想到arguments是個偽數(shù)組晒杈,通過Array.prototype.slice.call
可以轉(zhuǎn)成標(biāo)準(zhǔn)的數(shù)組,再使用數(shù)組的unshift方法孔厉。
function log() {
let arg = Array.prototype.slice.call(arguments);
arg.unshift('(app)');
console.log.apply(console,arg);
}
log('hello world'); //(app) hello world
bind
在學(xué)習(xí)bind之前我們先來看一下這道題題目:
var altwrite = document.write;
altwrite("hello");
結(jié)果報(bào)錯:Uncaught TypeError: Illegal invocation
,altwrite()函數(shù)改變了this的指向global或者window對象拯钻,導(dǎo)致執(zhí)行的時(shí)候提示非法調(diào)用異常帖努,正確的方案就是使用bind()方法。
altwrite.bind(document)('hello');
當(dāng)然也可以使用call()方法粪般。
altwrite.call(document,'hello');
綁定函數(shù)
bind()最簡單的方法就是綁定函數(shù)拼余,使這個函數(shù)無論怎么調(diào)用都有同樣的this值,常見的錯誤就像上面的例子一樣亩歹,將方法從對象中拿出來匙监,然后調(diào)用,并且希望this指向原來的對象小作。如果不做特殊處理亭姥,一般會丟失原來的對象。使用bind()方法能夠很好的解決這個問題:
this.num = 9;
var mymodule = {
num: 81,
getNum: function() {
console.log(this.num);
}
};
mymodule.getNum(); //81 //this是mymodule
var getNum = mymodule.getNum;
getNum(); //9 //這時(shí)候this是window
var boundGetNum = getNum.bind(mymodule);
boundGetNum(); // 81
bind() 方法與apply和call相似顾稀,也是可以改變函數(shù)體內(nèi)this的指向达罗。
bind()方法會創(chuàng)建一個新的函數(shù),稱為綁定函數(shù)静秆,當(dāng)調(diào)用這個綁定函數(shù)時(shí)粮揉,綁定函數(shù)會以創(chuàng)建它時(shí)傳入bind()方法的第一個參數(shù)作為this,傳入bind()方法的第二個以及以后的參數(shù)加上綁定函數(shù)運(yùn)行時(shí)本身的參數(shù)按照順序作為原函數(shù)的參數(shù)來調(diào)用原函數(shù)诡宗。
直接來看看具體如何使用滔蝉,在常見的單體模式中,通常我們會使用 _this , that , self 等保存 this 塔沃,這樣我們可以在改變了上下文之后繼續(xù)引用到它。 像這樣:
var foo = {
bar : 1,
eventBind: function(){
$('.someClass').on('click',function(event) {
/* Act on the event */
console.log(this.bar); //1
}.bind(this));
}
}
在上述代碼里阳谍,bind() 創(chuàng)建了一個函數(shù)蛀柴,當(dāng)這個click事件綁定在被調(diào)用的時(shí)候,它的 this 關(guān)鍵詞會被設(shè)置成被傳入的值(這里指調(diào)用bind()時(shí)傳入的參數(shù))矫夯。因此鸽疾,這里我們傳入想要的上下文 this(其實(shí)就是 foo ),到 bind() 函數(shù)中训貌。然后制肮,當(dāng)回調(diào)函數(shù)被執(zhí)行的時(shí)候, this 便指向 foo 對象递沪。再來一個簡單的例子:
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
bar(); // undefined
var func = bar.bind(foo);
func(); // 3
這里我們創(chuàng)建了一個新的函數(shù)func豺鼻,當(dāng)使用bind()創(chuàng)建一個綁定函數(shù)之后,它被執(zhí)行的時(shí)候款慨,它的this會被設(shè)置成foo儒飒,而不是像我們調(diào)用bar()時(shí)全局作用域。
偏函數(shù)(Partial Functions)
Partial Function也叫Partial Application檩奠,這里截取一段關(guān)于偏函數(shù)的定義:
Partial application can be described as taking a function that accepts some number of arguments, binding values to one or more of those arguments, and returning a new function that only accepts the remaining, un-bound arguments.
可以將部分應(yīng)用程序描述為采用一個接受一些參數(shù)的函數(shù)桩了,將值綁定到這些參數(shù)中的一個或多個附帽,然后返回一個僅接受其余未綁定參數(shù)的新函數(shù)。
這是一個很好的特性井誉,使用bind()我們設(shè)定函數(shù)的預(yù)定義參數(shù)蕉扮,然后調(diào)用的時(shí)候傳入其他參數(shù)即可:
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// 預(yù)定義參數(shù)37
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
和setTimeout一起使用
function Bloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 1秒后調(diào)用declare函數(shù)
Bloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 100);
};
Bloomer.prototype.declare = function() {
console.log('我有 ' + this.petalCount + ' 朵花瓣!');
};
var bloo = new Bloomer();
bloo.bloom(); //我有 5 朵花瓣!
注意:對于事件處理函數(shù)和setInterval方法也可以使用上面的方法
綁定函數(shù)和構(gòu)造函數(shù)
綁定函數(shù)也適用于使用new操作符來構(gòu)造目標(biāo)函數(shù)的實(shí)例,當(dāng)使用綁定函數(shù)來構(gòu)造實(shí)例颗圣,注意:this會被忽略慢显,但是傳入的參數(shù)仍然可用。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
console.log(this.x + ',' + this.y);
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// 實(shí)現(xiàn)中的例子不支持,
// 原生bind支持:
var YAxisPoint = Point.bind(null, 0/*x*/);
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'
axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(17, 42) instanceof YAxisPoint; // true
捷徑
bind()也可以為需要特定this值的函數(shù)創(chuàng)造捷徑欠啤。
例如要將一個類數(shù)組對象轉(zhuǎn)換為真正的數(shù)組:
var slice = Array.prototype.slice;
slice.call(arguments);
如果使用bind()的話荚藻,情況變得更加簡單。
var unboundSlice = Array.prototype.slice;
var slice = Function.protorype.call.bind(unboundSlice);
//...
slice(arguments);
面試題--bind的應(yīng)用場景
1. 保存函數(shù)參數(shù):
首先來看一下這一道經(jīng)典的面試題:
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) // 依次輸出:6 6 6 6 6
}, i * 1000);
}
造成這個現(xiàn)象的原因是等到setTimeout異步執(zhí)行時(shí)洁段,i已經(jīng)變成6了应狱。
那么如何使它輸出:1,2祠丝,3疾呻,4,5呢写半?
- 可以使用閉包保存變量
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function () {
console.log('閉包:', i); // 依次輸出:1 2 3 4 5
}, i * 1000);
}(i));
}
- bind
for (var i = 1; i <= 5; i++) {
// 緩存參數(shù)
setTimeout(function (i) {
console.log('bind', i) // 依次輸出:1 2 3 4 5
}.bind(null, i), i * 1000);
}
實(shí)際山這里也是用了閉包岸蜗,我們知道bind會返回一個函數(shù),這個函數(shù)也是閉包叠蝇。
它保存了函數(shù)的this指向璃岳、初始參數(shù),每次i的變更都會被bind的閉包存起來悔捶,所以輸出1-5.
具體細(xì)節(jié)可從下面的手寫bind深入研究铃慷。
- let
用let聲明i也可以輸出1-5;因?yàn)閘et是塊級作用域蜕该,所以每次都會創(chuàng)建一個新的變量犁柜,所以setTimeout每次讀的值都是不同的。
參考:
https://segmentfault.com/a/1190000018270750
https://www.imooc.com/article/290456