実践JavaScript Prototype編

「プログラミング経験はそこそこだけど、JavaScriptはあんまり」な人向け社内勉強会資料vol.2

Shogo Ohta, 2009-03-24

基礎編

JavaScriptのObject, Function

var obj1 = new Object();
var obj2 = new Object;//引数を渡さない場合、()は省略できる
var obj3 = {};//シンタックスシュガー

obj1,2,3はどれも同様に空のObjectを生成する(当然だが、インスタンスは異なるので == での比較は false になる)。

var obj1 = new Object();
obj1
.prop1 = 1;
var obj2 = new Object;
obj2
['prop1'] = 1;
var obj3 = {prop1:1};//シンタックスシュガー

obj1, 2, 3もやはり同様の Object を生成する。シンプルに記述できる3を使うのが一般的です。

var func1 = function(){}; // FunctionExpression,関数式
function func2(){} // FunctionDeclaration,関数宣言

func1とfunc2はどちらも関数を定義しているが、こちらは少々異なる部分があります。

func1は代入が実行されるまで、undefiendであるのに対して、func2は(スコープ内の)どこから参照しても定義済みとなります。

関数宣言は良く使う共通の関数を定義しておくのに適しています。対して、関数式は状況(ブラウザなど)に応じて関数の定義を切り替え易いメリットがあります。

function log(){
 
if (window.console) console.log(arguments);
}
if (document.addEventListener) {
 
var addEvent = function(node, type, handler, useCapture) {
    node
.addEventListener(type, handler, useCapture);
 
};
} else if (document.attachEvent) {
 
var addEvent = function(node, type, handler) {
    node
.attachEvent('on'+type, function(){
      handler
.call(node,window.event);
   
});
 
};
}

関数宣言では関数に名前が付くので、デバッグ時にどの関数か把握しやすくなります。本来は関数式にも名前を付けることができますが、IEでは関数式に対して名前をつけることができないので注意が必要です。(正確には、名前を付けると関数宣言として扱ってしまう。)

new 演算子

var str1 = new String('s');
alert
(typeof str1);//object
var str2 = 's';
alert
(typeof str2);//string

str1とstr2はどちらも s という文字列を持つが、1 は object 型であり、2 は string 型です。このように、newは新しいObject(インスタンス)を生成する。(ただし、例外としてSafari、Chromeではtypeof new RegExp('\\d')がfunctionになります。これはIE以外のエンジンでは正規表現オブジェクトを関数として呼び出すことが出来ることに関係します。)

このnewの対象となるのは単純な関数であり、自前で定義した関数をnewすることも可能です。

var DisplayObject = function(tag){
 
this.__element = document.createElement(tag||'div');
};
var box = new DisplayObject();

prototype

prototypeでインスタンスメソッド、プロパティを定義できます。

var DisplayObject = function(tag){
 
this.__element = document.createElement(tag||'div');
 
this.__childlen = [];
};
DisplayObject.prototype = {
  addChild
:function(do){
   
this.__childlen.push(do);
   
return this.__element.appendChild(do.__element);
 
},
  removeChild
:function(do){
   
for (var i = 0,l = this.__childlen.length;i < l; i++)
     
if (this.__childlen[i] === do)
       
this.__childlen.splice(i,1);
   
return this.__element.removeChild(do.__element);
 
}
};
var outerbox = new DisplayObject();
var innerBox = new DisplayObject();
outerbox
.addChild(innerBox);
outerbox
.removeChild(innerBox);

Array/forEach/Compatibilityのように、元々あるクラスを拡張することも可能です。が、これは互換性の問題(後述)から慎重に行う必要があります。

if (!Array.prototype.forEach)
{
 
Array.prototype.forEach = function(fun /*, thisp*/)
 
{
   
var len = this.length >>> 0;
   
if (typeof fun != "function")
     
throw new TypeError();

   
var thisp = arguments[1];
   
for (var i = 0; i < len; i++)
   
{
     
if (i in this)
        fun
.call(thisp, this[i], i, this);
   
}
 
};
}
var Sprite = function(tag){
 
this.__element = document.createElement(tag||'div');
 
this.__childlen = [];
};
Sprite.prototype = new DisplayObject();//DisplayObjectを継承

なぜ、下記ではいけないのか?

Sprite.prototype = DisplayObject.prototype;

継承の詳細は、JavaScript継承パターンまとめ - Thousand Yearsにまとまっています。

実践編

prototype汚染

Array.prototypeを拡張すると、汚染問題が発生します。

var array1 = ['one','two'];
array1
.forEach(function(s,i,a){
  alert
([s,i,a]);
});
for (var i in array1){
  alert
([i+':'+array[i],array1.hasOwnProperty(i)]);
}

prototypeに定義したメソッドはfor in を使った際に列挙されてしまいます。prototype由来のプロパティか、そのオブジェクト自身が持つプロパティかはhasOwnPropertyを使うことで判断することは可能ですが、for inに手を入れられる状況ならばそもそもfor inを使わないようにしたほうが良いです。配列に対するfor inを使っていないことが保証できないのであれば、Array.prototypeの拡張は控えたほうが良いでしょう(Object.prototypeの拡張については禁止といっても良いくらいです)。

StringやNumber、Functionなどは、そのインスタンスに対してfor inを使うことが滅多にないので、こういったクラスのprototypeの拡張は良い手段かもしれません。

ドラッグを実装してみよう

var DisplayObject = function(tag){
 
this.__element = document.createElement(tag||'div');
 
this.__childlen = [];
};
DisplayObject.prototype = {
  addChild
:function(_do){
   
this.__childlen.push(_do);
   
return this.__element.appendChild(_do.__element);
 
},
  removeChild
:function(_do){
   
for (var i = 0,l = this.__childlen.length;i < l; i++)
     
if (this.__childlen[i] === _do)
       
this.__childlen.splice(i,1);
   
return this.__element.removeChild(_do.__element);
 
}
};
var Sprite = function(node){
 
if (this === window && node) {
   
return new Sprite(node);
 
}
 
var elem = this.__element = node || document.createElement('div');
  elem
.className = 'Sprite';
  elem
.id = 'Sprite' + ( (Sprite.__id += 1) || (Sprite.__id=1));
 
this.style = elem.style;
 
this.__childlen = [];
};
Sprite.bind = function(method,self){
 
if (!self) self = Sprite;
 
return function(evt){
   
self[method](evt||window.event);
 
}
};
Sprite.initDrag = function(){
  addEvent
(document,'mousemove',Sprite.bind('__dragging'), false);
  addEvent
(document,'mouseup', Sprite.bind('__drag_end'), false);
 
Sprite.__drag_objects = [];
 
Sprite.__init_drag = true;
};
Sprite.__dragging = function(evt){
 
if (!Sprite.__has_dragging) return;
 
for (var i = 0,l = Sprite.__drag_objects.length; i < l;i++) {
   
var self = Sprite.__drag_objects[i];
   
if (!self.isDragging) continue;
   
var elem = self.__element;
   
var left = document.documentElement.scrollLeft + evt.clientX;
    elem
.style.left = (self.offset.x + left) + 'px';
   
var top = document.documentElement.scrollTop  + evt.clientY;
    elem
.style.top  = (self.offset.y + top) + 'px';
   
if (evt.preventDefault) {
      evt
.preventDefault();
   
} else {
      evt
.returnValue = false;
   
}
 
}
};
Sprite.__drag_end = function(evt){
 
for (var i = 0, l = Sprite.__drag_objects.length; i < l;i++) {
   
Sprite.__drag_objects[i].isDragging = false;
 
}
 
Sprite.__has_dragging = false;
};
Sprite.prototype = new DisplayObject();//DisplayObjectを継承
Sprite.prototype.__drag_start = function(evt){
 
this.isDragging = Sprite.__has_dragging = true;
 
var elem = this.__element;
 
this.offset = {
    x
: (parseFloat(elem.style.left) || 0) -
       
(evt.clientX + document.documentElement.scrollLeft),
    y
: (parseFloat(elem.style.top ) || 0) -
       
(evt.clientY + document.documentElement.scrollTop)
 
};
 
if (evt.preventDefault){
    evt
.preventDefault();
 
} else {
    evt
.returnValue = false;
 
}
 
if (evt.stopPropagation){
    evt
.stopPropagation();
 
} else {
    evt
.cancelBubble = true;
 
}
};
Sprite.prototype.startDrag = function(){
 
if (!Sprite.__init_drag) {
   
Sprite.initDrag();
 
}
  addEvent
(this.__element, 'mousedown',
   
Sprite.bind('__drag_start',this), false);
 
Sprite.__drag_objects.push(this);
};
var body = Sprite(document.body);

var box1 = new Sprite();
var box2 = new Sprite();

body
.addChild(box1);
box1
.addChild(box2);
box1
.startDrag();
box2
.startDrag();