技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

Object.freeze / Object.seal の糖衣構文

もともとの発端はここから。

明示的に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の話になったのでそれはもし書きたくなったら書く。

プロポーザル

github.com

Brian TerlsonがFishrock123のプロポーザルとこれ似ているからって話があって思い出した。

今回のミーティングでstage-1となった。

github.com

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から入った、preventExtensionssealfreezeというメソッドがあります。
以下が対応表です。

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ではconfigurablefalseへ、freezeではwritableconfigurablefalseとなる。

また、強い方へしか変更ができなくなります。

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'

このように関数のパラメータの束縛したりすることによりデストラクチャリングに対してバリデートしやすくなる。

さいごに

ライブラリのコードではよく使われるが、業務で使われていたのを見たことがない気がする。