趣談js的call和apply兩大召喚術

hanmin發表於2018-05-21

前言

《趣談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有以如下相同點:
  1. 第一個引數指明瞭宿主物件
  2. 指明瞭新宿主物件後,立即執行該函式
  • 唯一不同點:apply接收的是陣列格式的引數,call接受的是若干個引數。關於兩種傳參形式,我是這樣理解的:apply帶有“授予”之意,類似皇帝的封賞(是一種自上而下的交接),皇帝的封賞會給你一個清單,有些啥子東西都在清單裡,call帶有“呼喚”之意(是一種比較親密的交接),你呼喚一個朋友過來,給他講些小祕密,你會一五一十的把這些祕密逐個講出來。

How? ——> 怎樣使用call和apply?

call技能 —— 北風驟起:

趣談js的call和apply兩大召喚術

技能詳解: “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技能 —— 末日風暴:

趣談js的call和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指向
  • 不同點:
    1. call和applly會立即執行函式,bind只是繫結了函式,並不會立即執行函式
    2. 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作為一個“妥協方案”終將會慢慢的退出舞臺,但在它們被遺忘之前理解設計者們的智慧和想法,我覺得是很有意思的。

結語

文章涉及內容很多,難免會有紕漏,望理性指正,一起進步哦。

相關文章