2016年10月15日土曜日

DOMイベントのキャプチャ/バブリングを整理する

入れ子なDOMのイベント発生順序制御

DOMが入れ子構造になっていてそれぞれにイベント(例えばClickイベント)が設定されていた場合
「このように動いてほしい」という期待はケースバイケースであると思います。
期待通りの処理になるようカッチリ制御しちゃいましょう。

addEventListenerの第三引数「useCapture」

例えば下記の様なHTMLがあったとします。
body内にdevが入れ子になっておりそれぞれにClickイベントが登録されています。

キャプチャとバブリング
<html>  <head lang="en">      <meta charset="UTF-8">      <title>event</title>      <style>          #outer {              width: 150px; height: 150px;              background: #00AAFF;              padding-top: 50px; padding-left: 50px;          }          #inner {              width:100px; height: 100px;              background: #EEFF00          }      </style>  </head>  <body>      <div id="outer">          <div id="inner" align="center"></div>      </div>      <script>          function out(s) {return function() {console.log(s);}}          document.getElementById('outer').addEventListener('click', out('outer'), true);          document.getElementById('inner').addEventListener('click', out('inner'));      </script>  </body>  </html>  

外側のdiv要素のaddEventListenerには第三匹数にtrueをセットしています。
第三引数は内部的にはuseCaptureという名のフラグです。
このuseCaptureは省略可能でデフォルトはfalseとなっています。

内側要素クリック時の挙動

  • outerのuseCaptureをtrueにした場合

スクリーンショット 2014-07-16 22.14.22.png←outerが先

  • outerのuseCaptureをfalseまたは省略した場合

スクリーンショット 2014-07-16 22.18.45.png←innerが先

そう、useCaptureフラグの値によってイベントが発生する順序が逆になるのです!

イベントフェーズとは?

イベントが発生すると下記の流れでイベント伝播が発生します。

  • (キャプチャフェーズ) DOMツリーをたどってルート要素から発生要素を探しに行く
  • (ターゲットフェーズ) 発生要素を検出する
  • (バブリングフェーズ) 今度はルート要素まで遡る

W3Cのドキュメントに載っていた図がわかりやすいので拝借します。
eventflow.png

親要素が同種のイベント(今回はClickイベント)を持っていた場合、
キャプチャフェーズでイベントを発生させてしまうよう設定するのがuseCapture=trueという事です。
逆にuseCaptureをfalseまたは省略すると親要素のイベントはバブリングフェーズで実行されます。

内側をクリックしたときに外側のイベントを発生させない

さて、もうひとつまとめておきます。
そもそも、イベント伝播実行自体やめてくれー
というケースは結構あると思います。
内側要素クリック時は内側要素のイベントだけ動けばええんや。。
というケースでは、最初のHTML例のscript部分を下記のようにします。

<script>    function out(s) {      return function(e) {        e.stopPropagation();        console.log(s);      }    }    document.getElementById('outer').addEventListener('click', out('outer'));    document.getElementById('inner').addEventListener('click', out('inner'));  </script>  

変更ポイントは下記です。

  • イベント関数に引数eを渡し、e.stopPropagation();を実行している
  • 親要素のaddEventListenerのuseCaptureは省略(バブリングフェーズ実行)している

イベントオブジェクト

イベントで実行される関数は暗黙的に引数にイベントオブジェクトを受け取ります。

event.stopPropagationメソッド

イベントオブジェクトが持つstopPropagation()メソッドを実行するとイベント伝播が停止されます。
つまりバブリングフェーズで外側要素のイベントが発生しなくなるという事ですね。

ちなみに外側要素のuseCaptureをtrueにしてしまうと
キャプチャフェーズで外側要素のイベントが実行され、そこでイベント伝播を止めてしまいます。
ターゲットフェーズで本来クリックされた要素のイベントが発生されなくなる事にご注意下さい。


アチラコチラにイベントをセットしていったら伝播しまくりでパニック!なんて事にはならないよう
イベントフェーズはしっかり意識しておきたいなと思います。

Javascriptのbind関数と部分適用

bind関数の仕様

bind関数はFunction.prototypeに属し、新たな関数を生成して返します。
下記で仕様を見ていきます。

その1:thisを強制変更する

第一引数は関数内で参照されるthisを置換えます。

thisを強制変更する
// 人間  function Man(name) {    this.name = name;    this.greet = function() {      console.log("Hello, my name is " + this.name);    };  }    // ネコ  function Cat(name) {    this.name = name;  }    // 人間の挨拶  var steve = new Man("Steve");  steve.greet();           // Hello, my name is Steve    // ネコの挨拶!?  var tama = new Cat("Tama");  var tamaGreet = steve.greet.bind(tama);  tamaGreet();            // Hello, my name is Tama  

bind関数を使う事で本来ネコが持っていないgreet関数が使えます。怖可愛いですねぇ。。

ん、コレって既視感を覚える、と思った方、Yesです!
そう、前回やったcall関数にクリソツなんですね!
大きな違いとしては
call関数は関数を実行するのに対し、
bind関数は新たな関数を生成して返すんですね。

その2:引数を予約する

bind関数は第二引数以降も取る事が出来ます。
第二引数以降は引数の予約に使われます。

引数を予約する
var neoMax = Math.max.bind(null, 200, 300, 400);  console.log(neoMax(100));    // 400  console.log(neoMax(800));    // 800  

上記のようにMath.max関数を流用して、あらかじめ第1〜3引数を特定値200,300,400で占有させたneoMax関数を生成出来ます。
call/apply関数の時は 「汎化」 という言葉を使いましたが、
このように引数を予約して生成された関数は 「特化」 されると言って良いのではないでしょうか?

部分適用

特化関数の生成をちょっと掘り下げましょう。
bind関数の引数予約を使うと部分適用が簡潔にできます。
ビフォーアフター的にbind関数を使うケース、使わないケースを見ていきましょう。

bindをつかわない部分適用

bindをつかわない部分適用
function add(x, y) {    if (typeof y === 'undefined') {      return function(y) {        return x + y;      };    }  }    // 関数を部分適用  var add200 = add(200);  console.log(add200(300));    // 500  

第二引数であるyが無ければ新たな部分適用された関数を返します。
次はbindをつかったパターンも見ていきます。

bindをつかった部分適用

bindをつかった部分適用
function add(x, y) {    return x + y;  }    // 関数を部分適用  var add200 = add.bind(null, 200);  console.log(add200(300));     // 500  

と、簡潔に書くことができます。

ただし、気をつけるポイントとして
bind関数を使う場合はadd関数内部では部分適用が意識されていません。
別の場所で実行された処理がadd関数を流用して特化関数を作成しているという事がひとつの特徴になっており、
どれが正解という訳ではないですが、プログラムの文脈に即した使い方が出来るように配慮しておく事が大事かなと思います。

カリー化について

もともとのポストで部分適用とカリー化について誤解があった状態で内容記述していた為、改めて整理しておきます。

カリー化 (currying, カリー化された=curried) とは、複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。from wikipedia

下記のような感じですね。 ご指摘いただいた皆様には心より感謝申し上げます。。:pray:

function add(x) { return function(y) { return x + y; } }  console.log(add(150)(350));  // 500

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が出てきたら「何で使っているのか?」を考える事が必要です。
そして、その考察や仲間との議論はとても愉しいものではないかと思います。