Google Apps ScriptでObject.seal()を使うとハマる

#概要
特に制限しない限りJavaScriptのオブジェクトには任意のプロパティを追加できるが、`Object.seal()`メソッドを使ってこれを抑制することができる。

>Object.seal() メソッドは、オブジェクトを封印して、新しいオブジェクトを追加することを抑制し、すべての既存のプロパティを設定変更不可にします。現存するプロパティの値は、書き込み可能である限り変更できます。[MDN web docs](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/seal)


例えば`obj2 = Object.seal(obj1)`とすると、`obj2`にプロパティを追加できなくなる。モダンなJavaScript実行環境において`"use strict"`でstrict modeにしていれば、プロパティの追加を試た時点で`TypeError`例外が発生する。

しかしGoogle Apps Scriptの`Object.seal()`はプロパティの追加を抑制するだけでなく、プロパティ値が`null`のものを`undefined`にしてしまう副作用がある。あと、GASのstrict-modeは中途半端なので信用できない。

#JavaScriptの正しい挙動を確認
僕の認識が間違っているかもしれないので、まずはChromeのコンソールでJavaScriptの本来あるべき挙動を確認する。

##sealされていないオブジェクトへのプロパティの追加 (Chrome)

```javascript
function MyClass1() {
  this.prop1 = "hello";
  this.prop2 = null;
  this.prop3 = undefined;
  this.prop4 = 3.1415;
}//MyClass1

function myClass1Test(){
  var myClass1 = new MyClass1();
  myClass1.prop5 = "world"; // add new property
  var Logger = console;
  Logger.log(myClass1.prop1); //hello
  Logger.log(myClass1.prop2); //null
  Logger.log(myClass1.prop3); //undefined
  Logger.log(myClass1.prop4); //3.1415
  Logger.log(myClass1.prop5); //world <- the new property
}//myClass1Test
```
期待したとおり、sealされていないオブジェクト`myClass1`に新しいプロパティ`prop5`を追加することができた。

##sealされたオブジェクトへのプロパティの追加 (Chrome)
```javascript
function MyClass2() {
  this.prop1 = "hello";
  this.prop2 = null;
  this.prop3 = undefined;
  this.prop4 = 3.1415;
  return Object.seal(this);
}//MyClass2

function myClass2Test(){
  var myClass2 = new MyClass2();
  myClass2.prop5 = "world"; // adding a new property
  Logger.log(myClass2.prop1); //hello
  Logger.log(myClass2.prop2); //null
  Logger.log(myClass2.prop3); //undefined
  Logger.log(myClass2.prop4); //3.1415
  Logger.log(myClass2.prop5); //undefined <- as expected
  Logger.log(Object.keys(myClass2)); //[prop1, prop2, prop3, prop4]  
}//myClass2Test
```
期待したとおり、sealされたオブジェクトで`myClass2`に新しいプロパティ`prop5`を追加することはできない。non-strictモードなので`TypeError`例外は発生せず一見すると`world`を値として持つ新しいプロパティ`prop5`を追加できたように見えるが、`Object.keys()`でプロパティ名を列挙しても`prop5`は存在せず、`prop5`の値を取得しようとすると`undefined`である。

#Google Apps Scriptの奇妙な挙動を確認

Google Apps Scriptでもsealすることによりプロパティの追加を抑制できる。しかし`obj2 = Object.seal(obj1)`すると`obj1`において`null`なプロパティ値が`obj2`において`undefined`にされてしまう。

```javascript
function MyClass2() {
  this.prop1 = "hello";
  this.prop2 = null;
  this.prop3 = undefined;
  this.prop4 = 3.1415;
  var sealed = Object.seal(this);
  Logger.log(this.prop2); // undefined <- UNEXPECTED, should be null
  Logger.log(Object.keys(myClass2)); //[prop1, prop2, prop3, prop4]  
  return sealed;
}//MyClass2

function myClass2Test(){
  var myClass2 = new MyClass2();
  Logger.log(myClass2.prop2); // undefined <- UNEXPECTED!!

  myClass2.prop5 = "world";   // add new property
  Logger.log(myClass2.prop5); // undefined <- as expected
  Logger.log(Object.keys(myClass2)); //[prop1, prop2, prop3, prop4]

  myClass2.prop2 = null; // workaround
  Logger.log(myClass2.prop2); // null <- as expected
}//myClass2Test
```
コンストラクタ`function MyClass2`を観察すると`Object.seal()`が返すオブジェクトではもともと`null`だったプロパティ値が`undefined`になっていることがわかる。プロパティが失われているわけではない。sealされたあとに再び`obj2.prop2`の値を`null`にすることはできる。そしてそれがworkaroundである。

#結論
Google Apps Scriptの`obj2 = Object.seal(obj1)`は`obj1`のプロパティのうち値が`null`であるものをすべて`undefined`に書き換えて返す。したがって値が`null`なプロパティを持つオブジェクトをsealしたなら、`obj2`で`undefined`になっている当該プロパティの値を再び`null`にしてやらなくてはならない。

#想像
Google Apps ScriptがベースにしているバージョンのMozilla Rhinoのバグ。