読者です 読者をやめる 読者になる 読者になる

JS.next

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

Object.observeについて

概要

Object.observeとは、オブジェクトの変更を監視するためのAPIであった。
ES2015,2016の候補として挙げられており、V8でデフォルトで有効にされるまでに至っていたが、
実装コストがかかる上、世のニーズとそれほど合っていないということで結局廃止された。


改めて注意勧告

これは廃止された仕様です

APIの概要

提供されるメソッド

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) と同じ


基本的な使い方

第一引数に監視したいオブジェクト、第二引数にオブジェクト変更時の通知を受ける関数を指定する。

var obj = {}

function log(changeRecords) {
  console.log(changeRecords)
}

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

プロパティの追加を監視する。

let obj = {}
Object.observe(obj, log, ['add'])

obj.x = 1

//  *log*
  [
    {
      type   : "add",
      object : {x: 1},
      name   : "x"
    }
  ] 


update

プロパティの更新を監視する。

let obj = {x: 1}
Object.observe(obj, log, ['update'])

obj.x = 2

//  *log*
  [
    {
      type     : "update",
      object   : {x: 2},
      name     : "x",
      oldValue : 1
    }
  ] 


delete

プロパティの削除を監視する。

let obj = {x: 1}
Object.observe(obj, log, ['delete'])

delete obj.x

//  *log*
  [
    {
      type     : "delete",
      object   : {},
      name     : "x",
      oldValue : 1
    }
  ]


setPrototype

オブジェクトのプロトタイプの変更を監視する。

let obj = {__proto__: null}
Object.observe(obj, log, ['setPrototype'])

Object.setPrototypeOf(obj, {})

//  *log*
  [
    {
      type     : "setPrototype",
      object   : {},
      name     : "__proto__",
      oldValue : null
    }
  ] 


reconfigure

プロパティの設定の変更を監視する。

let 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"
    }
  ] 
let 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  // 値が変更された時には付く
    }
  ] 
let obj = {x: 1}
Object.observe(obj, log, ['reconfigure'])

Object.seal(obj)  // configurable -> false

//  *log*
  [
    {
      type   : "reconfigure",
      object : {x: 1},
      name   : "x"
    }
  ] 
let 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

オブジェクトが拡張禁止にされるのを監視する。

let obj = {}
Object.observe(obj, log, ['preventExtensions'])

Object.preventExtensions(obj)

//  *log*
  [
    {
      type   : "preventExtensions",
      object : {}
    }
/ ] 


splice

配列の長さが変わるような操作を監視する。他のタイプより優先される。
Object.observeではデフォルトで監視対象のタイプにされない。
Array.observeではデフォルトで監視対象のタイプにされる。

let ary = ['a', 'b']
Object.observe(ary, log, ['splice'])

ary[5] = 'f'

//  *log*
  [
    {
      type       : "splice",
      object     : ["a", "b", undefined x3, "f"],
      index      : 2,  // 起点となるインデックス
      addedCount : 4,  // 配列が伸長した長さ
      removed    : []  // 削除された全要素
    }
  ] 
//  インデックス2から4要素の追加
let ary = ['a', 'b', 'c', 'd']
Array.observe(ary, log)  // "splice"がデフォルトで監視対象

ary.shift()

//  *log*
  [
    {
      type       : "splice",
      object     : ["b", "c", "d"],
      index      : 0,
      addedCount : 0,
      removed    : ["a"]
    }
  ] 
//  インデックス0から要素'a'の削除
let 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,
      addedCount : 2,
      removed    : ["b", "c", "d"]
    }
  ]
//  インデックス1から2要素の追加、要素'b', 'c', 'd'の削除
let ary = new Array(5)
Array.observe(ary, log)

ary.length = 3

//  *log*
  [
    {
      type       : "splice",
      object     : [undefined x 3],
      index      : 3,
      addedCount : 0,
      removed    : [undefined x 2]
    }
  ]
//  インデックス3から2要素の削除


要素が更新されたときや、通常のプロパティ追加には反応しない。

let ary = ['a', 'b', 'c']
Array.observe(ary, log)

ary[1] = 'X'

//  *log*
  [
    {
      type     : "update",
      object   : ["a", "X", "c"],
      name     : "1",
      oldValue : "b"
    }
  ]
let 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, changeRecords => {
  changeRecords.forEach( record => {

    let type     = records.type,
        object   = records.object,
        name     = records.name,
        newValue = object[name],  // 補う
        oldValue = records.oldValue

    switch (type) {
      case 'add'    :
        …………
      break
      case 'update' :
        …………
      break
      case 'delete' :
        …………
      break
    }
    
  })
}, ['add', 'update', 'delete'])
Object.observe(obj, changeRecords => {
  changeRecords.forEach( record => {

    let object     = records.object,
        name       = records.name,
        descriptor = Object.getOwnPropertyDescriptor(object, name)  // 補う

    …………

  })
}, ['reconfigure'])
Array.observe(ary, changeRecords => {
  changeRecords.forEach( record => {

    let type    = records.type,
        array   = records.object,
        index   = records.index,
        added   = array.slice(index, index + records.addedCount),  // 補う
        removed = records.removed

    …………

  })
})


deliverChangeRecordsの使い道

Object.deliverChangeRecords(callback)

通常は一度に複数回変更があった場合、まとめて通知される。

let 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を使えば即座に通知を起こすことができる。

let 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( (a, b) => {
  Object.deliverChangeRecords(callback)
  return a - b
})


getNotifierの使い道

Object.getNotifier(target) -> <(Notifier)>

通知のためのnotifierオブジェクトを返す。


(Notifier).prototype.notify(record)

任意の通知を起こす。

let obj = {}

Object.observe(obj, log)
let nf = Object.getNotifier(obj)

nf.notify({ type: 'add' })

//  *log*
  [
    {
      type   : "add"
      object : {},
    }
  ] 
let obj = {}

Object.observe(obj, log, ['foo'])
let nf = Object.getNotifier(obj)

nf.notify({ type: 'foo', bar: 123 })

//  *log*
  [
    {
      type   : "foo",
      object : {},
      bar    : 123
    }
  ] 


(Notifier).prototype.performChange(changeType, changeFn)

まずchangeFnが同期的に呼ばれる。
changeTypeが監視されているタイプだった場合、changeFn内では標準の通知が抑制される。
そこでchangeFnがオブジェクトを返せば、代わりの通知を起こせる。

let obj = {}

Object.observe(obj, log, ['add', 'foo'])  // fooタイプを監視している
let nf = Object.getNotifier(obj)

nf.performChange('foo', () => {  // 標準の通知が抑制される
  obj.x = 1  // 通知されない
  return { bar: 123 }
    // オブジェクトを返すと、代わりに通知される
})

//  *log*
  [
    {
      type   : "foo",
      object : {x: 1},
      bar    : 123
    }
  ] 
let obj = {}

Object.observe(obj, log, ['add', 'foo'])  // fooタイプを監視している
let nf = Object.getNotifier(obj)

nf.performChange('foo', () => {  // 標準の通知が抑制される
  obj.x = 1  // 通知されない
    // オブジェクトを返さない場合、通知されない
})

//  *log*
let obj = {}

Object.observe(obj, log, ['add'])  // fooタイプを監視していない
let nf = Object.getNotifier(obj)

nf.performChange('foo', () => {  // 標準の通知が働く
  obj.x = 1  // 通知される
  return { bar: 123 }  // 通知されない
})

//  *log*
  [
    {
      type   : "add",
      object : {x: 1},
      name   : "x"
    }
  ] 


使用イメージ

基本的に、メソッドで標準の通知の代わりに、専用の通知を実装するために使う。

class Point {
  constructor(x, y) {
    this.x = x
    this.y = y
  }
  zero() {
    Object.getNotifier(this).performChange('zero', () => {   

      let originalRecord = { oldObject: {x: this.x, y: this.y} }
        // 専用の変更記録オブジェクトを作る

      this.x = this.y = 0
        // ここでzeroタイプが監視されていた場合は標準の通知は抑制される

      return originalRecord
        // その場合に代わりに通知して欲しい変更記録オブジェクトを返す

    })
    return this
  }
}


1.observeしていない場合は何も起こらない。

let p = new Point(20, 30)
p.zero()  // {x: 0, y: 0}

//  *log*


2.observeしているが、zeroタイプを指定していない場合、標準の通知がされる。

let p = new Point(20, 30)
Object.observe(p, log)
p.zero()  // {x: 0, y: 0}

//  *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タイプを指定している場合、代わりに指定した通知がされる。

let p = new Point(20, 30)
Object.observe(p, log, ['update', 'zero'])
p.zero()  // {x: 0, y: 0}

//  *log*
  [
    {
      type      : "zero",
      object    : {x: 0, y: 0},
      oldObject : {x: 20, y: 30}
    }
  ] 


補足・応用例

補足

シンボルプロパティの監視

シンボルプロパティに変更が加わっても通知はされない。

let obj = {}

Object.observe(obj, log)

let sym = Symbol()
obj[sym] = 123

//  *log*


応用例

MutationObserverと組み合わせる

オブジェクトとノードの属性を同期させる関数を作る。

function syncObj2Elem(obj, elem) {

  // オブジェクトの属性をノードの属性に設定する
  Object.keys(obj, name => {
    let value = obj[name]
    elem.setAttribute(name, value)
  })

  // ノードの属性をオブジェクトの属性に設定する
  ;[].forEach.call(elem.attributes, name => {
    let value = elem.getAttribute(name)
    obj[name] = value
  })

  // オブジェクトの属性の変更を監視する
  Object.observe(obj, changeRecords => {
    changeRecords.forEach( record => {

      let name  = record.name,
          value = obj[name] 

      switch (record.type) {
        case 'add' :  case 'update' :
          elem.setAttribute(name, value)
        break
        case 'delete' :
          elem.removeAttribute(name)
        break
      }
    
    })
  }, ['add', 'update', 'delete'])

  // ノードの属性の変更を監視する
  new MutationObserver( mutationRecords => {
    mutationRecords.forEach( record => {

      let name  = record.attributeName,
          value = elem.getAttribute(name),     
          type  = elem.hasAttribute(name) ? 'addOrUpdate' : 'delete'

      switch (type) {
        case 'addOrUpdate' :
          obj[name] = value
        break
        case 'delete' :
          delete obj[name]
        break
      }     

    })
  }).observe(elem, {attributes: true})

}

※注意※
MutationObserverでは実際には値が変更されない代入でも通知されるが、Object.observeでは実際に値が変更される操作が加わらないと通知されないという違いがある。
その性質により、このような単純な実装では一度の代入で無駄な再代入が何回か起こってしまう。(無限ループになることはない)


実装されるバージョン

V8 3.15.0(導入) (3.25.9) 3.26.30 - 5.0.42(デフォルト有効) 5.2.115(廃止)
Chrome M36 - 49(デフォルト有効)