javascriptをあまり理解していない物ですが、配列に値を設定した時を検出したいと考えています。
下記のソースを組んでみました。(実際には、期待した動作を行うわけではないですが)
function testfunc() {
Object.defineProperties(this, {
items: {
get: function (index) {
(typeof(this._items) == "undefined"){
this._items = {};
}
alert(["get",index]);
this._items;
},
set: function (index,val) {
alert(["set",index]);
this._items[idx] = val;
},
configurable: true,
enumerable: true
});
}
var aa = new testfunc();
aa.items[0] = "123";
実現したいことは、
・配列に値を追加する時を検出。
・配列の値を変更する時を検出。
検出した時点で、設定する値が、他の配列にすでにある時には、
throwさせたり、alertさせたりして、配列への追加を抑制したいと考えています。
javscriptで、こんな考えが出来る、できないを含めてご指導頂ければ幸いです。
(標準化して、使いまわししたいもので、クラス化しています、
ゴリゴリ組めばできるということはわかっています。)
現状ではオブジェクトの変更をフックするための普遍的な方法はありません。
次期バージョンのECMAScript6ではProxyが定義されており、これを使えば可能です。今のところ先行実装しているのはFirefoxだけです。
var ObservableArray = (function () { // isArrayIndex, handlerをグローバル変数にしないための即時関数 function isArrayIndex(p) { var n = p >>> 0; return String(n) === p && n !== (-1 >>> 0); } var handler = { set: function (target, name, val, receiver) { if (isArrayIndex(name)) { var detail; var index = name >>> 0; if (index >= target.length) { target.length = index + 1; detail = { type: 'add', object: target, name: name }; } else if (name in target) { detail = { type: 'update', object: target, name: name, oldValue: target[name] }; } target[name] = val; if (detail) { target.notifyObservers(detail); } } } }; function ObservableArray(length) { this.length = length >>> 0; this._observers = []; return new Proxy(this, handler); } // Array.prototypeのメソッドを継承する ObservableArray.prototype = Object.create(Array.prototype, { constructor: Object.getOwnPropertyDescriptor(ObservableArray.prototype, 'constructor') }); ObservableArray.prototype.observe = function (callbackfn/*, thisArg*/) { this._observers.push([callbackfn, arguments[1]]); }; ObservableArray.prototype.notifyObservers = function (detail) { this._observers.forEach(function (observer) { observer[0].call(observer[1], detail); }); }; return ObservableArray; }()); (function main() { var array = new ObservableArray(2); array.observe(function (e) { alert(e.type + ': array[' + e.name + '] = ' + array[e.name]); }); array[0] = 'Hello'; array[1] = 'World'; array[2] = '!'; // add array[1] = 'work'; // update array.push('?'); // add alert(array.join('')); // "Hellowork!?" }());
getter, setterであれば例えばlengthを1000まで決め打ちにするなどすれば似たようなことはできるでしょうが……。
では巷のライブラリ(Knockout.jsのobservableArray、WinJS.Binding.Listなど)はどうしているかというと、角括弧によるアクセスはできず、すべての操作をメソッド経由で行うように制限して実現しています。
同じように実装するとしたら、内部にプロパティとして「本物の」配列を保持しておき、角括弧によるアクセスの代わりとしてsetAt
, getAt
などのメソッドを用意し、またArray.prototype
のメソッドをwrapして間接的に操作するような形にすればいいと思います。
var ObservableArray = (function () { function ObservableArray(length) { this._backingArray = new Array(length); this._observers = []; } Object.defineProperty(ObservableArray.prototype, 'length', { get: function () { return this._backingArray.length; }, set: function (value) { this._backingArray.length = value; } }); ObservableArray.prototype.getAt = function (index) { return this._backingArray[index]; }; ObservableArray.prototype.setAt = function (index, value) { index >>>= 0; var detail; if (index >= this.length) { detail = { type: 'add', object: this, name: index }; } else if (index in this._backingArray) { detail = { type: 'update', object: this, name: index, oldValue: this._backingArray[index] }; } this._backingArray[index] = value; if (detail) { this.notifyObservers(detail); } }; ObservableArray.prototype.push = function push(item1) { var length = this.length; // pushの処理は委譲 var result = Array.prototype.push.apply(this._backingArray, arguments); // Observableの処理 for (var i = 0, I = arguments.length; i < I; ++i) { this.notifyObservers({ type: 'add', name: length + i }); } return result; }; ObservableArray.prototype.join = function join(separator) { // joinはObservableに関わらないのでそのまま委譲 return Array.prototype.join.call(this._backingArray, separator); }; // 同様にArray.prototypeのメソッドを定義していく…… ObservableArray.prototype.observe = function (callbackfn/*, thisArg*/) { this._observers.push([callbackfn, arguments[1]]); }; ObservableArray.prototype.notifyObservers = function (detail) { this._observers.forEach(function (observer) { observer[0].call(observer[1], detail); }); }; return ObservableArray; }()); (function main() { var array = new ObservableArray(2); array.observe(function (e) { alert(e.type + ': array[' + e.name + '] = ' + array.getAt(e.name)); }); array.setAt(0, 'Hello'); array.setAt(1, 'World'); array.setAt(2, '!'); // add array.setAt(1, 'work'); // update array.push('?'); // add alert(array.join('')); // "Hellowork!?" }());
今更ですが、Chromeでは
Object.observe
,Array.observe
というAPIが実装されており、これらを使えば簡単にオブジェクトの変異を検知できるようです。- 次世代JavaScriptでデータバインディング: Object.observe() を試す - ぼちぼち日記
- harmony:observe_api_usage [ES Wiki]
2014/01/08 20:40:04ありがとうございます、希望した動きが実装できそうです。
2014/01/09 11:57:08実際には、連想配列なので、indexの部分は、修正して、作りこみます。