Chapter 5. Object Creation Patterns
■Namespace Pattern
グローバルに一つの名前空間をおいてそこを拡張していく方法
// global object
var MYAPP = {};
// constructors
MYAPP.Parent = function () {};
MYAPP.Child = function () {};
// a variable
MYAPP.some_var = 1;
// an object container
MYAPP.modules = {};
// nested objects
MYAPP.modules.module1 = {};
MYAPP.modules.module1.data = {a: 1, b: 2};
MYAPP.modules.module2 = {};
この方法はすべにプレフィックスを置く必要があるので総コード量が増える。またネストが深くなると名前解決のlookupも遅くなる。
General Purpose Namespace Function
上の書き方はすでに同じ名前のグローバルオブジェクトが存在していたことを考慮していなかったので、次のように書きます。
// unsafe
var MYAPP = {};
// よりよい方法
if (typeof MYAPP === "undefined") {
var MYAPP = {};
}
// 短く書くと
var MYAPP = MYAPP || {};
プロパティに追加する場合も同様で、追加したいオブジェクトが存在しているかを確認してからでないと、存在しないオブジェクトに対してプロパティを追加しようとすればエラーになります。たとえば MYAPP.modules.module2 を持つようなオブジェクトを作ろうとすると、以下のように順番立ててオブジェクトを作らないといけないので面倒です。
// equivalent to:
var MYAPP = {
modules: {
module2: {}
}
};
これ次のようにシンプルに書ける関数を作ってみましょう。
MYAPP.namespace('MYAPP.modules.module2');
MYAPPの名前空間を拡張する関数
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.'),
parent = MYAPP,
i;
// strip redundant leading global
if (parts[0] === "MYAPP") {
parts = parts.slice(1);
}
for (i = 0; i < parts.length; i += 1) {
// create a property if it doesn't exist
if (typeof parent[parts[i]] === "undefined") {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};
この関数を使えば下のように簡単にネストの深い名前空間を作成できます。
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true
■Declaring Dependencies
YUI2の場合、次のようにYAHOO.util.Eventといったようにモジュールに分かれている。ローカルで使う際にエイリアスを張るシンプルなパターン
var myFunction = function () {
// dependencies
var event = YAHOO.util.Event,
dom = YAHOO.util.Dom;
// use event and dom variables
// for the rest of the function...
};
これのいい点としてはローカルで何を使うのか明示的に宣言できる点やeventといったローカル変数はYAHOOと言ったグローバル変数を参照するより高速になること。
またYUICompressor や Google Closure compilerといった圧縮ツールはeventをaなどの短い文字列に変えてくれる(グローバル変数の方が危険なので変更されない)のでローカルでこのモジュールを使うほど圧縮した際にコードの量はエイリアスを張らない時に比べて差が出る。
■Private Properties and Methods
JavaScriptにはprivateにするといった演算子はないのでどうする?Private Members
クロージャーを使用することでprivate menmverを作成できる。
function Gadget() {
// private member
var name = 'iPod';
// public function
this.getName = function () {
return name;
};
}
var toy = new Gadget();
// 'name' is undefined,
console.log(toy.name); // undefined
Privileged Methods
一個前の getName()は特権的なメソッドで、nameプロパティへのアクセスすることができる。(クロージャーの話ですね
Privacy Failures
基本的にはnameプロパティには外部からアクセスできないけど、いくつかの例外的なケースが存在する- eval() の第二引数 , __parent__ を使った方法(もう廃止されてる気もする)
- 特権的なメソッドがオブジェクトや配列と形で値を返す時、それらを外から書き換えることでprivateな値にアクセスできる
その返されたオブジェクトを書き換えることでprivateなものも書き換えできてしまっている。
function Gadget() {
// private member
var specs = {
screen_width: 320,
screen_height: 480,
color: "white"
};
// public function
this.getSpecs = function () {
return specs;
};
}
var toy = new Gadget(),
specs = toy.getSpecs();
console.dir(specs); // 返り値のオブジェクト
specs.color = "black";// 書き換え
specs.price = "free";
var toy_2 = new Gadget(),
specs_2 = toy.getSpecs();
console.dir(specs_2);// オブジェクトを書き換えられてしまった
対処法としては- Principle Of Least Authority (POLA) は「最小権限の原則」で返すものを最小限にする
- deep copyを作ってそれを返す
Object Literals and Privacy
オブジェクトリテラルの場合を見てみるいわゆるモジュールパターンと呼ばれるもの。
var myobj = (function () {
// private members
var name = "my, oh my";
// implement the public part
return {
getName: function () {
return name;
}
};
}());
myobj.getName(); // "my, oh my"
Prototypes and Privacy
上記のようにコンストラクタでthisを使ってやる方法には、毎回オブジェクトに複製されるためメモリをもりもり食べるという問題があります。そういう時にはprototypeプロパティを使ってインスタンスオブジェクトで共有させるのがいいでしょう。
function Gadget() {
// private member
var name = 'iPod';
// public function
this.getName = function () {
return name;
};
}
// prototype - インスタンスで共有
Gadget.prototype = (function () {
// private member
var browser = "Mobile Webkit";
// public prototype members
return {
getBrowser: function () {
return browser;
}
};
}());
var toy = new Gadget();
console.log(toy.getName()); // privileged "own" method
console.log(toy.getBrowser()); // privileged prototype method
Revealing Private Functions As Public Methods
ECMAScript 5にはオブジェクトを変更できなくさせるオプションあるよ。でも今のバージョンにはないのでrevelation patternを使う。(“revealing module pattern”)
var myarray;
(function () {
var astr = "[object Array]",
toString = Object.prototype.toString;
function isArray(a) {
return toString.call(a) === astr;
}
function indexOf(haystack, needle) {
var i = 0,
max = haystack.length;
for (; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return −1;
}
// ここ
myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
}());
myarray.isArray([1,2]); // true
myarray.isArray({0: 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2
// 仮にmyarray.indexOfが書き換えられても、privateなとこまでは変えられない。
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2
(言ってる意味がよくわからなかった■Module Pattern
モジュールパターンはいくつかのパターンとして組み合わせて利用できたりするのでよく使われている。何かのオブジェクトを拡張したりするときに使える。
MYAPP.utilities.array = (function () {
// この辺にprivateなmenber
var ...
// public API
return {
inArray: function (needle, haystack) {
// ...
},
isArray: function (a) {
// ...
}
};
}());
Revealing Module Pattern
さっきの revelation patternをモジュールに適応してみる。基本的にはあんまり変わらないように見えるけど、こっちの方がデバッガーには優しい。
MYAPP.utilities.array = (function () {
// private properties
var array_string = "[object Array]",
ops = Object.prototype.toString,
// private methods
inArray = function (haystack, needle) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return −1;
},
isArray = function (a) {
return ops.call(a) === array_string;
};
// end var
// revealing public API
return {
isArray: isArray,
indexOf: inArray
};
}());
Modules That Create Constructors
さっきのと大きな違いは返すのはオブジェクトではなく関数であること。
MYAPP.namespace('MYAPP.utilities.Array');
MYAPP.utilities.Array = (function () {
// dependencies
var uobj = MYAPP.utilities.object,
ulang = MYAPP.utilities.lang,
// private properties and methods...
Constr;
// end var
// optionally one-time init procedures
// ...
// public API -- constructor
Constr = function (o) {
this.elements = this.toArray(o);
};
// public API -- prototype
Constr.prototype = {
constructor: MYAPP.utilities.Array,
version: "2.0",
toArray: function (obj) {
for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
a[i] = obj[i];
}
return a;
}
};
// return the constructor
// to be assigned to the new namespace
return Constr;
}());
// 使う時は
var arr = new MYAPP.utilities.Array(obj);
Importing Globals into a Module
モジュールパターンはエイリアスがはれるので便利。
MYAPP.utilities.module = (function (app, global) {
// app, global でアクセスできる
}(MYAPP, this));
■Sandbox Pattern
Sandbox Patternはnamespacing patternの欠点に基づいてる。namespacing patternの欠点をあげてみると以下のような問題がある。
- 一つのグローバルオブジェクに依存するため、同じページで2つのバージョンのライブラリやアプリを動かせない。
- モジュールで分けていくとMYAPP.utilities.arrayのようにドットが深くなる(名前解決のコスト)
A Global Constructor
namespacing patternは一つのグローバルオブジェクトだが、Sandbox Patternは一つのSandbox()というコンストラクタを使う。
// 必要なモジュールを決めて使える
Sandbox(['ajax', 'event'], function (box) {
// console.log(box);
});
// 配列にしなくてもいい
Sandbox('ajax', 'dom', function (box) {
// console.log(box);
});
// すべてのモジュールが必要な時は*を使うか省略する
Sandbox('*', function (box) {
// console.log(box);
});
Sandbox(function (box) {
// console.log(box);
});
//Sandboxは入れ子にしても利用できる。
Sandbox('dom', 'event', function (box) {
// dom and eventを使う処理
Sandbox('ajax', function (box) {
// ajaxを使う処理
});
Adding Modules
Sandboxコンストラクタを実装する前にモジュールの追加方法を見てみる。boxをインスタンスとして追加していく。
Sandbox.modules = {};
Sandbox.modules.dom = function (box) {
box.getElement = function () {};
box.getStyle = function () {};
box.foo = "bar";
};
Sandbox.modules.event = function (box) {
// Sandbox prototypeへのアクセスが必要ならcostructorをたどる
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {};
box.dettachEvent = function () {};
};
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {};
box.getResponse = function () {};
};
Implementing the Constructor
最後にSandbox()コンストラクタを実装してみよう。
function Sandbox() {
// 引数は配列argsに変換
var args = Array.prototype.slice.call(arguments),
// 配列の最後はコールバック関数なので取り出す
callback = args.pop(),
// Sandbox(["A","B"],callback) or Sandbox("A","B",callback) どちらでも可能
modules = (args[0] && typeof args[0] === "string") ? args : args[0],
i;
// newを付けないで呼ばれたときにコンストラクタとして呼び直す
if (!(this instanceof Sandbox)) {
return new Sandbox(modules, callback);
}
// 必要ならthisにプロパティを追加できるよ
this.a = 1;
this.b = 2;
// now add modules to the core `this` object
// moduleがない=パラメータレス or *を指定したときは全てのモジュールを使う
if (!modules || modules === '*') {
modules = [];
for (i in Sandbox.modules) {
if (Sandbox.modules.hasOwnProperty(i)) {
modules.push(i);
}
}
}
// 必要としてモジュールの初期化
for (i = 0; i < modules.length; i += 1) {
Sandbox.modules[modules[i]](this);
}
// Sandboxのコールバック(処理本体)を呼ぶ
callback(this);
}
// prototypeプロパティとして定義すれば全部のモジュールから使える
// モジュール内からならbox.constructor.prototypeって書いたやつと同じ
Sandbox.prototype = {
name: "My Application",
version: "1.0",
getName: function () {
return this.name;
}
};
■Static Members
Public Static Members
この動きおもしろい。
// constructor
var Gadget = (function () {
// static variable/property
var counter = 0,
NewGadget;
// this will become the
// new constructor implementation
NewGadget = function () {
counter += 1;
};
// a privileged method
NewGadget.prototype.getLastId = function () {
return counter;
};
// overwrite the constructor
return NewGadget;
}()); // execute immediately
// test
var iphone = new Gadget();
iphone.getLastId(); // 1
var ipod = new Gadget();
ipod.getLastId(); // 2
var ipad = new Gadget();
ipad.getLastId(); // 3
■Object Constants
JavaScriptにはconstがないので、定数を作っても書き換える事ができてしまいます。次善策としてはNumber.MAX_VALUE;などのビルトインに従って大文字_のルールで変数を作るなどがあります。
もう一つは書き換えができないようにconstantオブジェクト経由で定数を操作する方法
var constant = (function () {
var constants = {},
ownProp = Object.prototype.hasOwnProperty,
allowed = {
number: 1,
boolean: 1
},
prefix = (Math.random() + "_").slice(2);
return {
set: function (name, value) {
if (this.isDefined(name)) {
return false;
}
if (!ownProp.call(allowed, typeof value)) {
return false;
}
constants[prefix + name] = value;
return true;
},
isDefined: function (name) {
return ownProp.call(constants, prefix + name);
},
get: function (name) {
if (this.isDefined(name)) {
return constants[prefix + name];
}
return null;
}
};
}());
// TEST
// まだ未定義
constant.isDefined("maxwidth"); // false
// 定義する
constant.set("maxwidth", 480); // true
// check again
constant.isDefined("maxwidth"); // true
// 上書きはできない
constant.set("maxwidth", 320); // false
// is the value still intact?
constant.get("maxwidth"); // 480
■Chaining Pattern
メソッドチェーンの作り方
myobj.method1("hello").method2().method3("world").method4();
このパターンはthisを返す事で作成できます。
var obj = {
value: 1,
increment: function () {
this.value += 1;
return this;
},
add: function (v) {
this.value += v;
return this;
},
shout: function () {
alert(this.value);
}
};
Pros and Cons of the Chaining Pattern
メソッドチェーンを書きすぎてデバッグがしにくくなることを"列車事故"という。via Clean Code - Robert Martinまあ便利なのでjQueryとかDOM APIでも使われている。
method() Method
methodというprototype拡張をするメソッド(オリジナルは Douglas Crockford )
if (typeof Function.prototype.method !== "function") {
Function.prototype.method = function (name, implementation) {
this.prototype[name] = implementation;
return this;// メソッドチェーン用
};
}
これを使うと以下のようにコンストラクタが書ける。
var Person = function (name) {
this.name = name;
}.
method('getName', function () {
return this.name;
}).
method('setName', function (name) {
this.name = name;
return this;
});
// TEST
var a = new Person('Adam');
a.getName(); // 'Adam'
a.setName('Eve').getName(); // 'Eve'
■おわり
Sandbox()はおもしろい感じコメント(0件)
- TB-URL http://efcl.info/adiary/093/tb/