JavaScriptでイミュータブルな変数を定義する

こんにちは、エンジニアのたべたつ(@ttabtt3)です。このエントリーは「イノベーター・ジャパンAdvent Calendar 2021」の22日目の記事です。

今回は、勘違いされがちなJavaScriptのconstの挙動とイミュータブルな変数の定義の方法について書こうと思います。

constで宣言した定数はすべて変更することができない。

MDN Web Docs によるとconstでできないことは 再代入 及び 再宣言 なので、ちょっとした工夫をすると変更 できてしまいます。

実際に配列とオブジェクトについて例を見てみましょう

const arr = ['hoge']
const obj = { name: 'object' }

// 再代入、再宣言することはできない
arr = ['foo']    // Uncaught TypeError: Assignment to constant variable.
const arr = ['foo']    // Uncaught SyntaxError: Identifier 'arr' has already been declared

obj = { name: 'object2' }    // Uncaught TypeError: Assignment to constant variable.
const obj = { name: 'object2' }    // Uncaught SyntaxError: Identifier 'obj' has already been declared


// 破壊的なメソッドを実行することで変更できる
arr.push('foo')
console.log(arr)    // ['hoge', 'foo']

// オブジェクトの中身は保護されていないので変更できる
obj.name = 'foo'
console.log(obj)    // { name: 'foo' }

constというと変更できない、と思ってしまいますよね。 ですが、実際は簡単に変更することができるので、100行ある関数の先頭で宣言された変数が終わりには全く別の形になっている、というようなことが起こり得ます。

では、上記のような危険なソースコードをリリースしないためにはどういった対策をするべきでしょうか。 答えは変数を イミュータブル にすること、です。

イミュータブルな変数の定義の方法

JavaScriptのデータ型にはミュータブルなものとイミュータブルなものがあります。 ミュータブルな型は、例えばArrayやObject。対してイミュータブルな型はStringやNumber, Booleanです。

ミュータブルな変数は上記で記述したような、破壊的なメソッドを持っているためバグを含んだソースコードになる可能性があります。

そこでJavaScriptにはArrayやObjectのような変数を完全に変更不可能にするObject.freeze()が用意されています。

const arr = Object.freeze(['hoge'])

// イミュータブルになっているため変更できない
arr.push('foo')    // Uncaught TypeError: Cannot add property 1, object is not extensible at Array.push (<anonymous>)
console.log(arr)    // ['hoge']

このような方法で変数を定義することで、実装者が意図していない変数の変更をすることができなくなります。

ただし、これだけでは不十分でネストした配列やオブジェクトは変更可能、という欠点があります。

const nestedObj = Object.freeze( { key: { name: 'hoge' } } )

// 最も浅い階層は変更不可能になっている
nestedObj.key = 'foo'
console.log(nestedObj)    // { key: { name: 'hoge' } } 

// 深い階層は変更できてしまう
nestedObj.key.name = 'foo'
console.log(nestedObj)    // { key: { name: 'foo' } } 

ネストした変数を変更不可能にするには全てをfreezeするような再起関数を自作してあげるといいでしょう。

const deepFreeze = (obj) => {
  Object.freeze(obj)

  for (key in obj) {
    if (obj[key] === null) continue

    if (typeof obj[key] === 'object') {
      deepFreeze(obj[key])
    }
  }
}

const obj = {
  key1: {
    key2: 'hoge'
  },
  key3: 'foo'
}

deepFreeze(obj)

console.log(Object.isFrozen(obj))         // true
console.log(Object.isFrozen(obj.key1))    // true
console.log(Object.isFrozen(obj.key2))    // true
console.log(Object.isFrozen(obj.key3))    // true

まとめ

今回はconstでは変数を変更不可能にするには不十分で、freezeを使って変数をイミュータブルにする方法について紹介しました。 イミュータブルなプログラミングはJavaScriptに限らず、PHPやその他のサーバーサイドでも安全にプログラミングをするための手法なので、ぜひ色々な箇所で実践してみてください。