JavaScript

Javascriptのcall/apply関数のプロっぽい使い方 〜 JSおくのほそ道 #014

More than 3 years have passed since last update.

こんにちは、ほそ道です。

今回からしばしJavascriptの基本的な処理と概念を整理していきます。
頻出でありながら詳細を忘れてしまいがちなものや、有効な使い方がわからなくなりがちなもの等をまとめていきます。
今回はcallとapplyを取り上げます。

目次はこちら

基本仕様

callもapplyもFunctionオブジェクトのprototypeであり、用法が良く似ています。
関数.call()関数.apply()という実行の仕方となり、実際に処理されるのは呼び出し側の関数です。馴れないと直感的じゃないかもしれませんが、何度かやってるとすぐ馴れると思います。

基本1:thisを指定する

第一引数は関数内で参照されるthisとなり、無理矢理thisを変更出来ます。
なんでもできちゃうJavascriptらしい処理です。
下記の例ではcallとapplyは同じ動作となります。

thisを指定する
function fn() { return this; }
fn();         // グローバルオブジェクト
fn.call({});  // {}
fn.apply({}); // {}

基本2:引数を渡す

第二引数以降は呼び出し側関数に渡す引数となります。
callの場合は引数の数は決まっていなくて、ひとつひとつ指定します。
applyの場合は引数は配列ひとつだけと決まっていて、関数内部では分割された引数として扱われます。
下記の例もcallとapplyは同じ動作となります。

引数を渡す
function fn(arg1, arg2) { return arguments; }
fn.call({}, 'foo', 'bar');    // { '0': 'foo', '1': 'bar' }
fn.apply({}, ['foo', 'bar']); // { '0': 'foo', '1': 'bar' }

使い道が想像しにくい?

と、仕様だけを見ると「関数をそのまんま実行するのと比べてどんな優位性があるの?」という疑問が普通は沸くんではないかと思います。
そう、この2つの関数はイメージを持って使うことが求められます。カッコイイですね:metal:
それでは以下で具体的な使用パターンをまとめていきます。

応用編1:なりすまし

それでは第一の使用パターンをご紹介します。
thisを変更できる特性を使って、自分が持っていない他のオブジェクトのメソッドを利用できます。Javascriptの世界ではヒトも鳥の力を借りて空を飛べるんですね。
これはやりまくると可読性が落ちたり、第三者を幻惑してしまうアブない用法ですのでちゃんと意図をコメントするなどした上で使ってほしいやり方です。

トイレに入るサンプル
var Girl = function() {
  this.name = '女の子';
  this.enterToilet = function(){
    console.log(this.name + "が女子トイレに入る");
  };
};
var Boy = function() {
  this.name = '男の子';
};

new Girl().enterToilet();                // 女の子が女子トイレに入る
new Girl().enterToilet.call(new Boy());  // 男の子が女子トイレに入る

最下行の出力結果は「男の子が女子トイレに入る」です。

これはいけません。。
しかしcallやapplyを使うとこういう事が出来ちゃいます。
現実世界ではやらないように気をつけましょうね。

応用編2:オブジェクト指向的に継承する

今度はコンストラクタを連鎖させることでスーパークラス/サブクラス的な実装を実現します。

携帯電話を実装してみたいと思います。
携帯電話といってもフィーチャーフォンやスマートフォンとか言いますし、共通する機能と独自機能がありますね。
もっと言えば端末毎にも些細な違いがあります。
今回は携帯電話の共通機能をスマートフォンに継承させます。

携帯電話の実装
// スーパークラス的な携帯電話
function CellPhone(number) {
  this.phoneNumber = number;
}

// サブクラス的なスマートフォン
function SmartPhone(number, wifispots) {
  this.wifispots = wifispots;
  CellPhone.call(this, number);
}
SmartPhone.prototype = new CellPhone(); // insatnceof用

// 利用コード(携帯番号とWifiスポットを固有データとして持たせます)
var myphone = new SmartPhone('09012344432', ['Home','StarBucksWifi']);

CellPhone.call(this, number);とする事でCellPhoneのスマートフォンのコンストラクタから携帯電話のコンストラクタを呼び出してスマートフォンに携帯電話の機能を継承させます。
またSmartPhone.prototype = new CellPhone()としてやるとmyphone instanceof CellPhoneがtrueとなります。

応用編3:applyの活用

次にここまで一回も登場させていないapplyを使った例です。
applyは引数リストを配列化出来るので引数の個数が変動するや配列を引き回すときに有効です。

最大値と最小値の取得
var nums = [100, 300, 500, 700, 900];
var min = Math.min.apply(null, nums);  // 100
var max = Math.max.apply(null, nums);  // 900

同じ配列に対してminとmaxが奇麗に取得できます。
引数リストに同じ引数を二度書きしなくて良いのがイケてますね。

応用編4:処理の汎化

最後に処理の汎化を取りあげます。
沢山のゴミを燃えるゴミと燃えないゴミに分別して捨てる処理を実装してみます。

まずは処理が汎化されていないパターンから紹介します。

ゴミの分別(Before)
// ゴミ箱
var frammableTrashBox = [],
    nonflammableTrashBox = [];

// ゴミクラス
var Rubbish = function(name, type) {
  this.name = name;
  this.type = type;
};

// 分別処理(ゴミクラスとの結合度が高い)
separatedJunk = function(rubbishes) {
  var i = 0; len = rubbishes.length;
  for ( ; i < len; i++) {
    if (rubbishes[i].type === 'flammable') {
      frammableTrashBox.push(rubbishes[i]);
    } else {
      nonflammableTrashBox.push(rubbishes[i]);
    }
  }
};

// メイン処理:ゴミクラスを分別処理にかける
separatedJunk([
  new Rubbish('チラシ', 'flammable'),
  new Rubbish('空き缶', 'nonflammable'), 
  new Rubbish('紙くず', 'flammable')
]);

これでもゴミの分別は問題なく出来ます。
むしろこれくらいの複雑性であればこれで完結して問題ないと思うんですが、ケチをつけるとすれば分別処理のseparatedJunkがゴミクラスに依存しまくっております。
ここで、この関係を疎結合にする為にはどうしたらいいか?
いろいろなアプローチが考えられますが今回はお題にそってcall関数を使ってみましょう!

ゴミの分別(after)
// ゴミ箱
var frammableTrashBox = [],
    nonflammableTrashBox = [];

// ゴミクラス
var Rubbish = function(name, type) {
  this.name = name;
  this.type = type;
};

// 処理の汎化
function repeat(arr, fn) {
    var i = 0; len = arr.length;
    for (; i < len; i++) {
      fn.call(arr[i]);
    }
}

// メイン処理
repeat([
  new Rubbish('チラシ', 'flammable'),
  new Rubbish('空き缶', 'nonflammable'),
  new Rubbish('紙くず', 'flammable')
  ], 
  function() {
    if (this.type === 'flammable') {
      frammableTrashBox.push(this);
    } else {
      nonflammableTrashBox.push(this);
    }
  });

repeat関数という汎用的な関数を作りました。
このrepeat関数は引数2「fn」関数を引数1「arr」で渡された配列の要素がthisとなるように実行します。
分別処理はrepeat関数の引数2「fn」関数として渡しております。
むしろ見難いやん!という方もいるかも知れませんね。
確かにミニサンプルだとcall/applyはそんなに威力を発揮しないとおもいます。が

beforeのseparatedJunk関数とafterのrepeat関数では汎用性が高いのは確実にrepeat関数でしょう。
repeat関数はゴミを捨てるという処理以外にも活用出来るのがミソです。

自作で大型のライブラリ/コンテナを作成するなど開発アイテムが複雑になってきた場合には汎化が生きてきます。
やり方に正解は無いですがいろんなライブラリ等を見ているとこの手のcall/applyはよく出てきますので「これは汎化しているんだな」というのが理解できるだけでも価値はあるかと思います。

まとめ

call/applyは抽象的な関数で、利用パターンは常に何かしらの意図を持って行われます。
おそらくその周辺には配列やコールバック関数、クラス定義関数等が存在している事でしょう。
人のコードを読む際にcall/applyが出てきたら「何で使っているのか?」を考える事が必要です。
そして、その考察や仲間との議論はとても愉しいものではないかと思います。

今回は以上です。
次回はbind関数を取り上げます。