【JavaScript】apply、callの使いどころ

 特に初心者にとって使いどころのわからない、Function関数であるapplyとcallについて解説します。

apply、callとは

 Function.prototypeに属する関数で、主に「thisの参照を置き換える」「引数を配列で渡す」などの用途に使われます。

thisの参照を置き換える

 Function.prototype.apply()および、Function.prototype.call()では、this参照値を第一引数に渡すことができます。

class User{
  showId(){
    console.log(this.id);
  }
}

const user = new User();
user.id = 10;
user.showId();                  // -> 10
user.showId.apply({id:1000});   // -> 1000
user.showId.call({id:1010});    // -> 1010

 UserクラスのshowId()関数は、自己のインスタンスに格納されているidの値をコンソールに出力する単純なものです。

 userにインスタンスを新しく作成し、idに10を代入してshowId()を呼び出すと、コンソールにuserインスタンスのidである10が出力されます。

 しかし、その後に続くapplyとcallの例では、渡している引数はUserのインスタンスではなく、オブジェクトリテラルから生成された、ただのObjectです。

 つまり、他のクラスの関数を、thisを指定してどこからでも自由に呼び出すことができるわけです。

 thisを変換する有名な例を挙げてみましょう。

function showArgs(){
  Array.prototype.forEach.call(arguments, arg => console.log(arg));
}

showArgs(1,2,3,4,5);

 argumentsは関数スコープで参照できる、渡された引数を配列ライクなオブジェクトに格納したものですが、配列ではないためforEachなどのメソッドが使えません。

 しかし、Arrayクラスの関数をcallでthis参照を与えて呼び出してやることで、あたかもforEachをargumentsに対して実行したかのような動作をすることができます。

 ただ、ES2015以降は、あまりこの使い方はしなくなりました。以前は、以下のように継承時の親クラス呼び出しに対して使っていたのですが……、

var User = function(){
  this.id = 10;
  this.name = "User";
};

User.prototype.showName = function(){
  console.log(this.name);
};

User.prototype.showId = function(){
  console.log(this.id);
};

var ExtendedUser = function(){
  User.prototype.constructor.call(this);    // 親クラスのコンストラクタを呼び出す
  this.name = "ExtendedUser";
};

ExtendedUser.prototype.__proto__ = User.prototype;

var extUser = new ExtendedUser();

extUser.showName();         // -> ExtendedUser
extUser.showId();           // -> 10

 ES2015が浸透してからは、extendsやsuperなどのシンタックスシュガーが利用できます。

class User{
  constructor(){
    this.id = 10;
    this.name = "User";
  }

  showName(){
    console.log(this.name);
  }

  showId(){
    console.log(this.id);
  }
}

class ExtendedUser extends User{
  constructor(){
    super();                // 親クラスのコンストラクタを呼び出す
    this.name = "ExtendedUser";
  }
}

const extUser = new ExtendedUser();

extUser.showName();         // -> ExtendedUser
extUser.showId();           // -> 10

 こういった構文は、非常に簡潔かつ理解しやすい形になるので、ES2015以前のブラウザに対応せざるを得ない場合でも、Babelなどを使って積極的に使用することをお勧めします。

applyとcallの違い

 applyとcallの違いは非常に単純で、callは第二引数以降を順番にそのまま引数として渡すのに対し、applyは第二引数に配列として渡す点にあります。

 この、「配列として渡す」というのが重要で、これによって様々なメソッドを簡潔に使うことができます。たとえば。

function max(array){
  return array.reduce((a,b)=>{
    if(a < b){
      return b;
    } else {
      return a;
    }
  });
}

const array = [1,2,6,3,10,2,5];

console.log(max(array));  // -> 10

 これは、max関数(配列の中で最も大きい数値を返すもの)を実装したものです。しかし、applyを利用すると、JavaScriptビルトインオブジェクトであるMath.max()関数が利用できます。

function max(array){
  return Math.max.apply(null,array)
}

const array = [1,2,6,3,10,2,5];

console.log(max(array));  // -> 10

 これを使う利点は二つあります。

 まず第一に、reduce関数等を使った場合に比べてコードが短くなること。そして二つ目は、動作がMath.maxの仕様に明確に準拠しているということです。

 即ち、関数の仕様をそこまで厳格に書く必要がなく、またテストも最小限でよくなります。これは非常に大きなメリットです。

 もう一つ例を挙げてみましょう。とあるREST APIのリソースをID=1からID=1000まで取得し、全てのフェッチが完了したら何か処理をしたいとします。本例ではjQueryを利用してみます。

$.when($.get("http://example.com/resources/1"), $.get("http://example.com/resources/2"), $.get("http://example.com/resources/3")) // ...

 これが1000回続くわけですが、さすがにやってられないですね。ajaxのasyncオプションをfalseにしてfor文を書きかねません。しかしそれでは、フェッチが全て完了するまでブラウザが停止してしまいます。

 applyを使うと、以下のようにできます。

let deferreds = [];
for(let i = 1; i <= 1000; i++){
  deferreds.push($.get(`http://example.com/resources/${i}`));
}
$.when.apply($,deferreds).done(()=>{
  // ...
});

 引数を配列で渡すことができるため、for文などでデータを生成する処理など、非常に便利な使い方がいろいろできるようになっています。

まとめ

 call、applyはJavaScript初心者が突き当たる壁のひとつですが、使いどころを押さえてしまえば、意外と理解しやすく、便利なメソッドだと思います。

 ぜひマスターして、コードを簡潔にしていきましょう!