Help us understand the problem. What is going on with this article?

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

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

  1. [[Value]]
  2. [[Writable]]
  3. [[Enumerable]]]
  4. [[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 .. inObject.keys()JSON.stringify()などの対象になる。
    • false: 上記の対象に含まれなくなる。
  • configurable
    • プロパティの属性設定を変更できるか?
    • true: プロパティの属性の変更ができるようになり、deleteでプロパティの削除も可能。
    • false: 上記の操作ができなくなる。

writableとconfigurableの違いが気になるところですが、writableは値(value)の変更について作用し、configurableはプロパティ属性(writableenumerable, configurableなど)を再設定できるかどうかに作用する点で異なります。

この2つを組み合わせることで、代入はできるけど、プロパティ属性の変更はできない、といった仕様にすることもできます。

Pasted_Image_2020_03_05_14_40.png

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

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

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

すでに存在しているプロパティの属性を変更するには、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にしている身近な例は、Functionnameプロパティです。

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 .. inObject.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のプロパティです。DateMapといった便利クラスは、windowオブジェクトのプロパティです。new window.Date()new Date()と同じです。windowオブジェクトのプロパティなら、windowfor .. inで探したら見つかりそうですが、出てきません。これはwindow.Dateプロパティのenumerable属性がfalseになっているためです。

Example_Domain.png

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にしている身近な例としては、Arraylengthプロパティがそうです。

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

ただし、valuewritable属性だけは変更可能です。

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>

参考文献


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


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

suin
Qiita 4位/TypeScript入門書執筆中/TypeScripterのための座談会「YYTypeScript」主催/『実践ドメイン駆動設計』書籍邦訳レビュア/分報Slack考案/YYPHP主催/CodeIQマガジン執筆/株式会社クラフトマンソフトウェア創設/Web自動テスト「ShouldBee」の開発/TypeScript/DDD/OOP
https://yyts.connpass.com/
shouldbee
開発者向けテスト支援サービスShouldBeeを開発・運営するスタートアップ(onlab第8期)
http://shouldbee.at
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした