JavaScriptのプロパティ1には内部的に次の4つの属性が付与されているのはご存知でしょうか?
[[Value]]
[[Writable]]
[[Enumerable]]]
[[Configurable]]
普段、オブジェクトを扱っているときはあまり意識することがないものかも知れません。しかし、これらを理解しておくと、より細やかにプロパティの仕様を定義することができます。例えば、実行時に変更されてはまずいプロパティを保護したり、JSON.stringify
に特定のプロパティを含めないようにできたりと、一歩踏み込んだコーディングができるようになります。
これらのプロパティ属性は、Object.getOwnPropertyDescriptors()
で簡単に調べることができます。
const obj = { foo: 1, bar: true }
console.log(Object.getOwnPropertyDescriptors(obj))
{
foo: { value: 1, writable: true, enumerable: true, configurable: true },
bar: { value: true, writable: true, enumerable: true, configurable: true }
}
本稿では、この4つの属性のうち、writable、enumerable、configurableの3つについて説明します。
まとめ
先に結論を示しておきます。
- writable
- プロパティの値を上書きできるか?
-
true
: ミュータブルな値となり、代入演算が変更できる。 -
false
: イミュータブルな値となり、代入演算ができなくなる。
- enumerable
- プロパティを列挙できるか?
-
true
:for .. in
やObject.keys()
、JSON.stringify()
などの対象になる。 -
false
: 上記の対象に含まれなくなる。
- configurable
- プロパティの属性設定を変更できるか?
-
true
: プロパティの属性の変更ができるようになり、delete
でプロパティの削除も可能。 -
false
: 上記の操作ができなくなる。
writableとconfigurableの違いが気になるところですが、writableは値(value
)の変更について作用し、configurableはプロパティ属性(writable
やenumerable
, configurable
など)を再設定できるかどうかに作用する点で異なります。
この2つを組み合わせることで、代入はできるけど、プロパティ属性の変更はできない、といった仕様にすることもできます。
オブジェクト属性を操作するには?
ここでは、オブジェクト属性を操作する方法を見ていきます。
プロパティ属性を変更するには?
すでに存在しているプロパティの属性を変更するには、Object.definePropertyメソッドを使います。第三引数に属性設定値を渡すと、それに沿った設定に変更されます。
const obj = { foo: 1 }
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: true }
// ^^^^ ^^^^ ^^^^
Object.defineProperty(obj, 'foo', {
writable: false,
enumerable: false,
configurable: false,
})
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: false, configurable: false }
// ^^^^^ ^^^^^ ^^^^^
なお、属性値を一部省略した場合は、元の属性値が残ります。つまり、差分適用になります。
// writableだけ指定した場合
Object.defineProperty(obj, 'foo', { writable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: true, configurable: true }
// ^^^^^ ここだけ変更になる
プロパティを追加しながら属性を設定する
上では既存のプロパティの属性変更の操作でしたが、Object.definePropertyメソッドは、プロパティを追加しながら、その属性を定義することもできます。
const obj = { }
Object.defineProperty(obj, 'foo', {
value: 1,
writable: true,
enumerable: true,
configurable: true,
})
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: true }
ちなみに、これらの属性値を省くと、デフォルトでfalse
になります。
const obj = { }
Object.defineProperty(obj, 'foo', { value: 1 })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: false, configurable: false }
3つの属性
ここからは、writable、enumerable、configurableの3つの属性について見ていきます。
writable
writable属性は、オブジェクトプロパティの値を変更できるかの設定です。代入演算子をプロパティに対して使えるかということです。代入演算子とは、=
をはじめ、+=
や-=
のことです。
writableをfalse
にすることで、オブジェクトプロパティをイミュータブルにすることができます。
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false }) // 書き込み不可に変更
obj.foo = 2 // 書き込みしてみる
console.log(obj)
//=> { foo: 1 }
writableがfalse
の場合、代入演算は無視されます。
writableをfalse
にしている身近な例は、Function
のname
プロパティです。
function foo() {}
console.log(foo.name) //=> foo
foo.name = 'hoge' // 変更は効かない
console.log(foo.name) //=> foo
console.log(Object.getOwnPropertyDescriptor(foo, 'name'))
//=> { value: 'foo', writable: false, enumerable: false, configurable: true }
Strictモードが有効になっている場合、無視ではなくTypeErrorが発生します。
"use strict"
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false })
obj.foo = 2
//=> TypeError: Cannot assign to read only property 'foo' of object '#<Object>'
Object.assign
による値の変更は、Strictモードに限らず、TypeErrorが発生します。
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false })
Object.assign(obj, { foo: 2 })
//=> TypeError: Cannot assign to read only property 'foo' of object '#<Object>'
enumerable
enumerable属性は、プロパティが列挙可能かどうかの設定です。列挙可能なプロパティはfor .. in
やObject.keys
などに現れるようになります。
逆に、enumerableにfalse
がセットされたプロパティは、Object.keys
などの対象にならなくなります。
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(Object.keys(obj))
//=> [ 'b', 'c' ]
enumerableが活用されている身近な例としては、window
のプロパティです。Date
やMap
といった便利クラスは、window
オブジェクトのプロパティです。new window.Date()
はnew Date()
と同じです。window
オブジェクトのプロパティなら、window
をfor .. in
で探したら見つかりそうですが、出てきません。これはwindow.Date
プロパティのenumerable属性がfalse
になっているためです。
console.log
でオブジェクトプロパティをデバッグすることがあると思いますが、enumerableがfalse
だとconsole.log
の結果にも現れなくなります。これを応用すると、デベロッパーに見せる必要のない内部プロパティを作るといったことができます。
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(obj)
//=> { b: 2, c: 3 }
もちろん、見えなくなっただけで、プロパティへのアクセスはできます。
console.log(obj.a) //=> 1
他にもJSON.strigify
の結果にも現れなくなるので、JSONにエンコードされるべきでないプロパティにも応用可能です。
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(JSON.stringify(obj))
//=> {"b":2,"c":3}
プロパティがenumerableかどうかは、Object.prototype.propertyIsEnumerableメソッドで手っ取り早く調べることができます。
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(obj.propertyIsEnumerable('a')) //=> false
console.log(obj.propertyIsEnumerable('b')) //=> true
configurable
configurable属性は、プロパティの属性変更を許可するかどうかの設定です。Object.defineProperty
で属性の操作ができることは先述しましたが、これが行えなくなるということです。
configurableをfalse
にしている身近な例としては、Array
のlength
プロパティがそうです。
const arr = [1, 2, 3]
console.log(Object.getOwnPropertyDescriptor(arr, 'length'))
//=> { value: 3, writable: true, enumerable: false, configurable: false }
configurableにfalse
が設定されたプロパティの属性の特徴を見ていきましょう。
まず、属性を変更しようとすると、TypeErrorが発生します。
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }
Object.defineProperty(obj, 'foo', { enumerable: false }) // 設定変更を試みる
//=> TypeError: Cannot redefine property: foo
ただし、value
とwritable
属性だけは変更可能です。
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }
Object.defineProperty(obj, 'foo', { value: 2, writable: false }) // この設定変更はOK
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 2, writable: false, enumerable: true, configurable: false }
一方で、writableがfalse
の場合は、writable属性すらも変更できなくなります。つまり、configurableがfalseの場合、属性設定をゆるくすることはできないということです。
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false, configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: true, configurable: false }
Object.defineProperty(obj, 'foo', { writable: true }) // 変更を試みる
//=> TypeError: Cannot redefine property: foo
さらに、delete
でプロパティを削除することが不可能になります。
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }
delete obj.foo // 効かない
console.log(obj.foo) //=> 1
Strictモードでプロパティ削除を試みるとTypeErrorが発生します。
"use strict"
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: true, enumerable: true, configurable: false }
delete obj.foo
//=> TypeError: Cannot delete property 'foo' of #<Object>
参考文献
- プロパティの列挙可能性と所有権 - JavaScript | MDN
- 6.1.7.1 Property Attributes - ECMAScript® 2020 Language Specification
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin
-
本稿では、データプロパティのことをプロパティということにします。アクセッサプロパティについては割愛します。 ↩