JavaScriptのオブジェクトプロパティの隠し属性「writable」「enumerable」「configurable」を知る

JavaScriptのプロパティ[^1]には内部的に次の4つの属性が付与されているのはご存知でしょうか？

1. `[[Value]]`
* `[[Writable]]`
* `[[Enumerable]]]`
* `[[Configurable]]`

[^1]: 本稿では、データプロパティのことをプロパティということにします。アクセッサプロパティについては割愛します。

普段、オブジェクトを扱っているときはあまり意識することがないものかも知れません。しかし、これらを理解しておくと、より細やかにプロパティの仕様を定義することができます。例えば、実行時に変更されてはまずいプロパティを保護したり、`JSON.stringify`に特定のプロパティを含めないようにできたりと、一歩踏み込んだコーディングができるようになります。

これらのプロパティ属性は、`Object.getOwnPropertyDescriptors()`で簡単に調べることができます。

```javascript
const obj = { foo: 1, bar: true }
console.log(Object.getOwnPropertyDescriptors(obj))
```

```javascript:出力結果
{
  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つを組み合わせることで、代入はできるけど、プロパティ属性の変更はできない、といった仕様にすることもできます。

![Pasted_Image_2020_03_05_14_40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/0be2638e-f09c-a309-15fa-b83d976238b9.png)


## オブジェクト属性を操作するには？

ここでは、オブジェクト属性を操作する方法を見ていきます。

### プロパティ属性を変更するには？

すでに存在しているプロパティの属性を変更するには、[Object.defineProperty]メソッドを使います。第三引数に属性設定値を渡すと、それに沿った設定に変更されます。

[Object.defineProperty]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

```javascript
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 }
//                         ^^^^^              ^^^^^                ^^^^^
```

なお、属性値を一部省略した場合は、元の属性値が残ります。つまり、差分適用になります。

```javascript
// writableだけ指定した場合
Object.defineProperty(obj, 'foo', { writable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'))
//=> { value: 1, writable: false, enumerable: true, configurable: true }
//                         ^^^^^ ここだけ変更になる
```

### プロパティを追加しながら属性を設定する

上では既存のプロパティの属性変更の操作でしたが、[Object.defineProperty]メソッドは、プロパティを追加しながら、その属性を定義することもできます。

```javascript
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`になります。

```javascript
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`にすることで、オブジェクトプロパティをイミュータブルにすることができます。

```javascript
const obj = { foo: 1 }
Object.defineProperty(obj, 'foo', { writable: false }) // 書き込み不可に変更
obj.foo = 2 // 書き込みしてみる
console.log(obj)
//=> { foo: 1 }
```

writableが`false`の場合、代入演算は無視されます。

writableを`false`にしている身近な例は、`Function`の`name`プロパティです。

```javascript
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が発生します。

```javascript
"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が発生します。

```javascript
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`などの対象にならなくなります。

```javascript
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`になっているためです。

<img width="484" alt="Example_Domain.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889/b49ce4eb-8037-e4cb-665d-61d154d49990.png">


`console.log`でオブジェクトプロパティをデバッグすることがあると思いますが、enumerableが`false`だと`console.log`の結果にも現れなくなります。これを応用すると、デベロッパーに見せる必要のない内部プロパティを作るといったことができます。

```javascript
const obj = { a: 1, b: 2, c: 3 }
Object.defineProperty(obj, 'a', { enumerable: false })
console.log(obj)
//=> { b: 2, c: 3 }
```

もちろん、見えなくなっただけで、プロパティへのアクセスはできます。

```javascript
console.log(obj.a) //=> 1
```

他にも`JSON.strigify`の結果にも現れなくなるので、JSONにエンコードされるべきでないプロパティにも応用可能です。

```javascript
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]メソッドで手っ取り早く調べることができます。

[Object.prototype.propertyIsEnumerable]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable

```javascript
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`プロパティがそうです。

```javascript
const arr = [1, 2, 3]
console.log(Object.getOwnPropertyDescriptor(arr, 'length'))
//=> { value: 3, writable: true, enumerable: false, configurable: false }
```

configurableに`false`が設定されたプロパティの属性の特徴を見ていきましょう。

まず、属性を変更しようとすると、TypeErrorが発生します。

```javascript
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`属性だけは変更可能です。

```javascript
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の場合、属性設定をゆるくすることはできないということです。

```javascript
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`でプロパティを削除することが不可能になります。

```javascript
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が発生します。

```javascript
"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](https://developer.mozilla.org/ja/docs/Web/JavaScript/Enumerability_and_ownership_of_properties)
- [6.1.7.1 Property Attributes - ECMAScript® 2020 Language Specification](https://tc39.es/ecma262/#sec-property-attributes)

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)