超軽量なjQueryのようなヘルパーオブジェクトを作る

2016-06-08

このブログでは、jQueryを使わずにVanillaなJavaScriptで書くことを推奨していますが、jQueryを使ったほうがコードが簡潔に書けて便利なことも多いです。
とは言え、ちょっとしたコードのためにjQueryをいちいち使っていると通信帯域の無駄遣いです。
そこでjQueryのようにDOMを扱うことができるオブジェクトを定義して使うっていうのはいかがでしょうか?

みなさん、jQuery使ってますか?jQueryに慣れたら、素のJavaScriptでDOMをいじるのかったるいですよね。
まずは、DOM要素を指定するのに

var smpl = document.getElementById('sample');

とかいちいち書くの面倒なんで、

var $ = function(selector){
   return document.querySelector(selector);
}

みたいな関数を定義してやれば

var smpl = $('#sample');

こんな感じで、DOM指定が多少簡単になりました。

次に湧いてくる欲求は、指定したDOM要素のスタイルや属性をいじるときに、jQueryのように簡単にやりたいってやつですよね。

<input id="sample" type="text">
<script>
$('#sample')
.css({
   background: '#333',
   color: '#fff',
})
.attr({
   disabled: 'disabled',
});
</script>

ちょっと複雑になってきましたが、以下のようにオブジェクトとプロトタイプを設定してやればいけそうです。

<input id="sample" type="text">
<script>
var $ = function(selector) {
   return new Tiny(selector);
};

var Tiny = function(selector) {
   var elems = document.querySelectorAll(selector);
   [].push.apply(this, elems);
};

Tiny.prototype = {

   // .css(name, value)形式には対応しない。セットするときは必ずObjectで
   css: function(obj) {
      [].forEach.call(this, function(elem) {
         for (var name in obj) {
            elem.style[name] = obj[name];
         }
      });
      return this;
   },

   // .attr(name, value)形式には対応しない。セットするときは必ずObjectで
   attr: function(obj) {
      [].forEach.call(this, function(elem) {
         for (var name in obj) {
            elem.setAttribute(name, obj[name]);
         }
      });
      return this;
   },
}

$('#sample')
.css({
   background: '#333',
   color: '#fff',
})
.attr({
   disabled: 'disabled',
});
</script>

こんな感じでTinyオブジェクトってやつを定義してやれば、jQueryっぽくちゃんとプロトタイプチェインでスタイルや属性をいじることができます。
これだと、全体をGoogle Closure Compilerにかけることができて、

function d(a) {
  [].push.apply(this, document.querySelectorAll(a));
}
d.prototype = {};
(function(a) {
  var b = {disabled:"disabled"};
  [].forEach.call(a, function(a) {
    for (var c in b) {
      a.setAttribute(c, b[c]);
    }
  });
})(function(a) {
  var b = {background:"#333", color:"#fff"};
  [].forEach.call(a, function(a) {
    for (var c in b) {
      a.style[c] = b[c];
    }
  });
  return a;
}(new d("#sample")));

Advanced Optimizationで、たったの345バイトになりました。これだったら、setAttributeとかを何回も書くよりもコードも簡潔になって、おまけにこのヘルパー関数を含んでもコードサイズが小さくなることもあり得ます。

もっと欲深い人は、DOM要素を作って配置したいと考えるはずです。jQueryでいうと以下のようなコードですね。

$('body').append('<p>追加されたパラグラフ</p>');

そういう場合には、Tinyオブジェクトを変更して

<script>
var Tiny = function(selector) {
   var elems;
   if(selector[0]=='<') {
      var container = document.createElement('div');
      container.innerHTML = selector;
      elems = container.childNodes;
   } else {
      elems = document.querySelectorAll(selector);
   }
   [].push.apply(this, elems);
}
</script>

こんな感じにしてやると、< で始まる文字列を入れた時には新規のDOMを生成してくれるようになります。そしてTinyオブジェクトのprototypeにappendを定義してやればOKです。
ただし、$('<td>')とか$('<tr>')みたいなtable構成要素を作ろうとしても作れません。div要素の直下にtdとかtrは置けないので、childNodesとして取得できないためです。どんな状況でも対応できるjQuery互換機能ではなくてあくまでもヘルパーとして使いたいので、機能に限定があることを承知でコードサイズとスピードを優先させます。

どうしてもtrとかもやりたい時には、上記のcontainerを以下のように変更してみればよいでしょう。

<script>

var container;
if(/<[tr|td]/.test(selector)){
   container = document.createElement('tbody');
   if(/^<td/.test(selector)){
      var tr = document.createElement('tr');
      container = container.appendChild(tr);
   }
} else {
   container = document.createElement('div');
}

</script>

ひとまず、以下のような感じでできあがりました。
一応css('height')とかattr('src')のように値を取得することができるようにも設定しています。

<input id="sample" type="text">
<script>
var $ = function(selector) {
   return new Tiny(selector);
};

var Tiny = function(selector) {
   var elems;
   if(selector[0]=='<') {
      var container = document.createElement('div');
      container.innerHTML = selector;
      elems = container.childNodes;
   } else {
      elems = document.querySelectorAll(selector);
   }
   [].push.apply(this, elems);
};

Tiny.prototype = {

   each: function(callback) {
      [].forEach.call(this, callback);
      return this
   },

   // .css(name, value)形式には対応しない。セットするときは必ずObjectで
   css: function(obj) {
      return (typeof obj != 'string') ?
         this.each(function(elem) {
            for (var name in obj) {
               elem.style[name] = obj[name];
            }
         }) : this.length ? (this[0].currentStyle || document.defaultView.getComputedStyle(this[0], ''))[obj] : '';
   },

   // .attr(name, value)形式には対応しない。セットするときは必ずObjectで
   attr: function(obj) {
      return (typeof obj != 'string') ?
         this.each(function(elem) {
            for (var name in obj) {
               elem.setAttribute(name, obj[name]);
            }
         }) : this.length ? this[0].getAttribute(obj) : '';
   },

   append: function(children) {
      return this.each(function(elem) {
         if (typeof children == 'string') {
            children = $(children);
         }
         children.each(function(child) {
            elem.appendChild(child);
         });
      });
   },
}

$('#sample')
.css({
   background: '#333',
   color: '#fff',
})
.attr({
   disabled: 'disabled',
});

$('body').append('<p>追加されたパラグラフ</p>');
</script>

あとは自分で必要な関数をTinyのプロトタイプに追加してやれば実際のプロジェクトでも使えるんじゃないでしょうか。ちなみに、イベントリスナの登録のonは、こんな感じです。

on: function(events, func) {
   return this.each(function(elem) {
      events.split(/\s/).forEach(function(e) {
         elem.addEventListener(e, func, false)
      });
   })
},

このブログでも、このTinyオブジェクトを使って動いてます。

JavaScript