概要
特に制限しない限りJavaScriptのオブジェクトには任意のプロパティを追加できるが、Object.seal()メソッドを使ってこれを抑制することができる。
Object.seal() メソッドは、オブジェクトを封印して、新しいオブジェクトを追加することを抑制し、すべての既存のプロパティを設定変更不可にします。現存するプロパティの値は、書き込み可能である限り変更できます。MDN web docs
例えば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)
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)
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にされてしまう。
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のバグ。