Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

超絶簡単!JavaScriptの性質を10分で理解するための重要なポイントのまとめ

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScriptのニーズ

NodeJSやTitaniumMobileの普及によりサーバサイド/スマフォアプリの作成をJavaScriptで書こうとする動きが盛んです。それだけ注目を集めているせいかブログの記事でもJavaScriptのネタを書くとはてぶ登録されやすい傾向が現れます。一応今までJavaScript系の記事をいくつか書いてきたのでリンクを紹介します。

JavaScriptは記述の制限が緩い言語で記述者が自由に定義できてしまいます。自由度が高いという言葉は良いように聞こえますが、各人それぞれの志向に左右されることが多く、特に他人のコードを読む時に苦労します。今日はJavaScriptの性質を理解するためにdebugを仕込みながら勉強を進めたいと思います。

JavaScriptを理解する上で大切な事

個人的にJavaScriptの性質を理解する上で重要な事はObjectと関数を理解することだと認識しています。JavaScriptのほとんどのデータ定義がObjectで表現されます(関数、文字列、数値以外)。JavaScriptにはClassといった概念が無く、関数定義を他言語でのClassのConstructorのように扱い、Classメソッドのようなものをprototypeといった暗黙参照のObjectで定義します。左の言葉に少し違和感を感じられるかもしれません。後にコード例を示すのでそちらも参照してください。

配列/連想配列の定義

配列と連想配列は当然ながら別物です。配列はList型のデータに対して連想配列はKeyからValueを得るHashになります。JavaScriptの表記としても別々で扱いますが、両方ともObjectとしての性質を持ちます

配列の定義

配列の宣言はArrayもしくは[]を利用します。私はArrayで宣言か添字で入れる書き方をします。newを使っても使わなくても同じということであれば使用しない事にしています。また添字を文字列で入れるまたhashで一度宣言という方法は使った事が無いです。

// Arrayで宣言
var a = Array( 'a', 'b' );

// Arrayをnew
var a = new Array( 'a', 'b' );

// []に入れちゃう
var a = [ 'a', 'b' ];

// 添字で入れる
var a = Array();
a[ 0 ] = 'a';
a[ 1 ] = 'b';

// 添字を文字列で入れる
var a = [];
a[ '0' ] = 'a';
a[ '1' ] = 'b';

// hashで入れてArrayを継承させる
var a = { 0 : 'a', 1 : 'b', length : 2 };
a.__proto__ = Array.prototype;
連想配列の定義

連想配列の定義はObjectもしくは{}を利用します。。当然ながらKeyとValueが必要でKey:ValueやKeyを添字にしてValueを格納といった表現をします。配列のパターンと同じでnewをするしないで結果が同じであるのであればnewをしないようにしています。

// Objectで宣言
var o = Object();
o = { 0 : 'a', 1 : 'b' };

// Objectをnew
var o = new Object();
o = { 0 : 'a', 1 : 'b' };

// {}に入れちゃう
var o = { 0 : 'a', 1 : 'b' };

// keyを指定して入れる
var o = {};
o[ '0' ] = 'a';

// property参照させる
var o = {};
o.0 = 'a'; //これはエラーになる propertyはkeyが文字列でないと駄目な様子
o.a = 'a';  //これはOK
typeofで表示してみると

ArrayもObjectも両方typeofで表示するとObjectになります。int、string、functionも調べた結果を以下に載せます。

var a = Array();
alert( typeof a ); //object

var o = Object();
alert( typeof o ); //object

var i = 1;
alert( typeof i ); //number

var s = 'string';
alert( typeof s ); //string

var f = function(){};
alert( typeof f ); //function
Objectの拡張

上で示したObject(連想配列)を拡張した定義を行います。ObjectはKey:Valueの表現を利用して変数だけでなく関数の定義も行えます。拡張定義したObjectの関数をproperty参照を利用する事でメソッドのように利用できます。以下に例を記します。

var newObject = {
    property : 'Object', //連想配列の定義なのでカンマが必要です。
    echo : function() {   // 関数だって定義できます。
        alert( this.property );
    }
};
newObject.echo(); //Object

これを覚える上で忘れてはいけないのが連想配列だと言う事です。連想配列はKey:Valueのペアをカンマ区切りで格納します。関数も連想配列のルールに従えば定義できるという事です。私は上のような表記をしますがpropertyを使って以下のように記述する事も出来ます。

var newObject = Object();
newObject.property = 'Object';
newObject.echo = function() {
    alert( this.property );
}
newObject.echo();

関数

通常の定義

関数はfunctionで定義します。varで宣言するか関数をそのまま宣言するかの方法がありますが、JavaScriptでより多く使われるのは前者の書き方だと思っています。(他言語だと後者の書き方しかできないと思いますが。)

// varで宣言する
var f = function(){};

// 関数としてfを宣言する
function f(){}
無名関数

無名関数とはその名の通り名前の無い関数を定義できる仕組みです。まずは単純な例を記述しますがfunctionとだけ宣言して();にて実行します。Bookmarkletを作る時も良くこの記述をします。

(function(){
    alert( 'OK' );
})();

この無名関数を使うとクロージャが定義できます。クロージャの簡単な説明としてはグローバル空間に影響を与えないようにする仕組みです。よくある使い方としては無名関数で名前空間を一つ定義して、名前空間の下にObjectやクラスを定義するとincludeするJSファイル間で名前のバッティングを回避できます。以下は名前空間の下にObjectを定義して実行する例です。

var NameSpace = (function(){
    return {
        property : 'NameSpace Object',  
        echo : function() {
            alert( this.property );
        },
    };    
}
)();
NameSpace.echo(); // NameSpace Object
Class定義

JavaScriptにはClassという概念がありません。正確に言うとfunctionを基にClassっぽいものを表現しているだけです。functionでの宣言がClassのConstructorのような働きをします。

var Class = function() {      //Constructor
    this.property = 'Class';  //property
    this.echo = function() {  //method
        alert( this.property );
    }
};
new Class().echo(); //Class

実は上の書き方は良い例とは言えません。なぜならばConstructorの中にmethodを書いてしまっているからnewされる度にメソッドまでもが再定義されてしまいます。これを防ぐためにConstructorとmethodの定義を分離します。更にJavaScript特有のprototype属性(Object)を定義する事で暗黙的な参照を利用します。上の例を書き換えると以下のようになります。

var Class = function() {}; //Constructorだけ定義
Class.prototype = {        //prototypeはObject
    property : 'Class',    //propertyを定義
    echo : function() {    //methodを定義
        alert( this.property );
    },
};
new Class().echo(); // Class

基本的にprototypeはConstructorの外に書きます。以下はprototypeが参照がうまく利用できない例と非推奨の書き方です。errorが出てしまうのはメソッドを呼び出すタイミングでundefinedとなります。Constructor内部でprototypeを定義すると新たにprototypeというオブジェクトを生成してしまう事になります。これだと暗黙参照ができません。それを回避する方法としてprototypeをConstructor内部でreturnすると思うように動作しますが、prototypeの暗黙参照の役割を考えるとConstructor内部にprototypeを定義すべきではありません。

// errorが発生。Constructor内部でprototypeを定義すると暗黙参照とは別物になってしまう。
var Class = function() { 
    this.prototype = {
        property : 'Class',    //propertyを定義
        echo : function() {    //methodを定義
            alert( this.property );
        },
    }; 
};
new Class().echo(); // undefined 
new Class().prototype.echo(); // これは動作する。新しいprototypeというpropertyを作ってしまっている

// 非推奨の書き方 Constructor内部にprototypeを記述している。
var Class = function() { 
    this.prototype = {
        property : 'Class',    //propertyを定義
        echo : function() {    //methodを定義
            alert( this.property );
        },
    }; 
    return this.prototype;
};
new Class().echo(); // Class
Classの継承

親クラスの継承は子クラスのprototypeにnewして定義するだけです。

//親クラス
var ParentClass = function() {}; //Constructorだけ定義
ParentClass.prototype = {        //prototypeはObject
    property : 'Parent',         //propertyを定義
    echo : function() {          //methodを定義
        alert( this.property ); 
    },
};

//子クラス
var ChildClass = function() {};
//継承
ChildClass.prototype = new ParentClass();
ChildClass.prototype.property = 'Child';
new ChildClass().echo(); //Child

上の例だと子Class側でChild.prototype.〜という定義を繰り返し記述しなければなりません。私はそれが面倒だと思うので無名関数とapplyを使ってChildClass.prototypeに追加します。

//親クラス
var ParentClass = function() {}; //Constructorだけ定義
ParentClass.prototype = {        //prototypeはObject
    property : 'Parent',         //propertyを定義
    echo : function() {          //methodを定義
        alert( this.property );
    },
};
//子クラス
var ChildClass = function() {};
//継承
ChildClass.prototype = new ParentClass();
(function(){
    this.property = 'Child';
    this.call = function() {
        alert( 'Call ' + this.property );
    };
}).apply( ChildClass.prototype );
new ChildClass().call();

本当は上の無名関数とapplyを使った例を以下のように書きたいのですがerrorが出ます。

//親クラス
var ParentClass = function() {}; //Constructorだけ定義
ParentClass.prototype = {        //prototypeはObject
    property : 'Parent',         //propertyを定義
    echo : function() {          //methodを定義
        alert( this.property );
    },
};

//子クラス
var ChildClass = function() {};
//継承
ChildClass.prototype = new ParentClass();
(function(){
    return {
        property : 'Child',
        call : function() {
            alert( 'Call ' + this.property );
        },
    };
}).apply( ChildClass.prototype );
new ChildClass().call(); //callが定義されていないというerrorが出ます。
Singletonで記述

一つのinstance生成で実行できるClassはnewにより余計なinstanceを生成させないようにします。Singletonはnewされても同一のObjectを返すようにClass側で制御するデザインパターンです。JavaScriptでも簡単に記述できます。arguments.calleeを利用するだけです。2回instanceを生成しようとしても同じものがClassから返されるので比較してみると同一のものを示します。

// SingletonClassの定義
var SingletonClass = function() {
    if( arguments.callee.singletonInstance ) {
        return arguments.callee.singletonInstance;
    }
    arguments.callee.singletonInstance = this;
};
// prototype
SingletonClass.prototype = {
    property : 'Singleton Class',
    echo : function() {
        alert( this.property );
    },
};
var i = new SingletonClass();
var i2 = new SingletonClass();
alert( i === i2 ); //true
Object.createによる生成

newの要/不要が色々なブロブで議論されていますが、crockfordさんが言うようにnewを出来る限り避けるという方針を考えますECMAScript 5th EditionからObject.createというメソッドが利用できるようになっています。これを基にObjectを生成します。Object.createのサポートが各種ブラウザで異なるので、定義されていない場合は自前で準備した関数を利用するように切り替えます。Object.createメソッドにClass.prototypeを入れます。

if (!Object.create) {  
    Object.create = function (o) {  
        if (arguments.length > 1) {  
            throw new Error('Object.create implementation only accepts the first parameter.');  
        }  
        function F() {}  
        F.prototype = o;  
        return new F();  
    };  
}  
var Class = function(){};
Class.prototype = {        //prototypeはObject
    property : 'Class',    //propertyを定義
    echo : function() {    //methodを定義
        alert( this.property );
    },
};
Object.create( Class.prototype ).echo();

気をつけて欲しいのが上で紹介したSingletonパターンに対してObject.createを挟んでしまうと別Instanceとなってしまいます。上の例では生成instanceの比較が同じでしたがObject.createを入れるとfalseとして認識されます。原因はObject.create自体がSingleton化していないだけだと思います。

// SingletonClassの定義
var SingletonClass = function() {
    if( arguments.callee.singletonInstance ) {
        return arguments.callee.singletonInstance;
    }
    arguments.callee.singletonInstance = this;
};
// prototype
SingletonClass.prototype = {
    property : 'Singleton Class',
    echo : function() {
        alert( this.property );
    },
};
var i = Object.create( SingletonClass.prototype );
i.echo();
var i2 = Object.create( SingletonClass.prototype );
i2.echo();
alert( i === i2 ); //false
prototypeによる機能拡張

prototypeを利用すると既存Objectの機能を拡張する事が可能です。例えば配列に対して合計値を計算するようなメソッド(sum)を追加します。

Array.prototype.sum = function() {
    var sum = 0;
    for(var i = 0; i < this.length; i++ ) {
        sum += this[i];
    }
    return sum;
};
var a = [0,1,2,3,4];
alert( a.sum() ); // 10
thisについて

thisを指し示すものが非常にややこしいのがJavaScriptの特徴でもあります。結論を簡単に言うとthisを読み込む箇所によって全く性質が異なるものを示します。thisが参照するパターンは大きく分けて2つで、イベント発生源のObjectかObject自身のインスタンスです。何も定義されていない、もしくは関数の内部でthisを呼び出すとDOMWindowを示します。ClassやObject内部でthisを呼び出すと自身のインスタンスを示します。

alert( this ); //object DOMWindow

(function(){
    alert( this ); //object DOMWindow
})();

var Class = function(){};
Class.prototype = {        //prototypeはObject
    property : 'Class',    //propertyを定義
    echo : function() {    //methodを定義
        alert( this ); 
    },
};
Object.create( Class.prototype ).echo(); //object object

var SampleObject = { 
    property : 'Object!',
    echo : function() {
        alert( this );
    },
};
Object.create( SampleObject ).echo(); //object object