前言
在《趣談js的bind牌膠水》這篇文章中,我聊到了js的bind牌膠水,這篇文章我來聊聊bind牌膠水的升級版:call和apply方法。
Why? ——> 為什麼會出現apply和call?
在《趣談js的bind牌膠水》中,我通過js的相關歷史,敘述了bind、call、apply三方法誕生的背景,同時也指出這三個方法出現的共同目的就是就是為js的一等公民Function函式找個門當戶對的人家(指明Function函式的this指向),既然bind方法已經滿足了目的,為什麼還需要創造出call、apply兩個方法呢?這兩個方法和bind有哪些異同點?帶著些許疑問,且隨小生遨遊前行。
What? ——> call和apply是啥玩意兒?
1、漢語釋義:
call:召喚、呼叫、訪問
apply:應用、適用、申請
在call和apply的中文釋義中我們可以看出call、apply這兩個方法帶有明顯的連線特性,比如“召喚call”:who召喚who?“應用apply”:who應用到who上?還有bind的中文釋意義:“繫結”,從這三個中文釋義中不難看出滿足連線特性的動詞需要三元素:1.主動連線方、2.被動連線方、3.連線二者的中介。對比這三個中文釋義,可以看出bind和call、apply的釋義略有不同,bind的中文釋義帶有明顯的靜態連線特性(只連線),call、apply的中文釋義中帶有明顯的動態連線特性(連線之後還使用),所以在三個方法的使用上,bind只負責連線函式與相應的物件,call、apply在連線好函式與相應的物件後還主動把“連線了指定物件的函式”給當場執行了!
2、語法解析:
function.call(thisArg, arg1, arg2, ...); // call語法
function.apply(thisArg, [argsArray]); // apply語法
複製程式碼
具體的語法可以去MDN上看詳情,這裡關於thisArg
說以下幾個注意點:
- 不傳,或者傳null,undefined,this指向window物件(如果沒有房子,那就只能露宿天地了,55555)
- 傳遞另一個函式的函式名fun2,this指向函式fun2的this指向(fun2隨誰,俺就隨誰,嫁雞隨雞嫁狗隨狗?)
- 值為原始值(數字,字串,布林值),this會指向該原始值的自動包裝物件,如Number、 String、Boolean
- 傳遞一個物件,函式中的this指向這個物件
在上面的幾種thisArg
引數例子中,我們發現一個共同的事實就是:thisArg
引數永遠會是個物件,原始值就用原始值對應的包裝物件,函式就用該引用該函式的物件,無物件時就是全域性物件,那些看上去沒物件的情況,其實也是有物件的,不難看出,js是一門物件導向程式設計的語言,處處都是物件,萬物皆有物件,那你呢,你有沒有物件?
3、詳細敘述:
call和apply方法都是為了改變函式的this值而生,具體使用如下:
var obj = {
age: 22
}
function say(name) {
console.log('我是:' + name + '|今年:' + this.age);
}
say.call(obj, 'jack'); // 我是:jack|今年:22
say.apply(obj, ['mike']); // 我是:mike|今年:22
複製程式碼
- 通過程式碼可以看出call和apply有以如下相同點:
- 第一個引數指明瞭宿主物件
- 指明瞭新宿主物件後,立即執行該函式
- 唯一不同點:apply接收的是陣列格式的引數,call接受的是若干個引數。關於兩種傳參形式,我是這樣理解的:apply帶有“授予”之意,類似皇帝的封賞(是一種自上而下的交接),皇帝的封賞會給你一個清單,有些啥子東西都在清單裡,call帶有“呼喚”之意(是一種比較親密的交接),你呼喚一個朋友過來,給他講些小祕密,你會一五一十的把這些祕密逐個講出來。
How? ——> 怎樣使用call和apply?
call技能 —— 北風驟起:
技能詳解: “Master”從天地中召喚出一個強力風暴,逐一對多個目標造成60/85/135/160(+0.35)點魔法傷害。
技能演示:
var Master = {
name: '召喚師'
};
var target1 = 'enemy1';
var target2 = 'enemy2';
var target3 = 'enemy3';
var target4 = 'enemy4';
var target5 = 'enemy5';
function NorthernStorm(target1, target2, target3, target4, target5) {
console.log(this.name + ' have slained an enemy ' + target1);
console.log(this.name + ' have slained an enemy ' + target2);
console.log(this.name + ' have slained an enemy ' + target3);
console.log(this.name + ' have slained an enemy ' + target4);
console.log(this.name + ' have slained an enemy ' + target5);
}
NorthernStorm.call(Master, target1, target2, target3, target4, target5);
複製程式碼
apply技能 —— 末日風暴:
技能詳解:“Master”從天地中召喚出一個強大的末日風暴,可以瞬間應用到一個目標群體上,造成200/250/300/444(+1)點AOE魔法傷害。
技能演示:
var Master = {
name: '召喚師'
};
var target1 = 'enemy1';
var target2 = 'enemy2';
var target3 = 'enemy3';
var target4 = 'enemy4';
var target5 = 'enemy5';
function PowerfulStorm(arr) {
console.log(this.name + ' Penta Kill!');
}
PowerfulStorm.apply(Master, [target1, target2, target3, target4, target5]);
複製程式碼
哈哈,上面我用遊戲技能簡單的演示了一下call和apply方法的使用,希望能幫助大家理解相關概念,為了加深理解這裡我針對幾個具體的使用場景做了幾個示例:
1. 獲取陣列中的最大/小值
var nums = [11, 15, 2, 20, 10];
var max = Math.max.apply(null, nums);
var min = Math.min.apply(null, nums);
console.log(max); // 20
console.log(min); // 2
複製程式碼
2. 將函式的arguments轉換為陣列
function func() {
var args = Array.prototype.slice.call(arguments);
console.log(args);
}
func('hello', 'world'); // ["hello", "world"]
複製程式碼
3. 判斷是否為陣列格式
var arr = [];
var res = Object.prototype.toString.call(arr); // 這裡獲取的是變數的 [[class]]屬性,一般方法沒有,只有借用Object原型上的toString方法才可以
console.log(res); // [Object Array]
複製程式碼
關於apply和call的使用例子不做過多敘述,因為網上一大把,之前一直覺得js的call、apply、bind三方法使用很彆扭,很醜陋(現在也覺得),後來我學會換個角度看世界後就舒服了很多,以這個例子為例:
var nums = [11, 15, 2, 20, 10];
var max = Math.max.apply(null, nums);
複製程式碼
我們把不相關的剔除掉(1、為空時this指向的物件就是Window全域性物件;2、Window物件取代Math物件使用max方法),程式碼如下:
Window.max(nums);
複製程式碼
注意:上面的程式碼只是輔助理解,在實際執行時,Window物件上只會短暫的存在max方法,一次性的使用了max方法之後,就會從Window上delete掉max方法,所以通過call、apply繫結給指定物件的函式最終並不會存在於指定物件上。
總結
1. bind和apply、call的異同
- 相同點:都立足於改變函式的this指向
- 不同點:
- call和applly會立即執行函式,bind只是繫結了函式,並不會立即執行函式
- call、apply因為要立即執行函式,所以第二個引數或之後的引數都是當前的真實引數,bind是“預設引數”(這裡可以參考文章《趣談js的bind牌膠水》中關於bind預設引數的闡述)
一些想法
我個人一直覺得bind、call、apply使用起來不舒服,感覺可有可無,但後來發現這三個方法還是有很多用武之地的,比如在dom物件中繫結事件就需要bind方法,比如想複用某些函式就可以用到call和apply,js出現這三個方法很大程度上是因為js用的是函數語言程式設計的樣子,但其實又是物件導向(DOM物件,資料物件等)的裡子,兩種程式設計思路參雜在了一起,參雜其實沒問題,但二者的參雜沒能很好融合,設計bind、apply、call就是為了討好兩方,融合二者,但這種帶有臨時性質的妥協方案,效果不咋地,因為一山不容二虎,總得有人做紅花,有人甘當綠葉,不是嗎?直到以Angular、React、Vue等為代表的MVVM架構和改進的ES6新標準出現,前端開發進入新的模式,MVVM架構能讓前端開發較好的實現“物件導向”的程式設計模式,同時利用ES6的相關特性兼顧函數語言程式設計的靈活性,以往很多問題都不需要bind、call、apply這三兄弟了,比如ES6的箭頭函式就是解決bind的神器,在React的開發中,如果按照傳統思路給事件的匿名函式繫結物件,需要手動用bind繫結,但利用ES6的“箭頭函式”可以這樣繫結:
<div
onClick={(res) => {
// 這裡的this就是
this.setState({
name: 'jack'
});
}}
>
Click Me
</div>
複製程式碼
比如在上面如何使用call、apply的例子中可以用ES6的擴充套件操作符...替代來處理:
// 將arguments轉換為陣列
function func() {
var args = ([...arguments]);
console.log(args);
}
func('hello', 'world'); // ["hello", "world"]
// 求陣列最大值
var res = Math.max(...[2,20,22]);
console.log(res); // 22
複製程式碼
JS在不斷的升級,這三個方法在當前開發的某些場景中可能還會有用武之地,但在我看來,bind、apply、call作為一個“妥協方案”終將會慢慢的退出舞臺,但在它們被遺忘之前理解設計者們的智慧和想法,我覺得是很有意思的。
結語
文章涉及內容很多,難免會有紕漏,望理性指正,一起進步哦。