JS.next

JavaScriptの最新実装情報を追うブログ

Object.observeについて

概要

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(デフォルト有効)