人力検索はてな
モバイル版を表示しています。PC版はこちら
i-mobile

javascriptで配列の値が変更されたことを検出する方法はないでしょうか?

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で、こんな考えが出来る、できないを含めてご指導頂ければ幸いです。
(標準化して、使いまわししたいもので、クラス化しています、
ゴリゴリ組めばできるということはわかっています。)

●質問者: kameoyaji_2
●カテゴリ:ウェブ制作
○ 状態 :終了
└ 回答数 : 1/1件

▽最新の回答へ

1 ● rikuba
●200ポイント ベストアンサー

現状ではオブジェクトの変更をフックするための普遍的な方法はありません。
次期バージョンの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.jsobservableArrayWinJS.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!?"
}());

kameoyaji_2さんのコメント
色々やり方がるんですね・・・。 他の言語でできるるので、出来るかなとは思っていたのですが、出来ることが分かったので、自分なりの思考に合ったクラスを何とか実現出来そうです。 ターゲットが、クロームなので、角括弧が使えないのが残念ですが。 また、ご提供いただいたソースを自分なりに理解するには、少々時間がかかりそうですが、勉強させていただきます。 ところで >>>= なのですが、>>>でビット演算しているのはわかるのですが、 >>>= 0 では、どのような処理を行っていことになるのでしょうか? javaに詳しくないので、ご指導いただければ幸いです。 (正規表現もいまだにいまいち理解できないレベルのポンコツプログラマーで申し訳ございません。)

rikubaさんのコメント
<a href="http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/11_Expressions.html#section-11.7.3">&gt;&gt;&gt;</a>= 0 は32ビット符号なし整数に変換する(内部メソッドの<a href="http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/9_Type_Conversion.html#section-9.6">ToUint32</a>を適用する)ためのイディオムです。 少数や負数を渡されたときも<code>index</code>を符号なし整数に丸め込むために書きましたが、<code>getAt</code>でやっていないなどお粗末……というわけでまあ、あまり気にしないでください。自然数でなければエラーにするなどにした方がいいかもしれないですね。自分しか使わないのであれば、<code>typeof index === "number"</code>の確認くらいで十分かもしれません。 ちなみに<code>isArrayIndex</code>は仕様書の<a href="http://es5.github.io/#x15.4">Array Objects</a>に書かれていることをそのままコードにしたものです。 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/prototype"><code>Array.prototype</code></a>にある関数は、<a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/call"><code>Function.prototype.call</code></a>や<a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/apply"><code>Function.prototype.apply</code></a>を使うことで配列以外のオブジェクトにも適用することができます(仕様にそう定義されています(<q>The <var>push</var> function is intentionally generic</q>))。 >|javascript| var object = {}; Array.prototype.push.call(object, 'foo', 'bar', 'baz'); alert(JSON.stringify(object, null, 4)); /* { "0": "foo", "1": "bar", "2": "baz", "length": 3 } */ ||< 配列に関する処理は<code>Array.prototype</code>の関数に丸投げして、<code>Observable</code>にあたる部分の処理を自分で考えていけばいい、というわけです。 最初の回答ではああ書きましたが、初めから<code>Array.prototype</code>のメソッドを全部移植しようとせずとも、必要なメソッドから定義していけばいいと思います。 個人的には、汎用的な<code>ObservableArray</code>を使うよりも、より具体的な<i>クラス</i>に配列を持たせて、その<i>クラス</i>自身を<code>Observable</code>にすることが多いです。 >|javascript| function ContactList() { this._contacts = []; // privateのつもり } ContactList.prototype.add = function (contact) { this._contacts.push(contact); this.notifyObservers({ type: 'add', value: contact }); }; // 省略 ||<

rikubaさんのコメント
今更ですが、Chromeでは<code>Object.observe</code>, <code>Array.observe</code>というAPIが実装されており、これらを使えば簡単にオブジェクトの変異を検知できるようです。 >|javascript| var array = []; Array.observe(array, function (changes) { alert(JSON.stringify(changes, null, 2)); }); array.push('foo', 'bar', 'baz'); array.splice(0, 2, 'hoge', 'fuga'); array[2] = 'piyo'; array.length = 0; ||< -[http://d.hatena.ne.jp/jovi0608/20121206/1354762082:title] -[http://wiki.ecmascript.org/doku.php?id=harmony:observe_api_usage:title]

kameoyaji_2さんのコメント
ありがとうございます、希望した動きが実装できそうです。 実際には、連想配列なので、indexの部分は、修正して、作りこみます。
関連質問

●質問をもっと探す●



0.人力検索はてなトップ
8.このページを友達に紹介
9.このページの先頭へ
対応機種一覧
お問い合わせ
ヘルプ/お知らせ
ログイン
無料ユーザー登録
はてなトップ