JavaScript

【JavaScript】必須パターン:リテラルとコンストラクタ

More than 1 year has passed since last update.

オブジェクトリテラル

JavaScriptではオブジェクトをキーと値の組のハッシュテーブル(連想配列みたいなもの)とみなすことができる。値は、プリミティブまたは他のオブジェクトになる。どちらの場合もプロパティと呼ばれる。値は関数にすることもでき、その場合はメソッドと呼ぶ。

JavaScriptで作成したカスタムオブジェクト(ユーザ定義のオブジェクト)はいつでも変更可能。組み込みのネイティブオブジェクトの場合、プロパティの多くも変更可能。
まず空のオブジェクトから始めて、これに機能を追加していくことができる。
このような形で必要に応じてその都度オブジェクトを作る場合、オブジェクトリテラル記法が最適である。

example.js
var hoge = {};

// プロパティを追加
hoge.name = "hogehoge";

// メソッドを追加
hoge.getName = function () {
    return hoge.name;
};

プログラムが動作しているどの時点でも、以下の処理が可能。

プロパティの値とメソッド変更できる
hoge.getName = function () {
    // メソッドの再定義
    return "hogehogehoge";
};
プロパティ/メソッドを完全に削除
delete hoge.name;
プロパティやメソッドをさらに追加
hoge.say = function () {
    return "Woooooooooooooooooooow";
};
hoge.life = true;

必ずしも空のオブジェクトから始める必要もない。次のようにオブジェクトパターンを使えば、オブジェクトの作成時点で機能を追加しておくことも可能。

sample.js
var hoge = {
    name: "hogehoge",
    getName: function () {
        return this.name;
    }
};

オブジェクトリテラルの構文

オブジェクトリテラルの構文規則の要点は次の通り。

  • オブジェクトを波括弧({})で囲む。
  • オブジェクト内部のプロパティやメソッドをカンマで区切る。名前と値の組の最後をカンマで終わらせると違反ではないがIEでエラーが出るので注意。
  • プロパティの名前とプロパティの値をコロンで区切る。
  • オブジェクトに変数を代入するときは、閉じ括弧}の後のセミコロンを忘れないこと。

コンストラクタからオブジェクトを作る

JavaScriptにはクラスがないので、オブジェクトについて事前に何かしらを知っておく必要がない。しかし、JavaScriptにはコンストラクタ関数がある。
独自のコンストラクタ関数やObject()、Date()、String()などの組み込みのコンストラクタを使って、オブジェクトを作成できる。

作り方は以下の2通り

// リテラルを使う方法
var car = {goes: "far"};

// 組み込みコンストラクタを使う方法
// アンチパターン
var car = new Object();
car.goes = "far";

上の例で見た通り、リテラル記法はたいぴんぐの量が少なく済む。
また、オブジェクト作成にリテラル記法が好まれるもう一つの理由は、オブジェクトが単純で変更可能なハッシュであるため、クラスから作成する必要がないことが強調される点である。

Objectコンストラクよりもリテラルを使うべきもうひとつの理由は、スコープの解決がいらない点が挙げられる。オブジェクトと同じ名前でローカルなコンストラクタを作ることはできるが、そうするとインタープリタはスコープ連鎖をたどってObject()が呼ばれた場所からグローバルなObjectコンストラクタを見つける必要がある。

オブジェクトコンストラクタの落とし穴

ここで問題となるオブジェクトコンストラクタの機能とは、Object()コンストラクタが受け取ったパラメータの値によっては、オブジェクトの作成を別の組み込みコンストラクタに委譲し、期待したのと違うオブジェクトを返すことがある点である。

example.js
// アンチパターン

// 空のオブジェクト
var hoge = new Object();
console.log(hoge.constructor === Object); // true

// 数値オブジェクト
var hoge = new Object(1);
console.log(hoge.constructor === Number); // true
console.log(hoge.toFixed(2)); // 1.00

// 文字列オブジェクト
var hoge = new Object("I am a string");
console.log(hoge.constructor === String); // true
// 通常のオブジェクトにはsubstring()がないが、文字列オブジェクトにはある
console.log(typeof hoge.substring); // "function"

// ブーリアンオブジェクト
var hoge = new Object(true);
console.log(hoge.constructor === Boolean); // true

Object()コンストラクタのこの振る舞いは、コンストラクタに渡す値が動的で実行時でないとわからない場合、予期せぬ結果をもたらす。
new Object()は使わないでオブジェクトリテラルを使うべきである。

カスタムのコンストラクタ関数

オブジェクトリテラルのパターンと組み込みコンストラクタ関数を使う以外に、カスタムのコンストラクタ関数を独自に使ってオブジェクトを作成することもできる。

var hoge = new Person("Hoge");
hoge.say(); // "Foooooooooooooooooooooo!!!!!"

ここで出てくるPersonはクラスではなく、ただの関数である。
Personコンストラクタ関数は次のように定義できる。

ver Person = function (name) {
    this.name = name;
    this.say = function () {
        return "Foooooooooooooooooooooo!!!!!";
    };
};

このコンストラクタ関数をnewを使って呼び出すとき、関数内部での動作は以下のようになる。

  • 空のオブジェクトが作成され、変数thisで参照される。thisはこの関数のプロトタイプを継承している。
  • thisを参照するオブジェクトにプロパティとメソッドが追加される。
  • thisが参照する新しく作られたオブジェクトは、関数の最後で(明示的に別のオブジェクトを返していなければ)暗黙に返される。

この例では説明を簡単にするために、thisにメソッドsay()を追加している。これによって、new Person()が呼ばれるたびにメモリに新しい関数が作成される。しかし、メソッドsay()はインスタンスごとに内容が変わるわけではないので、明らかに非効率である。これであればPersonのプロトタイプにこのメソッドを追加したほうが良い。

Person.prototype.say = function () {
    return "Foooooooooooooooooooooo!!!!!";
};

メソッドのように再利用可能なメンバはプロトタイプで定義する。

コンストラクタの戻り値

newを使ってコンストラクタを呼ぶとき、常にオブジェクトが返される。デフォルトではthisが参照するオブジェクトが返る。コンストラクタの内部でthisにプロパティを追加していなければ、空のオブジェクトが返る。

コンストラクタは、関数にreturn文を書かなかったとしても、暗黙にコンストラクタを返す。

example.js
var Objectmaker = function () {
    // このコンストラクタは別のオブジェクトを返すため
    // このnameプロパティは無視される
    this.name = "This is it";

    // 新しいオブジェクトを作成して返す
    var that = {};
    thad.name = "And that's that";
    return that;
};

var hoge = new Objectmaker();
console.log(hoge.name); // "And that's that"

このように、オブジェクトでありさえすれば任意のオブジェクトをコンストラクタから自由に返すことができる。オブジェクト以外を返そうとすると、エラーにはならないが無視され、かわりにthisが参照するオブジェクトが返される。

newを強制するパターン

コンストラクタはnewを使って呼び出すことができるが、もしこのnewを忘れてしまった場合、構文エラーや実行委エラーにはならないが、論理上のエラーとなり、予期しない振る舞いになる。newを忘れると、コンストラクタ内部ではthisはグローバルオブジェクトを指すからである。
コンストラクタ内部にthis.memberのようなものがある場合、newを使わないで呼び出すと、グローバルオブジェクトが作成され、window.memberあるいはmemberでアクセスできてしまう。これではグローバル名前空間を汚染しない、という原則に反している。

この動作についてはECMAScript5で取り組まれ、strictモードではthisはグローバルオブジェクトを刺さないようになる。ES5を利用できない場合は、newなしで呼び出しても正しく動くようにコンストラクタ関数を作るしかない。

命名の作法

頭文字を大文字にする。通常の関数は頭文字が小文字。

thatを使う

命名の作法に従うだけでは正しい振る舞いにならないことがある。しっかりとふるまうようにするためのパターンとして、thisにメンバをすべて追加する代わりに、thatにメンバを追加し、thatを返す

sample.js
function Hoge () {
    var that = {};
    that.hogehoge = "hogeeeeeeee";
    return that;
}

より簡潔にしたい場合は、thatのようなローカル変数は使わず、リテラルでオブジェクトを返す。

sample.js
function Hoge() {
    return {
        hogehoge: "hogeeeeeeeee"
    };
}

var first = new Hoge(),
    second = Hoge();
console.log(first.hogehoge); // hogeeeeeeeee
console.log(second.hogehoge); // hogeeeeeeeee

上のような実装をすれば、呼び出し方に関係なく、常にオブジェクトが返る。

このパターンの問題はプロトタイプへのリンクが失われてしまうことである。Hoge()のプロトタイプに追加したメンバをオブジェクトから利用できなくなる。

自己呼び出しコンストラクタ

インスタンスオブジェクトでプロトタイププロパティを利用できるようにするためには、次のパターンを。まずコンストラクタの中でthisがコンストラクタのインスタンスであるかを検査し、そうでなければ、そのコンストラクタからnewを正しく使って自分自身を呼ぶようにする。

sample.js
function Hoge() {
    if(!(this instanceof Hoge)) {
        return new Hoge();
    }

    this.hogehoge = "hogeeeee";
}
Hoge.prototype.wantAnother = true;

// test
var first = new Hoge(),
    second = Hoge();

console.log(first.hogehoge); // "hogeeeee"
console.log(second.hogehoge); // "hogeeeee"

console.log(first.wantAnother); // true
console.log(second.wantAnother); // true

インスタンスを検査する汎用的なやり方がある。コンストラクタの名前をハードコーディングする代わりにarguments.calleeを呼ぶ。

if(!(this instanceof arguments.callee)) {
    return new arguments.callee();
}

このパターンは関数の内部で、argumentsというオブジェクトが生成されていることを利用している。このオブジェクトには関数を呼び出したときに渡されたパラメータがすべて含まれる。
また、argumentsにはcalleeというプロパティがあり、これは呼び出された関数を指す。ただし、ES5のstrictモードでは許可されていないので注意。

配列リテラル

JavaScriptの配列は他のほとんどの言語と同様にオブジェクトである。組み込みのコンストラクタ関数Array()を使って作成できるが、オブジェクトリテラルと同様、配列のリテラル記法もあり、こちらのほうが簡潔である。

sample.js
// アンチパターン
var hoge = new Array("a", "b", "c");

// 上と全く同じ配列
var hoge = ["a", "b", "c"];

console.log(typeof hoge); // "object"
console.log(hoge.aonstructor === Array); // true

配列リテラルの構文

要素をカンマで区切り、リスト全体を角括弧で囲むだけ。オブジェクト、ほかの配列などあらゆる型の値を代入できる。
配列は値のリストにすぎず、コンストラクタを取り込みnew演算子を使って...といった面倒なことは必要ない

配列コンストラクタの変な動き

new Array()を避けるべき理由はもう一つある。
Array()コンストラクタに数値を一つだけ渡した場合、その数値は配列の最初の要素の値にはならない。かわりに配列の長さが設定される。
以下は配列リテラルを使った場合とnew Array()を使った場合のサンプルコード。

sample.js
// 要素がひとつの配列
var hoge = [3];
console.log(hoge.length); // 1
console.log(hoge[0]); // 3

// 要素が3個の配列
var hoge = new Array(3);
console.log(hoge.length); // 3
console.log(hoge[0]); // "undefined"

さらに、new Array()に整数ではなく浮動小数点数を渡すとひどいことになる。浮動小数点数は長さとして妥当な値ではないため、エラーになる。

sample.js
// 配列リテラルを使う
var hoge = [3.14];
console.log(hoge[0]); // 3.14

var hoge = new Array(3.14); // RangeError: invaild array length
console.log(typeof hoge); // "undefined"

実行時に配列を動的に作成する場合、エラーの可能性を避けるためには、配列リテラル記法以外は使わないほうが良い。

配列かどうかの検査

配列を対象にtypeof演算子を使うと"object"が返る。
これではオブジェクトであることはわかるが、役には立たない。lengthプロパティやslice()などの配列メソッドの存在を調べることもできるが、配列でないオブジェクトが同じ名前のプロパティやメソッドを持っていないことを前提としているため、確実とは言えない。instanceof Arrayを使うやり方もあるが、IEのバージョンによってはフレームにまたがって使われると、正しく動かない。

ES5ではArray.isArray()というメソッドが定義され、これは配列の時、trueを返す。

Array.isArray([]) // true

// 配列もどきのオブジェクトを使って検査をごまかしてみると...
Array.isArray({
    length: 1,
    "0": 1,
    slice: function () {}
}); // false

これが使えない場合、Object.prototype.toString()メソッドを呼んで検査できる。配列のコンテキストでtoStringのcall()メソッドを呼び出すと、"[object Array]"という文字列が返る。オブジェクトのコンテキストでは"[object Object]"という文字列が返る。で、これを利用する。

if (typeof Array.isArray === "undefined") {
    Array.isArray = function (arg) {
        return Object.prototype.toString.call(arg) === "[object Array]";
    };
}

JSON(JavaScript Object Notation)

JSONはデータ転送フォーマット。軽量で便利。JSONについて新たに学ぶことはなく、配列とオブジェクトのリテラル記法を組み合わせるだけでよい。

{"name": "value", "some": [1, 2, 3]}

JSONとオブジェクトリテラルの構文の唯一の違いは、JSONではプロパティ名を引用符で囲む必要がある点。オブジェクトリテラルでは、引用符で囲む必要があるのは、プロパティ名が識別子として妥当でない場合だけ。{"first name": "Dane"}みたいに空白を含む場合など。
また、JSONデータでは、関数や正規表現リテラルを使うことはできない。

JSONを使った処理

JSONデータをeval()を使ってやみくもに評価するのは、セキュリティの問題があるので勧められたものじゃない。JSON.parse()メソッドを使うのが最善策。これはES5以降言語の一部になっていて、新作ブラウザのJavaScriptエンジンはネイティブ対応している。旧式のJavaScriptエンジンの場合、JSON.orgライブラリ(http://www.json.org/json2.js)を使えばJSONオブジェクトとそのメソッドにアクセスできる。

sample.js
// JSONデータを入力
var jstr = '{"mykey": "my value"}';

// アンチパターン
var data = eval('(' + jstr + ')');

// 推奨パターン
var data = JSON.parse(jstr);

console.log(data.mykey); // "my value"

すでにJSライブラリを使っていれば、JSONを解析するユーティリティがライブラリに組み込まれている可能性があるため、JSON.orgライブラリを追加する必要はないかもしれない。
例えばYUI3であれば、次のような処理が可能になる。

// JSONデータを入力
var jstr = '{"mykey": "my value"}';

//YUIインスタンスを使ってデータを解析し、オブジェクトに変換
YUI().use('json-parse', function (Y) {
    var data = Y.JSON.parse(jstr);
    console.log(data.mykey); // "my value"
});

jQueryの場合、parseJSON()メソッドがある。

// JSONデータを入力
var jstr = '{"mykey": "my value"}';

var data = jQuery.parseJSON(jstr);
console.log(data.mykey); // "my value"

JSON.parse()と逆の変換を行うメソッドがJSON.stringify()。任意のオブジェクトや配列、プリミティブを受け取り、それをJSONデータにシリアライズ(複数の並列データを直列化すること)する。

var hoge = {
    name: "hogehoge",
    dob: new Date(),
    legs: [1, 2, 3, 4]
};

var jsonstr = JSON.stringify(hoge);
// jsonstrの中身は
// {"name": "hogehoge", "dob": "2017-12-08T19:00:00.000Z", "legs": [1,2,3,4]}

正規表現リテラル

JavaScriptにおける正規表現もオブジェクトである。正規表現を作る方法は以下の2通り。

  • new RegExp()コンストラクタを使う
  • 正規表現リテラルを使う
// 正規表現リテラル
var re = /\\/gm;

// コンストラクタ
var re = new RegExp("\\\\", "gm");

このように正規表現リテラル記法は簡潔で、クラス風コンストラクタで考えずに済むため、正規表現リテラルを使うほうが好ましい。

RegExp()コンストラクタを使う場合は、引用符はエスケープし、バックスラッシュは二重にエスケープする必要がある。上の例のように、バックスラッシュ1文字に一致させるにはバックスラッシュ4文字が必要になる。これによって、長くなり、読むのも変更するのも面倒になる。
リテラル記法にこだわったほうが良い。

正規表現リテラルの構文

正規表現リテラル記法では、正規表現パターンをスラッシュで囲む。2番目のスラッシュの後にパターン修飾子を続けることができる。このときパターン修飾子の文字は引用符で囲まない。

  • g → グローバル検索
  • m → マルチ(複数)行
  • i → 大文字と子もいzを区別しない

パターン修飾子の順序や組み合わせには制約がない。

var re = /正規表現パターン/gmi;

String.prototype.replace()のようなパラメータに正規表現オブジェクトを受け取れるメソッドを呼ぶとき、正規表現リテラルを使えばコードがより簡潔になる。

var no_letters = "abc123XYZ".replace(/[a-z]/gi, "");
console.log(no_letters); // 123
new RegExp()を使ったほうがいい場合

正規表現パターンが事前にわからず、正規表現データを実行時に作成する場合には、new RegExp()を使ったほうが良い。

正規表現リテラルとコンストラクタのもう一つの違いは、リテラルの場合、オブジェクトが作成されるのは構文解析の段階で1回きりであるという点。ループの中で同一の正規表現を作成する場合、それ以前に作成されたオブジェクトが返される。このときプロパティ(lastIndexなど)はすべて初回に設定された値のままである。
以下の例では、二度とも同一のオブジェクトが返される。

function getRE() {
    var re = /[a-z]/;
    re.foo = "bar";
    return re;
}

var reg = getRE();
var re2 = getRE();

console.log(reg === re2); // true
reg.foo = "baz";
console.log(re2.foo); // "baz";

最後に、
RegExp()を呼ぶとき、これをコンストラクタとみなしてnewを使っても、関数とみなしてnewを使わなくても、同じ振る舞いになる。