Object.freeze / Object.seal の糖衣構文
もともとの発端はここから。
Fishrock123/proposal-const-function-arguments: A proposal to introduce constant function argument references.: https://t.co/rMvEtdYrrg
— hiroppy😶 (@about_hiroppy) 2017年11月26日
明示的にconst
かどうかを示すプロポーザルでまだチャンピオンは取得してない。
気付いたら
- Brian Terlson(現在のTC39のエディタ)
- Jordan Harband(AirbnbのTC39の会員)
- Mikeal Rogers(Node.js Foundationの設立者)
- Bradley Farias(TC39の会員でESMに一番くわしい人)
- Benedikt Meurer(V8チームのスゴイ人)
- Ingvar Stepanyan(ASTスゴイ詳しい人)
たちが議論していた。
今回の話でも上がってきたし、TC39のミーティングでのstageの変動があったObject.freeze/sealの話をしようと思う。
途中でnull prototypesの話になったのでそれはもし書きたくなったら書く。
プロポーザル
Brian TerlsonがFishrock123のプロポーザルとこれ似ているからって話があって思い出した。
今回のミーティングでstage-1となった。
JSの問題点
関数の引数に対して、代入はしても元はもちろん変わらないが、Objectの中身の変更はできる。
JSのconst
はあくまでも再代入をさせないだけであるので、中身の変更は可能である。
const a = { b: 1 }; function modify(a) { // a = {c: 1}; // 再代入はしても意味がない a.b += 1; a.c = 2; } modify(a); console.log(a); // { b: 2, c: 2 }
Objectの制御について
ES5から入った、preventExtensions
、 seal
、freeze
というメソッドがあります。
以下が対応表です。
method | プロパティの追加 | プロパティの変更 | プロパティの削除 | プロパティの属性の変更 | 確認 |
---|---|---|---|---|---|
preventExtensions | N | Y | Y | Y | Object.isExtensible |
seal | N | Y | N | N | Object.isSealed |
freeze | N | N | N | N | Object.isFrozen |
このような属性へ変更されます。
{ const a = { b: 1 }; console.log(Object.getOwnPropertyDescriptors(a)); // { b: { value: 1, writable: true, enumerable: true, configurable: true } } Object.preventExtensions(a); console.log(Object.getOwnPropertyDescriptors(a)); // { b: { value: 1, writable: true, enumerable: true, configurable: true } } Object.seal(a); console.log(Object.getOwnPropertyDescriptors(a)); /* { b: { value: 1, writable: true, enumerable: true, configurable: false } } */ Object.freeze(a); console.log(Object.getOwnPropertyDescriptors(a)); /* { b: { value: 1, writable: false, enumerable: true, configurable: false } } */ }
つまり、seal
ではconfigurable
がfalse
へ、freeze
ではwritable
とconfigurable
がfalse
となる。
また、強い方へしか変更ができなくなります。
const a = { b: 1 }; Object.freeze(a); Object.preventExtensions(a); Reflect.deleteProperty(a, 'b'); console.log(a); // { b: 1 }
他の言語もおそらく同じですが、これらはそのObject以下のすべてを適応するわけではなく、直下しか適用しません。
{ const a = { b: { c: [ 'test' ] } }; console.log(Object.getOwnPropertyDescriptors(a)); /* { b: { value: { c: [Array] }, writable: true, enumerable: true, configurable: true } } */ const b = Object.freeze(a); console.log(Object.getOwnPropertyDescriptors(b)); // 適応 /* { b: { value: { c: [Array] }, writable: false, enumerable: true, configurable: false } } */ console.log(Object.getOwnPropertyDescriptors(b.b)); // 適応されてない /* { c: { value: [ 'test' ], writable: true, enumerable: true, configurable: true } } */ }
つまり、複数の階層構造になっているObjectを制御したければ各行で明記しなければなりません。
糖衣構文
さすがに、毎回Object.~~
って書くのはめんどいのでそのために提案された。
freeze
const foo = {# a: {# b: {# c: {# d: {# e: [# "some string!" #] #} #} #} #} #} // 展開 const foo = Object.freeze({ a: Object.freeze({ b: Object.freeze({ c: Object.freeze({ d: Object.freeze({ e: Object.freeze([ "some string!" ]) }) }) }) }) })
seal
const foo = {| a: {| b: {| c: {| d: {| e: [| "some string!" |] |} |} |} |} |} // 展開 const foo = Object.seal({ a: Object.seal({ b: Object.seal({ c: Object.seal({ d: Object.seal({ e: Object.seal(["some string!"]) }) }) }) }) })
また、以下のような使い方もできる。
function ajax({| url, headers, onSuccess |}) { fetch(url, { headers }).then(onSuccess) } ajax({ url: 'http://example.com', onsuccess: console.log }) // throws TypeError('cannot define property `onsuccess`. Object is not extensible')
function add(| a, b |) { return a + b } add(2, 2, 2) === 6 // throws TypeError('invalid third parameter, expected 2`)
function add1(# a #) { a += 1 // throws TypeError `invalid assignment...` return a } add1(1) === 2
const foo = { a: 1, b: 2 } const {| a, b, c |} = foo // Throws TypeError 'invalid assignment to unknown property c'
このように関数のパラメータの束縛したりすることによりデストラクチャリングに対してバリデートしやすくなる。
さいごに
ライブラリのコードではよく使われるが、業務で使われていたのを見たことがない気がする。