概要
Object.observeとは、ES7で導入予定の、オブジェクトの変更を監視するためのメソッドである。
仕様も安定し、この度V8でデフォルトで有効になったので紹介してみたいと思う。
紹介するメソッド
Object.observe(target, callback, acceptList = defaultAcceptTypes)
targetオブジェクトを監視する
監視するオブジェクト、変更があった時に呼ばれる関数、監視するタイプの配列を指定する
defaultAcceptTypes =
['add', 'update', 'delete', 'setPrototype', 'reconfigure', 'preventExtensions']
Object.unobserve(target, callback)
オブジェクトの監視をやめる
Object.deliverChangeRecords(callback)
オブジェクトの変更情報を即通知する
Object.getNotifier(target) -> <(Notifier)>
通知を行うためのオブジェクトが返される
(Notifier).prototype.notify(record)
任意の通知を行う
(Notifier).prototype.performChange(changeType, changeFn)
通知をオーバーライドする
Array.observe(target, callback)
Object.observe(target, callback, ['add', 'update', 'delete', 'splice']) と同じ
Array.unobserve(target, callback)
Object.unobserve(target, callback) と同じ
基本的な使い方
第一引数に監視したいオブジェクト、第二引数にオブジェクト変更時の通知を受ける関数を指定する。
obj = {} function log(changeRecords) { console.log(JSON.stringify(changeRecords, undefined, 2)) } Object.observe(obj, log)
プロパティが追加、更新されるなど、監視したオブジェクトが変化すると、変化に応した通知がまとめてされる。
obj.x = 1 // プロパティを追加する obj.x = 2 // プロパティを更新する // オブジェクトに変化があると、処理が一段落ついたアイドル時に // 変更記録オブジェクトが配列にまとめられてコールバック関数に渡される // *log* // [ // { // プロパティ追加レコード // type: "add", // 変化のタイプ // object: {x: 2}, // 対象のオブジェクト // name: "x" // 対象のプロパティ // }, // { // プロパティ更新レコード // type: "update", // object: {x: 2}, // name: "x", // oldValue: 1 // 更新前の値 // } // ]
通知される各オブジェクトには必ずtypeとobject属性が含まれ、場合によってその他の属性も付く。
タイプは全部で7つあり、第三引数に配列で指定することにより限定できる。
Object.observeではデフォルトで
['add', 'update', 'delete', 'setPrototype', 'reconfigure', 'preventExtensions']
Array.observeではデフォルトで
['add', 'update', 'delete', 'splice']
が監視対象とされる。
監視タイプ別説明
add
プロパティの追加を監視する。
obj = {} Object.observe(obj, log, ['add']) obj.x = 1 // *log* // [ // { // type : "add", // object : {x: 1}, // name : "x" // } // ]
update
プロパティの更新を監視する。
obj = {x: 1} Object.observe(obj, log, ['update']) obj.x = 2 // *log* // [ // { // type : "update", // object : {x: 2}, // name : "x", // oldValue : 1 // } // ]
delete
プロパティの削除を監視する。
obj = {x: 1} Object.observe(obj, log, ['delete']) delete obj.x // *log* // [ // { // type : "delete", // object : {}, // name : "x", // oldValue : 1 // } // ]
setPrototype
オブジェクトのプロトタイプの変更を監視する。
obj = {__proto__: null} Object.observe(obj, log, ['setPrototype']) Object.setPrototypeOf(obj, {}) // *log* // [ // { // type : "setPrototype", // object : {}, // name : "__proto__", // oldValue : null // } // ]
reconfigure
プロパティの属性の変更を監視する。
obj = {x: 1} // {value: 1, enumerable: true} Object.observe(obj, log, ['reconfigure']) Object.defineProperty(obj, 'x', {value: 1, enumerable: false}) // *log* // [ // { // type : "reconfigure", // object : {(x: 1)}, // name : "x" // } // ]
obj = {x: 1} // {value: 1, enumerable: true} Object.observe(obj, log, ['reconfigure']) Object.defineProperty(obj, 'x', {value: 3, enumerable: false}) // *log* // [ // { // type : "reconfigure", // object : {(x: 3)}, // name : "x", // oldValue : 1 // 値が変更された時には付く // } // ]
obj = {x: 1} Object.observe(obj, log, ['reconfigure']) Object.seal(obj) // configurable -> false // *log* // [ // { // type : "reconfigure", // object : {x: 1}, // name : "x" // } // ]
obj = {x: 1, y: 2} Object.observe(obj, log, ['reconfigure']) Object.freeze(obj) // writable -> false, configurable -> false // *log* // [ // { // type : "reconfigure", // object : {x: 1, y: 2}, // name : "x" // }, // { // type : "reconfigure", // object : {x: 1, y: 2}, // name : "y" // } // ]
preventExtensions
オブジェクトが拡張禁止にされるのを監視する。
obj = {} Object.observe(obj, log, ['preventExtensions']) Object.preventExtensions(obj) // *log* // [ // { // type : "preventExtensions", // object : {} // } // ]
splice
配列の長さが変わるような操作を監視する。他のタイプより優先される。
Object.observeではデフォルトで監視対象のタイプにされない。
Array.observeではデフォルトで監視対象のタイプにされる。
ary = ['a', 'b'] Object.observe(ary, log, ['splice']) ary[5] = 'f' // *log* // [ // { // type : "splice", // object : ["a", "b", undefined x3, "f"], // index : 2, // 起点となるインデックス // removed : [], // 削除された全要素 // addedCount : 4 // 配列が伸長した長さ // } // ] // インデックス2から4要素の追加
ary = ['a', 'b', 'c', 'd'] Array.observe(ary, log) ary.shift() // *log* // [ // { // type : "splice", // object : ["b", "c", "d"] // index : 0, // removed : ["a"] // addedCount : 0 // } // ] // インデックス0から要素'a'の削除
ary = ['a', 'b', 'c', 'd', 'e', 'f'] Array.observe(ary, log) ary.splice(1, 3, 'X', 'Y') // *log* // [ // { // type : "splice", // object : ["a", "X", "Y", "e", "f"], // index : 1, // removed : ["b", "c", "d"], // addedCount : 2 // } // ] // インデックス1から要素'b', 'c', 'd'の削除、2要素の追加
ary = ['a', 'b', 'c'] Array.observe(ary, log) ary.x = 123 // *log* // [ // { // type : "add", // object : ["a", "b", "c", x: 123], // name : "x" // } // ] // 通常のプロパティ操作には反応しない
使用イメージ
足りない分は自分で補う。
Object.observe(obj, function (changeRecords) { changeRecords.forEach(function (record) { var type = records.type var object = records.object var name = records.name var oldValue = records.oldValue var newValue = object[name] // 補う switch (type) { case 'add' : ………… break case 'update' : ………… break case 'delete' : ………… break } }) }, ['add', 'update', 'delete'])
Object.observe(obj, function (changeRecords) { changeRecords.forEach(function (record) { var object = records.object var name = records.name var descriptor = Object.getOwnPropertyDescriptor(object, name) // 補う ………… }) }, ['reconfigure'])
Array.observe(ary, function (changeRecords) { changeRecords.forEach(function (record) { var type = records.type var array = records.object var index = records.index var added = array.slice(index, index + records.addedCount) // 補う var removed = records.removed ………… }) })
deliverChangeRecordsの使い道
通常は一度に複数回変更があった場合、まとめて通知される。
obj = {} Object.observe(obj, log) obj.a = 1 obj.b = 2 obj.c = 3 // *log* // [ // { // type : "add", // object : {a: 1, b: 2, c: 3}, // name : "a" // }, // { // type : "add", // object : {a: 1, b: 2, c: 3}, // name : "b" // }, // { // type : "add", // object : {a: 1, b: 2, c: 3}, // name : "c" // }, // ]
deliverChangeRecordsを使えば即座に通知を起こすことができる。
obj = {} Object.observe(obj, log) obj.a = 1 Object.deliverChangeRecords(log) obj.b = 2 Object.deliverChangeRecords(log) obj.c = 3 // *log* // [ // { // type : "add", // object : {a: 1}, // name : "a" // } // ] // *log* // [ // { // type : "add", // object : {a: 1, b: 2}, // name : "b" // } // ] // *log* // [ // { // type : "add", // object : {a: 1, b: 2, c: 3}, // name : "c" // } // ]
使用イメージ
Array.observe(ary, callback) ary.sort(function (a, b) { Object.deliverChangeRecords(ary) return a - b })
getNotifierの使い道
通知のためのNotifierオブジェクトを返す。
(Notifier).prototype.notify(record)
任意の通知を起こす。
obj = {} Object.observe(obj, log) nf = Object.getNotifier(obj) nf.notify({ type: 'add' }) // *log* // [ // { // type : "add" // object : {}, // } // ]
obj = {} Object.observe(obj, log, ['hoge']) nf = Object.getNotifier(obj) nf.notify({ type: 'hoge', fuga: 123 }) // *log* // [ // { // type : "hoge", // object : {}, // fuga : 123 // } // ]
(Notifier).prototype.performChange(changeType, changeFn)
changeFnが呼ばれる。
changeTypeが監視されているタイプだった場合、changeFn内でオブジェクトを操作しても通知はされない。
その場合changeFnがオブジェクトを返すことで代わりに通知される。
obj = {} Object.observe(obj, log, ['add', 'hoge']) // hogeタイプを監視している nf = Object.getNotifier(obj) nf.performChange('hoge', function () { // 通常の通知が抑制される obj.x = 1 // 通知されない return {fuga : 123} // オブジェクトを返すと、代わりに通知される }) // *log* // [ // { // type : "hoge", // object : {x : 1}, // fuga : 123 // } // ]
obj = {} Object.observe(obj, log, ['add', 'hoge']) // hogeタイプを監視している nf = Object.getNotifier(obj) nf.performChange('hoge', function () { // 通常の通知が抑制される obj.x = 1 // 通知されない // オブジェクトを返さない場合、通知されない }) // *log*
obj = {} Object.observe(obj, log, ['add']) // hogeタイプを監視していない nf = Object.getNotifier(obj) nf.performChange('hoge', function () { // 通常の通知が働く obj.x = 1 // 通知される return {fuga : 123} // 通知されない }) // *log* // [ // { // type : "add", // object : {x: 1}, // name : "x" // } // ]
使用イメージ
基本的に、メソッドで専用の通知を行いたいとき、元々の通知を抑制するために使う。
function Point(x, y) { this.x = x this.y = y } Point.prototype.zero = function () { var p = this var nf = Object.getNotifier(p) nf.performChange('zero', function () { var _x = p.x var _y = p.y // zeroタイプが監視されていた場合は通知が起きない p.x = 0 p.y = 0 // その場合代わりに旧オブジェクトを通知する return { oldObject: {x: _x, y: _y} } }) return p }
1.observeしていない場合は何も起こらない。
p = new Point(20, 30) p.zero() // {x: 0, y: 0}
2.observeしているが、zeroタイプを指定していない場合、通常の通知がされる。
p = new Point(20, 30) Object.observe(p, log) p.zero() // *log* // [ // { // type : "update", // object : {x: 0, y: 0}, // name : "x", // oldValue : 20 // }, // { // type : "update", // object : {x: 0, y: 0}, // name : "y", // oldValue : 30 // } // ]
3.zeroタイプを指定している場合、代わりに指定した通知がされる。
p = new Point(20, 30) Object.observe(p, log, ['update', 'zero']) p.zero() // *log* // [ // { // type : "zero", // object : {x: 0, y: 0}, // oldObject : {x: 20, y: 30} // } // ]
実装されたバージョン
V8 3.15.0 - 3.25.6(デフォルト有効)