こんにちは、ほそ道です。
前回のconst
前提共有編に続いて、今回から「どうやって予期しない値の状態変化を抑止するか」をやっていきます。
前回、const
で宣言した変数は再代入ができないが、オブジェクトや配列などの子属性/要素が不意に変更されてしまう可能性をはらんでいる、という話をいれました。
今回はその対策の一つとして値を一切変更させない(凍結)手段についてやっていきます。
Object.freeze
ES5から追加となる、freeze
を使うことで不変オブジェクトを生成できます。
'use strict';
const o = Object.freeze({x: 1, y: 2});
o.x = 3; // TypeError: Cannot assign to read only property 'x' of #<Object>
オブジェクト生成後にfreeze
することも可能です。が、const
することで値に状態を持たせたくないという狙いを考えると、「freezeした」という状態が生まれてしまうので、代入時にfreezeしてしまう方が良いと思います。
'use strict';
const o = {x: 1, y: 2};
o.x = 3;
Object.freeze(o);
console.log(o); // { x: 3, y: 2 }
o.x = 4; // TypeError: Cannot assign to read only property 'x' of #<Object>
strictモード実行のススメ
このfreeze
はstrictモードであれば、きっちりエラーが発生しますので、是非ともstrictモードで。
strictモードでない場合は代入が無視されるので、逆に想定外の状況を誘発するようになって危険だと思います。。
const o = Object.freeze({x: 1, y: 2});
o.x = 3;
console.log(o.x); // 1(上の代入はなかったことにされる)
この後のサンプルはstrict宣言を省きますが、'use strict'している体で続けていきます。
子属性がオブジェクトを抱えている場合
Object.freezeはトップレベルの属性を凍結してくれるのですが、
子属性にオブジェクトがあると、そこまでは凍結してくれません。
const o = Object.freeze({a: 1, b: {x: 2}});
o.b.x = 3;
console.log(o); // { a: 1, b: { x: 3 } }
ではどうするか、子属性自体をfreezeすれば凍結することができます。
const o = Object.freeze({a: 1, b: Object.freeze({x: 2})});
o.b.x = 3;
console.log(o); // TypeError: Cannot assign to read only property 'x' of #<Object>
MDNのObject.freezeページにもこのこのことに言及しており、子属性の凍結を汎用的に行えるdeepFreeze
なる関数を提示してくれています。
ところがこのdeepFreeze
は引数となるオブジェクト自体をfreezeして書き換えてしまい、新しいオブジェクトを返してくれるでもなかったので、ちょっと自分が使う用に書き換えてみました。一部lodashを使っています。
const _ = require('lodash');
function deepFreeze(o) {
const oFrz = _.clone(o);
_.keys(oFrz).forEach(key => {
if (oFrz.hasOwnProperty(key) && (typeof oFrz[key] === "object") && !Object.isFrozen(oFrz[key])) {
oFrz[key] = deepFreeze(oFrz[key]);
}
});
return Object.freeze(oFrz);
}
これだとオブジェクトリテラルで生成したオブジェクトに関しては子属性まで含んだfreezeができます。
コピーを生成して返すので、元のオブジェクトには影響なく、constに一発で入れることができます。
const o = {a: 1, b: {x: 2}};
const odf = deepFreeze(o); // 一発で代入
o.b.y = 5;
console.log(o); // { a: 1, b: { x: 2, y: 5 } }
console.log(odf) // { a: 1, b: { x: 2 } }
odf.b.y = 5; // TypeError: Can't add property y, object is not extensible
ちなみにこのdeepFreeze
カスタマイズ版、注意点としてコンストラクタで生成するオブジェクトに関しては、prototypeのコピーができていませんのでご注意をば。__proto__
を使えば一応実現できるのですが、外法扱いということで今回は却下です。。
コレクションオブジェクトのfreeze
配列
配列もfreezeすることができます。
const a = Object.freeze([1,2,3,4,5]);
a.push(6); // TypeError: Can't add property 5, object is not extensible
オブジェクトの場合と似ていますが、子要素に配列があるとそれは凍結されません。
const a = Object.freeze([1,2,3,4,5,[6]]);
a[5].push(7);
console.log(a); // [ 1, 2, 3, 4, 5, [ 6, 7 ] ]
上で作ったdeepFreezeカスタマイズ版をかますと凍結は成功します。
const a = deepFreeze([1,2,3,4,5,[6]]);
a[5].push(7); // TypeError: Can't add property 1, object is not extensible
Map/Set
ES6で追加されたMap
やSet
に関してはfreezeできません。
現行のChrome46.0, FF42.0, Node5.0などいずれも同様の結果となります。
const m = Object.freeze(new Map([[1, 'a'], [2, 'b']]));
m.set(3, 'c');
console.log(m); // Map { 1 => 'a', 2 => 'b', 3 => 'c' }
const s = Object.freeze(new Set([1, 2, 3]));
s.add(4);
console.log(s); // Set { 1, 2, 3, 4 }
と、いった感じで今回はObject.freezeを使ってゴリッゴリの不変オブジェクトを作ってみました。
いくらかの課題はあるもののconst
で作った値の子属性が不意に変更されてしまう可能性を潰す一つの手法にはなりうるんではないかと思います。
ただ、アプリケーションを実装していくうえで、子属性を追加したり、変更したりという必要性にかられるケースは多々あると思いますので次回はその辺をやっていきたいと思います。
今回は以上です。