JavaScript : 配列とオブジェクトを再帰的に凍結する関数


JavaScriptでは、constで宣言しても配列やオブジェクトの要素は変更可能ですよね。
中味が変わらないといいなあと思ったことはありませんか?

中味も変更不可にした新しい配列、オブジェクトを返す関数deepFreezeを作ってみました。
元ネタは、 こちら の例にあるdeepFreeze。
再帰を今風に書くとこんなかな?というのと、使いやすいように新しいオブジェクトを返すようにしてみました。
対象は、配列、オブジェクトとプリミティブな値。関数もいけるかな? Map、Setはfreezeできないみたいです。

const mapForObj = f => a => 
  Object.keys(a).reduce( (acc, e)=>({ ...acc, [e]:f(a[e]) }), {})
const isObj = a => Object.prototype.toString.call(a)==="[object Object]"

const deepFreeze = a =>
  Array.isArray( a )?  Object.freeze( a.map( deepFreeze ) )
  : isObj( a )?  Object.freeze( mapForObj( deepFreeze )( a ) )
  : Object.freeze( a )

//使ってみる:
let func=x=>x

const obj = { a:1, b:[ { a:1 }, 2 ], c:func, d:{ x:[ 1, 2 ], y:2 } }

const frozen = deepFreeze(obj)
> frozen
=> { a: 1,
  b: [ { a: 1 }, 2 ],
  c: [Function: func],
  d: { x: [ 1, 2 ], y: 2 } }
> Object.isFrozen(frozen)
=> true
> Object.isFrozen(frozen.b)
=> true
> Object.isFrozen(frozen.b.a)
=> true
> Object.isFrozen(frozen.c)
=> true
> Object.isFrozen(frozen.d)
=> true
> Object.isFrozen(frozen.d.x)
=> true  // すべての階層で凍っている
> frozen.c=0  //関数frozen.cを変更してみるが...
=> 0
> frozen.c  
=> [Function: func] //変わらない。
> frozen.c(5)
=> 5      // x=>x 。変わらない
> frozen.c.a=0  //propertyを追加してみるが...
=> 0
> frozen.c.a
=> undefined  //追加できない
//ちなみにobjは凍っていない(当然ですが):
> Object.isFrozen(obj)
=> false
> 

ちゃんと中味も凍っているようです。

参照がループしてると無限ループに陥いる可能性とwindowとか凍らせて大変なことになる可能性があるそうな。 ご使用は自己責任で。

追記: 見た目をいじってみた。

JavaScriptは、オブジェクトの種類によってできることも方法も微妙に違っていたりするので、ちょっと複雑な感じになりましたが、やりたいことは単純です。

  • 深く凍結したものを返す関数は a を引数にとり、
  • a が配列かオブジェクトなら、aの要素を再帰的に深く凍結したものを新しく作り、それを破壊的操作で凍結して、それを返す。
  • そうでなければ、a を破壊的操作で凍結して、それを返す。

そう見えるようにコードにするとこんな感じでしょうか。

const pipe = (x, ...fs) => fs.reduce( (acc, f) => f(acc), x)

const isAryOrObj = a =>
  Array.isArray( a ) || Object.prototype.toString.call(a)==="[object Object]"
const map = f => a =>
  Array.isArray( a )? a.map( f )
  : Object.keys(a).reduce( (acc, e)=>({ ...acc, [e]:f(a[e]) }), {})
const freezeInPlace = a => Object.freeze( a )

const deepFreeze = a =>
  isAryOrObj( a )? 
    pipe( a, map(deepFreeze), freezeInPlace )
  : pipe( a, freezeInPlace )

関数の分割のしかたを変えて、それっぽい名前を付けてみました。同じように動きます。
パイプライン風またはメソッドチェーン風です。
前置きが長いですが、deepFreeze本体はかなりすっきりした感じです。