<p><a href="http://abouthiroppy.hatenablog.jp/archive">技術探し</a></p>

技術探し

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'

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

さいごに

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