JavaScriptのapply()とcall()

いつも忘れるので具体的な使い方を残しておこうと思います。自分のためのメモで目新しいことは何もありません。記事の最後にapply()とcall()の引数の型がどっちが配列でどっちが可変長引数リストか忘れない覚え方を載せました。そもそも間違えないとツッコまれそうですが、もし私と同様のお悩みをお持ちの方がいらっしゃいましたら、ご一読いただけたらと思います。

参考サイト

MDN(Function.prototype.apply)
MDN(Function.prototype.call)

apply(), call()とは

JavaScriptの関数はオブジェクトであり、すべての関数がプロパティとメソッドを持っています。このデフォルトで実装されているメソッドのなかにapply()とcall()があります。

JavaScriptの関数を実行すると、呼び出し元から引数とともにthis(コンテクスト、またはコンテキスト)が暗黙的に必ず渡されます。apply()とcall()はこのthisを自由に設定できるメソッドです。

構文

fun.apply(this, array);
引数:コンテクストと配列1個
fun.call(this, arg1, arg2, arg3, …);
引数:コンテクストと可変長の引数

使い方

最大値/最小値を取得する

var array = [0, 6, 12, -1, 3];

// 自力で検出する場合
var max = min = array[0];
for (var i=1; i<array.length; i++) {
  if (max < array[i]) max = array[i];
  if (min > array[i]) min = array[i];
}
console.log(min, max);	// -1 12

// Mathメソッドを使う
// 比較対象の個数が定数の場合はそのまま使える
// でも10個とか100個とか書きたくない
// ちなみに第一引数は使用されないのでnullでも問題なし
Math.min(Math, [0], [1], [2], [3], [4]);
Math.max(Math, [0], [1], [2], [3], [4]);
console.log(min, max);	// -1 12

// Mathメソッドのapply()を使う
// 配列を渡すことができる
var min = Math.min.apply(Math, array);
var max = Math.max.apply(Math, array);
console.log(min, max);	// -1 12

配列のようなオブジェクトに配列メソッドを使う

配列のようなオブジェクトは本物の配列ではないため、直接Arrayクラスのメソッドを使うことはできません。Arrayクラスのメソッドを使うには、Array.prototypeの配列メソッドからapply()またはcall()を呼び出し、第一引数のコンテクストに配列のようなオブジェクトを指定します。

配列のようなオブジェクト

argumentsオブジェクト
関数の引数
getElementsByClassName(“クラス名”)
指定したクラス名を持つ要素
getElementsByName(“name属性”)
指定したname属性を持つ要素
getElementsByTagName(“タグ名”)
指定したタグ名を持つ要素
querySelectorAll(“セレクタ”)
指定したセレクタにマッチする要素

よく使う配列メソッド

slice
配列の部分取り出し(元の配列を変更しない)
forEach/map
配列の各要素に対して関数を実行
// ex. 配列のようなオブジェクトを配列に変換
// 配列のメソッドが使えるようになる
var args = Array.prototype.slice.call(arguments);
var args = [].slice.call(arguments);	// 上記と同じ式

// ex. 指定したクラスを持つすべての要素のテキストを書き換える
// 上記の変換を使って配列のメソッドforEach()を呼び出す
var elems = document.getElementsByClassName("counter");
var array = Array.prototype.slice.call(elems);
array.forEach(function(elem) {
  elem.textContent = "0";
});

// これをcall()メソッドを使って書き直すと以下のようになる
var elems = document.getElementsByClassName("counter");
Array.prototype.forEach.call(elems, function (elem) {
  elem.textContent = "0";
});

// ex. セレクタで指定されたすべての要素のクラスを書き換える
// querySelectorAll()は便利
var elems = document.querySelectorAll(".signal");
Array.prototype.forEach.call(elems, function(elem) {
  elem.classList.remove("red");
  elem.classList.add("blue");
});

// ex. セレクタで指定されたすべての要素のサイズを配列にして出力する
// map()の場合もforEach()と同じように使える
var elems = document.querySelectorAll(".signal");
var size = Array.prototype.map.call(elems, function(elem) {
  return {w: elem.offsetWidth, h: elem.offsetHeight};
});

// ex. 可変長の引数を使用する関数を呼び出す
function sum() {
  for (var i=0, n=0; i<arguments.length; i++) {
    n += arguments[i];
  }
  return n;
}
var answer = sum(2,4,6,8,10,12,14);
var array = [2,4,6,8,10,12,14];		// 配列で扱いたい
var answer = sum.apply(this, array);	// そんなときはapply

apply()とcall()どっちを使う?

引数の型の違いがあるだけでどっちも同じように使えます。どっちを使ったらいいか迷ったら、コードがきれいに見えるほうを採用すればいいと思います。

apply()とcall()の引数の型がどっちが配列でどっちが可変長引数リストか忘れない覚え方

どっちがどっちか覚えられず悩んでいたときがありました。読み方、連想、語呂合わせ… いろいろ試しましたが効果がなく、毎回ググるしかないのかとあきらめかけていました。そもそも関数名が(apply: 適用する)、(call: 呼ぶ)とか、引数の型にぜんぜん関係ないではありませんか。単純にcallbyAry()やcallbyArg()みたいにしてくれていれば… そんなしょうもないことを考えていたある日のこと、突然天啓を授かりました。
その内容は以下の図式になります。

apply >>> arrly >>> array === 配列

これを見たらもう忘れないと思います。
以上です。

HTML5 Canvasの勉強

HTML5のCanvas要素の勉強のために作成したサンプルが少したまったのでご紹介します。

globalCompositeOperation

Canvasの魅力的な機能のひとつに合成処理があります。図形などを描画するときにglobalCompositeOperationプロパティを設定することで、描画先となるCanvasの内容と合成処理を行い、その結果を描画することができます。

合成とクリッピング(MDN)
globalCompositeOperation プロパティ(HTML5.jp)

ドキュメントを読むだけだと眠くなるので動作確認用のコードを書いてみました。動かす場合は現在大きいウィンドウサイズにしか対応していないので、お手数ですがCodePenに飛んで頂けたらと思います。

See the Pen canvas by senmyou (@senmyou) on CodePen.

lighterやmultiply、screenなど、重なった色を演算して描画するのっておもしろいですね。使いこなせると楽しいかも。

Memo

  • globalCompositeOperationプロパティを指定しない場合はデフォルトとしてsource-overが設定されます。source-overはこれから描画しようとする図形をCanvasの内容の上に描く設定です。
  • 気をつける点としては、globalCompositeOperationプロパティやglobalAlphaプロパティはいったん関数を抜けたりgetContext(“2d”)やbeginPath()を実行しても、値はクリアされずに保持されているということです。つまり「普通に描画するのでglobalCompositeOperationをわざわざ設定することないよね」と思っていても、前に合成処理を行っていればそれが反映されてしまいます。globalCompositeOperationプロパティやglobalAlphaプロパティを設定した場合は、描画後にデフォルトに戻すか、またはsave()/restore()で状態を管理しないとトラブルの元になります。

アニメーション+残像効果

Canvasアニメーションのサンプルです。

  • arc(), fill()でsin/cos式の軌跡を描画
  • requestAnimationFrame()によるブラウザの再描画タイミングに合わせた描画
  • globalAlphaによる透過設定でCanvasを塗り重ねていくことで残像を表現

See the Pen canvas by senmyou (@senmyou) on CodePen.

テキストのネオン効果

ネットでCanvasの使い方を探していたときに素晴らしいサイトを見つけました。

canvas のタイポグラフィ効果(HTML5 Rocks)

魅力的なサンプルに詳しい解説。情報の量、質ともにすごいと思います。そして、ありがたいことにサイトの下の方からソースコードを一式ダウンロードできます。
これをまねてみることにしました。
。。。とはいうものの、やってることが私にとってレベル高過ぎ。ちょっとあきらめかけましたが、根性で無理やり簡略化して動かしました。きっとなにか足りない部分とかあるかもしれませんが、とりあえず、ということで。

See the Pen canvas by senmyou (@senmyou) on CodePen.

これ、CSSでもtext-shadowを使って簡単にできるんですよね。苦労してコーディングしたのにこのままじゃ報われないということで、JavaScriptを活かしたサンプルを無理やり作りました。

See the Pen canvas by senmyou (@senmyou) on CodePen.

。。。ちょっと微妙ですかね。ソースの方はさらに微妙な具合に仕上がってます。

もうひとつ、ジッター効果なるものもまねをさせて頂きました。

See the Pen canvas by senmyou (@senmyou) on CodePen.

。。。うつくしぃ。

一番最後の「残像—ジェネレーティブ効果」も素晴らしい作品でまねてみました。簡略化してなんとか表示できたのですが、これだけは内容がよくわからないため載せるのはあとにしようと思います。

CanvasってAPIの数自体はそんなにないですし使い方もわりと簡単なんですが、ネットやCodePenなどで芸術的な作品を見るたびに驚きと感銘を受けます。さしあたり、CodePenで「canvas」を検索するとたくさんでてきますので、ご覧になってみてはいかがでしょうか。それでは、すてきなキャンバスライフを。

処理時間の計測

アプリの起動がみょーに遅くて、どこで時間がかかっているのか調べたくなることってありますよね。私はしょっちゅうあります。そこで、JavaScriptで処理時間を計測する方法について調べました。メジャーな方法は次の2つのようです。

  • consoleオブジェクトのtime()/timeEnd()メソッドを使う
  • DateオブジェクトのgetTime()メソッドを使う

それぞれについて簡単に説明します。

consoleオブジェクトのtime()/timeEnd()メソッドを使う

console.time()メソッドによりタイマーを開始し、console.timeEnd()メソッドでタイマーを停止してその間の経過時間をミリ単位で取得します。引数はタイマーを選別する任意の名前になります。

console.time() — MDN
console.timeEnd() — MDN

使用例

console.time("timer_0");
alert("click!");
console.timeEnd("timer_0");

console.timeEnd()メソッドを実行すると経過時間がコンソールに出力されます。

ここで、戻り値がどうなっているのか気になって実験してみました。

実験

var a = console.time("timer_0");
alert("click!");
var b = console.timeEnd("timer_0");

console.log("a: " + a + ", b: " + b);

コンソール出力

timer_0: 7798.10693359375ms
a: undefined, b: undefined

戻り値はundefinedでした。取得した経過時間をUIに表示したりプログラムで使うことはできないみたいです。console()メソッドですから当然といえば当然ですかね。

DateオブジェクトのgetTime()メソッドを使う

Date()コンストラクタからインスタンス化した日付オブジェクト(new Date())のgetTime()メソッドは、1970年1月1日00:00:00からインスタンス化までの時間をミリ秒で返します。処理時間は、計測開始時と終了時にgetTime()メソッドでミリ秒を取得し、その差分をとることで得ることができます。
ちなみに、インスタンス化せずに直接Dateオブジェクトのnow()メソッドを使用してもgetTime()メソッドと同じ結果になります。ただしIE9未満で未サポートになっています。

getTime() — MDN

使用例

var stime = new Date().getTime();
for(var i=0, a=0; i<100000; i++) a += i;
var etime = new Date().getTime();

console.log("time: " + (etime - stime));

時間計測用のオブジェクトを作ってみた

処理時間を計測したいポイントが複数ある場合、各ポイントにgetTime()の実行と取得した値を格納するコードを埋め込み、最後にループ文などで差分を計算して表示する、というパターンになるかと思います。そこで時間計測用オブジェクトを作成してみました。

Script

var timeLog = (function() {
  var timeLog = {};
  var memory = [];
  timeLog.rec = function(comment) {
    memory.push({
      comment: comment || "",
      time: new Date().getTime()
    });
  };
  timeLog.show = function(flg) {
    var n = memory.length;
    if (n<=1) {
      console.log("Too few records");
      return;
    }
    var stime = memory[0].time, etime, delta, log, logs="";
    for (var i=1; i<n; i++) {
      etime = memory[i].time;
      delta = etime - stime;
      stime = etime;
      log = "[" + i + "] " + memory[i].comment + ": " + delta + "ms";
      logs += log + "\n";
      console.log(log);
    }
    if (flg) window.alert(logs);
  };
  timeLog.clear = function() {
    memory.length = 0;
  };
  return timeLog;
})();

使用例

timeLog.rec("start");
for(var i=0, a=0; i<100000; i++) a += i;
timeLog.rec("一回目の処理");
for(var i=0, a=0; i<1000000; i++) a += i;
timeLog.rec("二回目の処理");
for(var i=0, a=0; i<10000000; i++) a += i;
timeLog.rec("最後の処理");

timeLog.show();

コンソール出力

[1] 一回目の処理: 9ms
[2] 二回目の処理: 33ms
[3] 最後の処理: 193ms

一連の処理をまとめただけなのでその必要性が微妙なのですが、使用感はすっきりさわやかです。また、show()の引数にtrueなどを渡して呼び出すとアラートダイアログを表示します。release版のapkなどで確認する場合に使えると思います。