【JavaScript】 オブジェクトのプロパティ追加を禁止する方法
更新日:2021/09/01
自作のオブジェクトを共有ライブラリとして外部に公開するときなど、他者にオブジェクトを変更してほしくないときもあります。
タイプミスが頻繁で、予定にない新規プロパティが増えていて困るという人(僕)もいます。
そんなとき、Object.freeze()またはObject.seal()またはObject.preventExtensions()を使用します。
オブジェクトにプロパティを追加させない方法
Object.freeze()またはObject.seal()またはObject.preventExtensions()を使用すると、オブジェクト内のプロパティの属性を一括で変更したり、オブジェクトのプロパティ追加を禁止したりできます。
メソッド | プロパティ追加 | writable属性 | configurable属性 | 目的 |
---|---|---|---|---|
Object.freeze() | 禁止 | false | false | 完全凍結: 値の変更もできなくする |
Object.seal() | 禁止 | 変更なし | false | プロパティ値の変更はできる |
Object.preventExtensions() | 禁止 | 変更なし | 変更なし | プロパティ追加禁止のみ |
オブジェクトは[[Extensible]]という、プログラムコードからアクセスできない内部プロパティを持っています。
このプロパティの値がtrueのときオブジェクトはプロパティを追加でき、falseのとき禁止されます。
[[Extensible]]の値は通常はtrueです。
上記の3つのメソッドにより、[[Extensible]]にfalseがセットされます。
所持する全てのプロパティに適用される
writable属性とconfigurable属性は、オブジェクトのプロパティが個別に所持している属性です。
writable属性がtrueのとき値の変更ができ、falseのとき変更ができません。
configurable属性がtrueのときは属性の変更ができます。falseのときは変更ができないので、writable属性がfalseだったものをtrueに変更してしまうなどの行為ができなくなります。
プロパティの属性変更は、通常はObject.defineProperty()というメソッドを個別に適用します。
しかし、上表のメソッドはオブジェクトが直接所持している全てのプロパティのwritable属性とconfigurable属性を変更します。
プロパティ追加属性はオブジェクトが所持
プロパティを追加可能かどうかは、オブジェクトが所持している内部属性です。
追加を禁止にする方法は、表の3つのメソッドだけです。
また、一度禁止したものを解除する方法はありません。
一見不便なように感じますが、これらのメソッドはセキュリティ強化という目的で呼び出されれます。
解除する方法があるとその目的を達成することができないので、仕方がないのです。
JavaScriptの用語としては、オブジェクトのプロパティ追加が禁止されているかどうかを、『拡張可能かどうか』と表現することがあります。
『拡張可能ではない』とは、プロパティ追加が禁止されているという意味です。
Object.freeze()
Object.freeze()は、既存プロパティのwritableとconfigurable属性をfalseに設定します。
さらに、オブジェクトのプロパティ追加を不可にするフラグをセットします。
これによりオブジェクトの新規プロパティの追加、および既存プロパティの値と属性の変更が禁止されます。
Object.freeze()は、オブジェクトの改変をプロパティが持っている値を含めて禁止したいときに使用します。
構文
Object.freeze(obj)
■引数
obj: 凍結させるオブジェクト
■戻り値
引数で指定したオブジェクト
■使用例
"use strict";
const obj ={
prop1:100,
prop2:200,
prop3:{}
};
// 現在の属性を列挙
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: true }
// prop2{ value: 200, writable: true, enumerable: true, configurable: true }
// prop3{ value: {}, writable: true, enumerable: true, configurable: true }
});
// オブジェクトをフリーズ
Object.freeze(obj);
// フリーズ後の属性を列挙 → writable・configurable属性がfalseに変更されている
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: false, enumerable: true, configurable: false }
// prop2{ value: 200, writable: false, enumerable: true, configurable: false }
// prop3{ value: {}, writable: false, enumerable: true, configurable: false }
});
obj.prop1 = 500; //TypeError: "prop1" is read-only
■strictモードと非strictモードでの動作の差
strictモードでは、書き込み禁止されたプロパティを変更しようとすると、エラーがスローされます。
非strictモードではエラーがスローされず、そのまま続行されます。
Object.seal()
Object.seal()は、既存プロパティのconfigurable属性をfalseに設定します。
さらに、オブジェクトのプロパティ追加を不可にするフラグをセットします。
これによりオブジェクトの新規プロパティの追加、および既存プロパティの属性変更が禁止されます。
Object.seal()は、オブジェクトの構造改変を禁止したいときに使用します。
構文
Object.seal(obj)
■引数
obj: シールさせるオブジェクト
■戻り値
引数で指定したオブジェクト
■使用例
"use strict";
const obj ={
prop1:100,
prop2:200,
prop3:{}
};
// 現在の属性を列挙
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: true }
// prop2{ value: 200, writable: true, enumerable: true, configurable: true }
// prop3{ value: {}, writable: true, enumerable: true, configurable: true }
});
// オブジェクトをシール
Object.seal(obj);
// シール後の属性を列挙 → configurable属性がfalseに変更されている
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: false }
// prop2{ value: 200, writable: true, enumerable: true, configurable: false }
// prop3{ value: {}, writable: true, enumerable: true, configurable: false }
});
obj.prop1 = 500; // writableがtrueのままなので、値を変更可能
Object.seal()を実行すると、obj直下への新規プロパティ追加と既存プロパティの削除がおこなえなくなります。
オブジェクトとその直下のプロパティ
obj{ プロパティ プロパティ プロパティ }
しかしプロパティ値の変更が禁止されていないので、オブジェクトを代入することで、オブジェクトの入れ子構造などの改変をおこなうことはできます。
入れ子構造などの改変
obj{ プロパティ プロパティ プロパティ: ←代入 obj{ プロパティ プロパティ } }
Object.seal()実行前にObject.defineProperty()でwritable属性をfalseにセットしておくことで、一部のプロパティのみ上書き禁止することができます。
"use strict";
const obj ={
prop1:100,
// prop2:200,
prop3:{}
};
Object.defineProperty(obj,"prop2",{
value:200,
writable:false , // 規定値
});
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: true }
// prop3{ value: {}, writable: true, enumerable: true, configurable: true }
// prop2{ value: 200, writable: false, enumerable: true, configurable: true }
});
Object.seal(obj);
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: false }
// prop3{ value: {}, writable: true, enumerable: true, configurable: false }
// prop2{ value: 200, writable: false, enumerable: true, configurable: false }
});
obj.prop2 = 500; //TypeError: "prop2" is read-only
Object.preventExtensions()
Object.preventExtensions()は、オブジェクトのプロパティ追加を不可にするフラグをセットします。
これにより、以降のプロパティ追加ができなくなります。
Object.preventExtensions()は、既存プロパティの属性はそのままで、オブジェクトのプロパティ追加を禁止したいとき使用します。
構文
Object.preventExtensions(obj)
■引数
obj: プロパティ追加を禁止するオブジェクト
"use strict";
const obj ={
prop1:100,
prop2:200,
prop3:{}
};
// 現在の属性を列挙
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: true }
// prop2{ value: 200, writable: true, enumerable: true, configurable: true }
// prop3{ value: {}, writable: true, enumerable: true, configurable: true }
});
// オブジェクトのプロパティ追加を禁止
Object.preventExtensions(obj);
// 禁止後の属性を列挙 → 属性の変更なし
Object.getOwnPropertyNames(obj).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(obj,e);
console.log( e ,od );
// prop1{ value: 100, writable: true, enumerable: true, configurable: true }
// prop2{ value: 200, writable: true, enumerable: true, configurable: true }
// prop3{ value: {}, writable: true, enumerable: true, configurable: true }
});
obj.newProp = 100; // Uncaught TypeError: can't define property "newProp": Object is not extensible
属性変更の範囲
既存プロパティの属性に左右されない
Object.defineProperty()を使用すると、for-in文やObject.keys()での列挙許可(enumerable属性)や属性の変更許可(configurable属性)を設定できます。
Object.freeze()とObject.seal()はこれらの属性に左右されず、オブジェクトが直接所持しているプロパティ全ての属性を変更します。
入れ子構造は対象外
Object.freeze()とObject.seal()の対象となるのは、オブジェクトの直下のプロパティのみです。
オブジェクトのプロパティ中にオブジェクトが存在する入れ子状態のとき、入れ子となっているオブジェクトの属性は変更されません。
変更したいときは、個別にメソッドを実行する必要があります。
const obj ={
prop1:100,
prop3:{
prop31:100,
prop32:{
prop321:100
}
}
};
Object.freeze(obj);
Object.freeze(obj.prop3);
Object.freeze(obj.prop3.prop32);
配列のフリーズ
配列はArrayオブジェクトのため、Object.freeze()とObject.seal()を使用できます。
const array =[1,2,3];
Object.freeze(array);
Object.getOwnPropertyNames(array).forEach(e=>{
const od = Object.getOwnPropertyDescriptor(array,e);
console.log( e ,od );
// 0 { value: 1, writable: false, enumerable: true, configurable: false }
// 1 { value: 2, writable: false, enumerable: true, configurable: false }
// 2 { value: 3, writable: false, enumerable: true, configurable: false }
// length { value: 3, writable: false, enumerable: false, configurable: false }
});
array[0]=100; // TypeError: 0 is read-only
ただし多次元配列は、オブジェクトの入れ子になります。
そのため配列全体を凍結するには、入れ子となっているオブジェクトにもメソッドを適用する必要があります。
const array =[ [10,11,12],[20,21,22],[30,31,32] ];
Object.freeze(array);
array[2][2]=100; // エラーにならない
for( let i = 0; i < array.length ; i++ ){
Object.freeze(array[i]);
}
array[2][2]=100; // TypeError: 2 is read-only
変更されるのは参照先のオブジェクト
Object.freeze()やObject.seal()などの対象となるのは、参照先のオブジェクトということに注意してください。
次のコードは、変数objを凍結しているような印象を受けます。
const obj ={
prop1:100,
};
Object.freeze(obj);
obj.prop1 = 1000; // 変更できない
しかし実際には変数objが参照する、オブジェクトを対象としています。
そのため次のようなコードに書き換えることができます。
const obj = Object.freeze({
prop1:100,
});
ただし、上のコードは変数objをconstで定義しているため、変数objを変更することができません。
プロトタイプのフリーズ
コンストラクターのprototypeオブジェクトは、インスタンスのプロトタイプチェーンに組み込まれます。
プロトタイプチェーンへの不正な関数組み込みを防止するために、コンストラクターのprototypeオブジェクトをフリーズしておくのも有効な手段です。
まずはprototypeプロパティにセットされているオブジェクトをフリーズします。
prototypeプロパティにセットされているオブジェクトをフリーズ
const f = function(){ };
f.prototype = { func1 : function(){} };
Object.freeze( f.prototype );
f.prototype = 1; // 変更可能
上の例では、f.prototype.func1 の値を変更できません。
しかし、f.prototype の値は変更できてしまいます。
f.prototype = 1; // 変更可能
コンストラクターそのものをフリーズすることで、対応します。
Object.freeze(f.prototype);
Object.freeze(f);
追加可能か確認する方法
オブジェクトにプロパティを追加できるかどうは、Object.isExtensible() で確認できます。
追加可能ならtrueを返します。
つまり、Object.freeze()などを適用したオブジェクトをObject.isExtensible() で確認すると、falseになります。
console.log( Object.isExtensible({} ) ); // true
console.log( Object.isExtensible( Object.freeze({}) ) ); // false
console.log( Object.isExtensible( Object.preventExtensions({}) ) ); // false
console.log( Object.isExtensible( Object.seal({}) ) ); // false
この他に、Object.isSealed()とObject.isFrozen()というメソッドが用意されています。
Object.isSealed()は、オブジェクトのプロパティ追加が禁止されていて、所持している全てのプロパティのconfigurable属性がfalseかどうか確認されます。
Object.isFrozen()は、Object.isSealed()の処理に加えて、writable属性がfalseかどうか確認されます。
Object.preventExtensions()を適用したオブジェクトは、両メソッドの結果はfalseです。
Object.seal()を適用したオブジェクトは、Object.isSealed()の結果がtrue、Object.isFrozen()はfalseです。
Object.freeze()を適用したオブジェクトは、両メソッドの結果はtrueです。
const preventObj = Object.preventExtensions({ prop:100 });
console.log( Object.isSealed( preventObj ) ); // false
console.log( Object.isFrozen( preventObj ) ); // false
const sealObj = Object.seal( { prop:100 } );
console.log( Object.isSealed( sealObj ) ); // true
console.log( Object.isFrozen( sealObj ) ); // false
const freezeObj = Object.freeze({ prop:100 });
console.log( Object.isSealed( freezeObj ) ); // true
console.log( Object.isFrozen( freezeObj ) ); // true
なおプロパティを所持していないオブジェクトは、Object.preventExtensions()、Object.seal()、Object.freeze()のどれを適用しても、Object.isSealed( )およびObject.isFrozen( )の結果がtrueになります。
const preventObj = Object.preventExtensions({ }); // ←プロパティを所持しないオブジェクト
console.log( Object.isSealed( preventObj ) ); // true
console.log( Object.isFrozen( preventObj ) ); // true
オブジェクト内の全てのプロパティが条件を満たすと判断されるためです。
更新日:2021/09/01
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。