本気でやるなら innerHTML を活用すべき

本気でやるならonclick属性は避けてライブラリを活用すべき - VTuberになったプログラマーの魂の残滓

僕は逆に onclick 属性を多用してしまいます。
たぶん『本気』の度合いが違うんだと思います。


『デザイナーとの分業の点からみれば使用しないほうが好ましい』というのは、『既に存在する HTML に対してイベントを追加する』という状況に限っていえば agree です。ただ JavaScrip で HTML を生成するという状況になると話が違います。JavaScript による HTML の生成においては createElement よりも innerHTML を使った方が数倍速い(特にIE)という事情から、onclick 属性を使うことが多くなります。正確にいうと onclick だけに限った話ではなく、イベントハンドラを HTML の属性として書けるので重宝するということです。


よくあるのは JSON で受け取ったデータから JavaScript で HTML を生成する場合などで、innerHTML を利用した書き方だと以下のようになります。

var items = [
  { title:'hoge', link:'/xxx/hoge', date:'2008/05/19 10:30' },
  { title:'fuga', link:'/yyy/fuga', date:'2008/05/19 14:00' }
];

function item_builder() {
  var html = [];
  for (var i = 0, len = items.length; i < len; i++)
      html.push('<li onclick="SomeFunc(', i, ')">', items[i].title, '</li>');

  $('container').innerHTML = html.join('');
}

イベントハンドラには index を渡して必要なデータは元データから参照することですっきり書けます。addEventLisner なしでも大抵の処理はこの書き方で書くことができます。


今の時代に JavaScript OFF な人を考慮していたらキリがないのですが、やるとすれば簡単に以下で足りると思います。JavaScript ON の人のみ上記のコードで内容が上書きされます。

<ul id="container">
  JavaScript を ON にして出直してきてください。 <br />
  どうしても嫌な人は <a href="hoge.html"> こちら </a>
</ul>


上の例では items の数が2つしかなかったけどこれが100個になったりもっと複雑な HTML になったりすると、明らかに体感速度で innerHTML の方が速くなります。prototype.js を用いた item_builder() の一般的な実装は以下のようになるでしょう。items の数を増やして速度を検証してみてください。

function item_builder2() {
  var container = $('container');
  items.each(function(e, i) {
    var element = new Element('li').update(items[i].title).observe('click', SomeFunc.bind(null, i));
    container.appendChild(element);
  }
}


「これライブラリ使ってるから不利じゃん!」という人は以下を使ってください。
以下をみてわかる通り、DOM Method を用いた HTML の組み立てはクロスブラウザを考えるとライブラリを使わないとやってられません。また、これだけやっても cloneNode などを駆使することで prototype.js を用いた例の方が速い可能性すらあります。

function item_builder3() {
  var container = document.getElementById('container');
  if (window.attachEvent) {  // if IE
    for (var i = 0, len = items.length; i < len; i++) {
      var element = document.createElement('li');
      element.innerText = items[i].title;
      element.attachEvent('onclick', (function(index){ return function(){SomeFunc(index)} })(i));
      container.appendChild(element);
    }
  } else {
    for (var i = 0, len = items.length; i < len; i++) {
      var element = document.createElement('li');
      element.textContent = items[i].title;
      element.addEventListener('click', (function(index){ return function(){SomeFunc(index)} })(i), false);
      container.appendChild(element);
    }
  }
}

innerHTML ってスゴい :-)

  • ライブラリ依存もなくクロスブラウザも完璧で、
  • コードも短く、
  • それでいて速い、

ただこれは『カレンダーを作る』とか『Gmailを作る』とかいったような、大量の HTML を生成&操作したい場合の話であって、通常(なにをもって通常というのかはわかりませんが)は DOM で操作した方が楽な場合が多いです。


なんか innerHTML vs DOM Method の話ばかりですが、これが僕が onclick を使用する理由です。
要は時と場合によりけりで、『古いので使うな』というのは違う気がします。

Array.shiftの罠

prototype.js 1.4 では IE5 とかのため?*1に Array.shift を独自に実装している。

Array.prototype.shift = function () {
    var result = this[0];
    for (var i = 0; i < this.length - 1; i++) {
        this[i] = this[i + 1];
    }
    this.length--;
    return result;
}

最近のブラウザで実装されている Array.shift では空の配列に対して shift() を行うと undefined を返すが、上記の実装だと this.length-- の行で invalid array length と怒られる。なので、

// これはセーフ
if (!array.length) return;
var e = array.shift();

// これはアウト
var e = array.shift();
if (!e) return;

ほんとなら prototype.js 側を直すべきなんですけど今更なので。はてなで未だに prototype.js 1.4 が使われてるので、グリモンとかでハマるかもしれません。僕もちょっとハマりました。

*1:調べていない。もっと前のブラウザ対応かも。

JavaScriptの怖い話

prototype.jsのバージョン1.5.xを使っていたコードでした。そのコードはElement(prototype.jsが定義したクラス)のオブジェクトに独自に select というプロパティを加えていました。用途はフラグ変数でした。

prototype.jsのバージョンをv1.6.0.2に上げた時、問題が発生しました。prototype.js側で、Elementクラスにselectというメソッドが追加されていたためです。フラグ変数を期待していた参照箇所がすべて真になりました。

http://dev.ariel-networks.com/Members/inoue/terror-bugs

このバグを生み出したのは、僕です。

select を selected に変えてもこのバグの恐怖からは逃れられません。今回僕は先頭にアンダースコアをつけて _selected と修正しましたがこれでも安心できません。

_unko と修正すれば少なくとも prototype.js 側の変更とバッティングすることはなくなりますが、代償として可読性、プライド、まわり信用など、多くのものを失います。


以下のようにすれば可読性の問題だけなら無理やり解決できるかもしれません

/* フラグを立てる */
var element = document.createElement('div');
element['選択されています'] = true;

/* フラグを参照する */
if (element['選択されています']) {
   // ...処理...
}

面白いとは思いますが、なかなか受け入れられないでしょう。

getAttribute / setAttribute とプロパティアクセス

引用元の記事で『言語仕様の理解不足で生じるバグ』について書かれていますが、今回のような場合でもJavaScriptの言語仕様・・・というより(例によって)ブラウザの仕様を理解していないとハマる可能性があります。


IEの場合

var element = document.createElement('div');

element.setAttribute('attr', 'hoge');
alert(element.getAttribute('attr'));  //-> 'hoge'
alert(element.attr);                  //-> 'hoge'

element.attr = 'fuga';
alert(element.getAttribute('attr'));  //-> 'fuga'
alert(element.attr);                  //-> 'fuga'


Firefoxの場合

var element = document.createElement('div');

element.setAttribute('attr', 'hoge');
alert(element.getAttribute('attr'));  //-> 'hoge'
alert(element.attr);                  //-> undefined

element.attr = 'fuga';
alert(element.getAttribute('attr'));  //-> 'hoge'
alert(element.attr);                  //-> 'fuga'


違いがわかるでしょうか。setAttribute / getAttribute とDOMオブジェクトのプロパティアクセスが、IEでは等価なのに対してFirefoxでは区別して扱われています。

この辺りを間違えて覚えやすい原因として、 id や href といったHTMLで定義されている一般的な属性において、Firefoxでは syntax sugar としてプロパティアクセスにより属性の書き換えができる点があげられます。

element.id = 'hoge';
alert(element.getAttribute('id'));  //-> 'hoge'


どうせならIEのようにすべて区別しないほうがわかりやすいと思うかもしれませんが、IEIEで恐ろしい実装になっています。

element.setAttribute('innerHTML', 'hoge');
alert(element.innerHTML);           //-> 'hoge'


ブラウザの仕様であれこれと悩むのは本当に不毛です。

実際にはこれに SafariOpera のことも併せて考えなければなりません。

恐ろしい話です。

JavaScripでハッシュテーブルを実装してみた

JavaScriptで実装してもまったく意味ない(笑)

ただの練習です。


こんな感じでいいんだろうか?

String.prototype.hash = function(mod) {
  var sum = 0;
  for (var i = 0, len = this.length; i < len; i++)
    sum += this.charCodeAt(i) * i;

  return (mod) ? (sum%mod) : sum;
}

var ht = (function(){
  var size = 100;
  var table = new Array(size);

  return {
    add: function(obj) {
      var hash = obj.toString().hash(size);
      if (table[hash]) table[hash].push(obj);
      else table[hash] = [obj];
    },

    has: function(obj) {
      var hash = obj.toString().hash(size);
      if (table[hash])
        for (var i = 0, len = table[hash].length; i < len; i++)
          if (table[hash][i] == obj) return true;

      return false;
    }
  }
})();


toStringメソッドを持つオブジェクトであれば何でも格納できます。

ht.add("hoge");
ht.has("hoge");  //-> true
ht.has("fuga");  //-> false


var func = function(){ alert("hoge") }
var func2 = function(){ alert("fuga") }

ht.add(func);
ht.has(func);  //-> true
ht.has(func2);  //-> false


ht.add(new String("hoge"));
ht.has(new String("hoge"));  //-> false  /* オブジェクトとして比較しているため */

ニコニコが会員登録なしで見られるようになった

3月5日よりニコニコ外部プレーヤーが実装されるサイトは、次の各サイトになります(50音順)。

So-net ブログ
●trunc
はてなダイアリー
livedoor Blog

対応サイトは順次追加予定です

これでニコニコ動画で生まれる様々な作品をさらに広く多くの人に広められます。

これはいいですね。

はてブからも再生できるようなのでwww.nicovideo.jp の注目エントリーが俄然いい感じになった。


ただ会員じゃない人にも気軽にみせられるようになったのは嬉しいけど、

個人的にはブログにパーツをペタペタ貼るより外部リンクとして置いておく方が好きなので微妙。

かといってはてブのページにリンクするのもなんかなぁ。


あと最近は動画を見る人も作る人もちょっとお疲れ気味な感じがします。

モチベーションを維持するのってむずかしいですね。

芝が生えるゲームのシミュレーションみたいに第2第3の人口爆発はくるのでしょうか。

うp職人が召される率をもっと下げないとダメですよね。

そのためには動画作るコストをもっともっと下げてくれなきゃキツいです。


もしくは動画うp職人版1000speakersとかやればいいのかも。

1000人のうp職人とか心強い。

JavaScriptのスコープとGoogleの謎

var x = 'hoge';
{
  var x = 'fuga';
}
alert(x)  //-> 'fuga'

この挙動をちゃんと理解していなかった。
言い訳になるかもしれないが、使う機会がないからだ。

JavaScriptではこの場合、中括弧はあってもなくても同じ動きをする(多分)。


ところが、Google Readerにこんなコードがあった。

X.prototype.Qv = function(a) {
  this.Du = a;
  if (this.Yj) {
    var b = Ih();
    a = yh(a, AB, b)
  }

  { a = yh(a, BB, ai) }

  a = Oh(a);
  this.Yh = a
};


考えられる理由をあげてみた

  1. 僕がこのコードの挙動を理解できていない(中括弧いらなくね?と思ってる)
  2. Googleの開発者のミス(スコープの勘違い?else書き忘れ?)
  3. Google ReaderGoogle Web Toolkitで作られている・・・?


3番・・・なのかなぁ?
GWTであんな規模のものを作れてしまうの??


だとしたら悔しいけどすごい。。。

Greasemonkeyその他でbindを使うための最小セット

僕がGreasemonkeyで何か作るとき、何はなくともbindだけは欲しくなる。

でもGreasemonkeyスクリプト程度でライブラリを丸々読み込むのは、

無駄が多いだけでなく他のブラウザーへの移植性の面からみてもよろしくない。


prototype.jsのbindを使いたければ最低限これだけ貼り付ければおk

function $A(iterable) {
  if (!iterable) return [];
  var length = iterable.length, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

Function.prototype.bind = function() {
  if (arguments.length < 2 && arguments[0] === undefined) return this;
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
};