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

回答の条件
  • 1人5回まで
  • 登録:
  • 終了:2014/01/05 19:10:04
※ 有料アンケート・ポイント付き質問機能は2023年2月28日に終了しました。

ベストアンサー

id:rikuba No.1

回答回数26ベストアンサー獲得回数12

ポイント200pt

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

今更ですが、ChromeではObject.observe, Array.observeというAPIが実装されており、これらを使えば簡単にオブジェクトの変異を検知できるようです。

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;
2014/01/08 20:40:04
id:kameoyaji_2

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

2014/01/09 11:57:08

コメントはまだありません

この質問への反応(ブックマークコメント)

「あの人に答えてほしい」「この質問はあの人が答えられそう」というときに、回答リクエストを送ってみてましょう。

これ以上回答リクエストを送信することはできません。制限について

回答リクエストを送信したユーザーはいません